feat: visual polish & UX — fade-in, theme toggle, scroll-to-top, footer, cursor, badge
- Add FadeIn component (IntersectionObserver scroll-reveal + load animation) - Add dark/light theme toggle (useSyncExternalStore + localStorage) - Add scroll-to-top button (appears after 300px scroll) - Add configurable footer with social links (gallery.yaml) - Add crosshair cursor on photo grid items - Add 'X photos' count badge on subpage cover images - Add light theme color palette in tokens.css
This commit is contained in:
61
lib/auth.ts
61
lib/auth.ts
@@ -12,52 +12,46 @@ 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');
|
||||
return crypto.createHmac('sha256', getConfig().immich.apiKey).update(data).digest('hex');
|
||||
}
|
||||
|
||||
function authToken(slug: string, password: string): string {
|
||||
return hmac(`${slug}:${password}`);
|
||||
return hmac(`${slug}:${password}`);
|
||||
}
|
||||
|
||||
function cookieName(slug: string): string {
|
||||
return `lb_auth_${slug}`;
|
||||
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);
|
||||
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;
|
||||
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;
|
||||
export function authenticate(slug: string, password: string): string | null {
|
||||
const sp = findSubpageBySlug(slug);
|
||||
if (!sp?.password) return null;
|
||||
|
||||
if (password !== sp.password) return null;
|
||||
if (password !== sp.password) return null;
|
||||
|
||||
const token = authToken(slug, password);
|
||||
const maxAge = TOKEN_EXPIRY_HOURS * 60 * 60;
|
||||
const token = authToken(slug, password);
|
||||
const maxAge = TOKEN_EXPIRY_HOURS * 60 * 60;
|
||||
|
||||
return `${cookieName(slug)}=${token}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict`;
|
||||
return `${cookieName(slug)}=${token}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,23 +59,20 @@ export function authenticate(
|
||||
* Works with the cookies() API from Next.js server components.
|
||||
*/
|
||||
export function isAuthenticated(
|
||||
slug: string,
|
||||
getCookie: (name: string) => string | undefined,
|
||||
slug: string,
|
||||
getCookie: (name: string) => string | undefined,
|
||||
): boolean {
|
||||
const sp = findSubpageBySlug(slug);
|
||||
if (!sp?.password) return true; // not protected
|
||||
const sp = findSubpageBySlug(slug);
|
||||
if (!sp?.password) return true; // not protected
|
||||
|
||||
const cookie = getCookie(cookieName(slug));
|
||||
if (!cookie) return false;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user