feat(auth): Appwrite-backed sign-in / sign-up / forgot-password + middleware guard

- 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
This commit is contained in:
kovakmedya
2026-04-30 03:04:15 +03:00
parent 6dd4f9e9c3
commit dfa1b28632
9 changed files with 569 additions and 406 deletions
@@ -1,57 +1,87 @@
"use client" "use client";
import { cn } from "@/lib/utils" import Link from "next/link";
import { Button } from "@/components/ui/button" import { useActionState } from "react";
import { import { ArrowLeft, Loader2, MailCheck } from "lucide-react";
Card,
CardContent, import { Button } from "@/components/ui/button";
CardDescription, import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
CardHeader, import { Input } from "@/components/ui/input";
CardTitle, import { Label } from "@/components/ui/label";
} from "@/components/ui/card" import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input" import { forgotPasswordAction, initialAuthState } from "@/lib/appwrite/auth-actions";
import { Label } from "@/components/ui/label"
export function ForgotPasswordForm1({ className, ...props }: React.ComponentProps<"div">) {
const [state, formAction, isPending] = useActionState(forgotPasswordAction, initialAuthState);
export function ForgotPasswordForm1({
className,
...props
}: React.ComponentProps<"div">) {
return ( return (
<div className={cn("flex flex-col gap-6", className)} {...props}> <div className={cn("flex flex-col gap-6", className)} {...props}>
<Card> <Card>
<CardHeader className="text-center"> <CardHeader className="text-center">
<CardTitle className="text-xl">Forgot your password?</CardTitle> <CardTitle className="text-xl">Şifremi unuttum</CardTitle>
<CardDescription> <CardDescription>
Enter your email address and we&apos;ll send you a link to reset your password Email adresinizi girin, sıfırlama bağlantısı gönderelim.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form> {state.ok ? (
<div className="grid gap-6"> <div className="flex flex-col items-center gap-3 py-4 text-center">
<div className="grid gap-6"> <div className="bg-primary/10 text-primary flex size-12 items-center justify-center rounded-full">
<div className="grid gap-3"> <MailCheck className="size-6" />
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<Button type="submit" className="w-full cursor-pointer">
Send Reset Link
</Button>
</div>
<div className="text-center text-sm">
Remember your password?{" "}
<a href="/auth/sign-in" className="underline underline-offset-4">
Back to sign in
</a>
</div> </div>
<p className="text-sm">
Bağlantı emailinize gönderildi. Gelen kutusunu kontrol edin.
</p>
<Link
href="/sign-in"
className="text-muted-foreground hover:text-foreground mt-2 flex items-center gap-1 text-sm underline-offset-4 hover:underline"
>
<ArrowLeft className="size-3.5" />
Giriş sayfasına dön
</Link>
</div> </div>
</form> ) : (
<form action={formAction} className="flex flex-col gap-4">
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="ornek@firma.com"
autoComplete="email"
required
/>
</div>
{state.error && (
<p className="text-destructive text-sm text-center" role="alert">
{state.error}
</p>
)}
<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />
Gönderiliyor...
</>
) : (
"Sıfırlama bağlantısı gönder"
)}
</Button>
<Link
href="/sign-in"
className="text-muted-foreground hover:text-foreground flex items-center justify-center gap-1 text-sm underline-offset-4 hover:underline"
>
<ArrowLeft className="size-3.5" />
Giriş sayfasına dön
</Link>
</form>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
) );
} }
+6 -5
View File
@@ -1,6 +1,7 @@
import { ForgotPasswordForm1 } from "./components/forgot-password-form-1" import Link from "next/link";
import { Logo } from "@/components/logo"
import Link from "next/link" import { ForgotPasswordForm1 } from "./components/forgot-password-form-1";
import { Logo } from "@/components/logo";
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
return ( return (
@@ -10,10 +11,10 @@ export default function ForgotPasswordPage() {
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md"> <div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
<Logo size={24} /> <Logo size={24} />
</div> </div>
ShadcnStore <span className="text-lg font-semibold">İşletmem</span>
</Link> </Link>
<ForgotPasswordForm1 /> <ForgotPasswordForm1 />
</div> </div>
</div> </div>
) );
} }
+4 -12
View File
@@ -1,18 +1,10 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Authentication - ShadcnStore", title: "İşletmem — Giriş",
description: "Sign in to your account or create a new one", description: "İşletmem KovakCRM hesabınıza giriş yapın veya yeni hesap oluşturun.",
}; };
export default function AuthLayout({ export default function AuthLayout({ children }: { children: React.ReactNode }) {
children, return <div className="min-h-screen bg-background">{children}</div>;
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-background">
{children}
</div>
);
} }
+145 -115
View File
@@ -1,127 +1,157 @@
"use client" "use client";
import { zodResolver } from "@hookform/resolvers/zod" import Link from "next/link";
import { useForm } from "react-hook-form" import { useActionState } from "react";
import { z } from "zod" import { Loader2 } from "lucide-react";
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"
const loginFormSchema = z.object({ import { Button } from "@/components/ui/button";
email: z.string().email("Invalid email address"), import { Card, CardContent } from "@/components/ui/card";
password: z.string().min(6, "Password must be at least 6 characters"), 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<typeof loginFormSchema> export function LoginForm1({ className, ...props }: React.ComponentProps<"div">) {
const [state, formAction, isPending] = useActionState(signInAction, initialAuthState);
export function LoginForm1({
className,
...props
}: React.ComponentProps<"div">) {
const form = useForm<LoginFormValues>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
email: "test@example.com",
password: "password",
},
})
return ( return (
<div className={cn("flex flex-col gap-6", className)} {...props}> <div className={cn("flex flex-col gap-6", className)} {...props}>
<Card> <Card className="overflow-hidden p-0">
<CardHeader className="text-center"> <CardContent className="grid p-0 md:grid-cols-2">
<CardTitle className="text-xl">Welcome back</CardTitle> <form action={formAction} className="p-6 md:p-10">
<CardDescription> <div className="flex flex-col gap-6">
Enter your email below to login to your account <div className="flex justify-center">
</CardDescription> <Link href="/" className="flex items-center gap-2 font-medium">
</CardHeader> <div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
<CardContent> <Logo size={22} />
<Form {...form}> </div>
<form action="/"> <span className="text-xl font-semibold">İşletmem</span>
<div className="grid gap-6"> </Link>
<div className="grid gap-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="test@example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormLabel>Password</FormLabel>
<a
href="/auth/forgot-password"
className="ml-auto text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full cursor-pointer">
Login
</Button>
<Button variant="outline" className="w-full cursor-pointer" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
Login with Google
</Button>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a href="/auth/sign-up" className="underline underline-offset-4">
Sign up
</a>
</div>
</div> </div>
</form>
</Form> <div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold tracking-tight">Tekrar hoş geldiniz</h1>
<p className="text-muted-foreground text-sm text-balance mt-1">
Hesabınıza giriş yaparak işletmenizi yönetmeye devam edin
</p>
</div>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="ornek@firma.com"
autoComplete="email"
required
/>
</div>
<div className="grid gap-3">
<div className="flex items-center">
<Label htmlFor="password">Şifre</Label>
<Link
href="/forgot-password"
className="ml-auto text-xs text-muted-foreground hover:text-foreground underline-offset-4 hover:underline"
>
Şifremi unuttum
</Link>
</div>
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
/>
</div>
{state.error && (
<p className="text-destructive text-sm text-center" role="alert">
{state.error}
</p>
)}
<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />
Giriş yapılıyor...
</>
) : (
"Giriş yap"
)}
</Button>
<div className="text-center text-sm text-muted-foreground">
Hesabınız yok mu?{" "}
<Link
href="/sign-up"
className="text-foreground font-medium underline-offset-4 hover:underline"
>
Hesap oluştur
</Link>
</div>
</div>
</form>
<BrandPanel />
</CardContent> </CardContent>
</Card> </Card>
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "} <p className="text-muted-foreground text-center text-xs text-balance">
and <a href="#">Privacy Policy</a>. Giriş yaparak{" "}
<Link href="#" className="underline-offset-4 hover:underline">
Kullanım Şartları
</Link>{" "}
ve{" "}
<Link href="#" className="underline-offset-4 hover:underline">
Gizlilik Politikası
</Link>
&apos;nı kabul etmiş olursunuz.
</p>
</div>
);
}
function BrandPanel() {
return (
<div className="bg-primary text-primary-foreground relative hidden md:flex md:flex-col md:justify-between overflow-hidden p-10">
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage:
"radial-gradient(circle at 20% 20%, rgba(255,255,255,0.4) 0%, transparent 40%), radial-gradient(circle at 80% 80%, rgba(255,255,255,0.25) 0%, transparent 45%)",
}}
aria-hidden
/>
<div
className="absolute -top-24 -right-24 size-72 rounded-full bg-white/10 blur-3xl"
aria-hidden
/>
<div
className="absolute -bottom-32 -left-20 size-80 rounded-full bg-black/10 blur-3xl"
aria-hidden
/>
<div className="relative z-10 flex items-center gap-2">
<div className="bg-primary-foreground/15 ring-1 ring-primary-foreground/20 backdrop-blur flex size-10 items-center justify-center rounded-md">
<Logo size={22} />
</div>
<span className="text-lg font-medium">İşletmem</span>
</div>
<div className="relative z-10 flex flex-col gap-3">
<h2 className="text-3xl font-semibold leading-tight">
Müşteriden faturaya, tek panelden işletmenizi yönetin.
</h2>
<p className="text-primary-foreground/80 text-sm">
Müşteriler, hizmetler, takvim, görevler ve finans hepsi tek yerde, multi-tenant ve ekibinize özel.
</p>
<div className="text-primary-foreground/70 mt-4 text-xs">KovakSoft tarafından</div>
</div> </div>
</div> </div>
) );
} }
+11 -13
View File
@@ -1,19 +1,17 @@
import { LoginForm1 } from "./components/login-form-1" import { redirect } from "next/navigation";
import { Logo } from "@/components/logo"
import Link from "next/link" 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 ( return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"> <div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6"> <div className="w-full max-w-sm md:max-w-4xl">
<Link href="/" className="flex items-center gap-2 self-center font-medium">
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
<Logo size={24} />
</div>
ShadcnStore
</Link>
<LoginForm1 /> <LoginForm1 />
</div> </div>
</div> </div>
) );
} }
@@ -1,195 +1,168 @@
"use client" "use client";
import { zodResolver } from "@hookform/resolvers/zod" import Link from "next/link";
import { useForm } from "react-hook-form" import { useActionState } from "react";
import { z } from "zod" import { Loader2 } from "lucide-react";
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"
const signupFormSchema = z.object({ import { Button } from "@/components/ui/button";
firstName: z.string().min(1, "First name is required"), import { Card, CardContent } from "@/components/ui/card";
lastName: z.string().min(1, "Last name is required"), import { Input } from "@/components/ui/input";
email: z.string().email("Invalid email address"), import { Label } from "@/components/ui/label";
password: z.string().min(6, "Password must be at least 6 characters"), import { Logo } from "@/components/logo";
confirmPassword: z.string().min(6, "Please confirm your password"), import { cn } from "@/lib/utils";
terms: z.boolean().refine(val => val === true, "You must agree to the terms"), import { initialAuthState, signUpAction } from "@/lib/appwrite/auth-actions";
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
})
type SignupFormValues = z.infer<typeof signupFormSchema> export function SignupForm1({ className, ...props }: React.ComponentProps<"div">) {
const [state, formAction, isPending] = useActionState(signUpAction, initialAuthState);
export function SignupForm1({
className,
...props
}: React.ComponentProps<"div">) {
const form = useForm<SignupFormValues>({
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
}
return ( return (
<div className={cn("flex flex-col gap-6", className)} {...props}> <div className={cn("flex flex-col gap-6", className)} {...props}>
<Card> <Card className="overflow-hidden p-0">
<CardHeader className="text-center"> <CardContent className="grid p-0 md:grid-cols-2">
<CardTitle className="text-xl">Create Account</CardTitle> <BrandPanel />
<CardDescription>
Enter your information to create a new account
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid gap-6">
<div className="grid gap-4">
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="m@example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="terms"
render={({ field }) => (
<FormItem className="flex items-start space-x-2">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
className="mt-0.5"
/>
</FormControl>
<FormLabel className="text-sm">
I agree to the terms of service and privacy policy
</FormLabel>
</FormItem>
)}
/>
<Button type="submit" className="w-full cursor-pointer">
Create Account
</Button>
<Button variant="outline" className="w-full cursor-pointer" type="button"> <form action={formAction} className="p-6 md:p-10">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <div className="flex flex-col gap-6">
<path <div className="flex justify-center">
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" <Link href="/" className="flex items-center gap-2 font-medium">
fill="currentColor" <div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
/> <Logo size={22} />
</svg> </div>
Sign up with Google <span className="text-xl font-semibold">İşletmem</span>
</Button> </Link>
</div>
<div className="text-center text-sm">
Already have an account?{" "}
<a href="/auth/sign-in" className="underline underline-offset-4">
Sign in
</a>
</div>
</div> </div>
</form>
</Form> <div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold tracking-tight">Hesap oluşturun</h1>
<p className="text-muted-foreground text-sm text-balance mt-1">
Birkaç saniye içinde hesabınız hazır
</p>
</div>
<div className="grid gap-3">
<Label htmlFor="name">Adınız Soyadınız</Label>
<Input
id="name"
name="name"
type="text"
placeholder="Ahmet Yılmaz"
autoComplete="name"
required
/>
</div>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="ornek@firma.com"
autoComplete="email"
required
/>
</div>
<div className="grid gap-3">
<Label htmlFor="password">Şifre</Label>
<Input
id="password"
name="password"
type="password"
autoComplete="new-password"
minLength={8}
required
/>
<p className="text-muted-foreground text-xs">En az 8 karakter</p>
</div>
{state.error && (
<p className="text-destructive text-sm text-center" role="alert">
{state.error}
</p>
)}
<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />
Hesap oluşturuluyor...
</>
) : (
"Hesap oluştur"
)}
</Button>
<div className="text-center text-sm text-muted-foreground">
Zaten hesabınız var mı?{" "}
<Link
href="/sign-in"
className="text-foreground font-medium underline-offset-4 hover:underline"
>
Giriş yap
</Link>
</div>
</div>
</form>
</CardContent> </CardContent>
</Card> </Card>
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "} <p className="text-muted-foreground text-center text-xs text-balance">
and <a href="#">Privacy Policy</a>. Hesap oluşturarak{" "}
<Link href="#" className="underline-offset-4 hover:underline">
Kullanım Şartları
</Link>{" "}
ve{" "}
<Link href="#" className="underline-offset-4 hover:underline">
Gizlilik Politikası
</Link>
&apos;nı kabul etmiş olursunuz.
</p>
</div>
);
}
function BrandPanel() {
return (
<div className="bg-primary text-primary-foreground relative hidden md:flex md:flex-col md:justify-between overflow-hidden p-10">
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage:
"radial-gradient(circle at 20% 20%, rgba(255,255,255,0.4) 0%, transparent 40%), radial-gradient(circle at 80% 80%, rgba(255,255,255,0.25) 0%, transparent 45%)",
}}
aria-hidden
/>
<div
className="absolute -top-24 -left-24 size-72 rounded-full bg-white/10 blur-3xl"
aria-hidden
/>
<div
className="absolute -bottom-32 -right-20 size-80 rounded-full bg-black/10 blur-3xl"
aria-hidden
/>
<div className="relative z-10 flex items-center gap-2">
<div className="bg-primary-foreground/15 ring-1 ring-primary-foreground/20 backdrop-blur flex size-10 items-center justify-center rounded-md">
<Logo size={22} />
</div>
<span className="text-lg font-medium">İşletmem</span>
</div>
<div className="relative z-10 flex flex-col gap-3">
<h2 className="text-3xl font-semibold leading-tight">
İşletmenizi büyütecek tek araç.
</h2>
<p className="text-primary-foreground/80 text-sm">
Hesap oluşturduktan sonra çalışma alanınızı kuruyor, ekibinizi davet ediyor ve hemen kullanmaya başlıyorsunuz.
</p>
<ul className="text-primary-foreground/85 mt-2 space-y-1 text-sm">
<li> Müşteri & hizmet yönetimi</li>
<li> Görev ve takvim</li>
<li> Finans ve fatura</li>
</ul>
<div className="text-primary-foreground/70 mt-4 text-xs">KovakSoft tarafından</div>
</div> </div>
</div> </div>
) );
} }
+11 -13
View File
@@ -1,19 +1,17 @@
import { SignupForm1 } from "./components/signup-form-1" import { redirect } from "next/navigation";
import { Logo } from "@/components/logo"
import Link from "next/link" 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 ( return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"> <div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6"> <div className="w-full max-w-sm md:max-w-4xl">
<Link href="/" className="flex items-center gap-2 self-center font-medium">
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
<Logo size={24} />
</div>
ShadcnStore
</Link>
<SignupForm1 /> <SignupForm1 />
</div> </div>
</div> </div>
) );
} }
+118
View File
@@ -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<AuthState> {
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<AuthState> {
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<AuthState> {
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;
+48 -25
View File
@@ -1,32 +1,55 @@
import { NextResponse } from 'next/server' import { NextResponse } from "next/server";
import type { NextRequest } 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) { export function middleware(request: NextRequest) {
// Add custom middleware logic here const { pathname } = request.nextUrl;
// For example: authentication, redirects, etc. const session = request.cookies.get(AUTH_COOKIE)?.value;
// Example: Redirect /login to /auth/sign-in // Legacy redirects
if (request.nextUrl.pathname === '/login') { if (pathname === "/login") {
return NextResponse.redirect(new URL('/auth/sign-in', request.url)) return NextResponse.redirect(new URL("/sign-in", request.url));
} }
if (pathname === "/register") {
// Example: Redirect /register to /auth/sign-up return NextResponse.redirect(new URL("/sign-up", request.url));
if (request.nextUrl.pathname === '/register') {
return NextResponse.redirect(new URL('/auth/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 = { export const config = {
matcher: [ matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
// 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).*)',
],
}