v1.6.0: Admin-Passwort-Reset, Login per Nachname, Default-PW-Sperre
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { resetPassword } from './actions';
|
||||
|
||||
interface Props {
|
||||
userId: number;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export default function ResetButton({ userId, userName }: Props) {
|
||||
const [state, action, isPending] = useActionState(resetPassword, undefined);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form
|
||||
action={action}
|
||||
onSubmit={(e) => {
|
||||
if (!confirm(`Passwort von „${userName}" wirklich zurücksetzen?`)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="id" value={userId} />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="px-3 py-1 text-xs font-medium bg-red-100 text-red-700 border border-red-300 rounded hover:bg-red-200 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? 'Bitte warten…' : 'Zurücksetzen'}
|
||||
</button>
|
||||
</form>
|
||||
{state?.success && (
|
||||
<p className="text-xs text-green-700 mt-1 max-w-xs">{state.success}</p>
|
||||
)}
|
||||
{state?.error && (
|
||||
<p className="text-xs text-red-600 mt-1">{state.error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
'use server';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getSession } from '@/lib/session';
|
||||
import { query } from '@/lib/db';
|
||||
|
||||
export interface BeoUser {
|
||||
id: number;
|
||||
kürzel: string | null;
|
||||
name: string;
|
||||
vorname: string | null;
|
||||
pw: string | null;
|
||||
role: string | null;
|
||||
}
|
||||
|
||||
export async function listUsers(): Promise<BeoUser[]> {
|
||||
const session = await getSession();
|
||||
if (!session || !session.role?.includes('admin')) redirect('/');
|
||||
|
||||
const rows = await query(
|
||||
'SELECT id, `kürzel`, name, vorname, pw, role FROM beos ORDER BY name, vorname',
|
||||
[]
|
||||
) as BeoUser[];
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function resetPassword(
|
||||
_prevState: { error?: string; success?: string } | undefined,
|
||||
formData: FormData
|
||||
): Promise<{ error?: string; success?: string }> {
|
||||
const session = await getSession();
|
||||
if (!session || !session.role?.includes('admin')) {
|
||||
return { error: 'Keine Berechtigung.' };
|
||||
}
|
||||
|
||||
const idRaw = formData.get('id');
|
||||
const id = Number(idRaw);
|
||||
if (!id || isNaN(id)) {
|
||||
return { error: 'Ungültige Benutzer-ID.' };
|
||||
}
|
||||
|
||||
await query(
|
||||
'UPDATE beos SET pw = NULL, MustChangePassword = 1 WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
return { success: 'Passwort wurde zurückgesetzt. Der Benutzer muss sich mit dem Standard-Passwort anmelden und es dann ändern.' };
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getSession } from '@/lib/session';
|
||||
import { listUsers } from './actions';
|
||||
import ResetButton from './ResetButton';
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/login');
|
||||
if (session.role === null || !session.role.includes('admin')) redirect('/');
|
||||
|
||||
const users = await listUsers();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white py-4 px-4">
|
||||
<main className="max-w-6xl mx-auto border-2 border-black rounded-lg p-6 bg-[#EEF4FF]">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">Logbuch — Sternwarte Welzheim</h1>
|
||||
<Link href="/" className="text-sm text-blue-600 hover:underline">← Zurück</Link>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Benutzerverwaltung</h2>
|
||||
|
||||
<div className="bg-white border border-gray-300 rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-100 text-gray-700">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-semibold">Kürzel</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Name</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Vorname</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Rolle</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">Passwort</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, idx) => (
|
||||
<tr key={user.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-4 py-3 font-mono">{user.kürzel ?? '—'}</td>
|
||||
<td className="px-4 py-3">{user.name}</td>
|
||||
<td className="px-4 py-3">{user.vorname ?? '—'}</td>
|
||||
<td className="px-4 py-3">{user.role ?? '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
{user.pw == null ? (
|
||||
<span className="text-amber-600 font-medium">Standard</span>
|
||||
) : (
|
||||
<span className="text-green-700">gesetzt</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<ResetButton userId={user.id} userName={`${user.vorname ?? ''} ${user.name}`.trim()} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-xs text-gray-500">
|
||||
„Zurücksetzen“ setzt das Passwort auf NULL. Der Benutzer muss sich danach mit dem Standard-Passwort anmelden und es sofort ändern.
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user