Compare commits
No commits in common. "main" and "v0.2" have entirely different histories.
|
|
@ -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) — ✅ fait (v0.2)
|
## Filtrage par en-têtes HTTP (ex: Authelia)
|
||||||
|
|
||||||
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
8
TODO.md
|
|
@ -1,8 +0,0 @@
|
||||||
# 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,18 +55,16 @@ 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 **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.
|
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
|
```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,7 +12,6 @@ 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.3.0",
|
"version": "0.2.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,16 +33,12 @@ html.theme-light {
|
||||||
--focus-ring-offset: #f8fafc;
|
--focus-ring-offset: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.dashboard-root {
|
||||||
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,14 +5,6 @@ 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() }
|
||||||
|
|
@ -38,17 +30,12 @@ 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 authValue = config.authentication ? headersList.get(authHeader) : null
|
const isAuthenticated = config.authentication != null && headersList.has(authHeader)
|
||||||
const isAuthenticated = isValidHeaderValue(authValue)
|
const username = isAuthenticated && usernameHeader ? headersList.get(usernameHeader) ?? null : null
|
||||||
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 && isValidHeaderValue(headersList.get(userHeader))
|
const currentUser = isAuthenticated && userHeader ? headersList.get(userHeader) ?? null : null
|
||||||
? 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,6 +1,5 @@
|
||||||
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 {
|
||||||
|
|
@ -21,7 +20,6 @@ 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,10 +11,9 @@ 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, version }: DashboardHeaderProps) {
|
export function DashboardHeader({ site, initialTheme, authentication, isAuthenticated = false, username = null, accent }: DashboardHeaderProps) {
|
||||||
const [isLight, setIsLight] = useState(initialTheme === "light")
|
const [isLight, setIsLight] = useState(initialTheme === "light")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -45,9 +44,6 @@ 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">
|
||||||
|
|
@ -107,7 +103,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"
|
||||||
|
|
@ -135,37 +131,6 @@ 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,7 +46,6 @@ 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(),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import pkg from "../../package.json"
|
|
||||||
|
|
||||||
export const VERSION = pkg.version
|
|
||||||
|
|
@ -41,7 +41,6 @@ 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