feat(profile): /settings/account — name, email, password

Replaces template's mock account form with real Appwrite-backed actions.

Server actions (lib/appwrite/profile-actions.ts):
- updateNameAction: account.updateName via session SDK; revalidates layout
  so the new name shows in sidebar/header right away.
- updateEmailAction: account.updateEmail (requires current password as
  Appwrite confirmation). Maps user_email_already_exists to a friendly
  Turkish message.
- updatePasswordAction: account.updatePassword(new, old). Validates
  old != empty, new >= 8 chars, new === confirm. Maps
  user_password_recently_used / user_password_mismatch.
- All audit-logged with entityType user_name / user_email / user_password
  and tenantId of the user's currently-active workspace (or 'global' if
  none). Audit failures swallowed.

UI:
- /settings/account is now an async server page that pulls the user via
  getCurrentUser, renders an account info card ($id, registration date,
  email verification, MFA), then 3 separate small forms — one card each
  — for name, email, password. Each form clears its own state and gives
  toast feedback independently.
- Removed the template's react-hook-form-based mock page.

Side note: skipped email-verification flow + MFA setup for later.
This commit is contained in:
kovakmedya
2026-04-30 06:47:53 +03:00
parent fc091b9e0d
commit 89d456fc76
6 changed files with 441 additions and 199 deletions
@@ -0,0 +1,56 @@
"use client";
import { useActionState, useEffect } from "react";
import { Loader2, Save } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { updateNameAction } from "@/lib/appwrite/profile-actions";
import { initialProfileState } from "@/lib/appwrite/profile-types";
export function NameForm({ currentName }: { currentName: string }) {
const [state, formAction, isPending] = useActionState(updateNameAction, initialProfileState);
useEffect(() => {
if (state.ok) toast.success("İsim güncellendi.");
else if (state.error) toast.error(state.error);
}, [state]);
return (
<Card>
<CardHeader>
<CardTitle>Görünür isim</CardTitle>
<CardDescription>
Header'da, davetlerde ve takım listesinde görünecek isim.
</CardDescription>
</CardHeader>
<CardContent>
<form action={formAction} className="grid gap-4 md:grid-cols-[1fr_auto] md:items-end">
<div className="grid gap-2">
<Label htmlFor="name">İsim</Label>
<Input id="name" name="name" defaultValue={currentName} required maxLength={128} />
{state.fieldErrors?.name && (
<p className="text-destructive text-xs">{state.fieldErrors.name}</p>
)}
</div>
<Button type="submit" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />
Kaydediliyor...
</>
) : (
<>
<Save className="size-4" />
Kaydet
</>
)}
</Button>
</form>
</CardContent>
</Card>
);
}