feat: authentication with logout button and username display

This commit is contained in:
maxsoch 2026-07-24 09:56:30 +02:00
parent 59d49d0299
commit 1c8284e79d
7 changed files with 114 additions and 15 deletions

View file

@ -7,6 +7,8 @@ site:
# Informations du site
theme:
# Apparence
authentication:
# Authentification (optionnel)
sections:
# Liste des sections et liens
```
@ -46,6 +48,24 @@ theme:
background: "gradient"
```
## `authentication`
Configuration optionnelle pour l'authentification via un reverse proxy (Authelia, Authentik, etc.).
| Champ | Requis | Type | Défaut | Description |
|---|---|---|---|---|
| `logout_url` | Oui | chaîne (URL) | — | URL de déconnexion (ex: `"https://auth.example.com/logout"`) |
| `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-User"`). Affiche le nom dans le header quand renseigné. |
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.
```yaml
authentication:
logout_url: "https://auth.example.com/logout"
header_format: "Remote-User"
```
## `sections`
Tableau de sections. Chaque section contient :
@ -124,6 +144,10 @@ theme:
accent: "#8b5cf6"
background: "gradient"
authentication:
logout_url: "https://auth.example.com/logout"
header_format: "Remote-User"
sections:
- id: "services"
title: "Services"

View file

@ -1,7 +1,8 @@
site:
name: "Soch Family"
title: "Les applications de la famille"
#logoUrl: ""
title: " "
#title: "Les applications de la famille"
logoUrl: "https://immich.socheleau.fr/api/assets/e010423f-ca95-45a4-a9ee-9bb1a5a48688/thumbnail?slug=us_6&size=preview&c=IhgKJYiHg4ZfhXh6h5aJp5eAdQlJ&edited=true"
#description: "Accès rapide à nos services"
showFooter: false
@ -10,10 +11,16 @@ theme:
accent: "#8b5cf6"
background: "gradient"
authentication:
logout_url: "https://auth.socheleau.fr/logout"
header_format: "Remote-User"
username: "Remote-User"
sections:
- id: "global"
title: "Global"
description: "Services accessibles à toute la famille"
cardLayout: "horizontal"
links:
- id: "nextcloud"
name: "Nextcloud"

View file

@ -1,3 +1,4 @@
import { headers } from "next/headers"
import { getConfig } from "@/lib/config"
import { Dashboard } from "@/components/dashboard/Dashboard"
import { ErrorDisplay } from "@/components/ui/ErrorDisplay"
@ -18,12 +19,25 @@ function getConfigResult() {
}
}
export default function Home() {
export default async function Home() {
const result = getConfigResult()
if (!result.ok) {
return <ErrorDisplay message={result.message} />
}
return <Dashboard config={result.config} />
const { config } = result
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
return (
<Dashboard
config={config}
isAuthenticated={isAuthenticated}
username={username}
/>
)
}

View file

@ -4,12 +4,20 @@ import type { DashboardConfig } from "@/types/config"
interface DashboardProps {
config: DashboardConfig
isAuthenticated?: boolean
username?: string | null
}
export function Dashboard({ config }: DashboardProps) {
export function Dashboard({ config, isAuthenticated = false, username = null }: DashboardProps) {
return (
<div className="dashboard-root mx-auto flex w-full max-w-7xl flex-col px-4 py-8 sm:px-6 lg:px-8">
<DashboardHeader site={config.site} initialTheme={config.theme.mode} />
<DashboardHeader
site={config.site}
initialTheme={config.theme.mode}
authentication={config.authentication}
isAuthenticated={isAuthenticated}
username={username}
/>
<main className="flex flex-col gap-12">
{config.sections.map((section, index) => (
<DashboardSection key={section.id} section={section} sectionIndex={index} />

View file

@ -1,14 +1,17 @@
"use client"
import { useState, useEffect } from "react"
import type { SiteConfig } from "@/types/config"
import type { SiteConfig, AuthenticationConfig } from "@/types/config"
interface DashboardHeaderProps {
site: SiteConfig
initialTheme: "dark" | "light"
authentication?: AuthenticationConfig
isAuthenticated?: boolean
username?: string | null
}
export function DashboardHeader({ site, initialTheme }: DashboardHeaderProps) {
export function DashboardHeader({ site, initialTheme, authentication, isAuthenticated = false, username = null }: DashboardHeaderProps) {
const [isLight, setIsLight] = useState(initialTheme === "light")
useEffect(() => {
@ -47,10 +50,10 @@ export function DashboardHeader({ site, initialTheme }: DashboardHeaderProps) {
</div>
</div>
<div className="flex items-center gap-3">
{site.description && (
<p className="hidden text-sm text-[var(--text-tertiary)] lg:block">
{site.description}
</p>
{isAuthenticated && username && (
<span className="hidden text-sm text-[var(--text-secondary)] lg:block">
{username}
</span>
)}
<button
type="button"
@ -103,11 +106,40 @@ export function DashboardHeader({ site, initialTheme }: DashboardHeaderProps) {
</svg>
)}
</button>
{isAuthenticated && authentication && (
<a
href={authentication.logout_url}
className="flex size-10 items-center justify-center rounded-lg border transition-colors hover:opacity-80 active:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)] sm:size-9"
style={{
backgroundColor: "rgba(128,128,128,0.15)",
borderColor: "rgba(128,128,128,0.25)",
color: "var(--text-tertiary)",
}}
aria-label="Déconnexion"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" x2="9" y1="12" y2="12" />
</svg>
</a>
)}
</div>
</div>
{site.description && (
<p className="text-sm text-[var(--text-tertiary)] lg:hidden">
{site.description}
{isAuthenticated && username && (
<p className="text-sm text-[var(--text-secondary)] lg:hidden">
{username}
</p>
)}
</header>

View file

@ -39,6 +39,12 @@ export const siteSchema = z.object({
showFooter: z.boolean().optional().default(true),
})
export const authenticationSchema = z.object({
logout_url: z.string().url("Invalid logout URL").min(1, "Logout URL is required"),
header_format: z.string().optional().default("Remote-User"),
username: z.string().optional(),
})
export const dashboardConfigSchema = z.object({
site: siteSchema,
theme: themeSchema.optional().default({
@ -46,5 +52,6 @@ export const dashboardConfigSchema = z.object({
accent: "#8b5cf6",
background: "gradient",
}),
authentication: authenticationSchema.optional(),
sections: z.array(sectionSchema).default([]),
})

View file

@ -32,8 +32,15 @@ export interface SectionConfig {
links: LinkConfig[]
}
export interface AuthenticationConfig {
logout_url: string
header_format?: string
username?: string
}
export interface DashboardConfig {
site: SiteConfig
theme: ThemeConfig
authentication?: AuthenticationConfig
sections: SectionConfig[]
}