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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user