feat(oidc): implement OIDC authentication flow and user management
This commit is contained in:
@@ -14,6 +14,8 @@ export interface AuthState {
|
||||
authEnabled: boolean;
|
||||
registrationEnabled: boolean;
|
||||
localAuthEnabled: boolean;
|
||||
oidcEnabled: boolean;
|
||||
oidcProviderName: string;
|
||||
hasUsers: boolean;
|
||||
needsSetup: boolean;
|
||||
}
|
||||
@@ -296,51 +298,77 @@ export function LoginForm({ onSuccess, onSwitchToRegister }: { onSuccess?: () =>
|
||||
<h1 className="auth-title">💊 MedAssist</h1>
|
||||
<h2 className="auth-subtitle">{t("auth.login", "Login")}</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
{error && <div className="auth-error">{error}</div>}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">{t("auth.username", "Username")}</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
/>
|
||||
{/* SSO Login Button */}
|
||||
{authState?.oidcEnabled && (
|
||||
<div className="auth-sso">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary auth-submit sso-btn"
|
||||
onClick={() => window.location.href = "/api/auth/oidc/login"}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="sso-icon">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
|
||||
<polyline points="10 17 15 12 10 7"/>
|
||||
<line x1="15" y1="12" x2="3" y2="12"/>
|
||||
</svg>
|
||||
{t("auth.loginWithSSO", "Login with {{provider}}", { provider: authState.oidcProviderName || "SSO" })}
|
||||
</button>
|
||||
{authState?.localAuthEnabled && (
|
||||
<div className="auth-divider">
|
||||
<span>{t("auth.or", "or")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">{t("auth.password", "Password")}</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
{/* Local Login Form - only show if local auth is enabled */}
|
||||
{authState?.localAuthEnabled && (
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
{error && <div className="auth-error">{error}</div>}
|
||||
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">{t("auth.username", "Username")}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
autoComplete="username"
|
||||
autoFocus={!authState?.oidcEnabled}
|
||||
/>
|
||||
<span>{t("auth.rememberMe", "Remember me")}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn btn-primary auth-submit" disabled={loading}>
|
||||
{loading ? t("common.loading", "Loading...") : t("auth.login", "Login")}
|
||||
</button>
|
||||
</form>
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">{t("auth.password", "Password")}</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{authState?.registrationEnabled && onSwitchToRegister && (
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
/>
|
||||
<span>{t("auth.rememberMe", "Remember me")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn btn-primary auth-submit" disabled={loading}>
|
||||
{loading ? t("common.loading", "Loading...") : t("auth.login", "Login")}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{authState?.registrationEnabled && authState?.localAuthEnabled && onSwitchToRegister && (
|
||||
<div className="auth-links">
|
||||
<button type="button" className="auth-link-btn" onClick={onSwitchToRegister}>
|
||||
{t("auth.createAccount", "Create account")}
|
||||
|
||||
@@ -267,7 +267,9 @@
|
||||
"uploadAvatar": "Avatar hochladen",
|
||||
"removeAvatar": "Avatar entfernen",
|
||||
"avatarUpdated": "Avatar aktualisiert",
|
||||
"avatarRemoved": "Avatar entfernt"
|
||||
"avatarRemoved": "Avatar entfernt",
|
||||
"loginWithSSO": "Mit {{provider}} anmelden",
|
||||
"or": "oder"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Wird geladen...",
|
||||
|
||||
@@ -269,7 +269,9 @@
|
||||
"uploadAvatar": "Upload avatar",
|
||||
"removeAvatar": "Remove avatar",
|
||||
"avatarUpdated": "Avatar updated",
|
||||
"avatarRemoved": "Avatar removed"
|
||||
"avatarRemoved": "Avatar removed",
|
||||
"loginWithSSO": "Login with {{provider}}",
|
||||
"or": "or"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
|
||||
@@ -2512,6 +2512,54 @@ h3 .reminder-icon.info-tooltip {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* SSO Login Button */
|
||||
.auth-sso {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sso-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-primary);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sso-btn:hover {
|
||||
background: var(--hover-bg);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.sso-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 1.25rem 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.auth-divider::before,
|
||||
.auth-divider::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border-primary);
|
||||
}
|
||||
|
||||
.auth-form .checkbox-group {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user