import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types.js'; import { requireAuth } from '$lib/server/guards.js'; import { hashPassword, verifyPassword } from '$lib/server/auth.js'; import { withBypassRLS } from '$lib/server/rls.js'; import { passwordChangeLimiter } from '$lib/server/ratelimit.js'; import { z } from 'zod'; const profileSchema = z.object({ name: z.string().min(1, 'Name is required').max(100), email: z.string().email('Invalid email address') }); const passwordSchema = z.object({ currentPassword: z.string().min(1, 'Current password is required'), newPassword: z.string().min(8, 'New password must be at least 8 characters') }); // Update profile export const PATCH: RequestHandler = async ({ request, locals, getClientAddress }) => { requireAuth(locals.user); const body = await request.json(); // Password change if (body.currentPassword !== undefined) { const limit = passwordChangeLimiter(getClientAddress()); if (!limit.ok) { return json({ error: 'Too many attempts', retryAfter: limit.retryAfter }, { status: 429 }); } const result = passwordSchema.safeParse(body); if (!result.success) { return json({ error: result.error.issues[0].message }, { status: 400 }); } const updated = await withBypassRLS(async (tx) => { const user = await tx.user.findUnique({ where: { id: locals.user!.id } }); if (!user) throw error(404, 'User not found'); const valid = await verifyPassword(result.data.currentPassword, user.passwordHash); if (!valid) { return null; } const newHash = await hashPassword(result.data.newPassword); await tx.user.update({ where: { id: locals.user!.id }, data: { passwordHash: newHash } }); // Invalidate all other sessions await tx.session.deleteMany({ where: { userId: locals.user!.id, id: { not: locals.session!.id } } }); return true; }); if (!updated) { return json({ error: 'Current password is incorrect' }, { status: 400 }); } return json({ ok: true }); } // Profile update const result = profileSchema.safeParse(body); if (!result.success) { return json({ error: result.error.issues[0].message }, { status: 400 }); } const user = await withBypassRLS(async (tx) => { // Check if email is already taken by another user in the same tenant if (result.data.email !== locals.user!.email) { const existing = await tx.user.findFirst({ where: { email: result.data.email, tenantId: (await tx.user.findUnique({ where: { id: locals.user!.id }, select: { tenantId: true } }))!.tenantId, id: { not: locals.user!.id } } }); if (existing) return null; } return tx.user.update({ where: { id: locals.user!.id }, data: { name: result.data.name, email: result.data.email }, select: { id: true, name: true, email: true } }); }); if (!user) { return json({ error: 'Email is already in use' }, { status: 409 }); } return json({ user }); }; // Delete all other sessions export const DELETE: RequestHandler = async ({ locals }) => { requireAuth(locals.user); const count = await withBypassRLS(async (tx) => { const result = await tx.session.deleteMany({ where: { userId: locals.user!.id, id: { not: locals.session!.id } } }); return result.count; }); return json({ ok: true, sessionsRevoked: count }); };