feat: authentication with logout button and username display
This commit is contained in:
parent
59d49d0299
commit
1c8284e79d
|
|
@ -7,6 +7,8 @@ site:
|
||||||
# Informations du site
|
# Informations du site
|
||||||
theme:
|
theme:
|
||||||
# Apparence
|
# Apparence
|
||||||
|
authentication:
|
||||||
|
# Authentification (optionnel)
|
||||||
sections:
|
sections:
|
||||||
# Liste des sections et liens
|
# Liste des sections et liens
|
||||||
```
|
```
|
||||||
|
|
@ -46,6 +48,24 @@ theme:
|
||||||
background: "gradient"
|
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`
|
## `sections`
|
||||||
|
|
||||||
Tableau de sections. Chaque section contient :
|
Tableau de sections. Chaque section contient :
|
||||||
|
|
@ -124,6 +144,10 @@ theme:
|
||||||
accent: "#8b5cf6"
|
accent: "#8b5cf6"
|
||||||
background: "gradient"
|
background: "gradient"
|
||||||
|
|
||||||
|
authentication:
|
||||||
|
logout_url: "https://auth.example.com/logout"
|
||||||
|
header_format: "Remote-User"
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
- id: "services"
|
- id: "services"
|
||||||
title: "Services"
|
title: "Services"
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
site:
|
site:
|
||||||
name: "Soch Family"
|
name: "Soch Family"
|
||||||
title: "Les applications de la famille"
|
title: " "
|
||||||
#logoUrl: ""
|
#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"
|
#description: "Accès rapide à nos services"
|
||||||
showFooter: false
|
showFooter: false
|
||||||
|
|
||||||
|
|
@ -10,10 +11,16 @@ theme:
|
||||||
accent: "#8b5cf6"
|
accent: "#8b5cf6"
|
||||||
background: "gradient"
|
background: "gradient"
|
||||||
|
|
||||||
|
authentication:
|
||||||
|
logout_url: "https://auth.socheleau.fr/logout"
|
||||||
|
header_format: "Remote-User"
|
||||||
|
username: "Remote-User"
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
- id: "global"
|
- id: "global"
|
||||||
title: "Global"
|
title: "Global"
|
||||||
description: "Services accessibles à toute la famille"
|
description: "Services accessibles à toute la famille"
|
||||||
|
cardLayout: "horizontal"
|
||||||
links:
|
links:
|
||||||
- id: "nextcloud"
|
- id: "nextcloud"
|
||||||
name: "Nextcloud"
|
name: "Nextcloud"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { headers } from "next/headers"
|
||||||
import { getConfig } from "@/lib/config"
|
import { getConfig } from "@/lib/config"
|
||||||
import { Dashboard } from "@/components/dashboard/Dashboard"
|
import { Dashboard } from "@/components/dashboard/Dashboard"
|
||||||
import { ErrorDisplay } from "@/components/ui/ErrorDisplay"
|
import { ErrorDisplay } from "@/components/ui/ErrorDisplay"
|
||||||
|
|
@ -18,12 +19,25 @@ function getConfigResult() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default async function Home() {
|
||||||
const result = getConfigResult()
|
const result = getConfigResult()
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
return <ErrorDisplay message={result.message} />
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,20 @@ import type { DashboardConfig } from "@/types/config"
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
config: DashboardConfig
|
config: DashboardConfig
|
||||||
|
isAuthenticated?: boolean
|
||||||
|
username?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dashboard({ config }: DashboardProps) {
|
export function Dashboard({ config, isAuthenticated = false, username = null }: DashboardProps) {
|
||||||
return (
|
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">
|
<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">
|
<main className="flex flex-col gap-12">
|
||||||
{config.sections.map((section, index) => (
|
{config.sections.map((section, index) => (
|
||||||
<DashboardSection key={section.id} section={section} sectionIndex={index} />
|
<DashboardSection key={section.id} section={section} sectionIndex={index} />
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import type { SiteConfig } from "@/types/config"
|
import type { SiteConfig, AuthenticationConfig } from "@/types/config"
|
||||||
|
|
||||||
interface DashboardHeaderProps {
|
interface DashboardHeaderProps {
|
||||||
site: SiteConfig
|
site: SiteConfig
|
||||||
initialTheme: "dark" | "light"
|
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")
|
const [isLight, setIsLight] = useState(initialTheme === "light")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -47,10 +50,10 @@ export function DashboardHeader({ site, initialTheme }: DashboardHeaderProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{site.description && (
|
{isAuthenticated && username && (
|
||||||
<p className="hidden text-sm text-[var(--text-tertiary)] lg:block">
|
<span className="hidden text-sm text-[var(--text-secondary)] lg:block">
|
||||||
{site.description}
|
{username}
|
||||||
</p>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -103,11 +106,40 @@ export function DashboardHeader({ site, initialTheme }: DashboardHeaderProps) {
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{site.description && (
|
{isAuthenticated && username && (
|
||||||
<p className="text-sm text-[var(--text-tertiary)] lg:hidden">
|
<p className="text-sm text-[var(--text-secondary)] lg:hidden">
|
||||||
{site.description}
|
{username}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ export const siteSchema = z.object({
|
||||||
showFooter: z.boolean().optional().default(true),
|
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({
|
export const dashboardConfigSchema = z.object({
|
||||||
site: siteSchema,
|
site: siteSchema,
|
||||||
theme: themeSchema.optional().default({
|
theme: themeSchema.optional().default({
|
||||||
|
|
@ -46,5 +52,6 @@ export const dashboardConfigSchema = z.object({
|
||||||
accent: "#8b5cf6",
|
accent: "#8b5cf6",
|
||||||
background: "gradient",
|
background: "gradient",
|
||||||
}),
|
}),
|
||||||
|
authentication: authenticationSchema.optional(),
|
||||||
sections: z.array(sectionSchema).default([]),
|
sections: z.array(sectionSchema).default([]),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,15 @@ export interface SectionConfig {
|
||||||
links: LinkConfig[]
|
links: LinkConfig[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthenticationConfig {
|
||||||
|
logout_url: string
|
||||||
|
header_format?: string
|
||||||
|
username?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface DashboardConfig {
|
export interface DashboardConfig {
|
||||||
site: SiteConfig
|
site: SiteConfig
|
||||||
theme: ThemeConfig
|
theme: ThemeConfig
|
||||||
|
authentication?: AuthenticationConfig
|
||||||
sections: SectionConfig[]
|
sections: SectionConfig[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue