release: v0.16.0

See CHANGELOG.md for the full v0.16.0 entry.

---------

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>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-09-22 14:12:13 +02:00
committed by GitHub
parent c3f92ca4ec
commit a3a6319c29
120 changed files with 6818 additions and 975 deletions

View File

@@ -1,9 +1,11 @@
import { notFound } from 'next/navigation';
import { cookies } from 'next/headers';
import type { Metadata } from 'next';
import { readJournalEntry } from '@/lib/admin/journal-service';
import { readJournalEntry, listJournalEntries } from '@/lib/admin/journal-service';
import type { ParsedJournal } from '@/lib/journal';
import { isAdminAuthenticated } from '@/lib/admin/auth';
import { isAuthenticated } from '@/lib/auth';
import { journalNeighbours } from '@/lib/journalNav';
import { getConfig } from '@/lib/config';
import { immich } from '@/lib/immich';
import {
@@ -16,6 +18,7 @@ import {
} from '@/lib/urls';
import { encodeAssetId } from '@/lib/tokens';
import { EssayView } from '@/app/[...path]/EssayView';
import { JournalNav } from '@/components/JournalNav';
import type { PhotoItem } from '@/app/[...path]/PhotoGrid';
import type { ImmichAsset } from '@/lib/immich';
import PasswordGate from '@/components/PasswordGate';
@@ -36,12 +39,30 @@ export async function generateMetadata({ params }: JournalDetailPageProps): Prom
if (!entry) return { title: t.journal.notFound };
const { frontmatter } = entry.parsed;
const title = frontmatter.title || slug;
const description = frontmatter.subtitle || t.journal.entryDescription;
const ogImages = frontmatter.coverAssetId
? [{ url: imageUrl(frontmatter.coverAssetId, 'preview') }]
: [];
// generateMetadata runs unconditionally, ahead of the page body's own
// draft/password gate below — without this check a draft's or a locked
// entry's real title, subtitle and cover image reached the <head> of a
// 200 response no authentication was ever asked for (GHSA-fvgv-97g3-wjr7).
const isAuthedAdmin = await isAdminAuthenticated();
const blocked =
(frontmatter.draft && !isAuthedAdmin) ||
(!!frontmatter.password &&
!isJournalAuthenticated(
slug,
frontmatter.password,
(await cookies()).get(`lb_auth_journal_${slug}`)?.value,
));
const title = blocked ? t.journal.title : frontmatter.title || slug;
const description = blocked
? t.journal.description
: frontmatter.subtitle || t.journal.entryDescription;
const ogImages =
!blocked && frontmatter.coverAssetId
? [{ url: imageUrl(frontmatter.coverAssetId, 'preview') }]
: [];
return {
title,
@@ -111,6 +132,26 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
}
}
// Prev/next through the other entries a visitor may actually see (#591) —
// mirrors AlbumNav's rule (#483) of drawing neighbours from the surrounding
// list rather than a separate order of its own. A draft or a
// password-protected entry the visitor has not unlocked must not appear
// here even by name, the same rule the index and this entry's own gate
// already enforce (GHSA-fvgv-97g3-wjr7).
const journalCookies = await cookies();
const allEntries = await listJournalEntries();
const visibleEntries = isAuthedAdmin
? allEntries
: allEntries.filter((e) => {
if (e.frontmatter.draft) return false;
if (!e.frontmatter.password) return true;
return isAuthenticated(e.slug, (name) => journalCookies.get(name)?.value, 'journal');
});
const nav = journalNeighbours(
visibleEntries.map((e) => ({ slug: e.slug, title: e.frontmatter.title || e.slug })),
slug,
);
const config = getConfig();
// Fetch all referenced assets from Immich
@@ -151,8 +192,15 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
const toToken = (assetId: string) => tokenByAssetId.get(assetId) ?? '';
const essayForClient: ParsedJournal = {
// Named fields, not a spread of `frontmatter`: EssayView reads only
// title, subtitle, coverAssetId, author and date, but a spread put the
// stored password — plaintext or a scrypt hash — into every visitor's
// RSC payload the moment they unlocked the entry once (GHSA-fvgv-97g3-wjr7).
frontmatter: {
...frontmatter,
title: frontmatter.title,
subtitle: frontmatter.subtitle,
author: frontmatter.author,
date: frontmatter.date,
coverAssetId: frontmatter.coverAssetId ? toToken(frontmatter.coverAssetId) : undefined,
},
blocks: blocks.map((block) => {
@@ -205,6 +253,7 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
subtitle={frontmatter.subtitle}
watermark={config.watermark}
/>
<JournalNav {...nav} />
</div>
);
}