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": {
|
"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",
|
||||||
|
|||||||
@@ -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())
|
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())
|
||||||
}
|
}
|
||||||
|
|||||||
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"
|
"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
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 { 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
|
||||||
|
|||||||
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 = () => {
|
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>>
|
||||||
Reference in New Issue
Block a user