Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of [CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md) for the full list. Highlights: client proofing links (#683), custom content pages (#738), asset picker search (#740), concurrent-edit detection (#742), env-locked settings (#739), an accessibility baseline (#741), three security fixes with advisories (#737, #755, #759), and a pre-release quality pass (#780–#797). --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ralf <rohlenbo79@gmail.com> Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com> Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com> Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
/**
|
|
* Settings fields that an environment variable overrides (#605).
|
|
*
|
|
* Only fields where the environment actually *wins* belong here — an edit in
|
|
* the panel would save, report success and then do nothing. `SITE_TITLE`,
|
|
* `SITE_SUBTITLE` and `SITE_URL` are deliberately absent: lib/config/index.ts
|
|
* resolves them as `settings.X ?? env.X`, so the field takes precedence and
|
|
* editing it works (see lib/siteUrl.ts).
|
|
*
|
|
* Client-safe: no `fs`, no `lib/env` import. The route passes the parsed env
|
|
* in, and only the variable *names* ever reach the browser.
|
|
*/
|
|
|
|
/** Dotted settings.yaml path → the environment variable that overrides it. */
|
|
export const ENV_LOCKS = {
|
|
sitePassword: 'SITE_PASSWORD',
|
|
'contact.notifyUrl': 'CONTACT_NOTIFY_URL',
|
|
} as const;
|
|
|
|
export type EnvLockedKey = keyof typeof ENV_LOCKS;
|
|
|
|
/** Locked path → variable name. Never carries the variable's value. */
|
|
export type EnvLocks = Partial<Record<EnvLockedKey, string>>;
|
|
|
|
/** The locks in effect for this environment: paths whose variable is set. */
|
|
export function resolveEnvLocks(env: object): EnvLocks {
|
|
const source = env as Record<string, unknown>;
|
|
const locks: EnvLocks = {};
|
|
for (const [path, name] of Object.entries(ENV_LOCKS) as [EnvLockedKey, string][]) {
|
|
const value = source[name];
|
|
if (typeof value === 'string' && value.trim() !== '') locks[path] = name;
|
|
}
|
|
return locks;
|
|
}
|
|
|
|
function getPath(obj: unknown, path: string[]): unknown {
|
|
let cur = obj;
|
|
for (const key of path) {
|
|
if (!cur || typeof cur !== 'object') return undefined;
|
|
cur = (cur as Record<string, unknown>)[key];
|
|
}
|
|
return cur;
|
|
}
|
|
|
|
/**
|
|
* The incoming settings with every locked path reset to what settings.yaml
|
|
* already holds. The panel disables those fields, so this only matters for a
|
|
* hand-crafted request or a stale draft — but a locked field must not change
|
|
* through a save whose effect the operator could never see.
|
|
*/
|
|
export function keepLockedValues<T extends object>(
|
|
incoming: T,
|
|
stored: object | null | undefined,
|
|
locks: EnvLocks,
|
|
): T {
|
|
const out = structuredClone(incoming) as Record<string, unknown>;
|
|
for (const dotted of Object.keys(locks)) {
|
|
const path = dotted.split('.');
|
|
const last = path[path.length - 1];
|
|
const previous = getPath(stored, path);
|
|
let parent: Record<string, unknown> | null = out;
|
|
for (const key of path.slice(0, -1)) {
|
|
const next: unknown = parent[key];
|
|
if (!next || typeof next !== 'object') {
|
|
if (previous === undefined) {
|
|
parent = null;
|
|
break;
|
|
}
|
|
parent[key] = {};
|
|
}
|
|
parent = parent[key] as Record<string, unknown>;
|
|
}
|
|
if (!parent) continue;
|
|
if (previous === undefined) delete parent[last];
|
|
else parent[last] = previous;
|
|
}
|
|
return out as T;
|
|
}
|