From 0ea960259c9143abec14c8adfea71bd311e7a34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Mon, 11 May 2026 12:20:44 +0200 Subject: [PATCH] v1.6.0: Admin-Passwort-Reset, Login per Nachname, Default-PW-Sperre --- ACHTUNG.md | 2 +- CLAUDE.md | 2 +- app/MainClient.tsx | 11 +++++- app/admin/ResetButton.tsx | 41 +++++++++++++++++++++ app/admin/actions.ts | 48 +++++++++++++++++++++++++ app/admin/page.tsx | 65 ++++++++++++++++++++++++++++++++++ app/change-password/actions.ts | 4 +++ app/login/actions.ts | 12 +++---- app/login/page.tsx | 4 +-- app/page.tsx | 1 + lib/auth.ts | 18 ++++++++-- package.json | 2 +- 12 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 app/admin/ResetButton.tsx create mode 100644 app/admin/actions.ts create mode 100644 app/admin/page.tsx diff --git a/ACHTUNG.md b/ACHTUNG.md index 9220c9d..6c203ed 100644 --- a/ACHTUNG.md +++ b/ACHTUNG.md @@ -3,7 +3,7 @@ Die Remote-Datenbank auf logbuch.fuerst-.stuttgart.de wird zum lokalen entwickeln über einen SSH-Tunner erreicht: ~~~ - ssh -L 3336:localhost:3336 rxf@logbuch.fuerst-stuttgart.de -N + ssh -L 3336:localhost:3336 rxf@logbuch.fuerst-stuttgart.de -N ~~~ Dieser ist vor dem Starten des Programme einmal einzurichten!! diff --git a/CLAUDE.md b/CLAUDE.md index b040693..2a25e67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ No test suite exists. Deploy via `./deploy.sh [tag]` — builds multiplatform Do Next.js 16 App Router application. All pages are server components; interactive parts are Client Components in `app/MainClient.tsx` and `components/`. -**Auth flow**: Users come from the existing MySQL `beos` table (not a separate users table). Login via `app/login/actions.ts` → `lib/auth.ts` (bcryptjs). Sessions are JWT cookies via jose (`lib/session.ts`, 1-hour expiry). If `pw IS NULL`, the default password is `logbuch123` and `mustChangePassword` is forced to `true`. The middleware file exports `proxy` (not `middleware`) — Next.js 16 requirement. +**Auth flow**: Users come from the existing MySQL `beos` table (not a separate users table). Login via `app/login/actions.ts` → `lib/auth.ts` (bcryptjs). Sessions are JWT cookies via jose (`lib/session.ts`, 1-hour expiry). If `pw IS NULL`, the default password is `welzheim` and `mustChangePassword` is forced to `true`. The middleware file exports `proxy` (not `middleware`) — Next.js 16 requirement. **Database**: MySQL, database name `sternwarte`, via `lib/db.ts` connection pool. The pre-existing `beos` table has non-standard columns: `` `kürzel` `` (umlaut → always needs backticks), `pw`, `id` (all lowercase). The DB charset is **utf8mb4** (collation `utf8mb4_unicode_ci`); connection pool uses `charset: 'utf8mb4'`. diff --git a/app/MainClient.tsx b/app/MainClient.tsx index 8c169c7..8345bbf 100644 --- a/app/MainClient.tsx +++ b/app/MainClient.tsx @@ -12,9 +12,10 @@ interface Props { kuerzel: string; beoId: number; beoName: string; + role: string | null; } -export default function MainClient({ kuerzel, beoId, beoName }: Props) { +export default function MainClient({ kuerzel, beoId, beoName, role }: Props) { const [activeKuppel, setActiveKuppel] = useState('West'); const [activeTab, setActiveTab] = useState<'eingabe' | 'liste' | 'statistik'>('eingabe'); const [refreshKey, setRefreshKey] = useState(0); @@ -54,6 +55,14 @@ export default function MainClient({ kuerzel, beoId, beoName }: Props) { Logbuch für {activeKuppel}-Kuppel
+ {role?.includes('admin') && ( + + Admin + + )} + + {state?.success && ( +

{state.success}

+ )} + {state?.error && ( +

{state.error}

+ )} +
+ ); +} diff --git a/app/admin/actions.ts b/app/admin/actions.ts new file mode 100644 index 0000000..c8d488d --- /dev/null +++ b/app/admin/actions.ts @@ -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 { + 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.' }; +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..2d3a8aa --- /dev/null +++ b/app/admin/page.tsx @@ -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 ( +
+
+
+

Logbuch — Sternwarte Welzheim

+ ← Zurück +
+ +

Benutzerverwaltung

+ +
+ + + + + + + + + + + + + {users.map((user, idx) => ( + + + + + + + + + ))} + +
KürzelNameVornameRollePasswort
{user.kürzel ?? '—'}{user.name}{user.vorname ?? '—'}{user.role ?? '—'} + {user.pw == null ? ( + Standard + ) : ( + gesetzt + )} + + +
+
+ +

+ „Zurücksetzen“ setzt das Passwort auf NULL. Der Benutzer muss sich danach mit dem Standard-Passwort anmelden und es sofort ändern. +

+
+
+ ); +} diff --git a/app/change-password/actions.ts b/app/change-password/actions.ts index a263159..84fda42 100644 --- a/app/change-password/actions.ts +++ b/app/change-password/actions.ts @@ -19,6 +19,10 @@ export async function changePassword( return { error: 'Das Passwort muss mindestens 6 Zeichen lang sein.' }; } + if (newPassword === 'welzheim') { + return { error: 'Das Standard-Passwort darf nicht als neues Passwort verwendet werden.' }; + } + if (newPassword !== confirmPassword) { return { error: 'Die Passwörter stimmen nicht überein.' }; } diff --git a/app/login/actions.ts b/app/login/actions.ts index c99a757..8e4f10e 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -8,23 +8,23 @@ export async function login( _prevState: { error: string } | undefined, formData: FormData ): Promise<{ error: string }> { - const kuerzel = (formData.get('username') as string)?.trim(); + const login = (formData.get('username') as string)?.trim(); const password = formData.get('password') as string; - if (!kuerzel || !password) { - return { error: 'Bitte Kürzel und Passwort eingeben.' }; + if (!login || !password) { + return { error: 'Bitte Kürzel/Nachname und Passwort eingeben.' }; } - const result = await verifyCredentials(kuerzel, password); + const result = await verifyCredentials(login, password); if (!result || !result.valid) { - return { error: 'Ungültiges Kürzel oder Passwort.' }; + return { error: 'Ungültiger Benutzername oder Passwort.' }; } const mustChange = result.beo.MustChangePassword === 1 || !result.beo.pw; await createSession({ - kuerzel: result.beo.kürzel ?? kuerzel, + kuerzel: result.beo.kürzel ?? login, beoId: result.beo.id, beoName: getBeoDisplayName(result.beo), mustChangePassword: mustChange, diff --git a/app/login/page.tsx b/app/login/page.tsx index 1df0f9f..67b6979 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -27,7 +27,7 @@ export default function LoginPage() {
diff --git a/app/page.tsx b/app/page.tsx index 376c387..d526acd 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -11,6 +11,7 @@ export default async function HomePage() { kuerzel={session.kuerzel} beoId={session.beoId} beoName={session.beoName} + role={session.role ?? null} /> ); } diff --git a/lib/auth.ts b/lib/auth.ts index 33a09be..a5440c8 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -19,15 +19,27 @@ export async function getBeoByKuerzel(kuerzel: string): Promise { return rows[0] ?? null; } +export async function getBeoByLogin(login: string): Promise { + // First try exact Kürzel match, then case-insensitive Nachname match + const byKuerzel = await getBeoByKuerzel(login); + if (byKuerzel) return byKuerzel; + + const rows = await query( + 'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM beos WHERE LOWER(name) = LOWER(?)', + [login] + ) as Beo[]; + return rows[0] ?? null; +} + export async function verifyCredentials( - kuerzel: string, + login: string, password: string ): Promise<{ beo: Beo; valid: boolean } | null> { - const beo = await getBeoByKuerzel(kuerzel); + const beo = await getBeoByLogin(login); if (!beo) return null; if (!beo.pw) { - const valid = password === 'logbuch123'; + const valid = password === 'welzheim'; return { beo, valid }; } diff --git a/package.json b/package.json index cf0e39b..1b5eab1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "logbuch", - "version": "1.5.1", + "version": "1.6.0", "private": true, "scripts": { "dev": "next dev",