feat: Implement comprehensive member management with user accounts, roles, and password handling for admin and mobile applications.

This commit is contained in:
Timo Knuth
2026-02-27 18:50:17 +01:00
parent 253c3c1c6d
commit 4863d032d9
12 changed files with 148 additions and 115 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { useActionState, useState } from 'react'
import { useActionState } from 'react'
import { changePasswordAndDisableMustChange } from '../actions'
export function ForcePasswordChange({ slug }: { slug: string }) {
@@ -9,25 +9,14 @@ export function ForcePasswordChange({ slug }: { slug: string }) {
return (
<div className="bg-white border rounded-xl p-8 max-w-md w-full shadow-sm">
<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">
Dies ist Ihre erste Anmeldung mit den vom Administrator vergebenen Zugangsdaten.
Bitte vergeben Sie ein neues, sicheres Passwort.
Bitte vergeben Sie jetzt ein persönliches Passwort für Ihren Account.
</p>
</div>
<form action={action} className="space-y-4">
<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>
<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"
required
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"
/>
</div>
@@ -62,7 +51,7 @@ export function ForcePasswordChange({ slug }: { slug: string }) {
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"
>
{isPending ? 'Speichern...' : 'Passwort aktualisieren'}
{isPending ? 'Speichern...' : 'Passwort festlegen'}
</button>
</form>
</div>

View File

@@ -94,7 +94,7 @@ export default async function DashboardLayout({
// @ts-ignore - mustChangePassword is added via additionalFields
if (session.user.mustChangePassword) {
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} />
</div>
)

View File

@@ -119,6 +119,7 @@ export default function MitgliedEditPage({
<div>
<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}>
<option value=""> Bitte wählen </option>
{SPARTEN.map((s) => <option key={s}>{s}</option>)}
</select>
</div>

View File

@@ -11,7 +11,7 @@ export default function MitgliedNeuPage() {
const [form, setForm] = useState({
name: '',
betrieb: '',
sparte: 'Elektrotechnik',
sparte: '',
ort: '',
telefon: '',
email: '',
@@ -55,28 +55,27 @@ export default function MitgliedNeuPage() {
/>
</div>
<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
required
value={form.betrieb}
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"
/>
</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="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>)}
</select>
</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
required
value={form.ort}
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"