Compare commits

..

No commits in common. "main" and "v0.1" have entirely different histories.
main ... v0.1

17 changed files with 74 additions and 283 deletions

View file

@ -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)
- 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.).

View file

@ -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

View file

@ -55,18 +55,16 @@ Configuration optionnelle pour l'authentification via un reverse proxy (Authelia
| Champ | Requis | Type | Défaut | Description |
|---|---|---|---|---|
| `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é |
| `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 **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
authentication:
logout_url: "https://auth.example.com/logout"
login_url: "https://auth.example.com"
header_format: "Remote-User"
```

View file

@ -1,132 +1,84 @@
site:
name: "Soch Family"
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
theme:
mode: "dark"
accent: "#8b5cf6"
background: "gradient"
icon_size: "large"
authentication:
logout_url: "https://auth.socheleau.fr/logout"
login_url: "https://auth.socheleau.fr"
header_format: "Remote-User"
username: "Remote-Name"
header_user: "Remote-User"
header_group: "Remote-Groups"
sections:
- id: "soch-services"
title: "Soch's services"
- id: "global"
title: "Global"
#description: "Services accessibles à toute la famille"
cardLayout: "horizontal"
visible_by_group: "arr"
links:
- id: "authelia"
name: "Authelia"
description: "Authentification unique"
url: "https://auth.socheleau.fr"
icon: "Shield"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg"
color: "#1a7f37"
- id: "nextcloud"
name: "Nextcloud"
description: "Fichiers et documents"
url: "https://cloud.example.com"
icon: "Cloud"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg"
color: "#2daae1"
openInNewTab: false
visible_by_user: "jean"
- id: "plex"
name: "Plex"
- id: "jellyfin"
name: "Jellyfin"
description: "Films et séries"
url: "https://plex.socheleau.fr"
url: "https://jellyfin.example.com"
icon: "Film"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg"
color: "#e5a00d"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg"
color: "#aa5cc3"
openInNewTab: false
- id: "bitwarden"
name: "Bitwarden"
description: "Mots de passe"
url: "https://bitwarden.socheleau.fr"
icon: "Shield"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/bitwarden.svg"
color: "#175ddc"
- id: "navidrome"
name: "Navidrome"
description: "Musique"
url: "https://music.example.com"
icon: "Music"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/navidrome.svg"
color: "#22c55e"
openInNewTab: false
- id: "immich"
name: "Immich"
description: "Photos et vidéos"
url: "https://immich.socheleau.fr"
icon: "Image"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg"
color: "#4250af"
- id: "minecraft"
name: "Minecraft"
description: "Serveur de jeu"
url: "https://minecraft.example.com"
icon: "Gamepad2"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/minecraft.svg"
color: "#f97316"
openInNewTab: false
- id: "freshrss"
name: "FreshRSS"
description: "Flux RSS"
url: "https://rss.socheleau.fr"
icon: "Rss"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/freshrss.svg"
color: "#0979b0"
openInNewTab: false
- id: "tandoor"
name: "Tandoor"
description: "Recettes de cuisine"
url: "https://tandoor.socheleau.fr"
icon: "ChefHat"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tandoor-recipes.svg"
color: "#dc7b2a"
openInNewTab: false
- id: "readeck"
name: "Readeck"
description: "Lecture différée"
url: "https://readeck.socheleau.fr"
icon: "BookOpen"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/readeck.svg"
color: "#14b8a6"
openInNewTab: false
- id: "homes"
title: "Homes"
- id: "custom"
title: "Liens personnalisés"
cardLayout: "horizontal"
links:
- id: "arasu"
name: "Arasu"
- id: "homeassistant"
name: "Home Assistant"
description: "Domotique"
url: "https://arasu.socheleau.fr"
url: "https://ha.example.com"
icon: "House"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg"
color: "#22c55e"
openInNewTab: false
- id: "arr"
title: "*arr"
cardLayout: "horizontal"
visible_by_group: "arr"
links:
- id: "radarr"
name: "Radarr"
description: "Gestion de films"
url: "https://radarr.arr.socheleau.fr"
icon: "Clapperboard"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg"
color: "#e73a2e"
openInNewTab: false
- id: "sonarr"
name: "Sonarr"
description: "Gestion de séries"
url: "https://sonarr.arr.socheleau.fr"
icon: "Tv"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg"
color: "#35c5f4"
openInNewTab: false
- id: "prowlarr"
name: "Prowlarr"
description: "Indexeur de torrents"
url: "https://prowlarr.arr.socheleau.fr"
icon: "Search"
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/prowlarr.svg"
color: "#e65c21"
- id: "router"
name: "Routeur"
description: "Administration réseau"
url: "http://192.168.1.1"
icon: "Router"
color: "#f59e0b"
openInNewTab: false

View file

@ -1,6 +1,6 @@
services:
sochboard:
image: registry2.socheleau.fr/maxence/sochboard:latest
build: .
container_name: sochboard
ports:
- "3000:3000"

View file

@ -1,6 +1,6 @@
{
"name": "sochboard",
"version": "0.3.0",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "node node_modules/next/dist/bin/next dev",

View file

@ -33,16 +33,12 @@ html.theme-light {
--focus-ring-offset: #f8fafc;
}
body {
.dashboard-root {
min-height: 100vh;
background: var(--bg-gradient);
color: var(--text-primary);
}
.dashboard-root {
min-height: 100vh;
}
@keyframes fade-in {
from {
opacity: 0;

View file

@ -1,7 +1,6 @@
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import { getConfig } from "@/lib/config"
import { SochboardLogoDataUri } from "@/components/ui/SochboardLogo"
import "./globals.css"
const inter = Inter({
@ -16,17 +15,12 @@ export async function generateMetadata(): Promise<Metadata> {
return {
title: config.site.name,
description: config.site.title,
icons: {
icon: config.site.favicon ?? SochboardLogoDataUri(config.theme.accent),
},
icons: config.site.favicon ? { icon: config.site.favicon } : undefined,
}
} catch {
return {
title: "Sochboard",
description: "Dashboard",
icons: {
icon: SochboardLogoDataUri(),
},
}
}
}

View file

@ -5,14 +5,6 @@ import { ErrorDisplay } from "@/components/ui/ErrorDisplay"
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() {
try {
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 usernameHeader = config.authentication?.username
const headersList = await headers()
const authValue = config.authentication ? headersList.get(authHeader) : null
const isAuthenticated = isValidHeaderValue(authValue)
const username = isAuthenticated && usernameHeader && isValidHeaderValue(headersList.get(usernameHeader))
? headersList.get(usernameHeader)!.trim()
: null
const isAuthenticated = config.authentication != null && headersList.has(authHeader)
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 && isValidHeaderValue(headersList.get(userHeader))
? headersList.get(userHeader)!.trim()
: null
const currentUser = isAuthenticated && userHeader ? headersList.get(userHeader) ?? null : null
const userGroups = isAuthenticated && groupHeader
? (headersList.get(groupHeader) ?? "").split(",").map((g) => g.trim()).filter(Boolean)
: []

View file

@ -1,6 +1,5 @@
import { DashboardHeader } from "./DashboardHeader"
import { DashboardSection } from "./DashboardSection"
import { VERSION } from "@/lib/version"
import type { DashboardConfig } from "@/types/config"
interface DashboardProps {
@ -20,12 +19,10 @@ export function Dashboard({ config, isAuthenticated = false, username = null, cu
authentication={config.authentication}
isAuthenticated={isAuthenticated}
username={username}
accent={config.theme.accent}
version={VERSION}
/>
<main className="flex flex-col gap-12">
{config.sections.map((section, index) => (
<DashboardSection key={section.id} section={section} sectionIndex={index} currentUser={currentUser} userGroups={userGroups} iconSize={config.theme.icon_size} />
<DashboardSection key={section.id} section={section} sectionIndex={index} currentUser={currentUser} userGroups={userGroups} />
))}
</main>
{config.site.showFooter !== false && (

View file

@ -2,7 +2,6 @@
import { useState, useEffect } from "react"
import type { SiteConfig, AuthenticationConfig } from "@/types/config"
import { SochboardLogo } from "@/components/ui/SochboardLogo"
interface DashboardHeaderProps {
site: SiteConfig
@ -10,11 +9,9 @@ interface DashboardHeaderProps {
authentication?: AuthenticationConfig
isAuthenticated?: boolean
username?: string | null
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 }: DashboardHeaderProps) {
const [isLight, setIsLight] = useState(initialTheme === "light")
useEffect(() => {
@ -36,7 +33,12 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
className="size-12 rounded-xl object-cover"
/>
) : (
<SochboardLogo size={48} accent={accent} />
<div
className="flex size-12 items-center justify-center rounded-xl bg-gradient-to-br from-[#8b5cf6] to-[#6d28d9] text-lg font-bold text-white shadow-lg shadow-[#8b5cf6]/20"
aria-hidden="true"
>
S
</div>
)}
<div>
<h1 className="text-xl font-bold text-[var(--text-primary)]">
@ -45,9 +47,6 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
<p className="mt-0.5 text-sm text-[var(--text-secondary)]">
{site.title}
</p>
{version && (
<p className="text-xs text-[var(--text-secondary)]">v{version}</p>
)}
</div>
</div>
<div className="flex items-center gap-3">
@ -107,7 +106,7 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
</svg>
)}
</button>
{isAuthenticated && authentication ? (
{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"
@ -135,37 +134,6 @@ export function DashboardHeader({ site, initialTheme, authentication, isAuthenti
<line x1="21" x2="9" y1="12" y2="12" />
</svg>
</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>

View file

@ -1,16 +1,15 @@
import { ServiceCard } from "./ServiceCard"
import { isVisible } from "@/lib/visibility"
import type { SectionConfig, IconSize } from "@/types/config"
import type { SectionConfig } from "@/types/config"
interface DashboardSectionProps {
section: SectionConfig
sectionIndex?: number
currentUser?: string | null
userGroups?: string[]
iconSize?: IconSize
}
export function DashboardSection({ section, sectionIndex = 0, currentUser = null, userGroups = [], iconSize = "medium" }: DashboardSectionProps) {
export function DashboardSection({ section, sectionIndex = 0, currentUser = null, userGroups = [] }: DashboardSectionProps) {
if (!isVisible(section, currentUser, userGroups)) return null
const visibleLinks = section.links.filter((link) => isVisible(link, currentUser, userGroups))
@ -33,14 +32,13 @@ export function DashboardSection({ section, sectionIndex = 0, currentUser = null
<p className="mt-1 text-sm text-[var(--text-secondary)]">{section.description}</p>
)}
</div>
<div className="grid grid-cols-1 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">
{visibleLinks.map((link, index) => (
<ServiceCard
key={link.id}
link={link}
index={index}
layout={section.cardLayout}
iconSize={iconSize}
/>
))}
</div>

View file

@ -1,26 +1,18 @@
import { IconRenderer } from "./IconRenderer"
import type { LinkConfig, IconSize } from "@/types/config"
import type { LinkConfig } from "@/types/config"
interface ServiceCardProps {
link: LinkConfig
index?: number
layout?: "horizontal" | "vertical"
iconSize?: IconSize
}
const sizeMap: Record<IconSize, { hCont: number; hImg: string; hIcon: number; vCont: number; vImg: string; vIcon: number }> = {
small: { hCont: 10, hImg: "size-5", hIcon: 18, vCont: 12, vImg: "size-6", vIcon: 22 },
medium: { hCont: 12, hImg: "size-7", hIcon: 24, vCont: 14, vImg: "size-8", vIcon: 30 },
large: { hCont: 12, hImg: "size-8", hIcon: 28, vCont: 14, vImg: "size-9", vIcon: 34 },
}
export function ServiceCard({ link, index = 0, layout = "horizontal", iconSize = "medium" }: ServiceCardProps) {
export function ServiceCard({ link, index = 0, layout = "horizontal" }: ServiceCardProps) {
const linkProps = link.openInNewTab
? { target: "_blank", rel: "noopener noreferrer" }
: {}
const isHorizontal = layout === "horizontal"
const s = sizeMap[iconSize]
return (
<a
@ -32,18 +24,16 @@ export function ServiceCard({ link, index = 0, layout = "horizontal", iconSize =
{isHorizontal ? (
<>
<div
className="flex shrink-0 items-center justify-center overflow-hidden rounded-lg"
className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg"
style={{
width: s.hCont * 4,
height: s.hCont * 4,
backgroundColor: `${link.color || "#8b5cf6"}20`,
color: link.color || "#8b5cf6",
}}
>
{link.iconUrl ? (
<img src={link.iconUrl} alt="" className={`${s.hImg} object-contain`} />
<img src={link.iconUrl} alt="" className="size-5 object-contain" />
) : (
<IconRenderer name={link.icon} size={s.hIcon} />
<IconRenderer name={link.icon} size={18} />
)}
</div>
<div className="flex min-w-0 flex-1 flex-col">
@ -83,18 +73,16 @@ export function ServiceCard({ link, index = 0, layout = "horizontal", iconSize =
<>
<div className="flex items-start justify-between">
<div
className="flex items-center justify-center overflow-hidden rounded-xl"
className="flex size-12 items-center justify-center overflow-hidden rounded-xl"
style={{
width: s.vCont * 4,
height: s.vCont * 4,
backgroundColor: `${link.color || "#8b5cf6"}20`,
color: link.color || "#8b5cf6",
}}
>
{link.iconUrl ? (
<img src={link.iconUrl} alt="" className={`${s.vImg} object-contain`} />
<img src={link.iconUrl} alt="" className="size-6 object-contain" />
) : (
<IconRenderer name={link.icon} size={s.vIcon} />
<IconRenderer name={link.icon} size={22} />
)}
</div>
{link.openInNewTab && (

View file

@ -1,69 +0,0 @@
interface SochboardLogoProps {
size?: number
accent?: string
className?: string
}
export function SochboardLogo({ size = 48, accent = "#f59e0b", className }: SochboardLogoProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden="true"
>
<rect width="48" height="48" rx="12" fill={accent} />
<rect x="4" y="4" width="40" height="40" rx="10" fill={`${accent}20`} />
<path
d="M24 12C21.5 12 19 13.5 19 16.5C19 18 20 19 21 19.5C19.5 20 18 21.5 18 24C18 26.5 20 28 22 28C20.5 28.5 19 30 19 32.5C19 35.5 21.5 37 24 37C26.5 37 29 35.5 29 32.5C29 31 28 30 27 29.5C28.5 29 30 27.5 30 25C30 22.5 28 21 26 21C27.5 20.5 29 19 29 16.5C29 13.5 26.5 12 24 12Z"
fill="white"
fillOpacity="0.95"
/>
<path
d="M12 12H36"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeOpacity="0.2"
/>
<path
d="M12 36H36"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeOpacity="0.2"
/>
<path
d="M12 24H36"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeOpacity="0.2"
/>
<path
d="M24 12V36"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeOpacity="0.2"
/>
</svg>
)
}
export function SochboardLogoDataUri(accent: string = "#f59e0b"): string {
const hex = accent.replace("#", "%23")
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<rect width="48" height="48" rx="12" fill="${hex}"/>
<rect x="4" y="4" width="40" height="40" rx="10" fill="${hex}20"/>
<path d="M24 12C21.5 12 19 13.5 19 16.5C19 18 20 19 21 19.5C19.5 20 18 21.5 18 24C18 26.5 20 28 22 28C20.5 28.5 19 30 19 32.5C19 35.5 21.5 37 24 37C26.5 37 29 35.5 29 32.5C29 31 28 30 27 29.5C28.5 29 30 27.5 30 25C30 22.5 28 21 26 21C27.5 20.5 29 19 29 16.5C29 13.5 26.5 12 24 12Z" fill="white" fill-opacity="0.95"/>
<path d="M12 12H36" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.2"/>
<path d="M12 36H36" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.2"/>
<path d="M12 24H36" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.2"/>
<path d="M24 12V36" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-opacity="0.2"/>
</svg>`
return `data:image/svg+xml,${encodeURIComponent(svg)}`
}

View file

@ -32,7 +32,6 @@ export const themeSchema = z.object({
.regex(hexColorRegex, "Invalid accent color")
.default("#8b5cf6"),
background: z.enum(["gradient", "solid"]).default("gradient"),
icon_size: z.enum(["small", "medium", "large"]).optional().default("medium"),
})
export const siteSchema = z.object({
@ -46,7 +45,6 @@ export const siteSchema = z.object({
export const authenticationSchema = z.object({
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"),
username: z.string().optional(),
header_user: z.string().optional(),
@ -59,7 +57,6 @@ export const dashboardConfigSchema = z.object({
mode: "dark",
accent: "#8b5cf6",
background: "gradient",
icon_size: "medium",
}),
authentication: authenticationSchema.optional(),
sections: z.array(sectionSchema).default([]),

View file

@ -1,3 +0,0 @@
import pkg from "../../package.json"
export const VERSION = pkg.version

View file

@ -7,13 +7,10 @@ export interface SiteConfig {
showFooter?: boolean
}
export type IconSize = "small" | "medium" | "large"
export interface ThemeConfig {
mode: "dark" | "light"
accent: string
background: "gradient" | "solid"
icon_size?: IconSize
}
export interface LinkConfig {
@ -41,7 +38,6 @@ export interface SectionConfig {
export interface AuthenticationConfig {
logout_url: string
login_url?: string
header_format?: string
username?: string
header_user?: string