release: v0.12.0 (#520)

Forty-one commits since v0.11.0. See CHANGELOG.md for the full list and the upgrade notes.
This commit is contained in:
Ralksta
2026-08-23 23:18:14 +02:00
committed by GitHub
parent 2f546f440d
commit 10ec68f493
119 changed files with 6888 additions and 758 deletions

View File

@@ -10,14 +10,16 @@ import { immich, ImmichUnavailableError } from '@/lib/immich';
import { decodeAssetId } from '@/lib/tokens';
import { getConfig } from '@/lib/config';
import { checkRateLimit, getClientIp, retryAfterSeconds } from '@/lib/rate-limit';
import { siteLockResponse } from '@/lib/auth';
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const config = getConfig();
// exifOnHover only gates the *technical* fields below. The asset description
// (EXPERIMENTAL caption) is editorial content, so the route stays reachable
// and serves captions even when EXIF display is disabled.
const exifEnabled = config.exifOnHover;
// Which groups this site publishes. The description used to be served
// unconditionally as editorial caption, which made it the one field an
// operator could not switch off — the problem behind #506. It is a group like
// any other now.
const show = config.exif;
// ── Rate limiting ──────────────────────────────────
const ip = getClientIp(request);
@@ -35,6 +37,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
);
}
// The page-level gate does not cover route handlers — without this a locked
// site would still serve to anyone holding the URL.
const locked = siteLockResponse(request);
if (locked) return locked;
const { id: token } = await params;
// Decode the opaque token back to an Immich asset ID
@@ -64,28 +71,38 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
}
const exif = asset.exifInfo;
const caption = exif.description?.trim() || undefined;
return NextResponse.json(
{
...(exifEnabled
? {
make: exif.make,
model: exif.model,
lensModel: exif.lensModel,
focalLength: exif.focalLength,
fNumber: exif.fNumber,
exposureTime: exif.exposureTime,
iso: exif.iso,
city: exif.city,
country: exif.country,
}
: {}),
...(caption ? { description: caption } : {}),
const caption = show.caption ? exif.description?.trim() || undefined : undefined;
const payload = {
...(show.camera
? {
make: exif.make,
model: exif.model,
lensModel: exif.lensModel,
focalLength: exif.focalLength,
}
: {}),
...(show.settings
? {
fNumber: exif.fNumber,
exposureTime: exif.exposureTime,
iso: exif.iso,
}
: {}),
...(show.location ? { city: exif.city, country: exif.country } : {}),
...(caption ? { description: caption } : {}),
};
// Every group is switched off, or this asset carries nothing from the ones
// that are on. Say "no data" rather than answering with an empty object, which
// the lightbox would render as a blank panel.
if (Object.values(payload).every((value) => value === null || value === undefined)) {
return NextResponse.json({ error: 'No EXIF data' }, { status: 404 });
}
return NextResponse.json(payload, {
headers: {
'Cache-Control': 'private, max-age=86400, stale-while-revalidate=3600',
},
{
headers: {
'Cache-Control': 'private, max-age=86400, stale-while-revalidate=3600',
},
},
);
});
}