From dfa1b28632bec9a1ec23f920f95d20b4da7a6cbc Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Thu, 30 Apr 2026 03:04:15 +0300 Subject: [PATCH] feat(auth): Appwrite-backed sign-in / sign-up / forgot-password + middleware guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Server actions in lib/appwrite/auth-actions.ts: signInAction, signUpAction, forgotPasswordAction, signOutAction All use node-appwrite admin client; session secret stored as httpOnly cookie (isletmem-session). Errors localized to Turkish. - Redesigned /sign-in and /sign-up using sign-in-3 split-card layout, branded as 'İşletmem' with gradient brand panel (no external image). Removed social login buttons (email/password only for now). - /forgot-password localized; success state shows email-sent confirmation. - Auth pages redirect to /dashboard if user already has a session. - middleware.ts: * Protects /dashboard, /onboarding, /settings — redirects to /sign-in?redirect=... * Auth pages redirect logged-in users to /dashboard * Keeps legacy /login and /register redirects --- .../components/forgot-password-form-1.tsx | 112 +++--- src/app/(auth)/forgot-password/page.tsx | 11 +- src/app/(auth)/layout.tsx | 16 +- .../sign-in/components/login-form-1.tsx | 260 ++++++++------ src/app/(auth)/sign-in/page.tsx | 24 +- .../sign-up/components/signup-form-1.tsx | 337 ++++++++---------- src/app/(auth)/sign-up/page.tsx | 24 +- src/lib/appwrite/auth-actions.ts | 118 ++++++ src/middleware.ts | 73 ++-- 9 files changed, 569 insertions(+), 406 deletions(-) create mode 100644 src/lib/appwrite/auth-actions.ts 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 055a05d..c1cdfce 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 @@ -1,57 +1,87 @@ -"use client" +"use client"; -import { cn } from "@/lib/utils" -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 Link from "next/link"; +import { useActionState } from "react"; +import { ArrowLeft, Loader2, MailCheck } from "lucide-react"; + +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 { cn } from "@/lib/utils"; +import { forgotPasswordAction, initialAuthState } from "@/lib/appwrite/auth-actions"; + +export function ForgotPasswordForm1({ className, ...props }: React.ComponentProps<"div">) { + const [state, formAction, isPending] = useActionState(forgotPasswordAction, initialAuthState); -export function ForgotPasswordForm1({ - className, - ...props -}: React.ComponentProps<"div">) { return (
- Forgot your password? + Şifremi unuttum - Enter your email address and we'll send you a link to reset your password + Email adresinizi girin, sıfırlama bağlantısı gönderelim. -
-
-
-
- - -
- -
-
- Remember your password?{" "} - - Back to sign in - + {state.ok ? ( +
+
+
+

+ Bağlantı emailinize gönderildi. Gelen kutusunu kontrol edin. +

+ + + Giriş sayfasına dön +
- + ) : ( +
+
+ + +
+ + {state.error && ( +

+ {state.error} +

+ )} + + + + + + Giriş sayfasına dön + + + )}
- ) + ); } diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx index a199936..f4ada09 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/(auth)/forgot-password/page.tsx @@ -1,6 +1,7 @@ -import { ForgotPasswordForm1 } from "./components/forgot-password-form-1" -import { Logo } from "@/components/logo" -import Link from "next/link" +import Link from "next/link"; + +import { ForgotPasswordForm1 } from "./components/forgot-password-form-1"; +import { Logo } from "@/components/logo"; export default function ForgotPasswordPage() { return ( @@ -10,10 +11,10 @@ export default function ForgotPasswordPage() {
- ShadcnStore + İşletmem
- ) + ); } diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 10f920a..1bc671c 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -1,18 +1,10 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Authentication - ShadcnStore", - description: "Sign in to your account or create a new one", + title: "İşletmem — Giriş", + description: "İşletmem KovakCRM hesabınıza giriş yapın veya yeni hesap oluşturun.", }; -export default function AuthLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( -
- {children} -
- ); +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return
{children}
; } diff --git a/src/app/(auth)/sign-in/components/login-form-1.tsx b/src/app/(auth)/sign-in/components/login-form-1.tsx index 7a66ba4..0915908 100644 --- a/src/app/(auth)/sign-in/components/login-form-1.tsx +++ b/src/app/(auth)/sign-in/components/login-form-1.tsx @@ -1,127 +1,157 @@ -"use client" +"use client"; -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import { z } from "zod" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -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 Link from "next/link"; +import { useActionState } from "react"; +import { Loader2 } from "lucide-react"; -const loginFormSchema = z.object({ - email: z.string().email("Invalid email address"), - password: z.string().min(6, "Password must be at least 6 characters"), -}) +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Logo } from "@/components/logo"; +import { cn } from "@/lib/utils"; +import { initialAuthState, signInAction } from "@/lib/appwrite/auth-actions"; -type LoginFormValues = z.infer - -export function LoginForm1({ - className, - ...props -}: React.ComponentProps<"div">) { - const form = useForm({ - resolver: zodResolver(loginFormSchema), - defaultValues: { - email: "test@example.com", - password: "password", - }, - }) +export function LoginForm1({ className, ...props }: React.ComponentProps<"div">) { + const [state, formAction, isPending] = useActionState(signInAction, initialAuthState); return (
- - - Welcome back - - Enter your email below to login to your account - - - -
- -
-
- ( - - Email - - - - - - )} - /> - ( - - - - - - - - )} - /> - - - -
-
- Don't have an account?{" "} - - Sign up - -
+ + + +
+
+ +
+ +
+ İşletmem +
- - + +
+

Tekrar hoş geldiniz

+

+ Hesabınıza giriş yaparak işletmenizi yönetmeye devam edin +

+
+ +
+ + +
+ +
+
+ + + Şifremi unuttum + +
+ +
+ + {state.error && ( +

+ {state.error} +

+ )} + + + +
+ Hesabınız yok mu?{" "} + + Hesap oluştur + +
+
+ + +
-
- By clicking continue, you agree to our Terms of Service{" "} - and Privacy Policy. + +

+ Giriş yaparak{" "} + + Kullanım Şartları + {" "} + ve{" "} + + Gizlilik Politikası + + 'nı kabul etmiş olursunuz. +

+
+ ); +} + +function BrandPanel() { + return ( +
+
+
+
+ +
+
+ +
+ İşletmem +
+ +
+

+ Müşteriden faturaya, tek panelden işletmenizi yönetin. +

+

+ Müşteriler, hizmetler, takvim, görevler ve finans — hepsi tek yerde, multi-tenant ve ekibinize özel. +

+
KovakSoft tarafından
- ) + ); } diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx index a036f7c..6e7f735 100644 --- a/src/app/(auth)/sign-in/page.tsx +++ b/src/app/(auth)/sign-in/page.tsx @@ -1,19 +1,17 @@ -import { LoginForm1 } from "./components/login-form-1" -import { Logo } from "@/components/logo" -import Link from "next/link" +import { redirect } from "next/navigation"; + +import { LoginForm1 } from "./components/login-form-1"; +import { getCurrentUser } from "@/lib/appwrite/server"; + +export default async function Page() { + const user = await getCurrentUser(); + if (user) redirect("/dashboard"); -export default function Page() { return ( -
-
- -
- -
- ShadcnStore - +
+
- ) + ); } diff --git a/src/app/(auth)/sign-up/components/signup-form-1.tsx b/src/app/(auth)/sign-up/components/signup-form-1.tsx index 3bcfb15..a00ae4b 100644 --- a/src/app/(auth)/sign-up/components/signup-form-1.tsx +++ b/src/app/(auth)/sign-up/components/signup-form-1.tsx @@ -1,195 +1,168 @@ -"use client" +"use client"; -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import { z } from "zod" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -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 { Checkbox } from "@/components/ui/checkbox" +import Link from "next/link"; +import { useActionState } from "react"; +import { Loader2 } from "lucide-react"; -const signupFormSchema = 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"), - password: z.string().min(6, "Password must be at least 6 characters"), - confirmPassword: z.string().min(6, "Please confirm your password"), - terms: z.boolean().refine(val => val === true, "You must agree to the terms"), -}).refine((data) => data.password === data.confirmPassword, { - message: "Passwords don't match", - path: ["confirmPassword"], -}) +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Logo } from "@/components/logo"; +import { cn } from "@/lib/utils"; +import { initialAuthState, signUpAction } from "@/lib/appwrite/auth-actions"; -type SignupFormValues = z.infer - -export function SignupForm1({ - className, - ...props -}: React.ComponentProps<"div">) { - const form = useForm({ - resolver: zodResolver(signupFormSchema), - defaultValues: { - firstName: "", - lastName: "", - email: "", - password: "", - confirmPassword: "", - terms: false, - }, - }) - - function onSubmit(data: SignupFormValues) { - console.log("Signup attempt:", data) - // Here you would typically handle the signup - } +export function SignupForm1({ className, ...props }: React.ComponentProps<"div">) { + const [state, formAction, isPending] = useActionState(signUpAction, initialAuthState); return (
- - - Create Account - - Enter your information to create a new account - - - -
- -
-
-
- ( - - First Name - - - - - - )} - /> - ( - - Last Name - - - - - - )} - /> -
- ( - - Email - - - - - - )} - /> - ( - - Password - - - - - - )} - /> - ( - - Confirm Password - - - - - - )} - /> - ( - - - - - - I agree to the terms of service and privacy policy - - - )} - /> - + + + - -
-
- Already have an account?{" "} - - Sign in - -
+ +
+
+ +
+ +
+ İşletmem +
- - + +
+

Hesap oluşturun

+

+ Birkaç saniye içinde hesabınız hazır +

+
+ +
+ + +
+ +
+ + +
+ +
+ + +

En az 8 karakter

+
+ + {state.error && ( +

+ {state.error} +

+ )} + + + +
+ Zaten hesabınız var mı?{" "} + + Giriş yap + +
+
+ -
- By clicking continue, you agree to our Terms of Service{" "} - and Privacy Policy. + +

+ Hesap oluşturarak{" "} + + Kullanım Şartları + {" "} + ve{" "} + + Gizlilik Politikası + + 'nı kabul etmiş olursunuz. +

+
+ ); +} + +function BrandPanel() { + return ( +
+
+
+
+ +
+
+ +
+ İşletmem +
+ +
+

+ İşletmenizi büyütecek tek araç. +

+

+ Hesap oluşturduktan sonra çalışma alanınızı kuruyor, ekibinizi davet ediyor ve hemen kullanmaya başlıyorsunuz. +

+
    +
  • • Müşteri & hizmet yönetimi
  • +
  • • Görev ve takvim
  • +
  • • Finans ve fatura
  • +
+
KovakSoft tarafından
- ) + ); } diff --git a/src/app/(auth)/sign-up/page.tsx b/src/app/(auth)/sign-up/page.tsx index 8264e4e..a3f16fe 100644 --- a/src/app/(auth)/sign-up/page.tsx +++ b/src/app/(auth)/sign-up/page.tsx @@ -1,19 +1,17 @@ -import { SignupForm1 } from "./components/signup-form-1" -import { Logo } from "@/components/logo" -import Link from "next/link" +import { redirect } from "next/navigation"; + +import { SignupForm1 } from "./components/signup-form-1"; +import { getCurrentUser } from "@/lib/appwrite/server"; + +export default async function SignUpPage() { + const user = await getCurrentUser(); + if (user) redirect("/dashboard"); -export default function SignUpPage() { return ( -
-
- -
- -
- ShadcnStore - +
+
- ) + ); } diff --git a/src/lib/appwrite/auth-actions.ts b/src/lib/appwrite/auth-actions.ts new file mode 100644 index 0000000..ab57b90 --- /dev/null +++ b/src/lib/appwrite/auth-actions.ts @@ -0,0 +1,118 @@ +"use server"; + +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { AppwriteException, ID } from "node-appwrite"; + +import { APPWRITE_SESSION_COOKIE, createAdminClient, createSessionClient } from "./server"; + +export type AuthState = { + ok: boolean; + error?: string; +}; + +const initial: AuthState = { ok: false }; + +function appwriteError(e: unknown): string { + if (e instanceof AppwriteException) { + switch (e.type) { + case "user_invalid_credentials": + return "Email veya şifre hatalı."; + case "user_blocked": + return "Hesabınız engellenmiş."; + case "user_already_exists": + case "user_email_already_exists": + return "Bu email ile zaten bir hesap var."; + case "user_password_mismatch": + return "Şifreler eşleşmiyor."; + case "general_rate_limit_exceeded": + return "Çok fazla deneme. Birkaç dakika sonra tekrar deneyin."; + default: + return e.message || "Beklenmeyen bir hata oluştu."; + } + } + return "Bağlantı hatası. Tekrar deneyin."; +} + +async function setSessionCookie(secret: string, expire: string) { + (await cookies()).set(APPWRITE_SESSION_COOKIE, secret, { + path: "/", + httpOnly: true, + sameSite: "strict", + secure: process.env.NODE_ENV === "production", + expires: new Date(expire), + }); +} + +export async function signInAction(_prev: AuthState, formData: FormData): Promise { + const email = String(formData.get("email") ?? "").trim(); + const password = String(formData.get("password") ?? ""); + + if (!email || !password) { + return { ok: false, error: "Email ve şifre zorunlu." }; + } + + try { + const { account } = createAdminClient(); + const session = await account.createEmailPasswordSession(email, password); + await setSessionCookie(session.secret, session.expire); + } catch (e) { + return { ok: false, error: appwriteError(e) }; + } + + redirect("/dashboard"); +} + +export async function signUpAction(_prev: AuthState, formData: FormData): Promise { + const name = String(formData.get("name") ?? "").trim(); + const email = String(formData.get("email") ?? "").trim(); + const password = String(formData.get("password") ?? ""); + + if (!name || !email || !password) { + return { ok: false, error: "Tüm alanlar zorunlu." }; + } + if (password.length < 8) { + return { ok: false, error: "Şifre en az 8 karakter olmalı." }; + } + + try { + const { account } = createAdminClient(); + await account.create(ID.unique(), email, password, name); + const session = await account.createEmailPasswordSession(email, password); + await setSessionCookie(session.secret, session.expire); + } catch (e) { + return { ok: false, error: appwriteError(e) }; + } + + redirect("/onboarding"); +} + +export async function forgotPasswordAction( + _prev: AuthState, + formData: FormData, +): Promise { + const email = String(formData.get("email") ?? "").trim(); + if (!email) return { ok: false, error: "Email zorunlu." }; + + try { + const { account } = createAdminClient(); + const recoveryUrl = `${process.env.APP_URL ?? "http://localhost:3000"}/reset-password`; + await account.createRecovery(email, recoveryUrl); + return { ok: true }; + } catch (e) { + return { ok: false, error: appwriteError(e) }; + } +} + +export async function signOutAction() { + try { + const { account } = await createSessionClient(); + await account.deleteSession("current"); + } catch { + // ignore — cookie will be cleared anyway + } + (await cookies()).delete(APPWRITE_SESSION_COOKIE); + redirect("/sign-in"); +} + +export const initialAuthState = initial; diff --git a/src/middleware.ts b/src/middleware.ts index d2134bb..6484198 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,32 +1,55 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +const AUTH_COOKIE = "isletmem-session"; + +const PUBLIC_AUTH_PATHS = [ + "/sign-in", + "/sign-in-2", + "/sign-in-3", + "/sign-up", + "/sign-up-2", + "/sign-up-3", + "/forgot-password", + "/forgot-password-2", + "/forgot-password-3", + "/reset-password", +]; + +const PROTECTED_PREFIXES = ["/dashboard", "/onboarding", "/settings"]; -// This function can be marked `async` if using `await` inside export function middleware(request: NextRequest) { - // Add custom middleware logic here - // For example: authentication, redirects, etc. - - // Example: Redirect /login to /auth/sign-in - if (request.nextUrl.pathname === '/login') { - return NextResponse.redirect(new URL('/auth/sign-in', request.url)) + const { pathname } = request.nextUrl; + const session = request.cookies.get(AUTH_COOKIE)?.value; + + // Legacy redirects + if (pathname === "/login") { + return NextResponse.redirect(new URL("/sign-in", request.url)); } - - // Example: Redirect /register to /auth/sign-up - if (request.nextUrl.pathname === '/register') { - return NextResponse.redirect(new URL('/auth/sign-up', request.url)) + if (pathname === "/register") { + return NextResponse.redirect(new URL("/sign-up", request.url)); } - - return NextResponse.next() + + const isAuthPath = PUBLIC_AUTH_PATHS.some( + (p) => pathname === p || pathname.startsWith(`${p}/`), + ); + const isProtected = PROTECTED_PREFIXES.some( + (p) => pathname === p || pathname.startsWith(`${p}/`), + ); + + if (isProtected && !session) { + const url = new URL("/sign-in", request.url); + url.searchParams.set("redirect", pathname); + return NextResponse.redirect(url); + } + + if (isAuthPath && session) { + return NextResponse.redirect(new URL("/dashboard", request.url)); + } + + return NextResponse.next(); } -// See "Matching Paths" below to learn more export const config = { - matcher: [ - // Match all request paths except for the ones starting with: - // - api (API routes) - // - _next/static (static files) - // - _next/image (image optimization files) - // - favicon.ico (favicon file) - '/((?!api|_next/static|_next/image|favicon.ico).*)', - ], -} + matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], +};