Auth in progress

This commit is contained in:
Philip
2025-04-07 22:24:50 +02:00
parent 249f3b6578
commit 72762cb02c
13 changed files with 932 additions and 11 deletions

735
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^6.5.0", "@prisma/client": "^6.5.0",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-navigation-menu": "^1.2.5",
@@ -20,10 +21,12 @@
"@trpc/next": "^11.0.2", "@trpc/next": "^11.0.2",
"@trpc/react-query": "^11.0.2", "@trpc/react-query": "^11.0.2",
"@trpc/server": "^11.0.2", "@trpc/server": "^11.0.2",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"next": "15.2.4", "next": "15.2.4",
"next-auth": "^4.24.11",
"prisma": "^6.5.0", "prisma": "^6.5.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@@ -35,6 +38,7 @@
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/bcrypt": "^5.0.2",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL DEFAULT 'test';

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "password" DROP DEFAULT;

View File

@@ -16,6 +16,7 @@ model User {
id String @id @default(uuid()) id String @id @default(uuid())
email String @unique email String @unique
name String? name String?
password String @db.Text
children Child[] children Child[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
} }

View File

@@ -0,0 +1,5 @@
import NextAuth from "next-auth"
import { authOptions } from "@/server/auth"
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

83
src/app/login/page.tsx Normal file
View File

@@ -0,0 +1,83 @@
"use client"
import { useState } from "react"
import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Card, CardContent } from "@/components/ui/card"
import Link from "next/link"
import { Header } from "@/components/layout/header"
import { Footer } from "@/components/layout/footer"
export default function LoginPage() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [error, setError] = useState("")
const router = useRouter()
const handleLogin = async () => {
setError("")
const res = await signIn("credentials", {
email,
password,
redirect: false,
})
if (res?.error) {
setError("Ungültige E-Mail oder Passwort.")
} else {
router.push("/app") // Zielseite nach Login
}
}
return (
<>
<Header />
<main className="min-h-[calc(100vh-160px)] flex items-center justify-center px-4 bg-gradient-to-br from-rose-100 to-rose-50">
<Card className="w-full max-w-md p-6 bg-white/80 backdrop-blur-md shadow-xl">
<CardContent className="space-y-6">
<h1 className="text-xl font-semibold text-center text-zinc-800">Login</h1>
<div className="space-y-2">
<Label htmlFor="email">E-Mail</Label>
<Input
id="email"
type="email"
placeholder="du@bambino.at"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Passwort</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500 text-center">{error}</p>}
<Button className="w-full" onClick={handleLogin}>
Einloggen
</Button>
<p className="text-center text-sm text-zinc-500">
Noch keinen Account?{" "}
<Link href="/register" className="text-rose-600 hover:underline">
Jetzt registrieren
</Link>
</p>
</CardContent>
</Card>
</main>
<Footer />
</>
)
}

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { trpc } from "@/utils/trpc"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
@@ -13,6 +14,7 @@ export default function RegisterPage() {
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
const [name, setName] = useState("") const [name, setName] = useState("")
const [password, setPassword] = useState("") const [password, setPassword] = useState("")
const registerMutation = trpc.auth.register.useMutation()
return ( return (
<> <>
@@ -54,8 +56,21 @@ export default function RegisterPage() {
/> />
</div> </div>
<Button className="w-full" disabled> <Button
Registrieren (bald verfügbar) className="w-full"
onClick={() => {
registerMutation.mutate({ name, email, password }, {
onSuccess: () => {
alert("Registrierung erfolgreich! 🎉")
// TODO: Weiterleitung zu /login oder direkt einloggen
},
onError: (err) => {
alert("Fehler: " + err.message)
}
})
}}
>
Registrieren
</Button> </Button>
<p className="text-center text-sm text-zinc-500"> <p className="text-center text-sm text-zinc-500">

4
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,4 @@
import { getServerSession } from "next-auth"
import { authOptions } from "@/server/auth"
export const getAuthSession = () => getServerSession(authOptions)

View File

@@ -1,8 +1,12 @@
import { router } from "@/server/trpc" import { router } from "@/server/trpc"
import { childRouter } from "./routers/child" import { childRouter } from "./routers/child"
import { authRouter } from "./routers/auth"
export const appRouter = router({ export const appRouter = router({
child: childRouter, child: childRouter,
auth: authRouter,
}) })
// Export type helper // Export type helper

View File

@@ -0,0 +1,36 @@
import { z } from "zod"
import { prisma } from "@/lib/prisma"
import { publicProcedure, router } from "@/server/trpc"
import bcrypt from "bcrypt"
export const authRouter = router({
register: publicProcedure
.input(
z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(6),
})
)
.mutation(async ({ input }) => {
const existing = await prisma.user.findUnique({
where: { email: input.email },
})
if (existing) {
throw new Error("Ein Benutzer mit dieser E-Mail existiert bereits.")
}
const hashedPassword = await bcrypt.hash(input.password, 10)
const user = await prisma.user.create({
data: {
name: input.name,
email: input.email,
password: hashedPassword,
},
})
return { success: true, userId: user.id }
}),
})

39
src/server/auth.ts Normal file
View File

@@ -0,0 +1,39 @@
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { type NextAuthOptions } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { prisma } from "@/lib/prisma"
import bcrypt from "bcrypt"
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "E-Mail", type: "text" },
password: { label: "Passwort", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null
const user = await prisma.user.findUnique({
where: { email: credentials.email },
})
if (!user || !user.password) return null
const isValid = await bcrypt.compare(credentials.password, user.password)
if (!isValid) return null
return user
},
}),
],
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
},
secret: process.env.NEXTAUTH_SECRET,
}

View File

@@ -1,5 +1,10 @@
export const createContext = () => { import { getAuthSession } from "@/lib/auth"
return {}
export const createContext = async () => {
const session = await getAuthSession()
return {
session,
}
} }
export type Context = Awaited<ReturnType<typeof createContext>> export type Context = Awaited<ReturnType<typeof createContext>>