Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client"

import { useState } from "react"
import Link from "next/link"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Shield, Loader2, ArrowLeft, Mail } from "lucide-react"

const forgotSchema = z.object({
email: z.string().email("Enter a valid email address"),
})

type ForgotForm = z.infer<typeof forgotSchema>

export default function ForgotPasswordPage() {
const [submitted, setSubmitted] = useState(false)

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<ForgotForm>({
resolver: zodResolver(forgotSchema),
})

const onSubmit = async (_data: ForgotForm) => {
// Simulate API call - backend would handle sending email
await new Promise((resolve) => setTimeout(resolve, 1000))
setSubmitted(true)
}

return (
<>
<div className="flex items-center gap-2 mb-8 lg:hidden">
<Shield className="w-6 h-6 text-primary" />
<span className="text-xl font-bold">Secure App</span>
</div>

<Card className="border-border/50 shadow-sm">
{submitted ? (
<>
<CardHeader className="text-center pb-4">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 mb-4">
<Mail className="h-6 w-6 text-primary" />
</div>
<CardTitle className="text-2xl font-bold">
Check your email
</CardTitle>
<CardDescription>
If an account exists with that email, we sent password reset
instructions.
</CardDescription>
</CardHeader>
<CardFooter className="justify-center">
<Link
href="/login"
className="text-sm text-primary font-medium hover:underline inline-flex items-center gap-1"
>
<ArrowLeft className="w-4 h-4" />
Back to sign in
</Link>
</CardFooter>
</>
) : (
<>
<CardHeader className="pb-4">
<CardTitle className="text-2xl font-bold">
Forgot password
</CardTitle>
<CardDescription>
{"Enter your email and we'll send you a reset link"}
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="flex flex-col gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
autoComplete="email"
{...register("email")}
/>
{errors.email && (
<p className="text-sm text-destructive">
{errors.email.message}
</p>
)}
</div>

<Button type="submit" disabled={isSubmitting} className="mt-2">
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
</>
) : (
"Send reset link"
)}
</Button>
</form>
</CardContent>
<CardFooter className="justify-center">
<Link
href="/login"
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
>
<ArrowLeft className="w-4 h-4" />
Back to sign in
</Link>
</CardFooter>
</>
)}
</Card>
</>
)
}
71 changes: 71 additions & 0 deletions app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client"

import { useEffect } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "@/lib/auth"
import { Shield } from "lucide-react"

export default function AuthLayout({
children,
}: {
children: React.ReactNode
}) {
const { isAuthenticated, isLoading } = useAuth()
const router = useRouter()

useEffect(() => {
if (!isLoading && isAuthenticated) {
router.replace("/dashboard")
}
}, [isAuthenticated, isLoading, router])

if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="animate-pulse text-muted-foreground">Loading...</div>
</div>
)
}

if (isAuthenticated) return null

return (
<div className="flex min-h-screen">
{/* Left panel - branding */}
<div className="hidden lg:flex lg:w-1/2 flex-col items-center justify-center bg-sidebar text-sidebar-foreground p-12">
<div className="flex flex-col items-center gap-6 max-w-md text-center">
<div className="flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10">
<Shield className="w-8 h-8 text-primary" />
</div>
<h1 className="text-3xl font-bold text-sidebar-accent-foreground tracking-tight text-balance">
Secure App
</h1>
<p className="text-sidebar-foreground/70 leading-relaxed text-balance">
Enterprise-grade security management platform with role-based access
control, comprehensive user administration, and granular permission
management.
</p>
<div className="flex flex-col gap-3 mt-4 text-sm text-sidebar-foreground/50">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
<span>JWT-based authentication</span>
</div>
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
<span>Role-based access control (RBAC)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
<span>Granular permission system</span>
</div>
</div>
</div>
</div>

{/* Right panel - form */}
<div className="flex flex-1 flex-col items-center justify-center p-6 lg:p-12">
<div className="w-full max-w-md">{children}</div>
</div>
</div>
)
}
154 changes: 154 additions & 0 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use client"

import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { toast } from "sonner"
import { useAuth } from "@/lib/auth"
import { ApiRequestError } from "@/lib/api"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Shield, Loader2, Eye, EyeOff } from "lucide-react"

const loginSchema = z.object({
email: z.string().email("Enter a valid email address"),
password: z.string().min(1, "Password is required"),
})

type LoginForm = z.infer<typeof loginSchema>

export default function LoginPage() {
const { login } = useAuth()
const router = useRouter()
const [showPassword, setShowPassword] = useState(false)

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
})

const onSubmit = async (data: LoginForm) => {
try {
await login(data.email, data.password)
toast.success("Login successful")
router.push("/dashboard")
} catch (error) {
if (error instanceof ApiRequestError) {
toast.error(error.message)
} else {
toast.error("An unexpected error occurred")
}
}
}

return (
<>
<div className="flex items-center gap-2 mb-8 lg:hidden">
<Shield className="w-6 h-6 text-primary" />
<span className="text-xl font-bold">Secure App</span>
</div>

<Card className="border-border/50 shadow-sm">
<CardHeader className="pb-4">
<CardTitle className="text-2xl font-bold">Welcome back</CardTitle>
<CardDescription>
Sign in to your account to continue
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="admin@example.com"
autoComplete="email"
{...register("email")}
/>
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
)}
</div>

<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<Link
href="/forgot-password"
className="text-xs text-primary hover:underline"
>
Forgot password?
</Link>
</div>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
autoComplete="current-password"
className="pr-10"
{...register("password")}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
{errors.password && (
<p className="text-sm text-destructive">
{errors.password.message}
</p>
)}
</div>

<Button type="submit" disabled={isSubmitting} className="mt-2">
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : (
"Sign in"
)}
</Button>
</form>
</CardContent>
<CardFooter className="justify-center">
<p className="text-sm text-muted-foreground">
{"Don't have an account? "}
<Link
href="/register"
className="text-primary font-medium hover:underline"
>
Create account
</Link>
</p>
</CardFooter>
</Card>
</>
)
}
Loading
Loading