release: v0.20.0
This commit is contained in:
70
app/[...path]/routeExists.ts
Normal file
70
app/[...path]/routeExists.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Whether the catch-all has anything to show for a path — asked before the
|
||||
* page streams, so that a miss can still answer with a real 404.
|
||||
*
|
||||
* Next commits to the status with the first byte. The page used to sit inside
|
||||
* a loading.tsx Suspense boundary, so the skeleton went out as a 200 and the
|
||||
* notFound() that followed could only add a noindex tag: every unknown slug, an
|
||||
* offline subpage, a draft content page and `/<subpage>/<unknown>` were soft
|
||||
* 404s. The page now calls this first, outside any boundary, and only then
|
||||
* streams the skeleton around the part that fetches photos.
|
||||
*
|
||||
* Cost: it reads the config, the album list and content/pages/. The album list
|
||||
* is the one the header nav already waits for before the first byte, so this
|
||||
* adds no Immich request and no latency; the album's assets — the slow part —
|
||||
* are still fetched behind the skeleton.
|
||||
*
|
||||
* It mirrors the order of gates and 404s in the page body, and may be looser
|
||||
* than the body, never stricter. A path it passes that the body then rejects
|
||||
* only loses the status (an album that vanished from Immich between the list
|
||||
* and the asset fetch stays a soft 404); a path it rejects that the body would
|
||||
* render is a page nobody can open.
|
||||
*
|
||||
* Errors propagate. An Immich outage with no stale list to fall back on must
|
||||
* surface as an error, never as "this album does not exist".
|
||||
*/
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { immich } from '@/lib/immich';
|
||||
import { isAuthenticated, isProtected } from '@/lib/auth';
|
||||
import { isAdminAuthenticated } from '@/lib/admin/auth';
|
||||
import { contentPageAccess } from './contentPage';
|
||||
|
||||
/** Whether `key` is password-protected and this request has not unlocked it. */
|
||||
export async function isLocked(key: string, type: 'subpage' | 'album'): Promise<boolean> {
|
||||
if (!isProtected(key, type)) return false;
|
||||
const cookieStore = await cookies();
|
||||
const getCookie = (name: string) => cookieStore.get(name)?.value;
|
||||
return !isAuthenticated(key, getCookie, type);
|
||||
}
|
||||
|
||||
/** `path` is already slug-normalised, as the page hands it over. */
|
||||
export async function routeExists(path: readonly string[] | undefined): Promise<boolean> {
|
||||
// One segment or two: nothing on this site lives deeper, for anyone.
|
||||
if (!path || path.length === 0 || path.length > 2) return false;
|
||||
if (await exists(path)) return true;
|
||||
// Where a visitor gets the 404, a signed-in admin gets a diagnostic banner
|
||||
// (or the draft content page).
|
||||
return isAdminAuthenticated();
|
||||
}
|
||||
|
||||
async function exists([first, second]: readonly string[]): Promise<boolean> {
|
||||
if (second !== undefined) {
|
||||
// A locked subpage shows its gate for any album slug below it. Answering
|
||||
// 404 for the unknown ones would tell a visitor without the password which
|
||||
// albums are behind it.
|
||||
if (await isLocked(first, 'subpage')) return true;
|
||||
return (await immich.findAlbumBySlug(second, first)) !== null;
|
||||
}
|
||||
|
||||
if (immich.isSubpageSlug(first)) {
|
||||
if (await isLocked(first, 'subpage')) return true;
|
||||
const result = await immich.getSubpageAlbums(first);
|
||||
return !!result && result.albums.length > 0;
|
||||
}
|
||||
|
||||
const page = await contentPageAccess(first);
|
||||
if (page) return page !== 'draft';
|
||||
|
||||
return (await immich.findAlbumBySlug(first)) !== null;
|
||||
}
|
||||
Reference in New Issue
Block a user