Skip to content

Commit a541518

Browse files
authored
Harden authentication and improve layout (#226)
* Harden authentication and API security * Show passkey guidance for SSO accounts * Fix stale auto-hidden APT updates Fixes #225 * Make page content full width
1 parent 40acfbc commit a541518

36 files changed

Lines changed: 1216 additions & 144 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ docker run -d \
145145

146146
Set `LUDASH_BASE_URL` to the URL users and integrations actually use. Behind a reverse proxy, set the public HTTPS URL and add `LUDASH_TRUST_PROXY=true`.
147147

148+
The API applies a Content Security Policy, limits request bodies to 1 MiB, rejects cross-origin browser mutations, and returns sensitive API responses with `private, no-store`. Command-line clients that do not send browser `Origin` or `Sec-Fetch-Site` headers remain supported. Configure HSTS at the TLS-terminating proxy when appropriate; the application does not set it itself.
149+
148150
### Docker Compose
149151

150152
```yaml

client/components/Layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const contentWidthClass: Record<LayoutWidth, string> = {
1313
full: "max-w-none",
1414
};
1515

16-
export function Layout({ children, title, actions, contentWidth = "wide" }: {
16+
export function Layout({ children, title, actions, contentWidth = "full" }: {
1717
children: ReactNode;
1818
title: ReactNode;
1919
actions?: ReactNode;

client/context/AuthContext.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { createContext, useContext, useCallback, useEffect, useRef, useState, type ReactNode } from "react";
1+
import {
2+
createContext,
3+
useContext,
4+
useCallback,
5+
useEffect,
6+
useRef,
7+
useState,
8+
type ReactNode,
9+
} from "react";
210
import { ApiError, apiFetch } from "../lib/client";
311

412
interface User {
@@ -13,13 +21,18 @@ export interface AuthState {
1321
oidcEnabled: boolean;
1422
passwordLoginDisabled: boolean;
1523
passkeysEnabled: boolean;
24+
passkeysAvailable: boolean;
1625
hasPassword: boolean;
1726
totpEnabled: boolean;
1827
backendUnavailable: boolean;
1928
}
2029

2130
interface AuthContextType extends AuthState {
22-
login: (username: string, password: string, totpCode?: string) => Promise<void>;
31+
login: (
32+
username: string,
33+
password: string,
34+
totpCode?: string,
35+
) => Promise<void>;
2336
logout: () => Promise<void>;
2437
setup: (username: string, password: string) => Promise<void>;
2538
refresh: () => Promise<void>;
@@ -28,7 +41,9 @@ interface AuthContextType extends AuthState {
2841
const AuthContext = createContext<AuthContextType | null>(null);
2942

3043
export function isHardAuthRefreshFailure(error: unknown): boolean {
31-
return error instanceof ApiError && (error.status === 401 || error.status === 403);
44+
return (
45+
error instanceof ApiError && (error.status === 401 || error.status === 403)
46+
);
3247
}
3348

3449
export function getRecoverableAuthRefreshState(state: AuthState): AuthState {
@@ -47,6 +62,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
4762
oidcEnabled: false,
4863
passwordLoginDisabled: false,
4964
passkeysEnabled: false,
65+
passkeysAvailable: false,
5066
hasPassword: false,
5167
totpEnabled: false,
5268
backendUnavailable: false,
@@ -63,6 +79,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
6379
oidcEnabled: boolean;
6480
passwordLoginDisabled: boolean;
6581
passkeysEnabled: boolean;
82+
passkeysAvailable: boolean;
6683
hasPassword: boolean;
6784
totpEnabled: boolean;
6885
}>("/auth/status");
@@ -74,6 +91,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
7491
oidcEnabled: data.oidcEnabled,
7592
passwordLoginDisabled: data.passwordLoginDisabled,
7693
passkeysEnabled: data.passkeysEnabled,
94+
passkeysAvailable: data.passkeysAvailable,
7795
hasPassword: data.hasPassword,
7896
totpEnabled: data.totpEnabled,
7997
backendUnavailable: false,
@@ -101,7 +119,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
101119
return () => clearTimeout(retryTimer.current);
102120
}, [refresh]);
103121

104-
const login = async (username: string, password: string, totpCode?: string) => {
122+
const login = async (
123+
username: string,
124+
password: string,
125+
totpCode?: string,
126+
) => {
105127
await apiFetch("/auth/login", {
106128
method: "POST",
107129
body: JSON.stringify({ username, password, totpCode }),

client/lib/systems.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export interface HiddenUpdate {
144144
repository: string | null;
145145
isSecurity: number;
146146
isKeptBack: number;
147+
hideReason: "manual" | "kept_back";
147148
active: number;
148149
lastMatchedAt: string;
149150
inactiveSince: string | null;

client/locales/ar.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
"pages.login.continueWithSso": "المتابعة باستخدام SSO",
244244
"pages.login.loginFailed": "فشل تسجيل الدخول",
245245
"pages.login.noCredentialReturned": "لم يتم إرجاع بيانات اعتماد",
246+
"pages.login.oidcOnlyPasskey": "ينتمي مفتاح المرور هذا إلى حساب يستخدم تسجيل الدخول الموحّد فقط. سجّل الدخول عبر SSO بدلاً من ذلك.",
246247
"pages.login.passkeyAuthenticationFailed": "فشلت مصادقة مفتاح المرور",
247248
"pages.login.passkeysRequireASecureContextOrLocalhost": "تتطلب مفاتيح المرور سياقا آمنا (HTTPS أو localhost)",
248249
"pages.login.password": "كلمة المرور",
@@ -907,7 +908,7 @@
907908
"pages.systemDetail.fullUpgrade": "ترقية كاملة",
908909
"pages.systemDetail.fullUpgradeAllMessage": "إجراء ترقية كاملة على {systemName}؟ قد يثبت هذا اعتماديات جديدة أو يزيل حزما قديمة لإكمال ترقية كل الحزم وعددها {count}.",
909910
"pages.systemDetail.fullUpgradeAllPackages": "ترقية كاملة لكل الحزم",
910-
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "مخفي حتى يختفي هذا التحديث بالضبط",
911+
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "تظل التحديثات المخفية يدويًا مخفية حتى يختفي الإصدار المحدد؛ وتعود التحديثات المخفية تلقائيًا عند توقف تأجيلها",
911912
"pages.systemDetail.hiddenUpdates": "التحديثات المخفية",
912913
"pages.systemDetail.hiddenVersion": "الإصدار المخفي",
913914
"pages.systemDetail.hidePackageVersion": "إخفاء {packageName} {version}",

client/locales/de.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
"pages.login.continueWithSso": "Mit SSO fortfahren",
244244
"pages.login.loginFailed": "Anmeldung fehlgeschlagen",
245245
"pages.login.noCredentialReturned": "Keine Anmeldedaten zurückgegeben",
246+
"pages.login.oidcOnlyPasskey": "Dieser Passkey gehört zu einem Konto, das nur SSO verwendet. Melden Sie sich stattdessen per SSO an.",
246247
"pages.login.passkeyAuthenticationFailed": "Passkey-Authentifizierung fehlgeschlagen",
247248
"pages.login.passkeysRequireASecureContextOrLocalhost": "Passkeys erfordern einen sicheren Kontext (HTTPS oder localhost)",
248249
"pages.login.password": "Passwort",
@@ -907,7 +908,7 @@
907908
"pages.systemDetail.fullUpgrade": "Vollständiges Upgrade",
908909
"pages.systemDetail.fullUpgradeAllMessage": "Vollständiges Upgrade auf {systemName} ausführen? Dabei können neue Abhängigkeiten installiert oder veraltete Pakete entfernt werden, um das Upgrade aller {count} Pakete abzuschließen.",
909910
"pages.systemDetail.fullUpgradeAllPackages": "Vollständiges Upgrade aller Pakete",
910-
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "Ausgeblendet, bis genau dieses Update verschwindet",
911+
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "Manuell ausgeblendete Updates bleiben bis zum Verschwinden der exakten Version verborgen; automatisch ausgeblendete Updates kehren zurück, sobald sie nicht mehr zurückgehalten werden",
911912
"pages.systemDetail.hiddenUpdates": "Ausgeblendete Updates",
912913
"pages.systemDetail.hiddenVersion": "Ausgeblendete Version",
913914
"pages.systemDetail.hidePackageVersion": "{packageName} {version} ausblenden",

client/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
"pages.login.continueWithSso": "Continue with SSO",
244244
"pages.login.loginFailed": "Login failed",
245245
"pages.login.noCredentialReturned": "No credential returned",
246+
"pages.login.oidcOnlyPasskey": "This passkey belongs to an SSO-only account. Sign in with SSO instead.",
246247
"pages.login.passkeyAuthenticationFailed": "Passkey authentication failed",
247248
"pages.login.passkeysRequireASecureContextOrLocalhost": "Passkeys require a secure context (HTTPS or localhost)",
248249
"pages.login.password": "Password",
@@ -907,7 +908,7 @@
907908
"pages.systemDetail.fullUpgrade": "Full Upgrade",
908909
"pages.systemDetail.fullUpgradeAllMessage": "Perform a full upgrade on {systemName}? This may install new dependencies or remove obsolete packages to complete the upgrade of all {count} packages.",
909910
"pages.systemDetail.fullUpgradeAllPackages": "Full Upgrade All Packages",
910-
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "Hidden until this exact update disappears",
911+
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "Manual hides last until the exact update disappears; auto-hidden updates return when no longer kept back",
911912
"pages.systemDetail.hiddenUpdates": "Hidden Updates",
912913
"pages.systemDetail.hiddenVersion": "Hidden Version",
913914
"pages.systemDetail.hidePackageVersion": "Hide {packageName} {version}",

client/locales/es.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
"pages.login.continueWithSso": "Continuar con SSO",
244244
"pages.login.loginFailed": "Inicio de sesión fallido",
245245
"pages.login.noCredentialReturned": "No se devolvió ninguna credencial",
246+
"pages.login.oidcOnlyPasskey": "Esta clave de acceso pertenece a una cuenta solo con SSO. Inicie sesión con SSO.",
246247
"pages.login.passkeyAuthenticationFailed": "Autenticación con passkey fallida",
247248
"pages.login.passkeysRequireASecureContextOrLocalhost": "Las passkeys requieren un contexto seguro (HTTPS o localhost)",
248249
"pages.login.password": "Contraseña",
@@ -907,7 +908,7 @@
907908
"pages.systemDetail.fullUpgrade": "Actualización completa",
908909
"pages.systemDetail.fullUpgradeAllMessage": "¿Realizar una actualización completa en {systemName}? Esto puede instalar nuevas dependencias o eliminar paquetes obsoletos para completar la actualización de los {count} paquetes.",
909910
"pages.systemDetail.fullUpgradeAllPackages": "Actualización completa de todos los paquetes",
910-
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "Oculto hasta que desaparezca esta actualización exacta",
911+
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "Las ocultaciones manuales duran hasta que desaparece la versión exacta; las actualizaciones ocultadas automáticamente vuelven cuando dejan de estar retenidas",
911912
"pages.systemDetail.hiddenUpdates": "Actualizaciones ocultas",
912913
"pages.systemDetail.hiddenVersion": "Versión oculta",
913914
"pages.systemDetail.hidePackageVersion": "Ocultar {packageName} {version}",

client/locales/fr.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
"pages.login.continueWithSso": "Continuer avec SSO",
244244
"pages.login.loginFailed": "Connexion échouée",
245245
"pages.login.noCredentialReturned": "Aucun identifiant retourné",
246+
"pages.login.oidcOnlyPasskey": "Cette clé d’accès appartient à un compte utilisant uniquement le SSO. Connectez-vous avec le SSO.",
246247
"pages.login.passkeyAuthenticationFailed": "Authentification par passkey échouée",
247248
"pages.login.passkeysRequireASecureContextOrLocalhost": "Les passkeys nécessitent un contexte sécurisé (HTTPS ou localhost)",
248249
"pages.login.password": "Mot de passe",
@@ -907,7 +908,7 @@
907908
"pages.systemDetail.fullUpgrade": "Mise à niveau complète",
908909
"pages.systemDetail.fullUpgradeAllMessage": "Effectuer une mise à niveau complète sur {systemName} ? Cela peut installer de nouvelles dépendances ou supprimer des paquets obsolètes afin de terminer la mise à niveau des {count} paquets.",
909910
"pages.systemDetail.fullUpgradeAllPackages": "Mise à niveau complète de tous les paquets",
910-
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "Masqué jusqu’à la disparition exacte de cette mise à jour",
911+
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "Les masquages manuels durent jusqu’à la disparition de la version exacte ; les mises à jour masquées automatiquement réapparaissent lorsqu’elles ne sont plus retenues",
911912
"pages.systemDetail.hiddenUpdates": "Mises à jour masquées",
912913
"pages.systemDetail.hiddenVersion": "Version masquée",
913914
"pages.systemDetail.hidePackageVersion": "Masquer {packageName} {version}",

client/locales/hi.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
"pages.login.continueWithSso": "SSO के साथ जारी रखें",
244244
"pages.login.loginFailed": "लॉगिन विफल",
245245
"pages.login.noCredentialReturned": "कोई क्रेडेंशियल वापस नहीं मिला",
246+
"pages.login.oidcOnlyPasskey": "यह पासकी केवल SSO वाले खाते से जुड़ी है। इसके बजाय SSO से साइन इन करें।",
246247
"pages.login.passkeyAuthenticationFailed": "पासकी प्रमाणीकरण विफल",
247248
"pages.login.passkeysRequireASecureContextOrLocalhost": "पासकी के लिए सुरक्षित संदर्भ (HTTPS या localhost) चाहिए",
248249
"pages.login.password": "पासवर्ड",
@@ -907,7 +908,7 @@
907908
"pages.systemDetail.fullUpgrade": "पूर्ण अपग्रेड",
908909
"pages.systemDetail.fullUpgradeAllMessage": "{systemName} पर पूर्ण अपग्रेड करें? यह सभी {count} पैकेजों का अपग्रेड पूरा करने के लिए नई निर्भरताएं इंस्टॉल कर सकता है या पुराने पैकेज हटा सकता है.",
909910
"pages.systemDetail.fullUpgradeAllPackages": "सभी पैकेजों का पूर्ण अपग्रेड",
910-
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "यह exact update गायब होने तक छिपा हुआ",
911+
"pages.systemDetail.hiddenUntilExactUpdateDisappears": "मैन्युअल रूप से छिपे अपडेट सटीक संस्करण के गायब होने तक छिपे रहते हैं; अपने आप छिपे अपडेट रोके न जाने पर फिर दिखाई देते हैं",
911912
"pages.systemDetail.hiddenUpdates": "छिपे अपडेट",
912913
"pages.systemDetail.hiddenVersion": "छिपा संस्करण",
913914
"pages.systemDetail.hidePackageVersion": "{packageName} {version} छिपाएं",

0 commit comments

Comments
 (0)