diff --git a/src/app/(dashboard)/settings/account/components/email-form.tsx b/src/app/(dashboard)/settings/account/components/email-form.tsx new file mode 100644 index 0000000..6b33bc8 --- /dev/null +++ b/src/app/(dashboard)/settings/account/components/email-form.tsx @@ -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(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 ( + + + Email adresi + + Email değiştirmek için mevcut şifrenizi de girin. Yeni email ile giriş yapmaya devam edersiniz. + + + +
+
+ + + {state.fieldErrors?.email && ( +

{state.fieldErrors.email}

+ )} +
+
+ + + {state.fieldErrors?.password && ( +

{state.fieldErrors.password}

+ )} +
+
+ +
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/settings/account/components/name-form.tsx b/src/app/(dashboard)/settings/account/components/name-form.tsx new file mode 100644 index 0000000..8861ea0 --- /dev/null +++ b/src/app/(dashboard)/settings/account/components/name-form.tsx @@ -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 ( + + + Görünür isim + + Header'da, davetlerde ve takım listesinde görünecek isim. + + + +
+
+ + + {state.fieldErrors?.name && ( +

{state.fieldErrors.name}

+ )} +
+ +
+
+
+ ); +} diff --git a/src/app/(dashboard)/settings/account/components/password-form.tsx b/src/app/(dashboard)/settings/account/components/password-form.tsx new file mode 100644 index 0000000..8d5e84e --- /dev/null +++ b/src/app/(dashboard)/settings/account/components/password-form.tsx @@ -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(null); + + useEffect(() => { + if (state.ok) { + toast.success("Şifre değiştirildi."); + formRef.current?.reset(); + } else if (state.error) { + toast.error(state.error); + } + }, [state]); + + return ( + + + Şifre + + Şifrenizi değiştirmek için mevcut şifrenizi ve yeni şifreyi iki kez girin. + + + +
+
+ + + {state.fieldErrors?.oldPassword && ( +

{state.fieldErrors.oldPassword}

+ )} +
+
+ + + {state.fieldErrors?.newPassword && ( +

{state.fieldErrors.newPassword}

+ )} +
+
+ + + {state.fieldErrors?.confirmPassword && ( +

{state.fieldErrors.confirmPassword}

+ )} +
+
+ +
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/settings/account/page.tsx b/src/app/(dashboard)/settings/account/page.tsx index 193ad0a..652f7d0 100644 --- a/src/app/(dashboard)/settings/account/page.tsx +++ b/src/app/(dashboard)/settings/account/page.tsx @@ -1,207 +1,62 @@ -"use client" +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import { z } from "zod" -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" +import { getCurrentUser } from "@/lib/appwrite/server"; +import { formatDateTime } from "@/lib/format"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -const accountFormSchema = z.object({ - firstName: z.string().min(1, "First name is required"), - lastName: z.string().min(1, "Last name is required"), - 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(), -}) +import { NameForm } from "./components/name-form"; +import { EmailForm } from "./components/email-form"; +import { PasswordForm } from "./components/password-form"; -type AccountFormValues = z.infer +export const metadata: Metadata = { + title: "İşletmem — Profil", +}; -export default function AccountSettings() { - const form = useForm({ - resolver: zodResolver(accountFormSchema), - defaultValues: { - firstName: "", - lastName: "", - email: "", - username: "", - currentPassword: "", - newPassword: "", - confirmPassword: "", - }, - }) - - function onSubmit(data: AccountFormValues) { - console.log("Form submitted:", data) - // Here you would typically save the data - } +export default async function AccountSettingsPage() { + const user = await getCurrentUser(); + if (!user) redirect("/sign-in"); return ( -
-
-

Account Settings

-

- Manage your account settings and preferences. -

-
- -
- - - - Personal Information - - Update your personal information that will be displayed on your profile. - - - -
- ( - - First Name - - - - - - )} - /> - ( - - Last Name - - - - - - )} - /> -
- ( - - Email Address - - - - - - )} - /> - ( - - Username - - - - - - )} - /> -
-
- - - - Change Password - - Update your password to keep your account secure. - - - - ( - - Current Password - - - - - - )} - /> - ( - - New Password - - - - - - )} - /> - ( - - Confirm New Password - - - - - - )} - /> - - - - - - Danger Zone - - Irreversible and destructive actions. - - - - -
-
-

Delete Account

-

- Permanently delete your account and all associated data. -

-
- -
-
-
- -
- - -
-
- +
+
+

Profil ayarları

+

{user.name || user.email}

+

+ Hesap bilgilerinizi ve şifrenizi buradan yönetin. +

- ) + + + + Hesap bilgileri + Kayıt tarihi ve hesap durumu + + +
+
+
Hesap ID
+
{user.$id}
+
+
+
Kayıt tarihi
+
{formatDateTime(user.registration)}
+
+
+
Email doğrulanmış
+
{user.emailVerification ? "Evet" : "Hayır"}
+
+
+
İki faktör (2FA)
+
{user.mfa ? "Açık" : "Kapalı"}
+
+
+
+
+ + + + +
+ ); } diff --git a/src/lib/appwrite/profile-actions.ts b/src/lib/appwrite/profile-actions.ts new file mode 100644 index 0000000..f4d33d4 --- /dev/null +++ b/src/lib/appwrite/profile-actions.ts @@ -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) { + 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 { + 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 { + 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 { + 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 }; +} diff --git a/src/lib/appwrite/profile-types.ts b/src/lib/appwrite/profile-types.ts new file mode 100644 index 0000000..b86f3cf --- /dev/null +++ b/src/lib/appwrite/profile-types.ts @@ -0,0 +1,7 @@ +export type ProfileState = { + ok: boolean; + error?: string; + fieldErrors?: Record; +}; + +export const initialProfileState: ProfileState = { ok: false };