diff --git a/src/lib/server/db/schema/user.ts b/src/lib/server/db/schema/user.ts index 5d70e00..c98fa01 100644 --- a/src/lib/server/db/schema/user.ts +++ b/src/lib/server/db/schema/user.ts @@ -4,4 +4,5 @@ export const user = sqliteTable("user", { id: integer("id").primaryKey({ autoIncrement: true }), email: text("email").notNull().unique(), password: text("password").notNull(), + nickname: text("nickname").notNull(), }); diff --git a/src/lib/server/db/session.ts b/src/lib/server/db/session.ts index 276090a..b51bf85 100644 --- a/src/lib/server/db/session.ts +++ b/src/lib/server/db/session.ts @@ -1,5 +1,5 @@ import { SqliteError } from "better-sqlite3"; -import { and, eq, gt, lte, isNull } from "drizzle-orm"; +import { and, eq, ne, gt, lte, isNull } from "drizzle-orm"; import env from "$lib/server/loadenv"; import db from "./drizzle"; import { IntegrityError } from "./error"; @@ -71,6 +71,10 @@ export const deleteSession = async (sessionId: string) => { await db.delete(session).where(eq(session.id, sessionId)); }; +export const deleteAllOtherSessions = async (userId: number, sessionId: string) => { + await db.delete(session).where(and(eq(session.userId, userId), ne(session.id, sessionId))); +}; + export const cleanupExpiredSessions = async () => { await db.delete(session).where(lte(session.lastUsedAt, new Date(Date.now() - env.session.exp))); }; diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts index 1efe43a..d970438 100644 --- a/src/lib/server/db/user.ts +++ b/src/lib/server/db/user.ts @@ -2,7 +2,20 @@ import { eq } from "drizzle-orm"; import db from "./drizzle"; import { user } from "./schema"; +export const getUser = async (userId: number) => { + const users = await db.select().from(user).where(eq(user.id, userId)).limit(1); + return users[0] ?? null; +}; + export const getUserByEmail = async (email: string) => { const users = await db.select().from(user).where(eq(user.email, email)).limit(1); return users[0] ?? null; }; + +export const setUserPassword = async (userId: number, password: string) => { + await db.update(user).set({ password }).where(eq(user.id, userId)); +}; + +export const setUserNickname = async (userId: number, nickname: string) => { + await db.update(user).set({ nickname }).where(eq(user.id, userId)); +}; diff --git a/src/lib/server/schemas/auth.ts b/src/lib/server/schemas/auth.ts index 91858c9..2d800d0 100644 --- a/src/lib/server/schemas/auth.ts +++ b/src/lib/server/schemas/auth.ts @@ -1,5 +1,11 @@ import { z } from "zod"; +export const changePasswordRequest = z.object({ + oldPassword: z.string().trim().nonempty(), + newPassword: z.string().trim().nonempty(), +}); +export type ChangePasswordRequest = z.infer; + export const loginRequest = z.object({ email: z.string().email().nonempty(), password: z.string().trim().nonempty(), diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index 615e2bc..6f8270b 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -4,3 +4,4 @@ export * from "./directory"; export * from "./file"; export * from "./hsk"; export * from "./mek"; +export * from "./user"; diff --git a/src/lib/server/schemas/user.ts b/src/lib/server/schemas/user.ts new file mode 100644 index 0000000..9841fba --- /dev/null +++ b/src/lib/server/schemas/user.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const userInfoResponse = z.object({ + email: z.string().email().nonempty(), + nickname: z.string().nonempty(), +}); +export type UserInfoResponse = z.infer; + +export const changeNicknameRequest = z.object({ + newNickname: z.string().min(2).max(8), +}); +export type ChangeNicknameRequest = z.infer; diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index c3fee31..81f0333 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -5,18 +5,46 @@ import { IntegrityError } from "$lib/server/db/error"; import { upgradeSession, deleteSession, + deleteAllOtherSessions, registerSessionUpgradeChallenge, consumeSessionUpgradeChallenge, } from "$lib/server/db/session"; -import { getUserByEmail } from "$lib/server/db/user"; +import { getUser, getUserByEmail, setUserPassword } from "$lib/server/db/user"; import env from "$lib/server/loadenv"; import { startSession } from "$lib/server/modules/auth"; import { verifySignature, generateChallenge } from "$lib/server/modules/crypto"; +const hashPassword = async (password: string) => { + return await argon2.hash(password); +}; + const verifyPassword = async (hash: string, password: string) => { return await argon2.verify(hash, password); }; +export const changePassword = async ( + userId: number, + sessionId: string, + oldPassword: string, + newPassword: string, +) => { + if (oldPassword === newPassword) { + error(400, "Same passwords"); + } else if (newPassword.length < 8) { + error(400, "Too short password"); + } + + const user = await getUser(userId); + if (!user) { + error(500, "Invalid session id"); + } else if (!(await verifyPassword(user.password, oldPassword))) { + error(403, "Invalid password"); + } + + await setUserPassword(userId, await hashPassword(newPassword)); + await deleteAllOtherSessions(userId, sessionId); +}; + export const login = async (email: string, password: string, ip: string, userAgent: string) => { const user = await getUserByEmail(email); if (!user || !(await verifyPassword(user.password, password))) { diff --git a/src/lib/server/services/user.ts b/src/lib/server/services/user.ts new file mode 100644 index 0000000..5c06458 --- /dev/null +++ b/src/lib/server/services/user.ts @@ -0,0 +1,15 @@ +import { error } from "@sveltejs/kit"; +import { getUser, setUserNickname } from "$lib/server/db/user"; + +export const getUserInformation = async (userId: number) => { + const user = await getUser(userId); + if (!user) { + error(500, "Invalid session id"); + } + + return { email: user.email, nickname: user.nickname }; +}; + +export const changeNickname = async (userId: number, nickname: string) => { + await setUserNickname(userId, nickname); +}; diff --git a/src/routes/api/auth/changePassword/+server.ts b/src/routes/api/auth/changePassword/+server.ts new file mode 100644 index 0000000..a0fbb8d --- /dev/null +++ b/src/routes/api/auth/changePassword/+server.ts @@ -0,0 +1,16 @@ +import { error, text } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { changePasswordRequest } from "$lib/server/schemas"; +import { changePassword } from "$lib/server/services/auth"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { sessionId, userId } = await authorize(locals, "any"); + + const zodRes = changePasswordRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { oldPassword, newPassword } = zodRes.data; + + await changePassword(userId, sessionId, oldPassword, newPassword); + return text("Password changed", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/user/+server.ts b/src/routes/api/user/+server.ts new file mode 100644 index 0000000..b10b13e --- /dev/null +++ b/src/routes/api/user/+server.ts @@ -0,0 +1,11 @@ +import { json } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { userInfoResponse, type UserInfoResponse } from "$lib/server/schemas"; +import { getUserInformation } from "$lib/server/services/user"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ locals }) => { + const { userId } = await authorize(locals, "any"); + const { email, nickname } = await getUserInformation(userId); + return json(userInfoResponse.parse({ email, nickname } satisfies UserInfoResponse)); +}; diff --git a/src/routes/api/user/changeNickname/+server.ts b/src/routes/api/user/changeNickname/+server.ts new file mode 100644 index 0000000..ab4f887 --- /dev/null +++ b/src/routes/api/user/changeNickname/+server.ts @@ -0,0 +1,16 @@ +import { error, text } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { changeNicknameRequest } from "$lib/server/schemas"; +import { changeNickname } from "$lib/server/services/user"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId } = await authorize(locals, "any"); + + const zodRes = changeNicknameRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { newNickname } = zodRes.data; + + await changeNickname(userId, newNickname); + return text("Nickname changed", { headers: { "Content-Type": "text/plain" } }); +};