Files
immich-folio/lib/auth.ts
ralksta b7217c4a76 feat: photography features & image optimizations
- Hero carousel: gallery.yaml hero accepts single UUID or array, crossfade component
- Full-bleed single albums: subpages with 1 album skip grid, render photos directly
- EXIF on hover: camera/lens/focal length overlay on grid items (toggle via exifOnHover)
- Customizable grid: columns/gap/aspectRatio/layout (masonry|uniform) via gallery.yaml
- Per-subpage grid overrides: each subpage can override global grid config
- True masonry: per-image aspect ratios from EXIF dimensions
- next/image migration: homepage hero, album covers, grid thumbnails, about portrait
- ThumbHash blur placeholders and dominant color backgrounds
- Custom next/image loader via image proxy with width/quality params
- Lightbox fade-in transition with dominant color background
- Password-protected subpages, health check, rate limiting, OG images
2026-02-14 10:30:27 +01:00

88 lines
2.3 KiB
TypeScript

/**
* Album authentication helpers.
* Uses HMAC tokens stored in HttpOnly cookies — no database needed.
*
* Token = HMAC-SHA256(slug + password, apiKey)
* Cookie = lb_auth_<slug> = <token>
*/
import crypto from 'crypto';
import { getConfig, SubpageConfig } from './config';
const TOKEN_EXPIRY_HOURS = 24;
function hmac(data: string): string {
return crypto
.createHmac('sha256', getConfig().immich.apiKey)
.update(data)
.digest('hex');
}
function authToken(slug: string, password: string): string {
return hmac(`${slug}:${password}`);
}
function cookieName(slug: string): string {
return `lb_auth_${slug}`;
}
/**
* Find the SubpageConfig for a given slug.
*/
export function findSubpageBySlug(slug: string): SubpageConfig | undefined {
return getConfig().subpages.find((sp) => sp.slug === slug);
}
/**
* Check if a slug corresponds to a password-protected subpage.
*/
export function isProtected(slug: string): boolean {
const sp = findSubpageBySlug(slug);
return !!sp?.password;
}
/**
* Validate a password attempt and return a Set-Cookie header value on success.
* Returns null if the password is wrong.
*/
export function authenticate(
slug: string,
password: string,
): string | null {
const sp = findSubpageBySlug(slug);
if (!sp?.password) return null;
if (password !== sp.password) return null;
const token = authToken(slug, password);
const maxAge = TOKEN_EXPIRY_HOURS * 60 * 60;
return `${cookieName(slug)}=${token}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict`;
}
/**
* Check if the request has a valid auth cookie for the given slug.
* Works with the cookies() API from Next.js server components.
*/
export function isAuthenticated(
slug: string,
getCookie: (name: string) => string | undefined,
): boolean {
const sp = findSubpageBySlug(slug);
if (!sp?.password) return true; // not protected
const cookie = getCookie(cookieName(slug));
if (!cookie) return false;
const expected = authToken(slug, sp.password);
// Constant-time comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(cookie, 'hex'),
Buffer.from(expected, 'hex'),
);
} catch {
return false;
}
}