2026-03-08 18:55:45 +01:00
|
|
|
import { env } from '../env';
|
2026-07-20 18:57:57 +02:00
|
|
|
import { resolveAuthSecret } from '../secret';
|
2026-05-30 15:38:55 +02:00
|
|
|
import { loadYaml, clearYamlCache, validateUuid } from './parser';
|
2026-03-08 18:55:45 +01:00
|
|
|
import { resolveTheme, VALID_LAYOUTS } from './theme';
|
2026-03-12 11:30:45 +00:00
|
|
|
import {
|
|
|
|
|
slugify,
|
|
|
|
|
AppConfig,
|
|
|
|
|
SubpageConfig,
|
|
|
|
|
SubpageSectionConfig,
|
2026-03-15 07:32:42 +01:00
|
|
|
SubpageObjectValue,
|
2026-03-12 11:30:45 +00:00
|
|
|
GalleryYaml,
|
|
|
|
|
SettingsYaml,
|
|
|
|
|
GridConfig,
|
|
|
|
|
} from './schema';
|
2026-03-08 18:55:45 +01:00
|
|
|
|
|
|
|
|
export * from './schema';
|
|
|
|
|
export * from './theme';
|
|
|
|
|
|
2026-08-05 00:27:06 +02:00
|
|
|
/** Invalidate the cached YAML so the next getConfig() call re-parses the files. */
|
feat: add visual admin panel with page builder and settings editor
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
2026-05-29 17:04:37 +02:00
|
|
|
export function invalidateConfigCache(): void {
|
2026-05-30 15:38:55 +02:00
|
|
|
clearYamlCache();
|
feat: add visual admin panel with page builder and settings editor
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
2026-05-29 17:04:37 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-13 11:56:40 +01:00
|
|
|
/** Converts raw YAML grid overrides into a typed partial GridConfig. */
|
2026-03-15 07:32:42 +01:00
|
|
|
export function buildSubpageGrid(raw?: {
|
|
|
|
|
columns?: number;
|
|
|
|
|
gap?: number;
|
|
|
|
|
aspectRatio?: string;
|
|
|
|
|
layout?: string;
|
|
|
|
|
}): { grid: Partial<GridConfig> } | Record<string, never> {
|
2026-03-13 11:56:40 +01:00
|
|
|
if (!raw) return {};
|
|
|
|
|
return {
|
|
|
|
|
grid: {
|
|
|
|
|
...(raw.columns != null ? { columns: raw.columns } : {}),
|
|
|
|
|
...(raw.gap != null ? { gap: raw.gap } : {}),
|
|
|
|
|
...(raw.aspectRatio != null ? { aspectRatio: raw.aspectRatio } : {}),
|
|
|
|
|
...(raw.layout != null
|
|
|
|
|
? {
|
|
|
|
|
layout: (VALID_LAYOUTS.includes(raw.layout)
|
|
|
|
|
? raw.layout
|
|
|
|
|
: 'masonry') as GridConfig['layout'],
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 18:55:45 +01:00
|
|
|
export function getConfig(): AppConfig {
|
2026-08-05 00:27:06 +02:00
|
|
|
// No in-memory config cache: admin saves can land in a different
|
|
|
|
|
// worker/process than page rendering, so a per-worker cache goes stale
|
|
|
|
|
// until restart. Freshness comes from the mtime-checked YAML cache in
|
|
|
|
|
// loadYaml() — a statSync per file, negligible next to Immich API calls.
|
2026-03-12 11:30:45 +00:00
|
|
|
const apiUrl = env.IMMICH_API_URL;
|
|
|
|
|
const apiKey = env.IMMICH_API_KEY;
|
2026-07-20 18:57:57 +02:00
|
|
|
const authSecret = resolveAuthSecret();
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
const gallery = loadYaml<GalleryYaml>('gallery.yaml');
|
|
|
|
|
const settings = loadYaml<SettingsYaml>('settings.yaml') || {};
|
2026-03-08 21:04:41 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
if (!gallery || !apiKey || !apiUrl) {
|
|
|
|
|
// Return dummy config if gallery.yaml is missing
|
2026-08-05 00:27:06 +02:00
|
|
|
return {
|
2026-03-12 11:30:45 +00:00
|
|
|
immich: { apiUrl: `${apiUrl}/api`, apiKey },
|
|
|
|
|
authSecret,
|
|
|
|
|
albums: [],
|
|
|
|
|
standaloneAlbums: [],
|
|
|
|
|
subpages: [],
|
|
|
|
|
siteTitle: env.SITE_TITLE || 'Immich Folio',
|
|
|
|
|
siteSubtitle: env.SITE_SUBTITLE || 'Setup Required',
|
2026-03-15 07:32:42 +01:00
|
|
|
lang: 'en',
|
2026-03-12 11:30:45 +00:00
|
|
|
seo: {
|
|
|
|
|
title: 'Setup Required',
|
|
|
|
|
description: 'Please configure Immich Folio',
|
|
|
|
|
noIndex: true,
|
|
|
|
|
noFollow: true,
|
|
|
|
|
},
|
|
|
|
|
heroImages: [],
|
|
|
|
|
exifOnHover: true,
|
|
|
|
|
grid: { columns: 3, gap: 12, aspectRatio: '1', layout: 'masonry' },
|
|
|
|
|
theme: resolveTheme('studio'),
|
|
|
|
|
footer: null,
|
|
|
|
|
legal: { enabled: false, name: '', address: '', zipCity: '', country: '' },
|
|
|
|
|
map: false,
|
|
|
|
|
transitions: false,
|
|
|
|
|
albumOverrides: {},
|
feat: add visual admin panel with page builder and settings editor
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
2026-05-29 17:04:37 +02:00
|
|
|
albumDescriptions: {},
|
2026-04-07 23:04:03 +02:00
|
|
|
albumPasswords: {},
|
2026-05-30 15:33:43 +02:00
|
|
|
albumHeroImages: {},
|
2026-03-12 11:30:45 +00:00
|
|
|
cacheTtl: env.CACHE_TTL * 1000,
|
|
|
|
|
rateLimitRpm: env.RATE_LIMIT_RPM,
|
2026-07-20 18:57:57 +02:00
|
|
|
trustedProxyHops: env.TRUSTED_PROXY_HOPS,
|
2026-03-12 11:30:45 +00:00
|
|
|
needsSetup: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
const theme = resolveTheme(settings.theme);
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
const albumOverrides: Record<string, string> = {};
|
feat: add visual admin panel with page builder and settings editor
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
2026-05-29 17:04:37 +02:00
|
|
|
const albumDescriptions: Record<string, string> = {};
|
2026-04-07 23:04:03 +02:00
|
|
|
const albumPasswords: Record<string, string> = {};
|
2026-05-30 15:33:43 +02:00
|
|
|
const albumHeroImages: Record<string, string> = {};
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-04-07 23:04:03 +02:00
|
|
|
function processAlbumEntry(
|
feat: add visual admin panel with page builder and settings editor
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
2026-05-29 17:04:37 +02:00
|
|
|
entry:
|
|
|
|
|
| string
|
2026-05-30 15:33:43 +02:00
|
|
|
| Record<string, string | { title: string; description?: string; password?: string; heroImage?: string }>,
|
2026-04-07 23:04:03 +02:00
|
|
|
context: string,
|
|
|
|
|
): string {
|
2026-03-12 11:30:45 +00:00
|
|
|
if (typeof entry === 'string') {
|
|
|
|
|
return validateUuid(entry, context);
|
2026-03-08 18:55:45 +01:00
|
|
|
}
|
2026-04-07 23:04:03 +02:00
|
|
|
const [uuid, value] = Object.entries(entry)[0];
|
2026-03-12 11:30:45 +00:00
|
|
|
const validatedUuid = validateUuid(uuid, context);
|
2026-04-07 23:04:03 +02:00
|
|
|
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
albumOverrides[validatedUuid] = value;
|
|
|
|
|
} else {
|
|
|
|
|
if (value.title) albumOverrides[validatedUuid] = value.title;
|
feat: add visual admin panel with page builder and settings editor
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
2026-05-29 17:04:37 +02:00
|
|
|
if (value.description) albumDescriptions[validatedUuid] = value.description;
|
2026-04-07 23:04:03 +02:00
|
|
|
if (value.password) albumPasswords[validatedUuid] = value.password;
|
2026-05-30 15:33:43 +02:00
|
|
|
if (value.heroImage) albumHeroImages[validatedUuid] = value.heroImage;
|
2026-04-07 23:04:03 +02:00
|
|
|
}
|
2026-03-12 11:30:45 +00:00
|
|
|
return validatedUuid;
|
|
|
|
|
}
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
const standaloneAlbumIds = (gallery.albums ?? []).map((entry) =>
|
|
|
|
|
processAlbumEntry(entry, 'gallery.yaml albums'),
|
|
|
|
|
);
|
|
|
|
|
if (
|
|
|
|
|
standaloneAlbumIds.length === 0 &&
|
|
|
|
|
(!gallery.subpages ||
|
|
|
|
|
(Array.isArray(gallery.subpages)
|
|
|
|
|
? gallery.subpages.length === 0
|
|
|
|
|
: Object.keys(gallery.subpages).length === 0))
|
|
|
|
|
) {
|
|
|
|
|
throw new Error('gallery.yaml must define at least one album or subpage');
|
|
|
|
|
}
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
let subpages: SubpageConfig[] = [];
|
2026-03-08 23:52:23 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
if (Array.isArray(gallery.subpages)) {
|
|
|
|
|
subpages = gallery.subpages.map((sp) => {
|
|
|
|
|
if (!sp.name) {
|
|
|
|
|
throw new Error(`Subpage "(unnamed)" must have a name`);
|
|
|
|
|
}
|
2026-03-08 23:52:23 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
// Parse sections if present
|
|
|
|
|
let sections: SubpageSectionConfig[] | undefined;
|
|
|
|
|
let albumIds: string[];
|
|
|
|
|
|
|
|
|
|
if (sp.sections && sp.sections.length > 0) {
|
|
|
|
|
sections = sp.sections.map((sec) => ({
|
|
|
|
|
title: sec.title,
|
|
|
|
|
slug: slugify(sec.title),
|
|
|
|
|
description: sec.description,
|
|
|
|
|
albumIds: sec.albums.map((entry) =>
|
|
|
|
|
processAlbumEntry(entry, `subpage "${sp.name}" section "${sec.title}"`),
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
// flat union of all section albums
|
|
|
|
|
albumIds = sections.flatMap((s) => s.albumIds);
|
|
|
|
|
// Also include any top-level albums (outside sections)
|
|
|
|
|
const topLevel = (sp.albums ?? []).map((entry) =>
|
|
|
|
|
processAlbumEntry(entry, `subpage "${sp.name}"`),
|
|
|
|
|
);
|
|
|
|
|
albumIds = [...topLevel, ...albumIds];
|
|
|
|
|
} else {
|
|
|
|
|
const albums = sp.albums ?? [];
|
|
|
|
|
if (albums.length === 0) {
|
|
|
|
|
throw new Error(`Subpage "${sp.name}" must have albums or sections`);
|
|
|
|
|
}
|
|
|
|
|
albumIds = albums.map((entry) => processAlbumEntry(entry, `subpage "${sp.name}"`));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
name: sp.name,
|
|
|
|
|
slug: slugify(sp.name),
|
|
|
|
|
title: sp.title,
|
|
|
|
|
subtitle: sp.subtitle,
|
|
|
|
|
albumIds,
|
|
|
|
|
sections,
|
|
|
|
|
password: sp.password,
|
2026-03-13 11:56:40 +01:00
|
|
|
...buildSubpageGrid(sp.grid),
|
2026-03-12 11:30:45 +00:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
} else if (gallery.subpages) {
|
|
|
|
|
subpages = Object.entries(gallery.subpages).map(([name, value]) => {
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
slug: slugify(name),
|
|
|
|
|
albumIds: value.map((entry) => processAlbumEntry(entry, `subpage "${name}"`)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const sp = value as SubpageObjectValue;
|
|
|
|
|
const albumEntries = sp.albums || [];
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
slug: slugify(name),
|
|
|
|
|
title: sp.title,
|
|
|
|
|
subtitle: sp.subtitle,
|
|
|
|
|
albumIds: albumEntries.map((entry) => processAlbumEntry(entry, `subpage "${name}"`)),
|
|
|
|
|
password: sp.password,
|
2026-03-13 11:56:40 +01:00
|
|
|
...buildSubpageGrid(sp.grid),
|
2026-03-12 11:30:45 +00:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds));
|
|
|
|
|
const allAlbumIds = [...new Set([...standaloneAlbumIds, ...subpageAlbumIds])];
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-08-05 00:27:06 +02:00
|
|
|
return {
|
2026-03-12 11:30:45 +00:00
|
|
|
immich: { apiUrl: `${apiUrl}/api`, apiKey },
|
|
|
|
|
authSecret,
|
|
|
|
|
albums: allAlbumIds,
|
|
|
|
|
standaloneAlbums: standaloneAlbumIds.filter((id) => !subpageAlbumIds.has(id)),
|
|
|
|
|
subpages,
|
|
|
|
|
siteTitle: settings.title ?? env.SITE_TITLE,
|
|
|
|
|
siteSubtitle: settings.subtitle ?? env.SITE_SUBTITLE,
|
2026-03-15 07:32:42 +01:00
|
|
|
lang: settings.lang ?? 'en',
|
2026-03-12 11:30:45 +00:00
|
|
|
seo: {
|
|
|
|
|
title: settings.seo?.title || settings.title || env.SITE_TITLE || 'Gallery',
|
|
|
|
|
description:
|
|
|
|
|
settings.seo?.description ||
|
|
|
|
|
settings.subtitle ||
|
|
|
|
|
env.SITE_SUBTITLE ||
|
|
|
|
|
'A curated photography portfolio',
|
|
|
|
|
noIndex: settings.seo?.noIndex === true,
|
|
|
|
|
noFollow: settings.seo?.noFollow === true,
|
|
|
|
|
},
|
|
|
|
|
heroImages: gallery.hero
|
|
|
|
|
? (Array.isArray(gallery.hero) ? gallery.hero : [gallery.hero]).map((id) =>
|
|
|
|
|
validateUuid(id, 'gallery.yaml hero'),
|
|
|
|
|
)
|
|
|
|
|
: [],
|
|
|
|
|
exifOnHover: settings.exifOnHover !== false,
|
|
|
|
|
grid: {
|
|
|
|
|
columns: settings.grid?.columns ?? 3,
|
|
|
|
|
gap: settings.grid?.gap ?? 12,
|
|
|
|
|
aspectRatio: settings.grid?.aspectRatio ?? '1',
|
|
|
|
|
layout: (VALID_LAYOUTS.includes(settings.grid?.layout ?? '')
|
|
|
|
|
? settings.grid!.layout
|
|
|
|
|
: 'masonry') as GridConfig['layout'],
|
|
|
|
|
},
|
|
|
|
|
theme,
|
|
|
|
|
footer: settings.footer
|
|
|
|
|
? {
|
|
|
|
|
name: settings.footer.name,
|
|
|
|
|
instagram: settings.footer.instagram,
|
|
|
|
|
email: settings.footer.email,
|
|
|
|
|
website: settings.footer.website,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
legal: {
|
|
|
|
|
enabled: settings.legal?.enabled === true,
|
|
|
|
|
name: settings.legal?.name || '',
|
|
|
|
|
address: settings.legal?.address || '',
|
|
|
|
|
zipCity: settings.legal?.zipCity || '',
|
|
|
|
|
country: settings.legal?.country || '',
|
|
|
|
|
email: settings.legal?.email,
|
|
|
|
|
phone: settings.legal?.phone,
|
|
|
|
|
taxId: settings.legal?.taxId,
|
|
|
|
|
vatId: settings.legal?.vatId,
|
|
|
|
|
extraInfo: settings.legal?.extraInfo,
|
|
|
|
|
},
|
|
|
|
|
map: settings.map === true,
|
|
|
|
|
transitions: settings.transitions !== false,
|
|
|
|
|
albumOverrides,
|
feat: add visual admin panel with page builder and settings editor
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
2026-05-29 17:04:37 +02:00
|
|
|
albumDescriptions,
|
2026-04-07 23:04:03 +02:00
|
|
|
albumPasswords,
|
2026-05-30 15:33:43 +02:00
|
|
|
albumHeroImages,
|
2026-03-12 11:30:45 +00:00
|
|
|
cacheTtl: env.CACHE_TTL * 1000,
|
|
|
|
|
rateLimitRpm: env.RATE_LIMIT_RPM,
|
2026-07-20 18:57:57 +02:00
|
|
|
trustedProxyHops: env.TRUSTED_PROXY_HOPS,
|
2026-03-12 11:30:45 +00:00
|
|
|
needsSetup: false,
|
|
|
|
|
};
|
2026-03-08 18:55:45 +01:00
|
|
|
}
|