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
This commit is contained in:
parent
4807899b38
commit
aa3af25ff3
|
|
@ -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)
|
- 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
|
- 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.).
|
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.).
|
||||||
|
|
||||||
|
|
|
||||||
8
TODO.md
Normal file
8
TODO.md
Normal file
|
|
@ -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:<token>@git.socheleau.fr/...`)
|
||||||
|
→ utiliser un credential helper ou SSH à la place
|
||||||
|
- Vérifier que le token n'apparaît pas ailleurs dans le repo
|
||||||
|
|
@ -55,16 +55,18 @@ Configuration optionnelle pour l'authentification via un reverse proxy (Authelia
|
||||||
| Champ | Requis | Type | Défaut | Description |
|
| Champ | Requis | Type | Défaut | Description |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `logout_url` | Oui | chaîne (URL) | — | URL de déconnexion (ex: `"https://auth.example.com/logout"`) |
|
| `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é |
|
| `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. |
|
| `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_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"`) |
|
| `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
|
```yaml
|
||||||
authentication:
|
authentication:
|
||||||
logout_url: "https://auth.example.com/logout"
|
logout_url: "https://auth.example.com/logout"
|
||||||
|
login_url: "https://auth.example.com"
|
||||||
header_format: "Remote-User"
|
header_format: "Remote-User"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ theme:
|
||||||
|
|
||||||
authentication:
|
authentication:
|
||||||
logout_url: "https://auth.socheleau.fr/logout"
|
logout_url: "https://auth.socheleau.fr/logout"
|
||||||
|
login_url: "https://auth.socheleau.fr"
|
||||||
header_format: "Remote-User"
|
header_format: "Remote-User"
|
||||||
username: "Remote-Name"
|
username: "Remote-Name"
|
||||||
header_user: "Remote-User"
|
header_user: "Remote-User"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sochboard",
|
"name": "sochboard",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node node_modules/next/dist/bin/next dev",
|
"dev": "node node_modules/next/dist/bin/next dev",
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,16 @@ html.theme-light {
|
||||||
--focus-ring-offset: #f8fafc;
|
--focus-ring-offset: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-root {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-gradient);
|
background: var(--bg-gradient);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-root {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,14 @@ import { ErrorDisplay } from "@/components/ui/ErrorDisplay"
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
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() {
|
function getConfigResult() {
|
||||||
try {
|
try {
|
||||||
return { ok: true as const, config: getConfig() }
|
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 authHeader = config.authentication?.header_format ?? "Remote-User"
|
||||||
const usernameHeader = config.authentication?.username
|
const usernameHeader = config.authentication?.username
|
||||||
const headersList = await headers()
|
const headersList = await headers()
|
||||||
const isAuthenticated = config.authentication != null && headersList.has(authHeader)
|
const authValue = config.authentication ? headersList.get(authHeader) : null
|
||||||
const username = isAuthenticated && usernameHeader ? headersList.get(usernameHeader) ?? null : 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 userHeader = config.authentication?.header_user
|
||||||
const groupHeader = config.authentication?.header_group
|
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
|
const userGroups = isAuthenticated && groupHeader
|
||||||
? (headersList.get(groupHeader) ?? "").split(",").map((g) => g.trim()).filter(Boolean)
|
? (headersList.get(groupHeader) ?? "").split(",").map((g) => g.trim()).filter(Boolean)
|
||||||
: []
|
: []
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { DashboardHeader } from "./DashboardHeader"
|
import { DashboardHeader } from "./DashboardHeader"
|
||||||
import { DashboardSection } from "./DashboardSection"
|
import { DashboardSection } from "./DashboardSection"
|
||||||
|
import { VERSION } from "@/lib/version"
|
||||||
import type { DashboardConfig } from "@/types/config"
|
import type { DashboardConfig } from "@/types/config"
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
|
|
@ -20,6 +21,7 @@ export function Dashboard({ config, isAuthenticated = false, username = null, cu
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
username={username}
|
username={username}
|
||||||
accent={config.theme.accent}
|
accent={config.theme.accent}
|
||||||
|
version={VERSION}
|
||||||
/>
|
/>
|
||||||
<main className="flex flex-col gap-12">
|
<main className="flex flex-col gap-12">
|
||||||
{config.sections.map((section, index) => (
|
{config.sections.map((section, index) => (
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ interface DashboardHeaderProps {
|
||||||
isAuthenticated?: boolean
|
isAuthenticated?: boolean
|
||||||
username?: string | null
|
username?: string | null
|
||||||
accent?: string
|
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")
|
const [isLight, setIsLight] = useState(initialTheme === "light")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -44,6 +45,9 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
<p className="mt-0.5 text-sm text-[var(--text-secondary)]">
|
<p className="mt-0.5 text-sm text-[var(--text-secondary)]">
|
||||||
{site.title}
|
{site.title}
|
||||||
</p>
|
</p>
|
||||||
|
{version && (
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">v{version}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -103,7 +107,7 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{isAuthenticated && authentication && (
|
{isAuthenticated && authentication ? (
|
||||||
<a
|
<a
|
||||||
href={authentication.logout_url}
|
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"
|
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"
|
||||||
|
|
@ -131,6 +135,37 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
<line x1="21" x2="9" y1="12" y2="12" />
|
<line x1="21" x2="9" y1="12" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
) : (
|
||||||
|
!isAuthenticated &&
|
||||||
|
authentication?.login_url && (
|
||||||
|
<a
|
||||||
|
href={authentication.login_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="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="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" x2="3" y1="12" y2="12" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export const siteSchema = z.object({
|
||||||
|
|
||||||
export const authenticationSchema = z.object({
|
export const authenticationSchema = z.object({
|
||||||
logout_url: z.string().url("Invalid logout URL").min(1, "Logout URL is required"),
|
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"),
|
header_format: z.string().optional().default("Remote-User"),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
header_user: z.string().optional(),
|
header_user: z.string().optional(),
|
||||||
|
|
|
||||||
3
src/lib/version.ts
Normal file
3
src/lib/version.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import pkg from "../../package.json"
|
||||||
|
|
||||||
|
export const VERSION = pkg.version
|
||||||
|
|
@ -41,6 +41,7 @@ export interface SectionConfig {
|
||||||
|
|
||||||
export interface AuthenticationConfig {
|
export interface AuthenticationConfig {
|
||||||
logout_url: string
|
logout_url: string
|
||||||
|
login_url?: string
|
||||||
header_format?: string
|
header_format?: string
|
||||||
username?: string
|
username?: string
|
||||||
header_user?: string
|
header_user?: string
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue