Add local username/password login as fallback when Microsoft login isn't available
Neues "users"-Table (bcrypt-Hash, nie Klartext), POST /api/auth/login gibt ein selbst-signiertes JWT (HS256) aus. requireAuth unterscheidet MSAL- (RS256) und lokale Tokens (HS256) anhand des alg-Headers. Server legt beim Start optional ein erstes lokales Konto an, wenn SEED_ADMIN_USERNAME/SEED_ADMIN_PASSWORD als Stack-Env gesetzt sind (idempotent, Klartext-Passwort landet nie im Repo). Frontend: Login-Screen hat jetzt einen Alternativ-Link zu Benutzername/Passwort, App.tsx kombiniert MSAL- und lokalen Auth-Status. Temporärer test-build-only.yml Workflow zur Docker-Build-Verifikation ohne Registry-Push (wird nach dem Test wieder entfernt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7bdf4f2897
commit
97c4aa1c68
@@ -1,12 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { useMsal } from '@azure/msal-react';
|
||||
import { localLogin } from '../lib/localAuth';
|
||||
|
||||
export default function Login() {
|
||||
export default function Login({ onLocalLogin }: { onLocalLogin: (name: string) => void }) {
|
||||
const { instance } = useMsal();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [err, setErr] = useState('');
|
||||
const [showLocal, setShowLocal] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
async function login() {
|
||||
async function loginMicrosoft() {
|
||||
setBusy(true);
|
||||
setErr('');
|
||||
try {
|
||||
@@ -17,6 +21,19 @@ export default function Login() {
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
async function submitLocal(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setBusy(true);
|
||||
setErr('');
|
||||
try {
|
||||
const name = await localLogin(username, password);
|
||||
onLocalLogin(name);
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-screen">
|
||||
<div className="lcard">
|
||||
@@ -24,7 +41,7 @@ export default function Login() {
|
||||
<div className="lsub">Elektrotechnik · Materialschein</div>
|
||||
<div className="ltitle">Willkommen</div>
|
||||
<div className="ldesc">Melde dich mit deinem BergVOLT<br />Office 365 Konto an.</div>
|
||||
<button className="btn-ms" onClick={login} disabled={busy}>
|
||||
<button className="btn-ms" onClick={loginMicrosoft} disabled={busy}>
|
||||
<svg viewBox="0 0 21 21" width="18" height="18">
|
||||
<rect x="0" y="0" width="10" height="10" fill="#f25022" />
|
||||
<rect x="11" y="0" width="10" height="10" fill="#7fba00" />
|
||||
@@ -33,6 +50,25 @@ export default function Login() {
|
||||
</svg>
|
||||
Mit Microsoft anmelden
|
||||
</button>
|
||||
|
||||
{!showLocal && (
|
||||
<button className="btn btn-ghost" style={{ width: '100%', marginTop: 14 }} onClick={() => setShowLocal(true)}>
|
||||
Alternativ mit Benutzername anmelden
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showLocal && (
|
||||
<form onSubmit={submitLocal} style={{ marginTop: 18, textAlign: 'left', display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<label className="field">Benutzername
|
||||
<input value={username} onChange={e => setUsername(e.target.value)} autoComplete="username" autoFocus />
|
||||
</label>
|
||||
<label className="field">Passwort
|
||||
<input type="password" value={password} onChange={e => setPassword(e.target.value)} autoComplete="current-password" />
|
||||
</label>
|
||||
<button className="btn btn-cyan" style={{ width: '100%' }} disabled={busy} type="submit">Anmelden</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{err && <div className="lerr">{err}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user