Auth in progress
This commit is contained in:
735
package-lock.json
generated
735
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^6.5.0",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
@@ -20,10 +21,12 @@
|
||||
"@trpc/next": "^11.0.2",
|
||||
"@trpc/react-query": "^11.0.2",
|
||||
"@trpc/server": "^11.0.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.487.0",
|
||||
"next": "15.2.4",
|
||||
"next-auth": "^4.24.11",
|
||||
"prisma": "^6.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -35,6 +38,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL DEFAULT 'test';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ALTER COLUMN "password" DROP DEFAULT;
|
||||
@@ -16,6 +16,7 @@ model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
name String?
|
||||
password String @db.Text
|
||||
children Child[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
5
src/app/api/auth/[...nextauth]/route.ts
Normal file
5
src/app/api/auth/[...nextauth]/route.ts
Normal 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
83
src/app/login/page.tsx
Normal 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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { trpc } from "@/utils/trpc"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -13,6 +14,7 @@ export default function RegisterPage() {
|
||||
const [email, setEmail] = useState("")
|
||||
const [name, setName] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const registerMutation = trpc.auth.register.useMutation()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -54,8 +56,21 @@ export default function RegisterPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button className="w-full" disabled>
|
||||
Registrieren (bald verfügbar)
|
||||
<Button
|
||||
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>
|
||||
|
||||
<p className="text-center text-sm text-zinc-500">
|
||||
|
||||
4
src/lib/auth.ts
Normal file
4
src/lib/auth.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { getServerSession } from "next-auth"
|
||||
import { authOptions } from "@/server/auth"
|
||||
|
||||
export const getAuthSession = () => getServerSession(authOptions)
|
||||
@@ -1,8 +1,12 @@
|
||||
import { router } from "@/server/trpc"
|
||||
import { childRouter } from "./routers/child"
|
||||
import { authRouter } from "./routers/auth"
|
||||
|
||||
|
||||
|
||||
export const appRouter = router({
|
||||
child: childRouter,
|
||||
auth: authRouter,
|
||||
})
|
||||
|
||||
// Export type helper
|
||||
|
||||
36
src/server/api/routers/auth.ts
Normal file
36
src/server/api/routers/auth.ts
Normal 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
39
src/server/auth.ts
Normal 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,
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
export const createContext = () => {
|
||||
return {}
|
||||
import { getAuthSession } from "@/lib/auth"
|
||||
|
||||
export const createContext = async () => {
|
||||
const session = await getAuthSession()
|
||||
return {
|
||||
session,
|
||||
}
|
||||
}
|
||||
|
||||
export type Context = Awaited<ReturnType<typeof createContext>>
|
||||
Reference in New Issue
Block a user