chore: remove unused 3P dependencies (zod, gray-matter, bcryptjs) and switch to native implementations
This commit is contained in:
49
lib/auth.ts
49
lib/auth.ts
@@ -7,18 +7,43 @@
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { getConfig, SubpageConfig } from './config';
|
||||
|
||||
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$');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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;
|
||||
|
||||
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 {
|
||||
// Plaintext fallback (deprecated)
|
||||
isValid = password === sp.password;
|
||||
if (isValid) {
|
||||
const recommendedHash = generateScryptHash(sp.password);
|
||||
console.warn(
|
||||
`\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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
86
lib/env.ts
86
lib/env.ts
@@ -1,50 +1,58 @@
|
||||
/**
|
||||
* Zod-validated environment variables.
|
||||
* Environment variables parser.
|
||||
* Parsed once at startup — all env access should go through this module.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
/** Base URL of your Immich instance, e.g. https://immich.example.com */
|
||||
IMMICH_API_URL: z
|
||||
.string()
|
||||
.url('IMMICH_API_URL must be a valid URL')
|
||||
.transform((url) => url.replace(/\/+$/, '')),
|
||||
|
||||
/** 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>;
|
||||
export interface Env {
|
||||
IMMICH_API_URL: string;
|
||||
IMMICH_API_KEY: string;
|
||||
SITE_TITLE: string;
|
||||
SITE_SUBTITLE: string;
|
||||
CACHE_TTL: number;
|
||||
RATE_LIMIT_RPM: number;
|
||||
AUTH_SECRET?: string;
|
||||
}
|
||||
|
||||
function parseEnv(): Env {
|
||||
const result = envSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
const formatted = result.error.issues
|
||||
.map((issue) => ` ✗ ${issue.path.join('.')}: ${issue.message}`)
|
||||
.join('\n');
|
||||
const errors: string[] = [];
|
||||
|
||||
const urlRaw = process.env.IMMICH_API_URL;
|
||||
let apiUrl = '';
|
||||
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`);
|
||||
}
|
||||
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. */
|
||||
|
||||
Reference in New Issue
Block a user