feat: Implement comprehensive member management with user accounts, roles, and password handling for admin and mobile applications.
This commit is contained in:
parent
253c3c1c6d
commit
4863d032d9
|
|
@ -2,12 +2,11 @@
|
||||||
|
|
||||||
import { auth, getSanitizedHeaders } from '@/lib/auth'
|
import { auth, getSanitizedHeaders } from '@/lib/auth'
|
||||||
import { prisma } from '@innungsapp/shared'
|
import { prisma } from '@innungsapp/shared'
|
||||||
import { headers } from 'next/headers'
|
|
||||||
import { revalidatePath } from 'next/cache'
|
|
||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
|
// @ts-ignore
|
||||||
|
import { hashPassword } from 'better-auth/crypto'
|
||||||
|
|
||||||
export async function changePasswordAndDisableMustChange(prevState: any, formData: FormData) {
|
export async function changePasswordAndDisableMustChange(prevState: any, formData: FormData) {
|
||||||
const currentPassword = formData.get('currentPassword') as string
|
|
||||||
const newPassword = formData.get('newPassword') as string
|
const newPassword = formData.get('newPassword') as string
|
||||||
const confirmPassword = formData.get('confirmPassword') as string
|
const confirmPassword = formData.get('confirmPassword') as string
|
||||||
|
|
||||||
|
|
@ -25,42 +24,45 @@ export async function changePasswordAndDisableMustChange(prevState: any, formDat
|
||||||
return { success: false, error: 'Nicht authentifiziert.' }
|
return { success: false, error: 'Nicht authentifiziert.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
let redirectUrl: string | null = null
|
const userId = session.user.id
|
||||||
|
|
||||||
try {
|
|
||||||
// Update password using better-auth
|
|
||||||
// This will throw if the current password is invalid or other error occurs
|
|
||||||
await auth.api.changePassword({
|
|
||||||
headers: sanitizedHeaders,
|
|
||||||
body: {
|
|
||||||
newPassword,
|
|
||||||
currentPassword,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update mustChangePassword flag in database
|
|
||||||
await prisma.user.update({
|
|
||||||
where: { id: session.user.id },
|
|
||||||
data: { mustChangePassword: false }
|
|
||||||
})
|
|
||||||
|
|
||||||
const slug = formData.get('slug') as string
|
const slug = formData.get('slug') as string
|
||||||
|
|
||||||
// Sign out so the user has to re-login with the new password
|
// Hash and save new password directly — user is already authenticated so no old password needed
|
||||||
|
const newHash = await hashPassword(newPassword)
|
||||||
|
|
||||||
|
const credAccount = await prisma.account.findFirst({
|
||||||
|
where: { userId, providerId: 'credential' },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (credAccount) {
|
||||||
|
await prisma.account.update({
|
||||||
|
where: { id: credAccount.id },
|
||||||
|
data: { password: newHash },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await prisma.account.create({
|
||||||
|
data: {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
accountId: userId,
|
||||||
|
providerId: 'credential',
|
||||||
|
userId,
|
||||||
|
password: newHash,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear mustChangePassword
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { mustChangePassword: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sign out so the user logs in fresh with the new password
|
||||||
|
try {
|
||||||
await auth.api.signOut({ headers: sanitizedHeaders })
|
await auth.api.signOut({ headers: sanitizedHeaders })
|
||||||
|
} catch {
|
||||||
redirectUrl = `/login?message=password_changed&callbackUrl=/${slug}/dashboard`
|
// ignore
|
||||||
} catch (e: any) {
|
|
||||||
console.error('Password reset exception:', e)
|
|
||||||
// BetterAuth errors often have a message or code
|
|
||||||
const errorMessage = e?.message?.toLowerCase() || ''
|
|
||||||
if (errorMessage.includes('invalid') && errorMessage.includes('password')) {
|
|
||||||
return { success: false, error: 'Das aktuelle Passwort ist nicht korrekt.' }
|
|
||||||
}
|
|
||||||
return { success: false, error: 'Ein unerwarteter Fehler ist aufgetreten.' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectUrl) {
|
redirect(`/login?message=password_changed&callbackUrl=/${slug}/dashboard`)
|
||||||
redirect(redirectUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useActionState, useState } from 'react'
|
import { useActionState } from 'react'
|
||||||
import { changePasswordAndDisableMustChange } from '../actions'
|
import { changePasswordAndDisableMustChange } from '../actions'
|
||||||
|
|
||||||
export function ForcePasswordChange({ slug }: { slug: string }) {
|
export function ForcePasswordChange({ slug }: { slug: string }) {
|
||||||
|
|
@ -9,25 +9,14 @@ export function ForcePasswordChange({ slug }: { slug: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border rounded-xl p-8 max-w-md w-full shadow-sm">
|
<div className="bg-white border rounded-xl p-8 max-w-md w-full shadow-sm">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-xl font-bold text-gray-900 mb-2">Passwort ändern</h1>
|
<h1 className="text-xl font-bold text-gray-900 mb-2">Passwort festlegen</h1>
|
||||||
<p className="text-gray-500 text-sm">
|
<p className="text-gray-500 text-sm">
|
||||||
Dies ist Ihre erste Anmeldung mit den vom Administrator vergebenen Zugangsdaten.
|
Bitte vergeben Sie jetzt ein persönliches Passwort für Ihren Account.
|
||||||
Bitte vergeben Sie ein neues, sicheres Passwort.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action={action} className="space-y-4">
|
<form action={action} className="space-y-4">
|
||||||
<input type="hidden" name="slug" value={slug} />
|
<input type="hidden" name="slug" value={slug} />
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Aktuelles (temporäres) Passwort</label>
|
|
||||||
<input
|
|
||||||
name="currentPassword"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
placeholder="••••••••"
|
|
||||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 outline-none transition-all"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Neues Passwort</label>
|
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Neues Passwort</label>
|
||||||
|
|
@ -36,7 +25,7 @@ export function ForcePasswordChange({ slug }: { slug: string }) {
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
placeholder="••••••••"
|
placeholder="Mindestens 8 Zeichen"
|
||||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 outline-none transition-all"
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,7 +51,7 @@ export function ForcePasswordChange({ slug }: { slug: string }) {
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="w-full bg-gray-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-all shadow-sm"
|
className="w-full bg-gray-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
{isPending ? 'Speichern...' : 'Passwort aktualisieren'}
|
{isPending ? 'Speichern...' : 'Passwort festlegen'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export default async function DashboardLayout({
|
||||||
// @ts-ignore - mustChangePassword is added via additionalFields
|
// @ts-ignore - mustChangePassword is added via additionalFields
|
||||||
if (session.user.mustChangePassword) {
|
if (session.user.mustChangePassword) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-4">
|
<div className="min-h-screen overflow-y-auto bg-gray-50 flex flex-col items-center justify-center p-4">
|
||||||
<ForcePasswordChange slug={slug} />
|
<ForcePasswordChange slug={slug} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ export default function MitgliedEditPage({
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Sparte</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Sparte</label>
|
||||||
<select value={form.sparte} onChange={(e) => setForm({ ...form, sparte: e.target.value })} className={inputClass}>
|
<select value={form.sparte} onChange={(e) => setForm({ ...form, sparte: e.target.value })} className={inputClass}>
|
||||||
|
<option value="">— Bitte wählen —</option>
|
||||||
{SPARTEN.map((s) => <option key={s}>{s}</option>)}
|
{SPARTEN.map((s) => <option key={s}>{s}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export default function MitgliedNeuPage() {
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
betrieb: '',
|
betrieb: '',
|
||||||
sparte: 'Elektrotechnik',
|
sparte: '',
|
||||||
ort: '',
|
ort: '',
|
||||||
telefon: '',
|
telefon: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
|
@ -55,28 +55,27 @@ export default function MitgliedNeuPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Betrieb *</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Betrieb</label>
|
||||||
<input
|
<input
|
||||||
required
|
|
||||||
value={form.betrieb}
|
value={form.betrieb}
|
||||||
onChange={(e) => setForm({ ...form, betrieb: e.target.value })}
|
onChange={(e) => setForm({ ...form, betrieb: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Sparte *</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Sparte</label>
|
||||||
<select
|
<select
|
||||||
value={form.sparte}
|
value={form.sparte}
|
||||||
onChange={(e) => setForm({ ...form, sparte: e.target.value })}
|
onChange={(e) => setForm({ ...form, sparte: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||||
>
|
>
|
||||||
|
<option value="">— Bitte wählen —</option>
|
||||||
{SPARTEN.map((s) => <option key={s}>{s}</option>)}
|
{SPARTEN.map((s) => <option key={s}>{s}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Ort *</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ort</label>
|
||||||
<input
|
<input
|
||||||
required
|
|
||||||
value={form.ort}
|
value={form.ort}
|
||||||
onChange={(e) => setForm({ ...form, ort: e.target.value })}
|
onChange={(e) => setForm({ ...form, ort: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/lib/auth'
|
||||||
|
import { prisma } from '@innungsapp/shared'
|
||||||
|
import { headers } from 'next/headers'
|
||||||
|
// @ts-ignore
|
||||||
|
import { hashPassword } from 'better-auth/crypto'
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() })
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Nicht eingeloggt' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newPassword } = await req.json()
|
||||||
|
if (!newPassword || newPassword.length < 8) {
|
||||||
|
return NextResponse.json({ error: 'Passwort muss mindestens 8 Zeichen haben.' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
const newHash = await hashPassword(newPassword)
|
||||||
|
|
||||||
|
const credAccount = await prisma.account.findFirst({
|
||||||
|
where: { userId, providerId: 'credential' },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (credAccount) {
|
||||||
|
await prisma.account.update({ where: { id: credAccount.id }, data: { password: newHash } })
|
||||||
|
} else {
|
||||||
|
const { randomUUID } = await import('node:crypto')
|
||||||
|
await prisma.account.create({
|
||||||
|
data: { id: randomUUID(), accountId: userId, providerId: 'credential', userId, password: newHash },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({ where: { id: userId }, data: { mustChangePassword: false } })
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
}
|
||||||
|
|
@ -54,7 +54,7 @@ export default function PasswortAendernPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
<div className="min-h-screen overflow-y-auto bg-gray-50 flex items-center justify-center p-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm">
|
||||||
<div className="bg-white rounded-lg border p-8">
|
<div className="bg-white rounded-lg border p-8">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ export const auth = betterAuth({
|
||||||
],
|
],
|
||||||
session: {
|
session: {
|
||||||
cookieCache: {
|
cookieCache: {
|
||||||
enabled: true,
|
enabled: false,
|
||||||
maxAge: 60 * 5, // 5 minutes cache
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,14 @@ async function createUserDirectly(opts: { email: string; name: string; password:
|
||||||
return { id: userId }
|
return { id: userId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nonEmptyString = (min = 2) =>
|
||||||
|
z.preprocess((val) => (val === '' ? undefined : val), z.string().min(min).optional())
|
||||||
|
|
||||||
const MemberInput = z.object({
|
const MemberInput = z.object({
|
||||||
name: z.string().min(2),
|
name: z.string().min(2),
|
||||||
betrieb: z.string().min(2),
|
betrieb: z.preprocess((v) => (v === '' ? undefined : v), z.string().optional()),
|
||||||
sparte: z.string().min(2),
|
sparte: z.preprocess((v) => (v === '' ? undefined : v), z.string().optional()),
|
||||||
ort: z.string().min(2),
|
ort: z.preprocess((v) => (v === '' ? undefined : v), z.string().optional()),
|
||||||
telefon: z.string().optional(),
|
telefon: z.string().optional(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).default('aktiv'),
|
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).default('aktiv'),
|
||||||
|
|
@ -375,7 +378,22 @@ export const membersRouter = router({
|
||||||
* Update member (admin only)
|
* Update member (admin only)
|
||||||
*/
|
*/
|
||||||
update: adminProcedure
|
update: adminProcedure
|
||||||
.input(z.object({ id: z.string(), data: MemberInput.partial().extend({ role: z.enum(['member', 'admin']).optional() }) }))
|
.input(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
data: z.object({
|
||||||
|
name: nonEmptyString(),
|
||||||
|
betrieb: nonEmptyString(),
|
||||||
|
sparte: nonEmptyString(),
|
||||||
|
ort: nonEmptyString(),
|
||||||
|
telefon: z.string().optional(),
|
||||||
|
email: z.preprocess((v) => (v === '' ? undefined : v), z.string().email().optional()),
|
||||||
|
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).optional(),
|
||||||
|
istAusbildungsbetrieb: z.boolean().optional(),
|
||||||
|
seit: z.number().int().min(1900).max(2100).optional(),
|
||||||
|
role: z.enum(['member', 'admin']).optional(),
|
||||||
|
password: z.preprocess((val) => (val === '' ? undefined : val), z.string().min(8).optional()),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { role, password, ...memberData } = input.data
|
const { role, password, ...memberData } = input.data
|
||||||
|
|
||||||
|
|
@ -524,19 +542,12 @@ export const membersRouter = router({
|
||||||
where: { userId: targetUserId, providerId: 'credential' }
|
where: { userId: targetUserId, providerId: 'credential' }
|
||||||
})
|
})
|
||||||
if (existingCredAccount) {
|
if (existingCredAccount) {
|
||||||
const authHeaders = await getSanitizedHeaders()
|
// Update password hash directly — auth.api.updateUser requires Better Auth admin role
|
||||||
let nameForUpdate = memberData.name
|
// which our custom UserRole system doesn't set, causing silent failures.
|
||||||
if (!nameForUpdate) {
|
const newHash = await hashPassword(password)
|
||||||
const existingUser = await ctx.prisma.user.findUnique({ where: { id: targetUserId }, select: { name: true } })
|
await ctx.prisma.account.update({
|
||||||
nameForUpdate = existingUser?.name ?? undefined
|
where: { id: existingCredAccount.id },
|
||||||
}
|
data: { password: newHash },
|
||||||
await auth.api.updateUser({
|
|
||||||
body: {
|
|
||||||
userId: targetUserId,
|
|
||||||
password,
|
|
||||||
...(nameForUpdate && { name: nameForUpdate }),
|
|
||||||
},
|
|
||||||
headers: authHeaders,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// No credential account — create one from scratch using Better Auth's own hash format
|
// No credential account — create one from scratch using Better Auth's own hash format
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ function ChatTabIcon({ color, focused }: { color: string; focused: boolean }) {
|
||||||
|
|
||||||
function ForcePasswordChangeScreen() {
|
function ForcePasswordChangeScreen() {
|
||||||
const { setSession, signOut } = useAuthStore()
|
const { setSession, signOut } = useAuthStore()
|
||||||
const [current, setCurrent] = useState('')
|
|
||||||
const [next, setNext] = useState('')
|
const [next, setNext] = useState('')
|
||||||
const [confirm, setConfirm] = useState('')
|
const [confirm, setConfirm] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
@ -65,32 +64,39 @@ function ForcePasswordChangeScreen() {
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
setError('')
|
setError('')
|
||||||
if (!current) { setError('Bitte temporäres Passwort eingeben.'); return }
|
|
||||||
if (next.length < 8) { setError('Das neue Passwort muss mindestens 8 Zeichen haben.'); return }
|
if (next.length < 8) { setError('Das neue Passwort muss mindestens 8 Zeichen haben.'); return }
|
||||||
if (next !== confirm) { setError('Die Passwörter stimmen nicht überein.'); return }
|
if (next !== confirm) { setError('Die Passwörter stimmen nicht überein.'); return }
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const result = await authClient.changePassword({ currentPassword: current, newPassword: next })
|
// Set password directly via tRPC (no old password needed — user is already authenticated)
|
||||||
|
try {
|
||||||
|
const apiUrl = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3032'
|
||||||
|
const sessionResult = await authClient.getSession()
|
||||||
|
const token = (sessionResult?.data as any)?.session?.token
|
||||||
|
const res = await fetch(`${apiUrl}/api/auth/force-set-password`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ newPassword: next }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok || data.error) {
|
||||||
|
setError(data.error ?? 'Passwort konnte nicht geändert werden.')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
setError(result.error.message ?? 'Passwort konnte nicht geändert werden.')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Update local session state
|
||||||
// Refresh session — mustChangePassword is now false
|
|
||||||
const sessionResult = await authClient.getSession()
|
|
||||||
if (sessionResult?.data?.user) {
|
if (sessionResult?.data?.user) {
|
||||||
const u = sessionResult.data.user as any
|
const u = sessionResult.data.user as any
|
||||||
await setSession({
|
await setSession({ user: { id: u.id, email: u.email, name: u.name, mustChangePassword: false } })
|
||||||
user: {
|
|
||||||
id: u.id,
|
|
||||||
email: u.email,
|
|
||||||
name: u.name,
|
|
||||||
mustChangePassword: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError('Verbindungsfehler. Bitte erneut versuchen.')
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -100,23 +106,11 @@ function ForcePasswordChangeScreen() {
|
||||||
<View style={fpc.iconWrap}>
|
<View style={fpc.iconWrap}>
|
||||||
<Ionicons name="lock-closed-outline" size={32} color="#003B7E" />
|
<Ionicons name="lock-closed-outline" size={32} color="#003B7E" />
|
||||||
</View>
|
</View>
|
||||||
<Text style={fpc.title}>Passwort ändern</Text>
|
<Text style={fpc.title}>Passwort festlegen</Text>
|
||||||
<Text style={fpc.subtitle}>
|
<Text style={fpc.subtitle}>
|
||||||
Ihr Administrator hat ein temporäres Passwort vergeben. Bitte legen Sie jetzt Ihr persönliches Passwort fest.
|
Bitte legen Sie jetzt Ihr persönliches Passwort fest.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={fpc.field}>
|
|
||||||
<Text style={fpc.label}>Temporäres Passwort</Text>
|
|
||||||
<TextInput
|
|
||||||
style={fpc.input}
|
|
||||||
value={current}
|
|
||||||
onChangeText={setCurrent}
|
|
||||||
secureTextEntry
|
|
||||||
placeholder="••••••••"
|
|
||||||
placeholderTextColor="#CBD5E1"
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={fpc.field}>
|
<View style={fpc.field}>
|
||||||
<Text style={fpc.label}>Neues Passwort</Text>
|
<Text style={fpc.label}>Neues Passwort</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|
@ -166,7 +160,7 @@ function ForcePasswordChangeScreen() {
|
||||||
|
|
||||||
const fpc = StyleSheet.create({
|
const fpc = StyleSheet.create({
|
||||||
safe: { flex: 1, backgroundColor: '#F8FAFC' },
|
safe: { flex: 1, backgroundColor: '#F8FAFC' },
|
||||||
content: { flex: 1, justifyContent: 'center', padding: 24 },
|
content: { flexGrow: 1, justifyContent: 'center', padding: 24, paddingBottom: 40 },
|
||||||
card: {
|
card: {
|
||||||
backgroundColor: '#FFFFFF', borderRadius: 20,
|
backgroundColor: '#FFFFFF', borderRadius: 20,
|
||||||
borderWidth: 1, borderColor: '#E2E8F0',
|
borderWidth: 1, borderColor: '#E2E8F0',
|
||||||
|
|
|
||||||
|
|
@ -482,7 +482,7 @@ export default function ProfilScreen() {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safeArea: { flex: 1, backgroundColor: '#F8FAFC' },
|
safeArea: { flex: 1, backgroundColor: '#F8FAFC' },
|
||||||
content: { paddingHorizontal: 18, paddingBottom: 30, gap: 14 },
|
content: { paddingHorizontal: 18, paddingBottom: 120, gap: 14 },
|
||||||
|
|
||||||
// Hero
|
// Hero
|
||||||
hero: {
|
hero: {
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue