diff --git a/src/app/(auth)/forgot-password/components/forgot-password-form-1.tsx b/src/app/(auth)/forgot-password/components/forgot-password-form-1.tsx
index 7f3ff9e..da0fa5f 100644
--- a/src/app/(auth)/forgot-password/components/forgot-password-form-1.tsx
+++ b/src/app/(auth)/forgot-password/components/forgot-password-form-1.tsx
@@ -9,11 +9,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
-import { forgotPasswordAction } from "@/lib/appwrite/auth-actions";
+import { requestPasswordResetAction } from "@/lib/appwrite/password-reset-actions";
import { initialAuthState } from "@/lib/appwrite/auth-types";
export function ForgotPasswordForm1({ className, ...props }: React.ComponentProps<"div">) {
- const [state, formAction, isPending] = useActionState(forgotPasswordAction, initialAuthState);
+ const [state, formAction, isPending] = useActionState(requestPasswordResetAction, initialAuthState);
return (
@@ -31,7 +31,7 @@ export function ForgotPasswordForm1({ className, ...props }: React.ComponentProp
- Bağlantı emailinize gönderildi. Gelen kutusunu kontrol edin.
+ Sıfırlama kodunuz e-posta adresinize gönderildi. Kodu girerek şifrenizi yenileyebilirsiniz.
{
+ token: string;
+}
+
+export function ResetPasswordForm({ token, className, ...props }: Props) {
+ const [state, formAction, isPending] = useActionState(resetPasswordAction, initialAuthState);
+
+ return (
+
+
+
+
+
+
+ Yeni şifre belirle
+
+ Kod doğrulandı. Yeni şifrenizi girin.
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx
new file mode 100644
index 0000000..e7e4950
--- /dev/null
+++ b/src/app/(auth)/reset-password/page.tsx
@@ -0,0 +1,58 @@
+import Link from "next/link";
+import { XCircle } from "lucide-react";
+
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { verifyResetToken } from "@/lib/appwrite/password-reset-actions";
+import { ResetPasswordForm } from "./components/reset-password-form";
+
+interface Props {
+ searchParams: Promise<{ token?: string }>;
+}
+
+export default async function ResetPasswordPage({ searchParams }: Props) {
+ const { token } = await searchParams;
+
+ if (!token) {
+ return ;
+ }
+
+ const { valid } = await verifyResetToken(token);
+
+ if (!valid) {
+ return ;
+ }
+
+ return (
+
+ );
+}
+
+function InvalidToken({ message }: { message: string }) {
+ return (
+
+
+
+
+
+
+
+ Geçersiz kod
+
+
+ {message}
+
+ Yeni kod talep et
+
+
+
+
+
+ );
+}
diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx
index 526efbb..bfd0256 100644
--- a/src/app/(auth)/sign-in/page.tsx
+++ b/src/app/(auth)/sign-in/page.tsx
@@ -6,15 +6,20 @@ import { getCurrentUser } from "@/lib/appwrite/server";
export default async function Page({
searchParams,
}: {
- searchParams: Promise<{ invite?: string }>;
+ searchParams: Promise<{ invite?: string; reset?: string }>;
}) {
- const { invite } = await searchParams;
+ const { invite, reset } = await searchParams;
const user = await getCurrentUser();
if (user) redirect(invite ? `/d/${invite}` : "/dashboard");
return (
+ {reset === "success" && (
+
+ Şifreniz güncellendi. Giriş yapabilirsiniz.
+
+ )}
diff --git a/src/lib/appwrite/password-reset-actions.ts b/src/lib/appwrite/password-reset-actions.ts
new file mode 100644
index 0000000..03c998a
--- /dev/null
+++ b/src/lib/appwrite/password-reset-actions.ts
@@ -0,0 +1,135 @@
+"use server";
+
+import { createHash, randomBytes } from "crypto";
+import { ID, Query } from "node-appwrite";
+import { cookies } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { createAdminClient } from "./server";
+import { DATABASE_ID, TABLES } from "./schema";
+import type { AuthState } from "./auth-types";
+
+const TOKEN_EXPIRY_MS = 15 * 60 * 1000;
+const RESET_SESSION_COOKIE = "pw_reset";
+
+function generateToken(): { plain: string; hash: string } {
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+ const bytes = randomBytes(8);
+ const plain = Array.from(bytes)
+ .map((b) => chars[b % chars.length])
+ .join("");
+ const hash = createHash("sha256").update(plain).digest("hex");
+ return { plain, hash };
+}
+
+export async function requestPasswordResetAction(
+ _prev: AuthState,
+ formData: FormData,
+): Promise {
+ const email = String(formData.get("email") ?? "").trim().toLowerCase();
+ if (!email) return { ok: false, error: "Email zorunlu." };
+
+ try {
+ const { tablesDB, users, messaging } = createAdminClient();
+
+ const found = await users.list([Query.equal("email", email), Query.limit(1)]);
+ // Kullanıcı yoksa hata vermiyoruz — timing attack önlemi
+ if (found.total === 0) return { ok: true };
+
+ const user = found.users[0];
+ const { plain, hash } = generateToken();
+ const expiresAt = new Date(Date.now() + TOKEN_EXPIRY_MS).toISOString();
+
+ await tablesDB.createRow(DATABASE_ID, TABLES.passwordResets, ID.unique(), {
+ email,
+ userId: user.$id,
+ tokenHash: hash,
+ expiresAt,
+ });
+
+ const appUrl = process.env.APP_URL?.replace(/\/$/, "") ?? "http://localhost:3000";
+ const resetLink = `${appUrl}/reset-password?token=${plain}`;
+
+ await messaging.createEmail(
+ ID.unique(),
+ "Şifre Sıfırlama Kodunuz",
+ `Merhaba,
+Şifre sıfırlama talebiniz alındı. Aşağıdaki kodu kullanın:
+${plain}
+Veya doğrudan linke tıklayın:
+${resetLink}
+Bu kod 15 dakika geçerlidir. Bu talebi siz yapmadıysanız bu e-postayı dikkate almayın.
`,
+ [],
+ [user.$id],
+ [],
+ );
+
+ return { ok: true };
+ } catch {
+ return { ok: false, error: "Bir hata oluştu. Tekrar deneyin." };
+ }
+}
+
+export async function verifyResetToken(
+ token: string,
+): Promise<{ valid: boolean; tokenId?: string; userId?: string }> {
+ if (!token) return { valid: false };
+
+ try {
+ const hash = createHash("sha256").update(token.toUpperCase().trim()).digest("hex");
+ const { tablesDB } = createAdminClient();
+
+ const result = await tablesDB.listRows({
+ databaseId: DATABASE_ID,
+ tableId: TABLES.passwordResets,
+ queries: [
+ Query.equal("tokenHash", hash),
+ Query.greaterThan("expiresAt", new Date().toISOString()),
+ Query.limit(1),
+ ],
+ });
+
+ if (result.total === 0) return { valid: false };
+
+ const row = result.rows[0] as { $id: string; userId: string; usedAt?: string };
+ if (row.usedAt) return { valid: false };
+
+ return { valid: true, tokenId: row.$id, userId: row.userId };
+ } catch {
+ return { valid: false };
+ }
+}
+
+export async function resetPasswordAction(
+ _prev: AuthState,
+ formData: FormData,
+): Promise {
+ const token = String(formData.get("token") ?? "").trim();
+ const password = String(formData.get("password") ?? "");
+ const confirmPassword = String(formData.get("confirmPassword") ?? "");
+
+ if (!token || !password) return { ok: false, error: "Tüm alanlar zorunlu." };
+ if (password.length < 8) return { ok: false, error: "Şifre en az 8 karakter olmalı." };
+ if (password !== confirmPassword) return { ok: false, error: "Şifreler eşleşmiyor." };
+
+ const { valid, tokenId, userId } = await verifyResetToken(token);
+ if (!valid || !tokenId || !userId) {
+ return { ok: false, error: "Kod geçersiz veya süresi dolmuş." };
+ }
+
+ try {
+ const { tablesDB, users } = createAdminClient();
+
+ await users.updatePassword(userId, password);
+
+ await tablesDB.updateRow(DATABASE_ID, TABLES.passwordResets, tokenId, {
+ usedAt: new Date().toISOString(),
+ });
+
+ (await cookies()).delete(RESET_SESSION_COOKIE);
+ } catch {
+ return { ok: false, error: "Şifre güncellenemedi. Tekrar deneyin." };
+ }
+
+ redirect("/sign-in?reset=success");
+}
diff --git a/src/lib/appwrite/schema.ts b/src/lib/appwrite/schema.ts
index 14021a5..98fcbe5 100644
--- a/src/lib/appwrite/schema.ts
+++ b/src/lib/appwrite/schema.ts
@@ -26,6 +26,7 @@ export const TABLES = {
savedCards: "saved_cards",
leads: "leads",
leadActivities: "lead_activities",
+ passwordResets: "password_resets",
} as const;
export type TableId = (typeof TABLES)[keyof typeof TABLES];
diff --git a/src/lib/appwrite/server.ts b/src/lib/appwrite/server.ts
index c4282d4..0afbc71 100644
--- a/src/lib/appwrite/server.ts
+++ b/src/lib/appwrite/server.ts
@@ -5,6 +5,7 @@ import {
Account,
Client,
Databases,
+ Messaging,
Storage,
TablesDB,
Teams,
@@ -40,6 +41,7 @@ export function createAdminClient() {
databases: new Databases(client),
tablesDB: new TablesDB(client),
storage: new Storage(client),
+ messaging: new Messaging(client),
};
}