diff --git a/.phase8-state.json b/.phase8-state.json index 7ec3b67..d6f164e 100644 --- a/.phase8-state.json +++ b/.phase8-state.json @@ -1,9 +1,10 @@ { "completed_features": [], - "current_feature": "customer-csv-import", + "current_feature": "account-deletion", "started_at": "2026-05-23T05:49:48.673340", "attempted_features": [ "recent-activity-widget", - "time-entry-bulk-actions" + "time-entry-bulk-actions", + "customer-csv-import" ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index cf36807..1dc5ce4 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -947,3 +947,22 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `05:55:08` **INFO** Committed feature customer-csv-import +- `05:55:08` **INFO** Pushed: rc=0 + +## Phase-3 Feature: account-deletion (2026-05-23 05:55:08) + +- `05:55:08` **INFO** Description: User kann eigenes Account löschen (Profile-Page) +- `05:55:08` **INFO** Generating apps/api/src/routes/users.ts (ERWEITERT — behalte alles. Füge DELETE /me (body: {password}): verify …) +- `05:55:56` **INFO** wrote 5072 chars in 47.5s (attempt 1) +- `05:55:56` **INFO** Generating apps/web/src/pages/Profile.tsx (ERWEITERT — behalte Name + Password Cards. Füge dritte Card 'Gefahrenz…) +- `05:57:25` **INFO** wrote 10913 chars in 89.1s (attempt 1) +- `05:57:25` **INFO** Running tsc --noEmit on api… +- `05:57:27` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/api/src/routes/users.ts b/apps/api/src/routes/users.ts index 1710c0a..90a8eb9 100644 --- a/apps/api/src/routes/users.ts +++ b/apps/api/src/routes/users.ts @@ -23,6 +23,10 @@ const PasswordChangeSchema = z.object({ newPassword: z.string().min(8) }) +const UserDeleteSchema = z.object({ + password: z.string() +}) + export default async function userRoutes(fastify: FastifyInstance) { fastify.addHook("preHandler", async (request, reply) => { try { @@ -110,6 +114,36 @@ export default async function userRoutes(fastify: FastifyInstance) { return reply.send({ message: "Password updated successfully" }) }) + fastify.delete("/me", 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 = UserDeleteSchema.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.password) + if (!isPasswordCorrect) { + return reply.code(401).send({ message: "Incorrect password" }) + } + + await db + .delete(users) + .where(eq(users.id, userId)) + + return reply.send({ message: "User account deleted 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 be94727..c97547c 100644 --- a/apps/web/src/pages/Profile.tsx +++ b/apps/web/src/pages/Profile.tsx @@ -1,16 +1,20 @@ import React, { useState, useEffect } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; import { api } from '../lib/api'; import { useToast } from '../components/Toast'; export default function Profile() { const toast = useToast(); + const navigate = useNavigate(); const [name, setName] = useState(''); const [passwords, setPasswords] = useState({ oldPassword: '', newPassword: '', confirmPassword: '', }); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [deletePassword, setDeletePassword] = useState(''); const { data: user, isLoading, refetch } = useQuery({ queryKey: ['me'], @@ -46,6 +50,19 @@ export default function Profile() { }, }); + const deleteMutation = useMutation({ + mutationFn: async (password: string) => { + return api.deleteAccount(password); + }, + onSuccess: () => { + toast.success('Account wurde dauerhaft gelöscht'); + navigate({ to: '/login' }); + }, + onError: () => { + toast.error('Fehler beim Löschen des Accounts. Passwort falsch?'); + }, + }); + useEffect(() => { if (user?.name) { setName(user.name); @@ -70,6 +87,15 @@ export default function Profile() { passwordMutation.mutate(passwords); }; + const handleDeleteSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!deletePassword) { + toast.error('Bitte gib dein Passwort ein'); + return; + } + deleteMutation.mutate(deletePassword); + }; + if (isLoading) { return (
@@ -120,25 +146,19 @@ export default function Profile() {
- - {user.role.toUpperCase()} + + {user.role}
- -
- -
+ @@ -157,8 +177,7 @@ export default function Profile() { value={passwords.oldPassword} onChange={(e) => setPasswords({ ...passwords, oldPassword: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all" - placeholder="••••••••" - required + placeholder="Altes Passwort" /> @@ -170,8 +189,7 @@ export default function Profile() { value={passwords.newPassword} onChange={(e) => setPasswords({ ...passwords, newPassword: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all" - placeholder="••••••••" - required + placeholder="Neues Passwort" />
@@ -181,24 +199,83 @@ export default function Profile() { value={passwords.confirmPassword} onChange={(e) => setPasswords({ ...passwords, confirmPassword: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all" - placeholder="••••••••" - required + placeholder="Bestätigen" />
- -
- -
+ + + {/* Danger Zone Card */} +
+
+

Gefahrenzone

+
+
+

+ Once you delete your account, there is no going back. Please be certain. +

+ +
+
+ + {/* Delete Confirmation Modal */} + {isDeleteModalOpen && ( +
+
+

Account wirklich löschen?

+

+ Diese Aktion kann nicht rückgängig gemacht werden. Alle deine Daten werden dauerhaft entfernt. +

+ +
+
+ + setDeletePassword(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500 transition-all" + placeholder="Dein Passwort" + /> +
+ +
+ + +
+
+
+
+ )} ); } \ No newline at end of file