chore: remove unused 3P dependencies (zod, gray-matter, bcryptjs) and switch to native implementations

This commit is contained in:
ralksta
2026-03-08 21:12:52 +01:00
parent 25aefa5b70
commit 7a1dfdc519
5 changed files with 111 additions and 183 deletions

View File

@@ -5,7 +5,7 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import matter from 'gray-matter'; import yaml from 'js-yaml';
import Image from 'next/image'; import Image from 'next/image';
import { imageUrl, assetPlaceholder } from '@/lib/urls'; import { imageUrl, assetPlaceholder } from '@/lib/urls';
import { immich } from '@/lib/immich'; import { immich } from '@/lib/immich';
@@ -26,8 +26,21 @@ function getAboutContent() {
return { meta: {} as AboutFrontmatter, body: '' }; return { meta: {} as AboutFrontmatter, body: '' };
} }
const raw = readFileSync(filePath, 'utf-8'); const raw = readFileSync(filePath, 'utf-8');
const { data, content } = matter(raw);
return { meta: data as AboutFrontmatter, body: content.trim() }; let meta = {} as AboutFrontmatter;
let body = raw;
const match = raw.match(/^(?:---\r?\n)([\s\S]*?)(?:\r?\n---\r?\n)([\s\S]*)$/);
if (match) {
try {
meta = (yaml.load(match[1]) || {}) as AboutFrontmatter;
} catch (e) {
console.error('Failed to parse about.md frontmatter', e);
}
body = match[2];
}
return { meta, body: body.trim() };
} }
export default async function AboutPage() { export default async function AboutPage() {

View File

@@ -7,18 +7,43 @@
*/ */
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { getConfig, SubpageConfig } from './config'; import { getConfig, SubpageConfig } from './config';
const TOKEN_EXPIRY_HOURS = 24; const TOKEN_EXPIRY_HOURS = 24;
/** /**
* Check if a string looks like a bcrypt hash. * Check if a string looks like a legacy bcrypt hash.
*/ */
function isHash(str: string): boolean { function isBcryptHash(str: string): boolean {
return str.startsWith('$2a$') || str.startsWith('$2b$') || str.startsWith('$2y$'); return str.startsWith('$2a$') || str.startsWith('$2b$') || str.startsWith('$2y$');
} }
/**
* Native SCrypt verify (format: 'scrypt:salt:hash_hex')
*/
function verifyScrypt(password: string, stored: string): boolean {
if (!stored.startsWith('scrypt:')) return false;
try {
const [, salt, hashHex] = stored.split(':');
const key = crypto.scryptSync(password, salt, 64);
const expectedKey = Buffer.from(hashHex, 'hex');
if (key.length !== expectedKey.length) return false;
return crypto.timingSafeEqual(key, expectedKey);
} catch {
return false;
}
}
/**
* Helper to generate an scrypt string to print in logs.
*/
function generateScryptHash(password: string): string {
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
return `scrypt:${salt}:${hash}`;
}
function hmac(data: string): string { function hmac(data: string): string {
return crypto.createHmac('sha256', getConfig().authSecret).update(data).digest('hex'); return crypto.createHmac('sha256', getConfig().authSecret).update(data).digest('hex');
} }
@@ -56,15 +81,27 @@ export function authenticate(slug: string, password: string): string | null {
if (!sp?.password) return null; if (!sp?.password) return null;
let isValid = false; let isValid = false;
if (isHash(sp.password)) {
isValid = bcrypt.compareSync(password, sp.password); if (isBcryptHash(sp.password)) {
console.error(
`\n❌ SECURITY ERROR: Subpage "${slug}" is using an outdated bcrypt password hash.\n` +
` Bcrypt dependency has been removed to reduce bundle size.\n` +
` Please switch temporarily to plaintext in your gallery.yaml, log in again\n` +
` to see your new secure "scrypt:..." hash in the logs, and update your file.\n`,
);
return null;
}
if (sp.password.startsWith('scrypt:')) {
isValid = verifyScrypt(password, sp.password);
} else { } else {
// Plaintext fallback (deprecated) // Plaintext fallback (deprecated)
isValid = password === sp.password; isValid = password === sp.password;
if (isValid) { if (isValid) {
const recommendedHash = generateScryptHash(sp.password);
console.warn( console.warn(
`\n⚠️ SECURITY WARNING: Subpage "${slug}" is using a plaintext password in gallery.yaml.\n` + `\n⚠️ SECURITY WARNING: Subpage "${slug}" is using a plaintext password in gallery.yaml.\n` +
` Please hash it for better security. You can use: npx bcryptjs ${sp.password}\n`, ` Please replace it with this native secure hash:\n\n ${recommendedHash}\n`,
); );
} }
} }

View File

@@ -1,50 +1,58 @@
/** /**
* Zod-validated environment variables. * Environment variables parser.
* Parsed once at startup — all env access should go through this module. * Parsed once at startup — all env access should go through this module.
*/ */
import { z } from 'zod'; export interface Env {
IMMICH_API_URL: string;
const envSchema = z.object({ IMMICH_API_KEY: string;
/** Base URL of your Immich instance, e.g. https://immich.example.com */ SITE_TITLE: string;
IMMICH_API_URL: z SITE_SUBTITLE: string;
.string() CACHE_TTL: number;
.url('IMMICH_API_URL must be a valid URL') RATE_LIMIT_RPM: number;
.transform((url) => url.replace(/\/+$/, '')), AUTH_SECRET?: string;
}
/** Immich API key (from Admin → API Keys) */
IMMICH_API_KEY: z.string().min(1, 'IMMICH_API_KEY is required'),
/** Public site title shown in the header */
SITE_TITLE: z.string().default('Gallery'),
/** Subtitle displayed below the title */
SITE_SUBTITLE: z.string().default(''),
/** Cache TTL in seconds */
CACHE_TTL: z.coerce.number().int().min(0).default(300),
/** Rate limit: max requests per minute per IP */
RATE_LIMIT_RPM: z.coerce.number().int().min(1).default(600),
/**
* Secret used for signing auth cookies and encrypting asset tokens.
* If not provided, IMMICH_API_KEY will be used as a fallback (configured in config.ts).
*/
AUTH_SECRET: z.string().optional(),
});
export type Env = z.infer<typeof envSchema>;
function parseEnv(): Env { function parseEnv(): Env {
const result = envSchema.safeParse(process.env); const errors: string[] = [];
if (!result.success) {
const formatted = result.error.issues const urlRaw = process.env.IMMICH_API_URL;
.map((issue) => ` ✗ ${issue.path.join('.')}: ${issue.message}`) let apiUrl = '';
.join('\n'); if (!urlRaw) {
errors.push('IMMICH_API_URL is required');
} else {
try {
apiUrl = new URL(urlRaw).toString().replace(/\/+$/, '');
} catch {
errors.push('IMMICH_API_URL must be a valid URL');
}
}
const apiKey = process.env.IMMICH_API_KEY;
if (!apiKey) {
errors.push('IMMICH_API_KEY is required');
}
if (errors.length > 0) {
const formatted = errors.map((err) => ` ✗ ${err}`).join('\n');
throw new Error(`\n❌ Environment validation failed:\n${formatted}\n`); throw new Error(`\n❌ Environment validation failed:\n${formatted}\n`);
} }
return result.data;
const cacheTtlStr = process.env.CACHE_TTL;
const cacheTtl = cacheTtlStr && !isNaN(parseInt(cacheTtlStr, 10)) ? parseInt(cacheTtlStr, 10) : 300;
const rateLimitStr = process.env.RATE_LIMIT_RPM;
const rateLimit = rateLimitStr && !isNaN(parseInt(rateLimitStr, 10)) ? parseInt(rateLimitStr, 10) : 600;
return {
IMMICH_API_URL: apiUrl,
IMMICH_API_KEY: apiKey as string,
SITE_TITLE: process.env.SITE_TITLE || 'Gallery',
SITE_SUBTITLE: process.env.SITE_SUBTITLE || '',
CACHE_TTL: Math.max(0, cacheTtl),
RATE_LIMIT_RPM: Math.max(1, rateLimit),
AUTH_SECRET: process.env.AUTH_SECRET,
};
} }
/** Validated, typed environment variables. */ /** Validated, typed environment variables. */

132
package-lock.json generated
View File

@@ -7,21 +7,18 @@
"": { "": {
"name": "immich-lightbox", "name": "immich-lightbox",
"version": "0.1.0", "version": "0.1.0",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"bcryptjs": "^3.0.3",
"gray-matter": "^4.0.3",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"thumbhash": "^0.1.1", "thumbhash": "^0.1.1"
"zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2", "@playwright/test": "^1.58.2",
"@types/bcryptjs": "^2.4.6",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
@@ -2091,13 +2088,6 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/chai": { "node_modules/@types/chai": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -3199,15 +3189,6 @@
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
} }
}, },
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -4283,19 +4264,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": { "node_modules/esquery": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
@@ -4362,18 +4330,6 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4724,43 +4680,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"license": "MIT",
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/gray-matter/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/has-bigints": { "node_modules/has-bigints": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -5089,15 +5008,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -5518,15 +5428,6 @@
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
} }
}, },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/language-subtag-registry": { "node_modules/language-subtag-registry": {
"version": "0.3.23", "version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -6517,19 +6418,6 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -6762,12 +6650,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/stable-hash": { "node_modules/stable-hash": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -6926,15 +6808,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -7765,6 +7638,7 @@
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@@ -16,19 +16,15 @@
}, },
"dependencies": { "dependencies": {
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"bcryptjs": "^3.0.3",
"gray-matter": "^4.0.3",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"thumbhash": "^0.1.1", "thumbhash": "^0.1.1"
"zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2", "@playwright/test": "^1.58.2",
"@types/bcryptjs": "^2.4.6",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
@@ -42,4 +38,4 @@
"typescript": "^5", "typescript": "^5",
"vitest": "^4.0.18" "vitest": "^4.0.18"
} }
} }