From 457a6a284a62aad2da6a1cc6b65a7ce3a9b4638a Mon Sep 17 00:00:00 2001 From: maxsoch Date: Sun, 2 Aug 2026 18:25:04 +0200 Subject: [PATCH] feat: v0.3 improvements - Show software version under site title - Fix background stopping on very wide screens (move gradient to body) - Validate Remote-User header value to avoid showing placeholder when not authenticated - Toggle login/logout button based on auth state via login_url config - Bump version to 0.3.0 --- ROADMAP.md | 2 +- TODO.md | 8 ++++ config/CONFIGURATION.md | 4 +- config/dashboard.yml | 1 + package.json | 2 +- src/app/globals.css | 6 ++- src/app/page.tsx | 19 ++++++++-- src/components/dashboard/Dashboard.tsx | 2 + src/components/dashboard/DashboardHeader.tsx | 39 +++++++++++++++++++- src/lib/validation.ts | 1 + src/lib/version.ts | 3 ++ src/types/config.ts | 1 + 12 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 TODO.md create mode 100644 src/lib/version.ts diff --git a/ROADMAP.md b/ROADMAP.md index 9ffa802..3224f93 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,7 +11,7 @@ Le bouton de bascule thème clair/sombre ne répond pas au toucher sur Firefox i - Tester avec un bouton complètement isolé (hors du header, hors des composants dashboard) - Vérifier si le problème persiste dans une page vide sans CSS gradient -## Filtrage par en-têtes HTTP (ex: Authelia) +## Filtrage par en-têtes HTTP (ex: Authelia) — ✅ fait (v0.2) Masquer/afficher des sections ou des liens en fonction des en-têtes HTTP envoyés par un reverse proxy ou un provider d'authentification (Authelia, Authentik, etc.). diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..378a3dd --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# TODO + +## Sécurité + +- Retirer le token Forgejo de l'URL du remote git dans `.git/config` + (actuellement en clair : `https://maxsoch:@git.socheleau.fr/...`) + → utiliser un credential helper ou SSH à la place +- Vérifier que le token n'apparaît pas ailleurs dans le repo diff --git a/config/CONFIGURATION.md b/config/CONFIGURATION.md index f31ee54..dd29acd 100644 --- a/config/CONFIGURATION.md +++ b/config/CONFIGURATION.md @@ -55,16 +55,18 @@ Configuration optionnelle pour l'authentification via un reverse proxy (Authelia | Champ | Requis | Type | Défaut | Description | |---|---|---|---|---| | `logout_url` | Oui | chaîne (URL) | — | URL de déconnexion (ex: `"https://auth.example.com/logout"`) | +| `login_url` | Non | chaîne (URL) | — | URL de connexion. Un bouton login remplace le bouton logout quand l'utilisateur n'est pas connecté (ex: `"https://auth.example.com"`) | | `header_format` | Non | chaîne | `"Remote-User"` | Nom de l'en-tête HTTP indiquant que l'utilisateur est connecté | | `username` | Non | chaîne | — | Nom de l'en-tête HTTP contenant le nom d'utilisateur (ex: `"Remote-Name"`). Affiche "Hi {valeur}" dans le header. | | `header_user` | Non | chaîne | — | Nom de l'en-tête HTTP pour filtrer par utilisateur (ex: `"Remote-User"`) | | `header_group` | Non | chaîne | — | Nom de l'en-tête HTTP pour filtrer par groupe (ex: `"Remote-Groups"`) | -Quand l'en-tête défini par `header_format` est présent dans la requête, un bouton de déconnexion apparaît à droite du bouton thème. +Quand l'en-tête défini par `header_format` est présent **avec une valeur valide** (non vide, sans placeholder non résolu), un bouton de déconnexion apparaît à droite du bouton thème. Sinon, si `login_url` est défini, un bouton de connexion est affiché à la place. ```yaml authentication: logout_url: "https://auth.example.com/logout" + login_url: "https://auth.example.com" header_format: "Remote-User" ``` diff --git a/config/dashboard.yml b/config/dashboard.yml index fbb4018..4ca2f7d 100644 --- a/config/dashboard.yml +++ b/config/dashboard.yml @@ -12,6 +12,7 @@ theme: authentication: logout_url: "https://auth.socheleau.fr/logout" + login_url: "https://auth.socheleau.fr" header_format: "Remote-User" username: "Remote-Name" header_user: "Remote-User" diff --git a/package.json b/package.json index 9c9584c..121e5a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sochboard", - "version": "0.2.0", + "version": "0.3.0", "private": true, "scripts": { "dev": "node node_modules/next/dist/bin/next dev", diff --git a/src/app/globals.css b/src/app/globals.css index deaf55d..d296763 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -33,12 +33,16 @@ html.theme-light { --focus-ring-offset: #f8fafc; } -.dashboard-root { +body { min-height: 100vh; background: var(--bg-gradient); color: var(--text-primary); } +.dashboard-root { + min-height: 100vh; +} + @keyframes fade-in { from { opacity: 0; diff --git a/src/app/page.tsx b/src/app/page.tsx index 50e6270..e228f39 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,14 @@ import { ErrorDisplay } from "@/components/ui/ErrorDisplay" export const dynamic = "force-dynamic" +function isValidHeaderValue(value: string | null | undefined): value is string { + if (!value) return false + const trimmed = value.trim() + if (trimmed.length === 0) return false + if (trimmed.includes("{") || trimmed.includes("}")) return false + return true +} + function getConfigResult() { try { return { ok: true as const, config: getConfig() } @@ -30,12 +38,17 @@ export default async function Home() { const authHeader = config.authentication?.header_format ?? "Remote-User" const usernameHeader = config.authentication?.username const headersList = await headers() - const isAuthenticated = config.authentication != null && headersList.has(authHeader) - const username = isAuthenticated && usernameHeader ? headersList.get(usernameHeader) ?? null : null + const authValue = config.authentication ? headersList.get(authHeader) : null + const isAuthenticated = isValidHeaderValue(authValue) + const username = isAuthenticated && usernameHeader && isValidHeaderValue(headersList.get(usernameHeader)) + ? headersList.get(usernameHeader)!.trim() + : null const userHeader = config.authentication?.header_user const groupHeader = config.authentication?.header_group - const currentUser = isAuthenticated && userHeader ? headersList.get(userHeader) ?? null : null + const currentUser = isAuthenticated && userHeader && isValidHeaderValue(headersList.get(userHeader)) + ? headersList.get(userHeader)!.trim() + : null const userGroups = isAuthenticated && groupHeader ? (headersList.get(groupHeader) ?? "").split(",").map((g) => g.trim()).filter(Boolean) : [] diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index cc03a06..c812197 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -1,5 +1,6 @@ import { DashboardHeader } from "./DashboardHeader" import { DashboardSection } from "./DashboardSection" +import { VERSION } from "@/lib/version" import type { DashboardConfig } from "@/types/config" interface DashboardProps { @@ -20,6 +21,7 @@ export function Dashboard({ config, isAuthenticated = false, username = null, cu isAuthenticated={isAuthenticated} username={username} accent={config.theme.accent} + version={VERSION} />
{config.sections.map((section, index) => ( diff --git a/src/components/dashboard/DashboardHeader.tsx b/src/components/dashboard/DashboardHeader.tsx index ce3c455..585471d 100644 --- a/src/components/dashboard/DashboardHeader.tsx +++ b/src/components/dashboard/DashboardHeader.tsx @@ -11,9 +11,10 @@ interface DashboardHeaderProps { isAuthenticated?: boolean username?: string | null accent?: string + version?: string } -export function DashboardHeader({ site, initialTheme, authentication, isAuthenticated = false, username = null, accent }: DashboardHeaderProps) { +export function DashboardHeader({ site, initialTheme, authentication, isAuthenticated = false, username = null, accent, version }: DashboardHeaderProps) { const [isLight, setIsLight] = useState(initialTheme === "light") useEffect(() => { @@ -44,6 +45,9 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti

{site.title}

+ {version && ( +

v{version}

+ )}
@@ -103,7 +107,7 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti )} - {isAuthenticated && authentication && ( + {isAuthenticated && authentication ? ( + ) : ( + !isAuthenticated && + authentication?.login_url && ( + + + + ) )}
diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 274a7b0..a273fcb 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -46,6 +46,7 @@ export const siteSchema = z.object({ export const authenticationSchema = z.object({ logout_url: z.string().url("Invalid logout URL").min(1, "Logout URL is required"), + login_url: z.string().url("Invalid login URL").optional(), header_format: z.string().optional().default("Remote-User"), username: z.string().optional(), header_user: z.string().optional(), diff --git a/src/lib/version.ts b/src/lib/version.ts new file mode 100644 index 0000000..8633270 --- /dev/null +++ b/src/lib/version.ts @@ -0,0 +1,3 @@ +import pkg from "../../package.json" + +export const VERSION = pkg.version diff --git a/src/types/config.ts b/src/types/config.ts index 61e3941..5fd7f84 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -41,6 +41,7 @@ export interface SectionConfig { export interface AuthenticationConfig { logout_url: string + login_url?: string header_format?: string username?: string header_user?: string