release: v0.13.0 (#535)
Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list and the upgrade notes. Ten additions — the SEO package (site URL, robots.txt, sitemap.xml, JSON-LD), slideshow mode, opt-in original downloads, copy-link, previous/next album navigation, alt text from the Immich description, per-album location precision, the update notice, a Help tab, and a subpage cover grid separate from its photo grid. Two fixes: CJK album slugs, and the lightbox control bar. Photo of the Day (#476) was built on `dev` and reverted as not planned; it never appears in a release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre --------- 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>
This commit is contained in:
40
lib/auth.ts
40
lib/auth.ts
@@ -8,6 +8,9 @@
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { getConfig, SubpageConfig } from './config';
|
||||
// From the pure schema module, not the config barrel: tests that stub out
|
||||
// '@/lib/config' wholesale would otherwise lose this helper.
|
||||
import { normalizeSlug } from './config/schema';
|
||||
import { verifyScrypt, generateScryptHash, isScryptHash } from './password';
|
||||
|
||||
const TOKEN_EXPIRY_HOURS = 24;
|
||||
@@ -92,11 +95,43 @@ function cookieName(key: string, type: ProtectedType): string {
|
||||
*/
|
||||
export const SITE_AUTH_COOKIE = 'lb_site_auth';
|
||||
|
||||
/**
|
||||
* Whether this visitor may have the album at all — every gate on every route
|
||||
* to it, not just the album's own (#475).
|
||||
*
|
||||
* A page checks the subpage's password before it renders anything; a route
|
||||
* handler is reached without ever passing through that page, so it has to ask
|
||||
* the same question itself. Checking only the album's own password let an
|
||||
* album that carries none through, even when the sole way to it was a subpage
|
||||
* that does.
|
||||
*
|
||||
* An album listed on the home page is published there whatever else lists it,
|
||||
* so it stays reachable. Where the only routes are subpages, one of them has
|
||||
* to be open — which mirrors what the visitor could actually have clicked.
|
||||
*/
|
||||
export function isAlbumReachable(
|
||||
albumId: string,
|
||||
getCookie: (name: string) => string | undefined,
|
||||
): boolean {
|
||||
if (!isAuthenticated(albumId, getCookie, 'album')) return false;
|
||||
|
||||
const config = getConfig();
|
||||
if (config.standaloneAlbums?.includes(albumId)) return true;
|
||||
|
||||
const routes = config.subpages.filter(
|
||||
(sp) => sp.enabled !== false && sp.albumIds.includes(albumId),
|
||||
);
|
||||
if (routes.length === 0) return true;
|
||||
|
||||
return routes.some((sp) => isAuthenticated(sp.slug, getCookie, 'subpage'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the SubpageConfig for a given slug.
|
||||
*/
|
||||
export function findSubpageBySlug(slug: string): SubpageConfig | undefined {
|
||||
return getConfig().subpages.find((sp) => sp.slug === slug);
|
||||
const wanted = normalizeSlug(slug);
|
||||
return getConfig().subpages.find((sp) => sp.slug === wanted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,7 +143,8 @@ function findPassword(key: string, type: ProtectedType): string | undefined {
|
||||
return config.sitePassword || undefined;
|
||||
}
|
||||
if (type === 'subpage') {
|
||||
return config.subpages.find((sp) => sp.slug === key)?.password;
|
||||
const wanted = normalizeSlug(key);
|
||||
return config.subpages.find((sp) => sp.slug === wanted)?.password;
|
||||
}
|
||||
if (type === 'album') {
|
||||
return config.albumPasswords[key];
|
||||
|
||||
Reference in New Issue
Block a user