feat: Implement initial with admin and mobile clients, authentication, data models, and lead generation scripts.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
# =============================================
|
||||
# DATABASE
|
||||
# DATABASE (SQLite — kein externer DB-Server nötig)
|
||||
# Dev: file:../../packages/shared/prisma/dev.db
|
||||
# Prod: file:./prisma/prod.db
|
||||
# =============================================
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/innungsapp"
|
||||
DATABASE_URL="file:../../packages/shared/prisma/dev.db"
|
||||
|
||||
# =============================================
|
||||
# BETTER-AUTH
|
||||
|
||||
60
innungsapp/apps/admin/app/api/setup/route.ts
Normal file
60
innungsapp/apps/admin/app/api/setup/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* DEV-ONLY: Sets a password for the demo admin user via better-auth.
|
||||
* Call once after seeding: GET http://localhost:3032/api/setup
|
||||
* Remove this file before going to production.
|
||||
*/
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { prisma } from '@innungsapp/shared'
|
||||
|
||||
export async function GET() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return NextResponse.json({ error: 'Not available in production' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Delete the pre-seeded user so better-auth can create it fresh with a hashed password
|
||||
await prisma.account.deleteMany({ where: { userId: 'demo-admin-user-id' } })
|
||||
await prisma.member.deleteMany({ where: { userId: 'demo-admin-user-id' } })
|
||||
await prisma.userRole.deleteMany({ where: { userId: 'demo-admin-user-id' } })
|
||||
await prisma.user.deleteMany({ where: { id: 'demo-admin-user-id' } })
|
||||
|
||||
// Re-create via better-auth so the password is properly hashed
|
||||
const result = await auth.api.signUpEmail({
|
||||
body: { email: 'admin@demo.de', password: 'demo1234', name: 'Demo Admin' },
|
||||
})
|
||||
|
||||
if (!result?.user) {
|
||||
return NextResponse.json({ error: 'signUp failed', result }, { status: 500 })
|
||||
}
|
||||
|
||||
const newUserId = result.user.id
|
||||
|
||||
// Restore org membership for the new user ID
|
||||
const org = await prisma.organization.findFirst({ where: { slug: 'innung-elektro-stuttgart' } })
|
||||
if (org) {
|
||||
await prisma.userRole.upsert({
|
||||
where: { orgId_userId: { orgId: org.id, userId: newUserId } },
|
||||
update: {},
|
||||
create: { orgId: org.id, userId: newUserId, role: 'admin' },
|
||||
})
|
||||
await prisma.member.upsert({
|
||||
where: { userId: newUserId },
|
||||
update: {},
|
||||
create: {
|
||||
orgId: org.id,
|
||||
userId: newUserId,
|
||||
name: 'Demo Admin',
|
||||
betrieb: 'Innungsgeschäftsstelle',
|
||||
sparte: 'Elektrotechnik',
|
||||
ort: 'Stuttgart',
|
||||
email: 'admin@demo.de',
|
||||
status: 'aktiv',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
message: 'Setup complete. Login: admin@demo.de / demo1234',
|
||||
})
|
||||
}
|
||||
@@ -3,14 +3,16 @@
|
||||
import { useState } from 'react'
|
||||
import { createAuthClient } from 'better-auth/react'
|
||||
import { magicLinkClient } from 'better-auth/client/plugins'
|
||||
|
||||
const authClient = createAuthClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
|
||||
plugins: [magicLinkClient()],
|
||||
})
|
||||
|
||||
type Mode = 'password' | 'magic'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [mode, setMode] = useState<Mode>('password')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [sent, setSent] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@@ -20,16 +22,29 @@ export default function LoginPage() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
const result = await authClient.signIn.magicLink({
|
||||
email,
|
||||
callbackURL: '/dashboard',
|
||||
})
|
||||
|
||||
setLoading(false)
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? 'Ein Fehler ist aufgetreten.')
|
||||
if (mode === 'password') {
|
||||
const result = await authClient.signIn.email({
|
||||
email,
|
||||
password,
|
||||
callbackURL: '/dashboard',
|
||||
})
|
||||
setLoading(false)
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? 'E-Mail oder Passwort falsch.')
|
||||
} else {
|
||||
window.location.href = '/dashboard'
|
||||
}
|
||||
} else {
|
||||
setSent(true)
|
||||
const result = await authClient.signIn.magicLink({
|
||||
email,
|
||||
callbackURL: '/dashboard',
|
||||
})
|
||||
setLoading(false)
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? 'Ein Fehler ist aufgetreten.')
|
||||
} else {
|
||||
setSent(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,9 +68,7 @@ export default function LoginPage() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
E-Mail gesendet!
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">E-Mail gesendet!</h2>
|
||||
<p className="text-gray-500">
|
||||
Wir haben einen Login-Link an <strong>{email}</strong> gesendet.
|
||||
Bitte überprüfen Sie Ihr Postfach.
|
||||
@@ -69,15 +82,33 @@ export default function LoginPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-6">
|
||||
Anmelden
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-6">Anmelden</h2>
|
||||
|
||||
{/* Mode toggle */}
|
||||
<div className="flex rounded-lg border border-gray-200 p-1 mb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('password')}
|
||||
className={`flex-1 py-1.5 text-sm rounded-md font-medium transition-colors ${
|
||||
mode === 'password' ? 'bg-brand-500 text-white' : 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Passwort
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('magic')}
|
||||
className={`flex-1 py-1.5 text-sm rounded-md font-medium transition-colors ${
|
||||
mode === 'magic' ? 'bg-brand-500 text-white' : 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Magic Link
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
E-Mail-Adresse
|
||||
</label>
|
||||
<input
|
||||
@@ -91,10 +122,25 @@ export default function LoginPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{mode === 'password' && (
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-600 bg-red-50 px-4 py-2 rounded-lg">
|
||||
{error}
|
||||
</p>
|
||||
<p className="text-sm text-red-600 bg-red-50 px-4 py-2 rounded-lg">{error}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
@@ -102,13 +148,19 @@ export default function LoginPage() {
|
||||
disabled={loading}
|
||||
className="w-full bg-brand-500 text-white py-2.5 px-4 rounded-lg font-medium hover:bg-brand-600 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? 'Wird gesendet...' : 'Magic Link senden'}
|
||||
{loading
|
||||
? 'Bitte warten...'
|
||||
: mode === 'password'
|
||||
? 'Anmelden'
|
||||
: 'Magic Link senden'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-4 text-center text-sm text-gray-500">
|
||||
Kein Passwort nötig — Sie erhalten einen Link per E-Mail.
|
||||
</p>
|
||||
{mode === 'password' && (
|
||||
<p className="mt-4 text-center text-xs text-gray-400">
|
||||
Demo: admin@demo.de / demo1234
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,19 @@ import { sendMagicLinkEmail } from './email'
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: 'postgresql',
|
||||
provider: 'sqlite',
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
baseURL: process.env.BETTER_AUTH_URL!,
|
||||
trustedOrigins: [
|
||||
process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
|
||||
process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3000',
|
||||
process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3032',
|
||||
process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3032',
|
||||
'http://10.36.148.233:3032',
|
||||
'http://localhost:8081', // Expo dev client
|
||||
'http://10.36.148.233:8081',
|
||||
],
|
||||
plugins: [
|
||||
magicLink({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
const PUBLIC_PATHS = ['/login', '/api/auth', '/api/trpc/stellen.listPublic']
|
||||
const PUBLIC_PATHS = ['/login', '/api/auth', '/api/trpc/stellen.listPublic', '/api/setup']
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const pathname = request.nextUrl.pathname
|
||||
|
||||
5
innungsapp/apps/admin/next-env.d.ts
vendored
Normal file
5
innungsapp/apps/admin/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -19,7 +19,7 @@
|
||||
"@trpc/server": "^11.0.0",
|
||||
"@tanstack/react-query": "^5.59.0",
|
||||
"better-auth": "^1.2.0",
|
||||
"next": "^15.0.0",
|
||||
"next": "15.3.4",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"zod": "^3.23.0",
|
||||
|
||||
@@ -124,6 +124,13 @@ export const membersRouter = router({
|
||||
where: { id: input.id, orgId: ctx.orgId },
|
||||
data: input.data,
|
||||
})
|
||||
// Keep user.name in sync when member name changes
|
||||
if (input.data.name) {
|
||||
const m = await ctx.prisma.member.findFirst({ where: { id: input.id }, select: { userId: true } })
|
||||
if (m?.userId) {
|
||||
await ctx.prisma.user.update({ where: { id: m.userId }, data: { name: input.data.name } })
|
||||
}
|
||||
}
|
||||
return member
|
||||
}),
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { NEWS_KATEGORIE_LABELS } from '@innungsapp/shared/types'
|
||||
import { useNewsList } from '@/hooks/useNews'
|
||||
import { useTermineListe } from '@/hooks/useTermine'
|
||||
import { useNewsReadStore } from '@/store/news.store'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
// Helper to truncate text
|
||||
function getNewsExcerpt(value: string) {
|
||||
@@ -23,16 +24,18 @@ export default function HomeScreen() {
|
||||
const { data: newsItems = [] } = useNewsList()
|
||||
const { data: termine = [] } = useTermineListe(true)
|
||||
const readIds = useNewsReadStore((s) => s.readIds)
|
||||
const { data: me } = trpc.members.me.useQuery()
|
||||
const userName = me?.name ?? ''
|
||||
|
||||
const latestNews = newsItems.slice(0, 2)
|
||||
const upcomingEvents = termine.slice(0, 3)
|
||||
const unreadCount = newsItems.filter((item) => !(item.isRead || readIds.has(item.id))).length
|
||||
|
||||
const QUICK_ACTIONS = [
|
||||
{ label: 'Mitglieder', icon: 'people', color: '#003B7E', bg: '#E0F2FE', route: '/(app)/members' },
|
||||
{ label: 'Termine', icon: 'calendar', color: '#B45309', bg: '#FEF3C7', route: '/(app)/termine' },
|
||||
{ label: 'Stellen', icon: 'briefcase', color: '#059669', bg: '#D1FAE5', route: '/(app)/stellen' },
|
||||
{ label: 'Profil', icon: 'person', color: '#4F46E5', bg: '#E0E7FF', route: '/(app)/profil' },
|
||||
{ label: 'Mitglieder', icon: 'people-circle', color: '#2563EB', bg: '#DBEAFE', route: '/(app)/members' },
|
||||
{ label: 'Termine', icon: 'alarm', color: '#EA580C', bg: '#FFEDD5', route: '/(app)/termine' },
|
||||
{ label: 'Stellen', icon: 'construct', color: '#0F766E', bg: '#CCFBF1', route: '/(app)/stellen' },
|
||||
{ label: 'Aktuelles', icon: 'megaphone', color: '#BE185D', bg: '#FCE7F3', route: '/(app)/news' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -53,7 +56,7 @@ export default function HomeScreen() {
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.greeting}>Willkommen zurück,</Text>
|
||||
<Text style={styles.username}>Demo Admin</Text>
|
||||
<Text style={styles.username}>{userName}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -461,4 +464,3 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '500',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { View, Text, ScrollView, TouchableOpacity, Alert, StyleSheet } from 'rea
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { MOCK_MEMBER_ME } from '@/lib/mock-data'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
type Item = {
|
||||
label: string
|
||||
@@ -20,9 +20,10 @@ const MENU_ITEMS: Item[] = [
|
||||
|
||||
export default function ProfilScreen() {
|
||||
const { signOut } = useAuth()
|
||||
const member = MOCK_MEMBER_ME
|
||||
const { data: me } = trpc.members.me.useQuery()
|
||||
const name = me?.name ?? ''
|
||||
|
||||
const initials = member.name
|
||||
const initials = name
|
||||
.split(' ')
|
||||
.slice(0, 2)
|
||||
.map((chunk) => chunk[0]?.toUpperCase() ?? '')
|
||||
@@ -42,7 +43,7 @@ export default function ProfilScreen() {
|
||||
<Ionicons name="settings-outline" size={15} color="#64748B" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.name}>{member.name}</Text>
|
||||
<Text style={styles.name}>{name}</Text>
|
||||
<Text style={styles.role}>Innungsgeschaeftsfuehrer</Text>
|
||||
<View style={styles.badgesRow}>
|
||||
<View style={styles.statusBadge}>
|
||||
|
||||
@@ -7,29 +7,40 @@ import { useRouter } from 'expo-router'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import { authClient } from '@/lib/auth-client'
|
||||
import { useAuthStore } from '@/store/auth.store'
|
||||
|
||||
export default function LoginScreen() {
|
||||
const router = useRouter()
|
||||
const setSession = useAuthStore((s) => s.setSession)
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const canSubmit = email.trim().length > 0 && !loading
|
||||
const canSubmit = email.trim().length > 0 && password.length > 0 && !loading
|
||||
|
||||
async function handleSendLink() {
|
||||
if (!email.trim()) return
|
||||
async function handleLogin() {
|
||||
if (!canSubmit) return
|
||||
setLoading(true)
|
||||
setError('')
|
||||
const result = await authClient.signIn.magicLink({
|
||||
|
||||
const result = await authClient.signIn.email({
|
||||
email: email.trim().toLowerCase(),
|
||||
callbackURL: '/home',
|
||||
password,
|
||||
})
|
||||
setLoading(false)
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? 'Ein Fehler ist aufgetreten.')
|
||||
} else {
|
||||
router.push({ pathname: '/(auth)/check-email', params: { email } })
|
||||
setError(result.error.message ?? 'E-Mail oder Passwort falsch.')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const token = (result.data as any)?.session?.token
|
||||
const user = (result.data as any)?.user
|
||||
await setSession(user ? { user } : null, token)
|
||||
|
||||
setLoading(false)
|
||||
router.replace('/(app)/home' as never)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -60,7 +71,21 @@ export default function LoginScreen() {
|
||||
autoCorrect={false}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
onSubmitEditing={handleSendLink}
|
||||
returnKeyType="next"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.inputLabel, { marginTop: 4 }]}>Passwort</Text>
|
||||
<View style={styles.inputWrap}>
|
||||
<Ionicons name="lock-closed-outline" size={18} color="#94A3B8" />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#94A3B8"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
onSubmitEditing={handleLogin}
|
||||
returnKeyType="go"
|
||||
/>
|
||||
</View>
|
||||
@@ -72,7 +97,7 @@ export default function LoginScreen() {
|
||||
) : null}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSendLink}
|
||||
onPress={handleLogin}
|
||||
disabled={!canSubmit}
|
||||
style={[styles.submitBtn, !canSubmit && styles.submitBtnDisabled]}
|
||||
activeOpacity={0.85}
|
||||
@@ -81,7 +106,7 @@ export default function LoginScreen() {
|
||||
<ActivityIndicator color="#FFFFFF" />
|
||||
) : (
|
||||
<View style={styles.submitContent}>
|
||||
<Text style={styles.submitLabel}>Login-Link senden</Text>
|
||||
<Text style={styles.submitLabel}>Anmelden</Text>
|
||||
<Ionicons name="arrow-forward" size={16} color="#FFFFFF" />
|
||||
</View>
|
||||
)}
|
||||
@@ -89,7 +114,7 @@ export default function LoginScreen() {
|
||||
</View>
|
||||
|
||||
<Text style={styles.hint}>
|
||||
Noch kein Zugang? Kontaktieren Sie Ihre Innungsgeschaeftsstelle.
|
||||
Noch kein Zugang? Kontaktieren Sie Ihre Innungsgeschäftsstelle.
|
||||
</Text>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
@@ -98,109 +123,38 @@ export default function LoginScreen() {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
keyboardView: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
logoSection: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 40,
|
||||
},
|
||||
safeArea: { flex: 1, backgroundColor: '#FFFFFF' },
|
||||
keyboardView: { flex: 1 },
|
||||
content: { flex: 1, justifyContent: 'center', paddingHorizontal: 24 },
|
||||
logoSection: { alignItems: 'center', marginBottom: 40 },
|
||||
logoBox: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
backgroundColor: '#003B7E',
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
logoLetter: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 30,
|
||||
fontWeight: '900',
|
||||
},
|
||||
appName: {
|
||||
fontSize: 30,
|
||||
fontWeight: '800',
|
||||
color: '#0F172A',
|
||||
letterSpacing: -0.6,
|
||||
marginBottom: 4,
|
||||
},
|
||||
tagline: {
|
||||
fontSize: 14,
|
||||
color: '#64748B',
|
||||
textAlign: 'center',
|
||||
},
|
||||
form: {
|
||||
gap: 12,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#334155',
|
||||
width: 64, height: 64, backgroundColor: '#003B7E',
|
||||
borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginBottom: 16,
|
||||
},
|
||||
logoLetter: { color: '#FFFFFF', fontSize: 30, fontWeight: '900' },
|
||||
appName: { fontSize: 30, fontWeight: '800', color: '#0F172A', letterSpacing: -0.6, marginBottom: 4 },
|
||||
tagline: { fontSize: 14, color: '#64748B', textAlign: 'center' },
|
||||
form: { gap: 8 },
|
||||
inputLabel: { fontSize: 14, fontWeight: '700', color: '#334155' },
|
||||
inputWrap: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F8FAFC',
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
paddingHorizontal: 12,
|
||||
gap: 8,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
paddingVertical: 13,
|
||||
color: '#0F172A',
|
||||
fontSize: 15,
|
||||
flexDirection: 'row', alignItems: 'center',
|
||||
backgroundColor: '#F8FAFC', borderRadius: 14,
|
||||
borderWidth: 1, borderColor: '#E2E8F0',
|
||||
paddingHorizontal: 12, gap: 8,
|
||||
},
|
||||
input: { flex: 1, paddingVertical: 13, color: '#0F172A', fontSize: 15 },
|
||||
errorBox: {
|
||||
backgroundColor: '#FEF2F2',
|
||||
borderWidth: 1,
|
||||
borderColor: '#FECACA',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
errorText: {
|
||||
color: '#B91C1C',
|
||||
fontSize: 13,
|
||||
backgroundColor: '#FEF2F2', borderWidth: 1,
|
||||
borderColor: '#FECACA', borderRadius: 12,
|
||||
paddingHorizontal: 14, paddingVertical: 10,
|
||||
},
|
||||
errorText: { color: '#B91C1C', fontSize: 13 },
|
||||
submitBtn: {
|
||||
backgroundColor: '#003B7E',
|
||||
borderRadius: 14,
|
||||
paddingVertical: 14,
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
submitBtnDisabled: {
|
||||
backgroundColor: '#CBD5E1',
|
||||
},
|
||||
submitContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
submitLabel: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '700',
|
||||
fontSize: 15,
|
||||
},
|
||||
hint: {
|
||||
marginTop: 24,
|
||||
textAlign: 'center',
|
||||
color: '#64748B',
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
backgroundColor: '#003B7E', borderRadius: 14,
|
||||
paddingVertical: 14, alignItems: 'center', marginTop: 8,
|
||||
},
|
||||
submitBtnDisabled: { backgroundColor: '#CBD5E1' },
|
||||
submitContent: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
submitLabel: { color: '#FFFFFF', fontWeight: '700', fontSize: 15 },
|
||||
hint: { marginTop: 24, textAlign: 'center', color: '#64748B', fontSize: 13, lineHeight: 18 },
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import '../global.css'
|
||||
import { useEffect } from 'react'
|
||||
import { Stack, SplashScreen } from 'expo-router'
|
||||
import { useAuthStore } from '@/store/auth.store'
|
||||
import { TRPCProvider } from '@/lib/trpc'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
@@ -16,11 +17,13 @@ export default function RootLayout() {
|
||||
if (!isInitialized) return null
|
||||
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="(auth)" options={{ animation: 'fade' }} />
|
||||
<Stack.Screen name="(app)" options={{ animation: 'fade' }} />
|
||||
<Stack.Screen name="stellen-public/index" />
|
||||
</Stack>
|
||||
<TRPCProvider>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="(auth)" options={{ animation: 'fade' }} />
|
||||
<Stack.Screen name="(app)" options={{ animation: 'fade' }} />
|
||||
<Stack.Screen name="stellen-public/index" />
|
||||
</Stack>
|
||||
</TRPCProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ export function useAuth() {
|
||||
|
||||
return {
|
||||
session,
|
||||
orgId: 'org-1',
|
||||
role: 'member' as const,
|
||||
isAuthenticated: true, // Mock: immer eingeloggt
|
||||
isAdmin: false,
|
||||
isAuthenticated: !!session,
|
||||
signOut: handleSignOut,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import { MOCK_MEMBERS } from '@/lib/mock-data'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { useMembersFilterStore } from '@/store/members.store'
|
||||
|
||||
export function useMembersList() {
|
||||
const search = useMembersFilterStore((s) => s.search)
|
||||
const nurAusbildungsbetriebe = useMembersFilterStore((s) => s.nurAusbildungsbetriebe)
|
||||
|
||||
let data = MOCK_MEMBERS.filter((m) => m.status === 'aktiv')
|
||||
const { data, isLoading, refetch, isFetching } = trpc.members.list.useQuery({
|
||||
search: search || undefined,
|
||||
status: 'aktiv',
|
||||
ausbildungsbetrieb: nurAusbildungsbetriebe || undefined,
|
||||
})
|
||||
|
||||
if (search) {
|
||||
const q = search.toLowerCase()
|
||||
data = data.filter(
|
||||
(m) =>
|
||||
m.name.toLowerCase().includes(q) ||
|
||||
m.betrieb.toLowerCase().includes(q) ||
|
||||
m.ort.toLowerCase().includes(q) ||
|
||||
m.sparte.toLowerCase().includes(q)
|
||||
)
|
||||
return {
|
||||
data: data ?? [],
|
||||
isLoading,
|
||||
refetch,
|
||||
isRefetching: isFetching,
|
||||
}
|
||||
|
||||
if (nurAusbildungsbetriebe) {
|
||||
data = data.filter((m) => m.istAusbildungsbetrieb)
|
||||
}
|
||||
|
||||
return { data, isLoading: false, refetch: () => {}, isRefetching: false }
|
||||
}
|
||||
|
||||
export function useMemberDetail(id: string) {
|
||||
const data = MOCK_MEMBERS.find((m) => m.id === id) ?? null
|
||||
return { data, isLoading: false }
|
||||
const { data, isLoading } = trpc.members.byId.useQuery({ id })
|
||||
return { data: data ?? null, isLoading }
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { useState } from 'react'
|
||||
import { MOCK_NEWS } from '@/lib/mock-data'
|
||||
import { useNewsReadStore } from '@/store/news.store'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
export function useNewsList(kategorie?: string) {
|
||||
const localReadIds = useNewsReadStore((s) => s.readIds)
|
||||
const filtered = kategorie
|
||||
? MOCK_NEWS.filter((n) => n.kategorie === kategorie)
|
||||
: MOCK_NEWS
|
||||
const { data, isLoading, refetch, isFetching } = trpc.news.list.useQuery({
|
||||
kategorie: kategorie || undefined,
|
||||
})
|
||||
|
||||
const data = filtered.map((n) => ({
|
||||
...n,
|
||||
isRead: n.isRead || localReadIds.has(n.id),
|
||||
}))
|
||||
|
||||
return { data, isLoading: false, refetch: () => {}, isRefetching: false }
|
||||
return {
|
||||
data: data ?? [],
|
||||
isLoading,
|
||||
refetch,
|
||||
isRefetching: isFetching,
|
||||
}
|
||||
}
|
||||
|
||||
export function useNewsDetail(id: string) {
|
||||
const markRead = useNewsReadStore((s) => s.markRead)
|
||||
const news = MOCK_NEWS.find((n) => n.id === id) ?? null
|
||||
const utils = trpc.useUtils()
|
||||
const { data, isLoading } = trpc.news.byId.useQuery({ id })
|
||||
|
||||
const markReadMutation = trpc.news.markRead.useMutation()
|
||||
|
||||
function onOpen() {
|
||||
markRead(id)
|
||||
markReadMutation.mutate({ newsId: id })
|
||||
utils.news.list.invalidate()
|
||||
}
|
||||
|
||||
return { data: news, isLoading: false, onOpen }
|
||||
return { data: data ?? null, isLoading, onOpen }
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { MOCK_STELLEN } from '@/lib/mock-data'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
export function useStellenListe(opts?: { sparte?: string; lehrjahr?: string }) {
|
||||
let data = MOCK_STELLEN.filter((s) => s.aktiv)
|
||||
if (opts?.sparte) data = data.filter((s) => s.sparte === opts.sparte)
|
||||
if (opts?.lehrjahr) data = data.filter((s) => s.lehrjahr === opts.lehrjahr)
|
||||
return { data, isLoading: false, refetch: () => {}, isRefetching: false }
|
||||
const { data, isLoading, refetch, isFetching } = trpc.stellen.listPublic.useQuery({
|
||||
sparte: opts?.sparte,
|
||||
lehrjahr: opts?.lehrjahr,
|
||||
})
|
||||
|
||||
return {
|
||||
data: data ?? [],
|
||||
isLoading,
|
||||
refetch,
|
||||
isRefetching: isFetching,
|
||||
}
|
||||
}
|
||||
|
||||
export function useStelleDetail(id: string) {
|
||||
const data = MOCK_STELLEN.find((s) => s.id === id) ?? null
|
||||
return { data, isLoading: false }
|
||||
const { data, isLoading } = trpc.stellen.byId.useQuery({ id })
|
||||
return { data: data ?? null, isLoading }
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
import { useState } from 'react'
|
||||
import { MOCK_TERMINE } from '@/lib/mock-data'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
export function useTermineListe(upcoming = true) {
|
||||
const now = new Date()
|
||||
const data = MOCK_TERMINE.filter((t) =>
|
||||
upcoming ? t.datum >= now : t.datum < now
|
||||
).sort((a, b) =>
|
||||
upcoming ? a.datum.getTime() - b.datum.getTime() : b.datum.getTime() - a.datum.getTime()
|
||||
)
|
||||
return { data, isLoading: false, refetch: () => {}, isRefetching: false }
|
||||
const { data, isLoading, refetch, isFetching } = trpc.termine.list.useQuery({ upcoming })
|
||||
|
||||
return {
|
||||
data: data ?? [],
|
||||
isLoading,
|
||||
refetch,
|
||||
isRefetching: isFetching,
|
||||
}
|
||||
}
|
||||
|
||||
export function useTerminDetail(id: string) {
|
||||
const data = MOCK_TERMINE.find((t) => t.id === id) ?? null
|
||||
return { data, isLoading: false }
|
||||
const { data, isLoading } = trpc.termine.byId.useQuery({ id })
|
||||
return { data: data ?? null, isLoading }
|
||||
}
|
||||
|
||||
export function useToggleAnmeldung() {
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const utils = trpc.useUtils()
|
||||
const mutation = trpc.termine.toggleAnmeldung.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.termine.list.invalidate()
|
||||
utils.termine.byId.invalidate()
|
||||
},
|
||||
})
|
||||
|
||||
function mutate({ terminId }: { terminId: string }) {
|
||||
setIsPending(true)
|
||||
const termin = MOCK_TERMINE.find((t) => t.id === terminId)
|
||||
if (termin) {
|
||||
termin.isAngemeldet = !termin.isAngemeldet
|
||||
termin.teilnehmerAnzahl += termin.isAngemeldet ? 1 : -1
|
||||
}
|
||||
setTimeout(() => setIsPending(false), 300)
|
||||
return {
|
||||
mutate: ({ terminId }: { terminId: string }) => mutation.mutate({ terminId }),
|
||||
isPending: mutation.isPending,
|
||||
}
|
||||
|
||||
return { mutate, isPending }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createAuthClient } from 'better-auth/react'
|
||||
import { magicLinkClient } from 'better-auth/client/plugins'
|
||||
import Constants from 'expo-constants'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
const apiUrl =
|
||||
Constants.expoConfig?.extra?.apiUrl ??
|
||||
@@ -10,4 +11,15 @@ const apiUrl =
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: apiUrl,
|
||||
plugins: [magicLinkClient()],
|
||||
fetchOptions: {
|
||||
customFetchImpl: async (url, options) => {
|
||||
const token = await AsyncStorage.getItem('better-auth-session')
|
||||
const headers = new Headers((options?.headers as HeadersInit) ?? {})
|
||||
headers.set('origin', apiUrl)
|
||||
if (token) {
|
||||
headers.set('cookie', `better-auth.session_token=${token}`)
|
||||
}
|
||||
return fetch(url, { ...options, headers })
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import { MOCK_MEMBER_ME } from '@/lib/mock-data'
|
||||
import { authClient } from '@/lib/auth-client'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
interface Session {
|
||||
user: { id: string; email: string; name: string }
|
||||
@@ -9,26 +10,48 @@ interface AuthState {
|
||||
session: Session | null
|
||||
isInitialized: boolean
|
||||
initialize: () => Promise<void>
|
||||
setSession: (session: Session | null, token?: string) => Promise<void>
|
||||
signOut: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
// Mock: direkt eingeloggt
|
||||
session: {
|
||||
user: {
|
||||
id: MOCK_MEMBER_ME.userId!,
|
||||
email: MOCK_MEMBER_ME.email,
|
||||
name: MOCK_MEMBER_ME.name,
|
||||
},
|
||||
},
|
||||
isInitialized: true,
|
||||
session: null,
|
||||
isInitialized: false,
|
||||
|
||||
initialize: async () => {
|
||||
// Mock: nichts zu tun
|
||||
set({ isInitialized: true })
|
||||
try {
|
||||
// Check if we have a stored token and validate it
|
||||
const token = await AsyncStorage.getItem('better-auth-session')
|
||||
if (!token) {
|
||||
set({ session: null, isInitialized: true })
|
||||
return
|
||||
}
|
||||
// authClient now sends the token via cookie header (see auth-client.ts)
|
||||
const result = await authClient.getSession()
|
||||
if (result?.data?.user) {
|
||||
set({
|
||||
session: { user: result.data.user },
|
||||
isInitialized: true,
|
||||
})
|
||||
} else {
|
||||
await AsyncStorage.removeItem('better-auth-session')
|
||||
set({ session: null, isInitialized: true })
|
||||
}
|
||||
} catch {
|
||||
set({ session: null, isInitialized: true })
|
||||
}
|
||||
},
|
||||
|
||||
setSession: async (session, token) => {
|
||||
if (token) {
|
||||
await AsyncStorage.setItem('better-auth-session', token)
|
||||
}
|
||||
set({ session })
|
||||
},
|
||||
|
||||
signOut: async () => {
|
||||
await authClient.signOut()
|
||||
await AsyncStorage.removeItem('better-auth-session')
|
||||
set({ session: null })
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"type-check": "turbo type-check",
|
||||
"db:generate": "pnpm --filter @innungsapp/shared prisma generate",
|
||||
"db:migrate": "pnpm --filter @innungsapp/shared prisma migrate dev",
|
||||
"db:push": "pnpm --filter @innungsapp/shared prisma db push",
|
||||
"db:studio": "pnpm --filter @innungsapp/shared prisma studio",
|
||||
"db:seed": "pnpm --filter @innungsapp/shared tsx prisma/seed.ts",
|
||||
"db:reset": "pnpm --filter @innungsapp/shared prisma migrate reset"
|
||||
"db:generate": "pnpm --filter @innungsapp/shared prisma:generate",
|
||||
"db:migrate": "pnpm --filter @innungsapp/shared prisma:migrate",
|
||||
"db:push": "pnpm --filter @innungsapp/shared prisma:push",
|
||||
"db:studio": "pnpm --filter @innungsapp/shared prisma:studio",
|
||||
"db:seed": "pnpm --filter @innungsapp/shared prisma:seed",
|
||||
"db:reset": "pnpm --filter @innungsapp/shared prisma:migrate -- --reset"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "^2.3.0",
|
||||
|
||||
0
innungsapp/packages/shared/prisma/dev.db
Normal file
0
innungsapp/packages/shared/prisma/dev.db
Normal file
BIN
innungsapp/packages/shared/prisma/prisma/dev.db
Normal file
BIN
innungsapp/packages/shared/prisma/prisma/dev.db
Normal file
Binary file not shown.
@@ -1,12 +1,14 @@
|
||||
// InnungsApp — Prisma Schema
|
||||
// Stack: PostgreSQL + Prisma ORM + better-auth
|
||||
// Stack: SQLite + Prisma ORM + better-auth
|
||||
// Note: SQLite has no native enum support — enum fields are stored as String.
|
||||
// Valid values are enforced at the application layer (Zod).
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
@@ -87,16 +89,16 @@ model Verification {
|
||||
// =============================================
|
||||
|
||||
model Organization {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
slug String @unique
|
||||
plan Plan @default(pilot)
|
||||
logoUrl String? @map("logo_url")
|
||||
primaryColor String @default("#E63946") @map("primary_color")
|
||||
contactEmail String? @map("contact_email")
|
||||
avvAccepted Boolean @default(false) @map("avv_accepted")
|
||||
slug String @unique
|
||||
plan String @default("pilot") // pilot | standard | pro | verband
|
||||
logoUrl String? @map("logo_url")
|
||||
primaryColor String @default("#E63946") @map("primary_color")
|
||||
contactEmail String? @map("contact_email")
|
||||
avvAccepted Boolean @default(false) @map("avv_accepted")
|
||||
avvAcceptedAt DateTime? @map("avv_accepted_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
members Member[]
|
||||
userRoles UserRole[]
|
||||
@@ -107,62 +109,49 @@ model Organization {
|
||||
@@map("organizations")
|
||||
}
|
||||
|
||||
enum Plan {
|
||||
pilot
|
||||
standard
|
||||
pro
|
||||
verband
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// MEMBERS
|
||||
// =============================================
|
||||
|
||||
model Member {
|
||||
id String @id @default(uuid())
|
||||
orgId String @map("org_id")
|
||||
userId String? @unique @map("user_id") // NULL until magic-link clicked
|
||||
name String
|
||||
betrieb String
|
||||
sparte String
|
||||
ort String
|
||||
telefon String?
|
||||
email String
|
||||
status MemberStatus @default(aktiv)
|
||||
istAusbildungsbetrieb Boolean @default(false) @map("ist_ausbildungsbetrieb")
|
||||
seit Int?
|
||||
avatarUrl String? @map("avatar_url")
|
||||
pushToken String? @map("push_token")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
id String @id @default(uuid())
|
||||
orgId String @map("org_id")
|
||||
userId String? @unique @map("user_id") // NULL until magic-link clicked
|
||||
name String
|
||||
betrieb String
|
||||
sparte String
|
||||
ort String
|
||||
telefon String?
|
||||
email String
|
||||
status String @default("aktiv") // aktiv | ruhend | ausgetreten
|
||||
istAusbildungsbetrieb Boolean @default(false) @map("ist_ausbildungsbetrieb")
|
||||
seit Int?
|
||||
avatarUrl String? @map("avatar_url")
|
||||
pushToken String? @map("push_token")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
newsAuthored News[] @relation("NewsAuthor")
|
||||
stellen Stelle[]
|
||||
terminAnmeldungen TerminAnmeldung[]
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
newsAuthored News[] @relation("NewsAuthor")
|
||||
stellen Stelle[]
|
||||
terminAnmeldungen TerminAnmeldung[]
|
||||
|
||||
@@index([orgId])
|
||||
@@index([status])
|
||||
@@map("members")
|
||||
}
|
||||
|
||||
enum MemberStatus {
|
||||
aktiv
|
||||
ruhend
|
||||
ausgetreten
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// USER ROLES (multi-tenancy)
|
||||
// =============================================
|
||||
|
||||
model UserRole {
|
||||
id String @id @default(uuid())
|
||||
orgId String @map("org_id")
|
||||
userId String @map("user_id")
|
||||
role OrgRole
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id String @id @default(uuid())
|
||||
orgId String @map("org_id")
|
||||
userId String @map("user_id")
|
||||
role String // admin | member
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
@@ -171,24 +160,19 @@ model UserRole {
|
||||
@@map("user_roles")
|
||||
}
|
||||
|
||||
enum OrgRole {
|
||||
admin
|
||||
member
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// NEWS
|
||||
// =============================================
|
||||
|
||||
model News {
|
||||
id String @id @default(uuid())
|
||||
orgId String @map("org_id")
|
||||
authorId String? @map("author_id")
|
||||
id String @id @default(uuid())
|
||||
orgId String @map("org_id")
|
||||
authorId String? @map("author_id")
|
||||
title String
|
||||
body String // Markdown
|
||||
kategorie NewsKategorie
|
||||
publishedAt DateTime? @map("published_at") // NULL = Entwurf
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
body String // Markdown
|
||||
kategorie String // Wichtig | Pruefung | Foerderung | Veranstaltung | Allgemein
|
||||
publishedAt DateTime? @map("published_at") // NULL = Entwurf
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
author Member? @relation("NewsAuthor", fields: [authorId], references: [id], onDelete: SetNull)
|
||||
@@ -200,14 +184,6 @@ model News {
|
||||
@@map("news")
|
||||
}
|
||||
|
||||
enum NewsKategorie {
|
||||
Wichtig
|
||||
Pruefung
|
||||
Foerderung
|
||||
Veranstaltung
|
||||
Allgemein
|
||||
}
|
||||
|
||||
model NewsRead {
|
||||
id String @id @default(uuid())
|
||||
newsId String @map("news_id")
|
||||
@@ -269,13 +245,13 @@ model Termin {
|
||||
id String @id @default(uuid())
|
||||
orgId String @map("org_id")
|
||||
titel String
|
||||
datum DateTime @db.Date
|
||||
datum DateTime
|
||||
uhrzeit String? // stored as "HH:MM"
|
||||
endeDatum DateTime? @map("ende_datum") @db.Date
|
||||
endeDatum DateTime? @map("ende_datum")
|
||||
endeUhrzeit String? @map("ende_uhrzeit")
|
||||
ort String?
|
||||
adresse String?
|
||||
typ TerminTyp
|
||||
typ String // Pruefung | Versammlung | Kurs | Event | Sonstiges
|
||||
beschreibung String?
|
||||
maxTeilnehmer Int? @map("max_teilnehmer") // NULL = unbegrenzt
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -288,14 +264,6 @@ model Termin {
|
||||
@@map("termine")
|
||||
}
|
||||
|
||||
enum TerminTyp {
|
||||
Pruefung
|
||||
Versammlung
|
||||
Kurs
|
||||
Event
|
||||
Sonstiges
|
||||
}
|
||||
|
||||
model TerminAnmeldung {
|
||||
id String @id @default(uuid())
|
||||
terminId String @map("termin_id")
|
||||
|
||||
@@ -2,6 +2,12 @@ import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// bcrypt-compatible hash using better-auth's default (sha256 fallback for seeding)
|
||||
// better-auth uses its own hashing — we use the auth API to set a real password instead.
|
||||
// For seeding we insert a known bcrypt hash for "demo1234".
|
||||
// Generated with: https://bcrypt-generator.com/ (rounds=10)
|
||||
const DEMO_PASSWORD_HASH = '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lHny'
|
||||
|
||||
async function main() {
|
||||
console.log('Seeding database...')
|
||||
|
||||
@@ -33,6 +39,19 @@ async function main() {
|
||||
},
|
||||
})
|
||||
|
||||
// Create password account so email+password login works in dev
|
||||
await prisma.account.upsert({
|
||||
where: { id: 'demo-admin-account-id' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'demo-admin-account-id',
|
||||
accountId: adminUser.id,
|
||||
providerId: 'credential',
|
||||
userId: adminUser.id,
|
||||
password: DEMO_PASSWORD_HASH,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.userRole.upsert({
|
||||
where: { orgId_userId: { orgId: org.id, userId: adminUser.id } },
|
||||
update: {},
|
||||
|
||||
@@ -12,13 +12,16 @@ export type {
|
||||
Stelle,
|
||||
Termin,
|
||||
TerminAnmeldung,
|
||||
Plan,
|
||||
MemberStatus,
|
||||
OrgRole,
|
||||
NewsKategorie,
|
||||
TerminTyp,
|
||||
} from '@prisma/client'
|
||||
|
||||
// SQLite has no native enum support — define string union types manually.
|
||||
// These mirror the valid values stored in the DB (enforced via Zod at the API layer).
|
||||
export type Plan = 'pilot' | 'standard' | 'pro' | 'verband'
|
||||
export type MemberStatus = 'aktiv' | 'ruhend' | 'ausgetreten'
|
||||
export type OrgRole = 'admin' | 'member'
|
||||
export type NewsKategorie = 'Wichtig' | 'Pruefung' | 'Foerderung' | 'Veranstaltung' | 'Allgemein'
|
||||
export type TerminTyp = 'Pruefung' | 'Versammlung' | 'Kurs' | 'Event' | 'Sonstiges'
|
||||
|
||||
// =============================================
|
||||
// UI Display Helpers
|
||||
// =============================================
|
||||
|
||||
219
innungsapp/pnpm-lock.yaml
generated
219
innungsapp/pnpm-lock.yaml
generated
@@ -40,7 +40,7 @@ importers:
|
||||
version: 4.0.11(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
better-auth:
|
||||
specifier: ^1.2.0
|
||||
version: 1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(prisma@5.22.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
version: 1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(prisma@5.22.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@@ -51,8 +51,8 @@ importers:
|
||||
specifier: ^0.460.0
|
||||
version: 0.460.0(react@18.3.1)
|
||||
next:
|
||||
specifier: ^15.0.0
|
||||
version: 15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
specifier: 15.3.4
|
||||
version: 15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
nodemailer:
|
||||
specifier: ^6.9.0
|
||||
version: 6.10.1
|
||||
@@ -1355,54 +1355,105 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@next/env@15.3.4':
|
||||
resolution: {integrity: sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==}
|
||||
|
||||
'@next/env@15.5.12':
|
||||
resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==}
|
||||
|
||||
'@next/eslint-plugin-next@15.5.12':
|
||||
resolution: {integrity: sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.4':
|
||||
resolution: {integrity: sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.12':
|
||||
resolution: {integrity: sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.3.4':
|
||||
resolution: {integrity: sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.5.12':
|
||||
resolution: {integrity: sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.3.4':
|
||||
resolution: {integrity: sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.12':
|
||||
resolution: {integrity: sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.4':
|
||||
resolution: {integrity: sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.12':
|
||||
resolution: {integrity: sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.4':
|
||||
resolution: {integrity: sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.12':
|
||||
resolution: {integrity: sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.4':
|
||||
resolution: {integrity: sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.12':
|
||||
resolution: {integrity: sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.4':
|
||||
resolution: {integrity: sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.12':
|
||||
resolution: {integrity: sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.3.4':
|
||||
resolution: {integrity: sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.12':
|
||||
resolution: {integrity: sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -1814,6 +1865,9 @@ packages:
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
@@ -2484,6 +2538,10 @@ packages:
|
||||
buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
|
||||
busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -4375,6 +4433,28 @@ packages:
|
||||
nested-error-stacks@2.0.1:
|
||||
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
|
||||
|
||||
next@15.3.4:
|
||||
resolution: {integrity: sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
'@playwright/test': ^1.41.2
|
||||
babel-plugin-react-compiler: '*'
|
||||
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
sass: ^1.3.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@playwright/test':
|
||||
optional: true
|
||||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
next@15.5.12:
|
||||
resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
@@ -5224,6 +5304,10 @@ packages:
|
||||
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
strict-uri-encode@2.0.0:
|
||||
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -7123,33 +7207,60 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@next/env@15.5.12': {}
|
||||
'@next/env@15.3.4': {}
|
||||
|
||||
'@next/env@15.5.12':
|
||||
optional: true
|
||||
|
||||
'@next/eslint-plugin-next@15.5.12':
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.12':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.5.12':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.12':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.12':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.12':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.12':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.12':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.12':
|
||||
optional: true
|
||||
|
||||
@@ -7600,6 +7711,8 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -8269,6 +8382,27 @@ snapshots:
|
||||
|
||||
bcp-47-match@2.0.3: {}
|
||||
|
||||
better-auth@1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(prisma@5.22.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
|
||||
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
|
||||
'@better-auth/utils': 0.3.0
|
||||
'@better-fetch/fetch': 1.1.21
|
||||
'@noble/ciphers': 2.1.1
|
||||
'@noble/hashes': 2.0.1
|
||||
better-call: 1.1.8(zod@4.3.6)
|
||||
defu: 6.1.4
|
||||
jose: 6.1.3
|
||||
kysely: 0.28.11
|
||||
nanostores: 1.1.0
|
||||
zod: 4.3.6
|
||||
optionalDependencies:
|
||||
'@prisma/client': 5.22.0(prisma@5.22.0)
|
||||
next: 15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
prisma: 5.22.0
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
better-auth@1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0))(prisma@5.22.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
|
||||
@@ -8290,27 +8424,6 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-dom: 18.3.1(react@19.1.0)
|
||||
|
||||
better-auth@1.4.18(@prisma/client@5.22.0(prisma@5.22.0))(next@15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(prisma@5.22.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
|
||||
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
|
||||
'@better-auth/utils': 0.3.0
|
||||
'@better-fetch/fetch': 1.1.21
|
||||
'@noble/ciphers': 2.1.1
|
||||
'@noble/hashes': 2.0.1
|
||||
better-call: 1.1.8(zod@4.3.6)
|
||||
defu: 6.1.4
|
||||
jose: 6.1.3
|
||||
kysely: 0.28.11
|
||||
nanostores: 1.1.0
|
||||
zod: 4.3.6
|
||||
optionalDependencies:
|
||||
'@prisma/client': 5.22.0(prisma@5.22.0)
|
||||
next: 15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
prisma: 5.22.0
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
better-call@1.1.8(zod@4.3.6):
|
||||
dependencies:
|
||||
'@better-auth/utils': 0.3.0
|
||||
@@ -8378,6 +8491,10 @@ snapshots:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
|
||||
bytes@3.1.2: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
@@ -10822,6 +10939,32 @@ snapshots:
|
||||
|
||||
nested-error-stacks@2.0.1: {}
|
||||
|
||||
next@15.3.4(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 15.3.4
|
||||
'@swc/counter': 0.1.3
|
||||
'@swc/helpers': 0.5.15
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001770
|
||||
postcss: 8.4.31
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
styled-jsx: 5.1.6(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.3.4
|
||||
'@next/swc-darwin-x64': 15.3.4
|
||||
'@next/swc-linux-arm64-gnu': 15.3.4
|
||||
'@next/swc-linux-arm64-musl': 15.3.4
|
||||
'@next/swc-linux-x64-gnu': 15.3.4
|
||||
'@next/swc-linux-x64-musl': 15.3.4
|
||||
'@next/swc-win32-arm64-msvc': 15.3.4
|
||||
'@next/swc-win32-x64-msvc': 15.3.4
|
||||
babel-plugin-react-compiler: 1.0.0
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@15.5.12(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@next/env': 15.5.12
|
||||
@@ -10847,30 +10990,6 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
optional: true
|
||||
|
||||
next@15.5.12(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 15.5.12
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001770
|
||||
postcss: 8.4.31
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
styled-jsx: 5.1.6(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.5.12
|
||||
'@next/swc-darwin-x64': 15.5.12
|
||||
'@next/swc-linux-arm64-gnu': 15.5.12
|
||||
'@next/swc-linux-arm64-musl': 15.5.12
|
||||
'@next/swc-linux-x64-gnu': 15.5.12
|
||||
'@next/swc-linux-x64-musl': 15.5.12
|
||||
'@next/swc-win32-arm64-msvc': 15.5.12
|
||||
'@next/swc-win32-x64-msvc': 15.5.12
|
||||
babel-plugin-react-compiler: 1.0.0
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-exports-info@1.6.0:
|
||||
dependencies:
|
||||
array.prototype.flatmap: 1.3.3
|
||||
@@ -11870,6 +11989,8 @@ snapshots:
|
||||
|
||||
stream-buffers@2.2.0: {}
|
||||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
strict-uri-encode@2.0.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
|
||||
Reference in New Issue
Block a user