This commit is contained in:
xCyanGrizzly
2026-02-18 14:26:36 +01:00
commit 3a5726e82b
167 changed files with 104081 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="w-full max-w-md space-y-6 px-4">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,32 @@
"use server";
import { signIn } from "@/lib/auth";
import { loginSchema } from "@/schemas/auth.schema";
import { AuthError } from "next-auth";
export async function loginAction(values: { email: string; password: string }) {
const parsed = loginSchema.safeParse(values);
if (!parsed.success) {
return { error: "Invalid email or password" };
}
try {
await signIn("credentials", {
email: parsed.data.email,
password: parsed.data.password,
redirect: false,
});
return { success: true };
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return { error: "Invalid email or password" };
default:
return { error: "Something went wrong" };
}
}
// This is a redirect error thrown by next-auth on success - rethrow it
throw error;
}
}

View File

@@ -0,0 +1,133 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Flame } from "lucide-react";
import { loginSchema, type LoginInput } from "@/schemas/auth.schema";
import { loginAction } from "./actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { APP_NAME } from "@/lib/constants";
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const form = useForm<LoginInput>({
resolver: zodResolver(loginSchema) as any,
defaultValues: { email: "", password: "" },
});
function onSubmit(values: LoginInput) {
setError(null);
startTransition(async () => {
try {
const result = await loginAction(values);
if (result?.error) {
setError(result.error);
return;
}
router.push(callbackUrl);
router.refresh();
} catch {
// Redirect from server action — Next.js handles this automatically
router.push(callbackUrl);
router.refresh();
}
});
}
return (
<>
<div className="flex flex-col items-center gap-2 text-center">
<Flame className="h-10 w-10 text-primary" />
<h1 className="text-2xl font-bold tracking-tight">{APP_NAME}</h1>
<p className="text-sm text-muted-foreground">Sign in to manage your inventory</p>
</div>
<Card>
<CardHeader>
<CardTitle>Sign In</CardTitle>
<CardDescription>Enter your credentials to continue</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="admin@dragonsstash.local"
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
autoComplete="current-password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? "Signing in..." : "Sign In"}
</Button>
</form>
</Form>
<p className="mt-4 text-center text-sm text-muted-foreground">
Don&apos;t have an account?{" "}
<Link href="/register" className="text-primary underline-offset-4 hover:underline">
Register
</Link>
</p>
</CardContent>
</Card>
</>
);
}

View File

@@ -0,0 +1,42 @@
"use server";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";
import { registerSchema } from "@/schemas/auth.schema";
import type { ActionResult } from "@/types/api.types";
export async function registerUser(input: unknown): Promise<ActionResult<{ id: string }>> {
const parsed = registerSchema.safeParse(input);
if (!parsed.success) {
return { success: false, error: "Validation failed" };
}
const existing = await prisma.user.findUnique({
where: { email: parsed.data.email },
});
if (existing) {
return { success: false, error: "An account with this email already exists" };
}
const hashedPassword = await bcrypt.hash(parsed.data.password, 10);
const user = await prisma.user.create({
data: {
name: parsed.data.name,
email: parsed.data.email,
hashedPassword,
role: "USER",
settings: {
create: {
lowStockThreshold: 10,
currency: "USD",
theme: "dark",
units: "metric",
},
},
},
});
return { success: true, data: { id: user.id } };
}

View File

@@ -0,0 +1,176 @@
"use client";
import { useState, useTransition } 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 { Flame } from "lucide-react";
import { registerSchema, type RegisterInput } from "@/schemas/auth.schema";
import { registerUser } from "./actions";
import { loginAction } from "../login/actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { APP_NAME } from "@/lib/constants";
export default function RegisterPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const form = useForm<RegisterInput>({
resolver: zodResolver(registerSchema) as any,
defaultValues: { name: "", email: "", password: "", confirmPassword: "" },
});
function onSubmit(values: RegisterInput) {
setError(null);
startTransition(async () => {
const result = await registerUser(values);
if (!result.success) {
setError(result.error);
return;
}
// Auto-login after registration using server action
try {
const loginResult = await loginAction({
email: values.email,
password: values.password,
});
if (loginResult?.error) {
setError("Account created but sign in failed. Please try logging in.");
return;
}
router.push("/dashboard");
router.refresh();
} catch {
// Redirect from server action
router.push("/dashboard");
router.refresh();
}
});
}
return (
<>
<div className="flex flex-col items-center gap-2 text-center">
<Flame className="h-10 w-10 text-primary" />
<h1 className="text-2xl font-bold tracking-tight">{APP_NAME}</h1>
<p className="text-sm text-muted-foreground">Create an account to get started</p>
</div>
<Card>
<CardHeader>
<CardTitle>Create Account</CardTitle>
<CardDescription>Fill in your details below</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" autoComplete="name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="you@example.com"
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="At least 6 characters"
autoComplete="new-password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Repeat your password"
autoComplete="new-password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? "Creating account..." : "Create Account"}
</Button>
</form>
</Form>
<p className="mt-4 text-center text-sm text-muted-foreground">
Already have an account?{" "}
<Link href="/login" className="text-primary underline-offset-4 hover:underline">
Sign in
</Link>
</p>
</CardContent>
</Card>
</>
);
}