feat: visibility filtering by user and group on sections and links
This commit is contained in:
parent
1c8284e79d
commit
d64fe77d13
|
|
@ -56,7 +56,9 @@ Configuration optionnelle pour l'authentification via un reverse proxy (Authelia
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `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"`) |
|
||||||
| `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-User"`). Affiche le nom dans le header quand renseigné. |
|
| `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 dans la requête, un bouton de déconnexion apparaît à droite du bouton thème.
|
||||||
|
|
||||||
|
|
@ -77,6 +79,8 @@ Tableau de sections. Chaque section contient :
|
||||||
| `description` | Non | chaîne | — | Texte sous le titre |
|
| `description` | Non | chaîne | — | Texte sous le titre |
|
||||||
| `cardLayout` | Non | `"horizontal"` ou `"vertical"` | `"horizontal"` | Disposition des cartes dans la section |
|
| `cardLayout` | Non | `"horizontal"` ou `"vertical"` | `"horizontal"` | Disposition des cartes dans la section |
|
||||||
| `links` | Non | tableau | `[]` | Liste des liens |
|
| `links` | Non | tableau | `[]` | Liste des liens |
|
||||||
|
| `visible_by_user` | Non | chaîne | — | Nom d'utilisateur requis pour voir cette section |
|
||||||
|
| `visible_by_group` | Non | chaîne | — | Groupe requis pour voir cette section |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
sections:
|
sections:
|
||||||
|
|
@ -101,6 +105,8 @@ Tableau de liens. Chaque lien contient :
|
||||||
| `iconUrl` | Non | chaîne (URL) | — | URL d'une image personnalisée (remplace `icon`) |
|
| `iconUrl` | Non | chaîne (URL) | — | URL d'une image personnalisée (remplace `icon`) |
|
||||||
| `color` | Non | chaîne (hex) | — | Couleur d'accent (ex: `"#2daae1"`) |
|
| `color` | Non | chaîne (hex) | — | Couleur d'accent (ex: `"#2daae1"`) |
|
||||||
| `openInNewTab` | Non | booléen | `true` | `false` pour ouvrir dans le même onglet |
|
| `openInNewTab` | Non | booléen | `true` | `false` pour ouvrir dans le même onglet |
|
||||||
|
| `visible_by_user` | Non | chaîne | — | Nom d'utilisateur requis pour voir ce lien |
|
||||||
|
| `visible_by_group` | Non | chaîne | — | Groupe requis pour voir ce lien |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
links:
|
links:
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,14 @@ theme:
|
||||||
authentication:
|
authentication:
|
||||||
logout_url: "https://auth.socheleau.fr/logout"
|
logout_url: "https://auth.socheleau.fr/logout"
|
||||||
header_format: "Remote-User"
|
header_format: "Remote-User"
|
||||||
username: "Remote-User"
|
username: "Remote-Name"
|
||||||
|
header_user: "Remote-User"
|
||||||
|
header_group: "Remote-Groups"
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
- id: "global"
|
- id: "global"
|
||||||
title: "Global"
|
title: "Global"
|
||||||
description: "Services accessibles à toute la famille"
|
#description: "Services accessibles à toute la famille"
|
||||||
cardLayout: "horizontal"
|
cardLayout: "horizontal"
|
||||||
links:
|
links:
|
||||||
- id: "nextcloud"
|
- id: "nextcloud"
|
||||||
|
|
@ -60,7 +62,6 @@ sections:
|
||||||
|
|
||||||
- id: "custom"
|
- id: "custom"
|
||||||
title: "Liens personnalisés"
|
title: "Liens personnalisés"
|
||||||
description: "Autres liens utiles"
|
|
||||||
cardLayout: "horizontal"
|
cardLayout: "horizontal"
|
||||||
links:
|
links:
|
||||||
- id: "homeassistant"
|
- id: "homeassistant"
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,20 @@ export default async function Home() {
|
||||||
const isAuthenticated = config.authentication != null && headersList.has(authHeader)
|
const isAuthenticated = config.authentication != null && headersList.has(authHeader)
|
||||||
const username = isAuthenticated && usernameHeader ? headersList.get(usernameHeader) ?? null : null
|
const username = isAuthenticated && usernameHeader ? headersList.get(usernameHeader) ?? null : null
|
||||||
|
|
||||||
|
const userHeader = config.authentication?.header_user
|
||||||
|
const groupHeader = config.authentication?.header_group
|
||||||
|
const currentUser = isAuthenticated && userHeader ? headersList.get(userHeader) ?? null : null
|
||||||
|
const userGroups = isAuthenticated && groupHeader
|
||||||
|
? (headersList.get(groupHeader) ?? "").split(",").map((g) => g.trim()).filter(Boolean)
|
||||||
|
: []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dashboard
|
<Dashboard
|
||||||
config={config}
|
config={config}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
username={username}
|
username={username}
|
||||||
|
currentUser={currentUser}
|
||||||
|
userGroups={userGroups}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ interface DashboardProps {
|
||||||
config: DashboardConfig
|
config: DashboardConfig
|
||||||
isAuthenticated?: boolean
|
isAuthenticated?: boolean
|
||||||
username?: string | null
|
username?: string | null
|
||||||
|
currentUser?: string | null
|
||||||
|
userGroups?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dashboard({ config, isAuthenticated = false, username = null }: DashboardProps) {
|
export function Dashboard({ config, isAuthenticated = false, username = null, currentUser = null, userGroups = [] }: 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
|
<DashboardHeader
|
||||||
|
|
@ -20,7 +22,7 @@ export function Dashboard({ config, isAuthenticated = false, username = null }:
|
||||||
/>
|
/>
|
||||||
<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} currentUser={currentUser} userGroups={userGroups} />
|
||||||
))}
|
))}
|
||||||
</main>
|
</main>
|
||||||
{config.site.showFooter !== false && (
|
{config.site.showFooter !== false && (
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{isAuthenticated && username && (
|
{isAuthenticated && username && (
|
||||||
<span className="hidden text-sm text-[var(--text-secondary)] lg:block">
|
<span className="hidden text-sm text-[var(--text-secondary)] lg:block">
|
||||||
{username}
|
Hi {username}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
|
@ -139,7 +139,7 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
|
||||||
</div>
|
</div>
|
||||||
{isAuthenticated && username && (
|
{isAuthenticated && username && (
|
||||||
<p className="text-sm text-[var(--text-secondary)] lg:hidden">
|
<p className="text-sm text-[var(--text-secondary)] lg:hidden">
|
||||||
{username}
|
Hi {username}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
import { ServiceCard } from "./ServiceCard"
|
import { ServiceCard } from "./ServiceCard"
|
||||||
|
import { isVisible } from "@/lib/visibility"
|
||||||
import type { SectionConfig } from "@/types/config"
|
import type { SectionConfig } from "@/types/config"
|
||||||
|
|
||||||
interface DashboardSectionProps {
|
interface DashboardSectionProps {
|
||||||
section: SectionConfig
|
section: SectionConfig
|
||||||
sectionIndex?: number
|
sectionIndex?: number
|
||||||
|
currentUser?: string | null
|
||||||
|
userGroups?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardSection({ section, sectionIndex = 0 }: DashboardSectionProps) {
|
export function DashboardSection({ section, sectionIndex = 0, currentUser = null, userGroups = [] }: DashboardSectionProps) {
|
||||||
if (section.links.length === 0) {
|
if (!isVisible(section, currentUser, userGroups)) return null
|
||||||
return null
|
|
||||||
}
|
const visibleLinks = section.links.filter((link) => isVisible(link, currentUser, userGroups))
|
||||||
|
if (visibleLinks.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
|
@ -29,7 +33,7 @@ export function DashboardSection({ section, sectionIndex = 0 }: DashboardSection
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 sm:gap-4">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 sm:gap-4">
|
||||||
{section.links.map((link, index) => (
|
{visibleLinks.map((link, index) => (
|
||||||
<ServiceCard
|
<ServiceCard
|
||||||
key={link.id}
|
key={link.id}
|
||||||
link={link}
|
link={link}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export const linkSchema = z.object({
|
||||||
iconUrl: z.string().url("Invalid icon URL").optional(),
|
iconUrl: z.string().url("Invalid icon URL").optional(),
|
||||||
color: z.string().regex(hexColorRegex, "Invalid hex color").optional(),
|
color: z.string().regex(hexColorRegex, "Invalid hex color").optional(),
|
||||||
openInNewTab: z.boolean().optional().default(true),
|
openInNewTab: z.boolean().optional().default(true),
|
||||||
|
visible_by_user: z.string().optional(),
|
||||||
|
visible_by_group: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const sectionSchema = z.object({
|
export const sectionSchema = z.object({
|
||||||
|
|
@ -19,6 +21,8 @@ export const sectionSchema = z.object({
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
cardLayout: z.enum(["horizontal", "vertical"]).optional().default("horizontal"),
|
cardLayout: z.enum(["horizontal", "vertical"]).optional().default("horizontal"),
|
||||||
links: z.array(linkSchema).default([]),
|
links: z.array(linkSchema).default([]),
|
||||||
|
visible_by_user: z.string().optional(),
|
||||||
|
visible_by_group: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const themeSchema = z.object({
|
export const themeSchema = z.object({
|
||||||
|
|
@ -43,6 +47,8 @@ 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"),
|
||||||
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_group: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const dashboardConfigSchema = z.object({
|
export const dashboardConfigSchema = z.object({
|
||||||
|
|
|
||||||
13
src/lib/visibility.ts
Normal file
13
src/lib/visibility.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function isVisible(
|
||||||
|
item: { visible_by_user?: string; visible_by_group?: string } | undefined | null,
|
||||||
|
currentUser: string | null,
|
||||||
|
userGroups: string[]
|
||||||
|
): boolean {
|
||||||
|
if (!item) return true
|
||||||
|
if (!item.visible_by_user && !item.visible_by_group) return true
|
||||||
|
|
||||||
|
if (item.visible_by_user && currentUser !== item.visible_by_user) return false
|
||||||
|
if (item.visible_by_group && !userGroups.includes(item.visible_by_group)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,8 @@ export interface LinkConfig {
|
||||||
iconUrl?: string
|
iconUrl?: string
|
||||||
color?: string
|
color?: string
|
||||||
openInNewTab?: boolean
|
openInNewTab?: boolean
|
||||||
|
visible_by_user?: string
|
||||||
|
visible_by_group?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SectionConfig {
|
export interface SectionConfig {
|
||||||
|
|
@ -30,12 +32,16 @@ export interface SectionConfig {
|
||||||
description?: string
|
description?: string
|
||||||
cardLayout?: "horizontal" | "vertical"
|
cardLayout?: "horizontal" | "vertical"
|
||||||
links: LinkConfig[]
|
links: LinkConfig[]
|
||||||
|
visible_by_user?: string
|
||||||
|
visible_by_group?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticationConfig {
|
export interface AuthenticationConfig {
|
||||||
logout_url: string
|
logout_url: string
|
||||||
header_format?: string
|
header_format?: string
|
||||||
username?: string
|
username?: string
|
||||||
|
header_user?: string
|
||||||
|
header_group?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardConfig {
|
export interface DashboardConfig {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue