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:
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useActionState, useEffect, useRef } 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 { updateEmailAction } from "@/lib/appwrite/profile-actions";
|
||||||
|
import { initialProfileState } from "@/lib/appwrite/profile-types";
|
||||||
|
|
||||||
|
export function EmailForm({ currentEmail }: { currentEmail: string }) {
|
||||||
|
const [state, formAction, isPending] = useActionState(updateEmailAction, initialProfileState);
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.ok) {
|
||||||
|
toast.success("Email güncellendi.");
|
||||||
|
// Clear password field after success
|
||||||
|
formRef.current?.reset();
|
||||||
|
} else if (state.error) {
|
||||||
|
toast.error(state.error);
|
||||||
|
}
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Email adresi</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Email değiştirmek için mevcut şifrenizi de girin. Yeni email ile giriş yapmaya devam edersiniz.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form ref={formRef} action={formAction} className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="email">Yeni email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
defaultValue={currentEmail}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{state.fieldErrors?.email && (
|
||||||
|
<p className="text-destructive text-xs">{state.fieldErrors.email}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="email-password">Şifre (doğrulama)</Label>
|
||||||
|
<Input
|
||||||
|
id="email-password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{state.fieldErrors?.password && (
|
||||||
|
<p className="text-destructive text-xs">{state.fieldErrors.password}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2 flex justify-end">
|
||||||
|
<Button type="submit" disabled={isPending}>
|
||||||
|
{isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Güncelleniyor...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="size-4" />
|
||||||
|
Email'i güncelle
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useActionState, useEffect, useRef } from "react";
|
||||||
|
import { KeyRound, Loader2 } 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 { updatePasswordAction } from "@/lib/appwrite/profile-actions";
|
||||||
|
import { initialProfileState } from "@/lib/appwrite/profile-types";
|
||||||
|
|
||||||
|
export function PasswordForm() {
|
||||||
|
const [state, formAction, isPending] = useActionState(
|
||||||
|
updatePasswordAction,
|
||||||
|
initialProfileState,
|
||||||
|
);
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.ok) {
|
||||||
|
toast.success("Şifre değiştirildi.");
|
||||||
|
formRef.current?.reset();
|
||||||
|
} else if (state.error) {
|
||||||
|
toast.error(state.error);
|
||||||
|
}
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Şifre</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Şifrenizi değiştirmek için mevcut şifrenizi ve yeni şifreyi iki kez girin.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form ref={formRef} action={formAction} className="grid gap-4 md:grid-cols-3">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="oldPassword">Mevcut şifre</Label>
|
||||||
|
<Input
|
||||||
|
id="oldPassword"
|
||||||
|
name="oldPassword"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{state.fieldErrors?.oldPassword && (
|
||||||
|
<p className="text-destructive text-xs">{state.fieldErrors.oldPassword}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="newPassword">Yeni şifre</Label>
|
||||||
|
<Input
|
||||||
|
id="newPassword"
|
||||||
|
name="newPassword"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
minLength={8}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{state.fieldErrors?.newPassword && (
|
||||||
|
<p className="text-destructive text-xs">{state.fieldErrors.newPassword}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="confirmPassword">Yeni şifre (tekrar)</Label>
|
||||||
|
<Input
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
minLength={8}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{state.fieldErrors?.confirmPassword && (
|
||||||
|
<p className="text-destructive text-xs">{state.fieldErrors.confirmPassword}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-3 flex justify-end">
|
||||||
|
<Button type="submit" disabled={isPending}>
|
||||||
|
{isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Güncelleniyor...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<KeyRound className="size-4" />
|
||||||
|
Şifreyi değiştir
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,207 +1,62 @@
|
|||||||
"use client"
|
import type { Metadata } from "next";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { getCurrentUser } from "@/lib/appwrite/server";
|
||||||
import { useForm } from "react-hook-form"
|
import { formatDateTime } from "@/lib/format";
|
||||||
import { z } from "zod"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
const accountFormSchema = z.object({
|
import { NameForm } from "./components/name-form";
|
||||||
firstName: z.string().min(1, "First name is required"),
|
import { EmailForm } from "./components/email-form";
|
||||||
lastName: z.string().min(1, "Last name is required"),
|
import { PasswordForm } from "./components/password-form";
|
||||||
email: z.string().email("Invalid email address"),
|
|
||||||
username: z.string().min(3, "Username must be at least 3 characters"),
|
|
||||||
currentPassword: z.string().optional(),
|
|
||||||
newPassword: z.string().optional(),
|
|
||||||
confirmPassword: z.string().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
type AccountFormValues = z.infer<typeof accountFormSchema>
|
export const metadata: Metadata = {
|
||||||
|
title: "İşletmem — Profil",
|
||||||
|
};
|
||||||
|
|
||||||
export default function AccountSettings() {
|
export default async function AccountSettingsPage() {
|
||||||
const form = useForm<AccountFormValues>({
|
const user = await getCurrentUser();
|
||||||
resolver: zodResolver(accountFormSchema),
|
if (!user) redirect("/sign-in");
|
||||||
defaultValues: {
|
|
||||||
firstName: "",
|
|
||||||
lastName: "",
|
|
||||||
email: "",
|
|
||||||
username: "",
|
|
||||||
currentPassword: "",
|
|
||||||
newPassword: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function onSubmit(data: AccountFormValues) {
|
|
||||||
console.log("Form submitted:", data)
|
|
||||||
// Here you would typically save the data
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 px-4 lg:px-6">
|
<div className="flex-1 space-y-6 px-6 pt-0">
|
||||||
<div>
|
<div className="flex flex-col gap-1">
|
||||||
<h1 className="text-3xl font-bold">Account Settings</h1>
|
<p className="text-muted-foreground text-sm">Profil ayarları</p>
|
||||||
<p className="text-muted-foreground">
|
<h1 className="text-2xl font-bold tracking-tight">{user.name || user.email}</h1>
|
||||||
Manage your account settings and preferences.
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Hesap bilgilerinizi ve şifrenizi buradan yönetin.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Personal Information</CardTitle>
|
<CardTitle>Hesap bilgileri</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>Kayıt tarihi ve hesap durumu</CardDescription>
|
||||||
Update your personal information that will be displayed on your profile.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<dl className="grid gap-4 text-sm md:grid-cols-2">
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="firstName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>First Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Enter your first name" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="lastName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Last Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Enter your last name" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email Address</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="email" placeholder="Enter your email" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Enter your username" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Change Password</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Update your password to keep your account secure.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="currentPassword"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Current Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Enter current password" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="newPassword"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>New Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Enter new password" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="confirmPassword"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Confirm New Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Confirm new password" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Danger Zone</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Irreversible and destructive actions.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<Separator />
|
|
||||||
<div className="flex flex-wrap gap-2 items-center justify-between">
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold">Delete Account</h4>
|
<dt className="text-muted-foreground text-xs uppercase">Hesap ID</dt>
|
||||||
<p className="text-sm text-muted-foreground">
|
<dd className="mt-1 font-mono text-xs">{user.$id}</dd>
|
||||||
Permanently delete your account and all associated data.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Button variant="destructive" type="button" className="cursor-pointer">
|
<div>
|
||||||
Delete Account
|
<dt className="text-muted-foreground text-xs uppercase">Kayıt tarihi</dt>
|
||||||
</Button>
|
<dd className="mt-1">{formatDateTime(user.registration)}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-muted-foreground text-xs uppercase">Email doğrulanmış</dt>
|
||||||
|
<dd className="mt-1">{user.emailVerification ? "Evet" : "Hayır"}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-muted-foreground text-xs uppercase">İki faktör (2FA)</dt>
|
||||||
|
<dd className="mt-1">{user.mfa ? "Açık" : "Kapalı"}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
<NameForm currentName={user.name || ""} />
|
||||||
<Button type="submit" className="cursor-pointer">Save Changes</Button>
|
<EmailForm currentEmail={user.email} />
|
||||||
<Button variant="outline" type="reset" className="cursor-pointer">Cancel</Button>
|
<PasswordForm />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
);
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { AppwriteException } from "node-appwrite";
|
||||||
|
|
||||||
|
import { logAudit } from "./audit";
|
||||||
|
import { createSessionClient } from "./server";
|
||||||
|
import { getActiveTenantId } from "./tenant";
|
||||||
|
import type { ProfileState } from "./profile-types";
|
||||||
|
|
||||||
|
function appwriteError(e: unknown): string {
|
||||||
|
if (e instanceof AppwriteException) {
|
||||||
|
if (e.type === "user_invalid_credentials") return "Şifre hatalı.";
|
||||||
|
if (e.type === "user_password_mismatch") return "Şifreler eşleşmiyor.";
|
||||||
|
if (e.type === "user_email_already_exists")
|
||||||
|
return "Bu email zaten başka bir hesapta kullanımda.";
|
||||||
|
if (e.type === "user_password_recently_used")
|
||||||
|
return "Bu şifreyi yakın zamanda kullandınız, başka bir şifre seçin.";
|
||||||
|
if (e.type === "general_rate_limit_exceeded")
|
||||||
|
return "Çok fazla deneme. Birkaç dakika sonra tekrar deneyin.";
|
||||||
|
return e.message || "Beklenmeyen hata.";
|
||||||
|
}
|
||||||
|
return "Bağlantı hatası. Tekrar deneyin.";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function audit(action: "update", entityType: string, changes: Record<string, unknown>) {
|
||||||
|
try {
|
||||||
|
const session = await createSessionClient();
|
||||||
|
const user = await session.account.get();
|
||||||
|
const tenantId = (await getActiveTenantId()) ?? "global";
|
||||||
|
await logAudit({
|
||||||
|
tenantId,
|
||||||
|
userId: user.$id,
|
||||||
|
action,
|
||||||
|
entityType,
|
||||||
|
entityId: user.$id,
|
||||||
|
changes,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateNameAction(
|
||||||
|
_prev: ProfileState,
|
||||||
|
formData: FormData,
|
||||||
|
): Promise<ProfileState> {
|
||||||
|
const name = String(formData.get("name") ?? "").trim();
|
||||||
|
if (!name) {
|
||||||
|
return { ok: false, error: "İsim boş olamaz.", fieldErrors: { name: "Zorunlu" } };
|
||||||
|
}
|
||||||
|
if (name.length > 128) {
|
||||||
|
return { ok: false, error: "İsim çok uzun." };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { account } = await createSessionClient();
|
||||||
|
await account.updateName(name);
|
||||||
|
await audit("update", "user_name", { name });
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: appwriteError(e) };
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath("/", "layout");
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateEmailAction(
|
||||||
|
_prev: ProfileState,
|
||||||
|
formData: FormData,
|
||||||
|
): Promise<ProfileState> {
|
||||||
|
const email = String(formData.get("email") ?? "").trim().toLowerCase();
|
||||||
|
const password = String(formData.get("password") ?? "");
|
||||||
|
|
||||||
|
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Geçerli bir email girin.",
|
||||||
|
fieldErrors: { email: "Geçersiz" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Doğrulama için şifrenizi girin.",
|
||||||
|
fieldErrors: { password: "Zorunlu" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { account } = await createSessionClient();
|
||||||
|
await account.updateEmail(email, password);
|
||||||
|
await audit("update", "user_email", { email });
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: appwriteError(e) };
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath("/", "layout");
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePasswordAction(
|
||||||
|
_prev: ProfileState,
|
||||||
|
formData: FormData,
|
||||||
|
): Promise<ProfileState> {
|
||||||
|
const oldPassword = String(formData.get("oldPassword") ?? "");
|
||||||
|
const newPassword = String(formData.get("newPassword") ?? "");
|
||||||
|
const confirmPassword = String(formData.get("confirmPassword") ?? "");
|
||||||
|
|
||||||
|
if (!oldPassword) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Mevcut şifrenizi girin.",
|
||||||
|
fieldErrors: { oldPassword: "Zorunlu" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (newPassword.length < 8) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Yeni şifre en az 8 karakter olmalı.",
|
||||||
|
fieldErrors: { newPassword: "En az 8 karakter" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Şifreler eşleşmiyor.",
|
||||||
|
fieldErrors: { confirmPassword: "Eşleşmiyor" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { account } = await createSessionClient();
|
||||||
|
await account.updatePassword(newPassword, oldPassword);
|
||||||
|
await audit("update", "user_password", { changed: true });
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: appwriteError(e) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export type ProfileState = {
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
fieldErrors?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialProfileState: ProfileState = { ok: false };
|
||||||
Reference in New Issue
Block a user