diff --git a/.phase6-state.json b/.phase6-state.json new file mode 100644 index 0000000..08e5d00 --- /dev/null +++ b/.phase6-state.json @@ -0,0 +1,5 @@ +{ + "completed_features": [], + "current_feature": "password-change", + "started_at": "2026-05-23T05:30:16.203066" +} \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 630d5ec..8aa29e5 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -647,3 +647,16 @@ Migrations completed successfully Checking for admin user... Admin user already exists + +## đ Phase-6 Codegen-Run gestartet (2026-05-23 05:30:16) + + +## Phase-3 Feature: password-change (2026-05-23 05:30:16) + +- `05:30:16` **INFO** Description: Change-Password Endpoint + Form in Profile +- `05:30:16` **INFO** Generating apps/api/src/routes/users.ts (ERWEITERT â behalte alles. FĂźge POST /me/password (body: {oldPassword,âŚ) +- `05:30:58` **INFO** wrote 4542 chars in 42.7s (attempt 1) +- `05:30:58` **INFO** Generating apps/web/src/pages/Profile.tsx (ERWEITERT â behalte bestehendes Form (Name update). FĂźge zweite Card 'âŚ) +- `05:32:00` **INFO** wrote 7584 chars in 61.9s (attempt 1) +- `05:32:00` **INFO** Running tsc --noEmit on api⌠+- `05:32:02` **INFO** tsc clean â diff --git a/apps/api/src/routes/users.ts b/apps/api/src/routes/users.ts index ffa46d7..05c569d 100644 --- a/apps/api/src/routes/users.ts +++ b/apps/api/src/routes/users.ts @@ -17,6 +17,11 @@ const UserCreateSchema = z.object({ password: z.string().min(8) }) +const PasswordChangeSchema = z.object({ + oldPassword: z.string(), + newPassword: z.string().min(8) +}) + export default async function userRoutes(fastify: FastifyInstance) { fastify.addHook("preHandler", async (request, reply) => { try { @@ -71,6 +76,39 @@ export default async function userRoutes(fastify: FastifyInstance) { return user }) + fastify.post("/me/password", async (request, reply) => { + const userId = (request.user as { sub: string } | undefined)?.sub + if (!userId) { + return reply.code(401).send({ message: "User ID not found in token" }) + } + + const body = PasswordChangeSchema.parse(request.body) + + const [user] = await db + .select() + .from(users) + .where(eq(users.id, userId)) + .limit(1) + + if (!user) { + return reply.code(404).send({ message: "User not found" }) + } + + const isPasswordCorrect = await argon2.verify(user.passwordHash, body.oldPassword) + if (!isPasswordCorrect) { + return reply.code(401).send({ message: "Incorrect old password" }) + } + + const newPasswordHash = await argon2.hash(body.newPassword) + + await db + .update(users) + .set({ passwordHash: newPasswordHash }) + .where(eq(users.id, userId)) + + return reply.send({ message: "Password updated successfully" }) + }) + fastify.get("/", async (request, reply) => { if (!isAdmin(request)) { return reply.code(403).send({ message: "Forbidden: Admin role required" }) diff --git a/apps/web/src/pages/Profile.tsx b/apps/web/src/pages/Profile.tsx index 7239cb5..be94727 100644 --- a/apps/web/src/pages/Profile.tsx +++ b/apps/web/src/pages/Profile.tsx @@ -6,6 +6,11 @@ import { useToast } from '../components/Toast'; export default function Profile() { const toast = useToast(); const [name, setName] = useState(''); + const [passwords, setPasswords] = useState({ + oldPassword: '', + newPassword: '', + confirmPassword: '', + }); const { data: user, isLoading, refetch } = useQuery({ queryKey: ['me'], @@ -25,17 +30,46 @@ export default function Profile() { }, }); + const passwordMutation = useMutation({ + mutationFn: async (data: typeof passwords) => { + return api.changePassword({ + oldPassword: data.oldPassword, + newPassword: data.newPassword, + }); + }, + onSuccess: () => { + toast.success('Passwort erfolgreich geändert'); + setPasswords({ oldPassword: '', newPassword: '', confirmPassword: '' }); + }, + onError: () => { + toast.error('Fehler beim Ăndern des Passworts. Bitte Daten prĂźfen.'); + }, + }); + useEffect(() => { if (user?.name) { setName(user.name); } }, [user]); - const handleSubmit = async (e: React.FormEvent) => { + const handleProfileSubmit = async (e: React.FormEvent) => { e.preventDefault(); updateMutation.mutate(name); }; + const handlePasswordSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (passwords.newPassword !== passwords.confirmPassword) { + toast.error('Die neuen PasswĂśrter stimmen nicht Ăźberein'); + return; + } + if (passwords.newPassword.length < 6) { + toast.error('Das neue Passwort muss mindestens 6 Zeichen lang sein'); + return; + } + passwordMutation.mutate(passwords); + }; + if (isLoading) { return (