chore: production-readiness audit fixes and linting cleanup
This commit is contained in:
@@ -55,6 +55,9 @@ export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridPr
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const idx = parsePhotoHash(window.location.hash);
|
const idx = parsePhotoHash(window.location.hash);
|
||||||
if (idx !== null && idx < assets.length) {
|
if (idx !== null && idx < assets.length) {
|
||||||
|
// state from URL hash on mount only (equivalent to useState lazy initializer for
|
||||||
|
// client-only browser APIs).
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setLightboxIndex(idx);
|
setLightboxIndex(idx);
|
||||||
}
|
}
|
||||||
}, [assets.length]);
|
}, [assets.length]);
|
||||||
@@ -139,8 +142,9 @@ export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridPr
|
|||||||
{assets.map((asset, index) => (
|
{assets.map((asset, index) => (
|
||||||
<FadeIn key={asset.id} delay={index < 12 ? index * 50 : 0}>
|
<FadeIn key={asset.id} delay={index < 12 ? index * 50 : 0}>
|
||||||
<div
|
<div
|
||||||
className={`photo-grid__item${layout === 'showcase' && index === 0 ? ' photo-grid__featured' : ''
|
className={`photo-grid__item${
|
||||||
}`}
|
layout === 'showcase' && index === 0 ? ' photo-grid__featured' : ''
|
||||||
|
}`}
|
||||||
onClick={() => openLightbox(index)}
|
onClick={() => openLightbox(index)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
|||||||
@@ -288,11 +288,7 @@ export default async function PathPage({ params }: PathPageProps) {
|
|||||||
{album.description && ` · ${album.description}`}
|
{album.description && ` · ${album.description}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PhotoGrid
|
<PhotoGrid assets={images} layout={resolveLayout()} gridStyle={buildGridStyle()} />
|
||||||
assets={images}
|
|
||||||
layout={resolveLayout()}
|
|
||||||
gridStyle={buildGridStyle()}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
return NextResponse.json({ error: 'Asset not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Asset not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Image API] ✅ Serving ${assetId} (${size}) - Type: ${result.contentType}`);
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': result.contentType.includes('application/octet-stream')
|
'Content-Type': result.contentType.includes('application/octet-stream')
|
||||||
? 'image/jpeg'
|
? 'image/jpeg'
|
||||||
@@ -73,7 +71,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
// Images are immutable once uploaded to Immich — cache aggressively
|
// Images are immutable once uploaded to Immich — cache aggressively
|
||||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||||
'X-RateLimit-Remaining': String(remaining),
|
'X-RateLimit-Remaining': String(remaining),
|
||||||
'Access-Control-Allow-Origin': '*', // Allow cross-origin for lightbox
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result.contentLength) {
|
if (result.contentLength) {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { getConfig } from '@/lib/config';
|
|||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = request.nextUrl;
|
const { searchParams } = request.nextUrl;
|
||||||
const title = searchParams.get('title') || 'Gallery';
|
const title = (searchParams.get('title') || 'Gallery').slice(0, 200);
|
||||||
const subtitle = searchParams.get('subtitle') || '';
|
const subtitle = (searchParams.get('subtitle') || '').slice(0, 100);
|
||||||
const { theme } = getConfig();
|
const { theme } = getConfig();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ type Tab = 'theme' | 'grid' | 'server';
|
|||||||
const PRESET_FONTS: Record<string, { heading: string; body: string; caption: string }> = {
|
const PRESET_FONTS: Record<string, { heading: string; body: string; caption: string }> = {
|
||||||
studio: { heading: 'Playfair Display', body: 'DM Sans', caption: 'EB Garamond' },
|
studio: { heading: 'Playfair Display', body: 'DM Sans', caption: 'EB Garamond' },
|
||||||
minimal: { heading: 'Geist', body: 'Geist', caption: 'IBM Plex Mono' },
|
minimal: { heading: 'Geist', body: 'Geist', caption: 'IBM Plex Mono' },
|
||||||
editorial: { heading: 'Bodoni Moda', body: 'Newsreader', caption: 'IBM Plex Mono' },
|
editorial: { heading: 'Bodoni Moda', body: 'Newsreader', caption: 'Spectral' },
|
||||||
classic: { heading: 'Cinzel', body: 'Crimson Pro', caption: 'EB Garamond' },
|
classic: { heading: 'Cinzel', body: 'Crimson Pro', caption: 'Crimson Pro' },
|
||||||
noir: { heading: 'Libre Baskerville', body: 'Source Sans 3', caption: 'IBM Plex Mono' },
|
noir: { heading: 'Libre Baskerville', body: 'Source Sans 3', caption: 'Space Mono' },
|
||||||
monograph: { heading: 'Instrument Serif', body: 'Inter', caption: 'JetBrains Mono' },
|
monograph: { heading: 'Instrument Serif', body: 'Inter', caption: 'IBM Plex Mono' },
|
||||||
botanica: { heading: 'Cormorant Garamond', body: 'Nunito Sans', caption: 'Lora' },
|
botanica: { heading: 'Cormorant Garamond', body: 'Nunito Sans', caption: 'Inconsolata' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRESET_ACCENTS: Record<string, string> = {
|
const PRESET_ACCENTS: Record<string, string> = {
|
||||||
@@ -307,7 +307,6 @@ exifOnHover: ${exifOnHover}`;
|
|||||||
|
|
||||||
{/* ── Tab Content ──────────────────────── */}
|
{/* ── Tab Content ──────────────────────── */}
|
||||||
<div style={{ padding: '12px 16px' }}>
|
<div style={{ padding: '12px 16px' }}>
|
||||||
|
|
||||||
{/* ── THEME TAB ── */}
|
{/* ── THEME TAB ── */}
|
||||||
{tab === 'theme' && (
|
{tab === 'theme' && (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||||
@@ -325,10 +324,7 @@ exifOnHover: ${exifOnHover}`;
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Film Grain</Label>
|
<Label>Film Grain</Label>
|
||||||
<ToggleButton
|
<ToggleButton on={grain === 'true'} onClick={toggleGrain} />
|
||||||
on={grain === 'true'}
|
|
||||||
onClick={toggleGrain}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>EXIF on Hover</Label>
|
<Label>EXIF on Hover</Label>
|
||||||
@@ -364,7 +360,11 @@ exifOnHover: ${exifOnHover}`;
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Aspect Ratio</Label>
|
<Label>Aspect Ratio</Label>
|
||||||
<PillGrid items={ASPECT_RATIOS} active={aspectRatio} onSelect={updateAspectRatio} />
|
<PillGrid
|
||||||
|
items={ASPECT_RATIOS}
|
||||||
|
active={aspectRatio}
|
||||||
|
onSelect={updateAspectRatio}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -395,8 +395,8 @@ exifOnHover: ${exifOnHover}`;
|
|||||||
{copiedYaml === 'layout' && <CopyBadge />}
|
{copiedYaml === 'layout' && <CopyBadge />}
|
||||||
</div>
|
</div>
|
||||||
<p style={{ color: '#52525b', fontSize: 10, margin: 0, lineHeight: 1.5 }}>
|
<p style={{ color: '#52525b', fontSize: 10, margin: 0, lineHeight: 1.5 }}>
|
||||||
These settings require a server re-render. Click to copy the YAML snippet, then paste into{' '}
|
These settings require a server re-render. Click to copy the YAML snippet, then
|
||||||
<code style={{ color: '#a1a1aa' }}>content/settings.yaml</code>.
|
paste into <code style={{ color: '#a1a1aa' }}>content/settings.yaml</code>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -416,8 +416,7 @@ exifOnHover: ${exifOnHover}`;
|
|||||||
padding: '9px 0',
|
padding: '9px 0',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
background:
|
background: copiedYaml === 'all' ? '#16a34a' : 'rgba(255,255,255,0.92)',
|
||||||
copiedYaml === 'all' ? '#16a34a' : 'rgba(255,255,255,0.92)',
|
|
||||||
color: copiedYaml === 'all' ? '#fff' : '#111',
|
color: copiedYaml === 'all' ? '#fff' : '#111',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -465,13 +464,9 @@ function ToggleButton({ on, onClick }: { on: boolean; onClick: () => void }) {
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.12s ease',
|
transition: 'all 0.12s ease',
|
||||||
background: on
|
background: on ? '#166534' : 'rgba(255,255,255,0.06)',
|
||||||
? '#166534'
|
|
||||||
: 'rgba(255,255,255,0.06)',
|
|
||||||
color: on ? '#86efac' : '#71717a',
|
color: on ? '#86efac' : '#71717a',
|
||||||
border: on
|
border: on ? '1px solid #166534' : '1px solid rgba(255,255,255,0.08)',
|
||||||
? '1px solid #166534'
|
|
||||||
: '1px solid rgba(255,255,255,0.08)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{on ? '● On' : '○ Off'}
|
{on ? '● On' : '○ Off'}
|
||||||
@@ -506,10 +501,7 @@ function PillGrid({
|
|||||||
onClick={() => onSelect(item)}
|
onClick={() => onSelect(item)}
|
||||||
style={{
|
style={{
|
||||||
...pillStyle,
|
...pillStyle,
|
||||||
background:
|
background: item === active ? 'rgba(255,255,255,0.9)' : 'rgba(255,255,255,0.06)',
|
||||||
item === active
|
|
||||||
? 'rgba(255,255,255,0.9)'
|
|
||||||
: 'rgba(255,255,255,0.06)',
|
|
||||||
color: item === active ? '#111' : '#71717a',
|
color: item === active ? '#111' : '#71717a',
|
||||||
border:
|
border:
|
||||||
item === active
|
item === active
|
||||||
|
|||||||
136
lib/config.ts
136
lib/config.ts
@@ -136,18 +136,18 @@ interface GalleryYaml {
|
|||||||
hero?: string | string[];
|
hero?: string | string[];
|
||||||
albums?: string[];
|
albums?: string[];
|
||||||
subpages?:
|
subpages?:
|
||||||
| Record<string, string[] | Array<string | Record<string, string>>>
|
| Record<string, string[] | Array<string | Record<string, string>>>
|
||||||
| Array<{
|
| Array<{
|
||||||
name: string;
|
name: string;
|
||||||
albums: Array<string | Record<string, string>>;
|
albums: Array<string | Record<string, string>>;
|
||||||
password?: string;
|
password?: string;
|
||||||
grid?: {
|
grid?: {
|
||||||
columns?: number;
|
columns?: number;
|
||||||
gap?: number;
|
gap?: number;
|
||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
layout?: string;
|
layout?: string;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Raw YAML structure for global settings. */
|
/** Raw YAML structure for global settings. */
|
||||||
@@ -164,17 +164,17 @@ interface SettingsYaml {
|
|||||||
map?: boolean;
|
map?: boolean;
|
||||||
transitions?: boolean;
|
transitions?: boolean;
|
||||||
theme?:
|
theme?:
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
preset?: string;
|
preset?: string;
|
||||||
accent?: string;
|
accent?: string;
|
||||||
fonts?: { heading?: string; body?: string; caption?: string };
|
fonts?: { heading?: string; body?: string; caption?: string };
|
||||||
radius?: number;
|
radius?: number;
|
||||||
photoFrame?: string;
|
photoFrame?: string;
|
||||||
grain?: boolean;
|
grain?: boolean;
|
||||||
headerDot?: boolean;
|
headerDot?: boolean;
|
||||||
heroStyle?: string;
|
heroStyle?: string;
|
||||||
};
|
};
|
||||||
grid?: {
|
grid?: {
|
||||||
columns?: number;
|
columns?: number;
|
||||||
gap?: number;
|
gap?: number;
|
||||||
@@ -195,7 +195,7 @@ function loadYaml<T>(filename: string): T {
|
|||||||
if (filename === 'gallery.yaml') {
|
if (filename === 'gallery.yaml') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Required config not found: ${yamlPath}\n` +
|
`Required config not found: ${yamlPath}\n` +
|
||||||
`Copy content/gallery.yaml.example to content/gallery.yaml and add your album IDs.`,
|
`Copy content/gallery.yaml.example to content/gallery.yaml and add your album IDs.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {} as T;
|
return {} as T;
|
||||||
@@ -357,7 +357,7 @@ export function getConfig(): AppConfig {
|
|||||||
if (!env.AUTH_SECRET && process.env.NODE_ENV === 'production') {
|
if (!env.AUTH_SECRET && process.env.NODE_ENV === 'production') {
|
||||||
console.warn(
|
console.warn(
|
||||||
'\n⚠️ SECURITY WARNING: AUTH_SECRET is not set. Falling back to IMMICH_API_KEY.\n' +
|
'\n⚠️ SECURITY WARNING: AUTH_SECRET is not set. Falling back to IMMICH_API_KEY.\n' +
|
||||||
' Please set a long random string as AUTH_SECRET in your .env for better security.\n',
|
' Please set a long random string as AUTH_SECRET in your .env for better security.\n',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,19 +413,19 @@ export function getConfig(): AppConfig {
|
|||||||
password: sp.password,
|
password: sp.password,
|
||||||
...(sp.grid
|
...(sp.grid
|
||||||
? {
|
? {
|
||||||
grid: {
|
grid: {
|
||||||
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
||||||
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
||||||
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
||||||
...(sp.grid.layout != null
|
...(sp.grid.layout != null
|
||||||
? {
|
? {
|
||||||
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
||||||
? sp.grid.layout
|
? sp.grid.layout
|
||||||
: 'masonry') as GridConfig['layout'],
|
: 'masonry') as GridConfig['layout'],
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -442,28 +442,38 @@ export function getConfig(): AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If it's an object, it might have password, grid, albums
|
// If it's an object, it might have password, grid, albums
|
||||||
const sp = value as any;
|
interface SubpageObjectValue {
|
||||||
|
albums?: Array<string | Record<string, string>>;
|
||||||
|
password?: string;
|
||||||
|
grid?: {
|
||||||
|
columns?: number;
|
||||||
|
gap?: number;
|
||||||
|
aspectRatio?: string;
|
||||||
|
layout?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const sp = value as SubpageObjectValue;
|
||||||
const albumEntries = sp.albums || [];
|
const albumEntries = sp.albums || [];
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
slug: slugify(name),
|
slug: slugify(name),
|
||||||
albumIds: albumEntries.map((entry: any) => processAlbumEntry(entry, `subpage "${name}"`)),
|
albumIds: albumEntries.map((entry) => processAlbumEntry(entry, `subpage "${name}"`)),
|
||||||
password: sp.password,
|
password: sp.password,
|
||||||
...(sp.grid
|
...(sp.grid
|
||||||
? {
|
? {
|
||||||
grid: {
|
grid: {
|
||||||
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
||||||
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
||||||
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
||||||
...(sp.grid.layout != null
|
...(sp.grid.layout != null
|
||||||
? {
|
? {
|
||||||
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
||||||
? sp.grid.layout
|
? sp.grid.layout
|
||||||
: 'masonry') as GridConfig['layout'],
|
: 'masonry') as GridConfig['layout'],
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -486,14 +496,18 @@ export function getConfig(): AppConfig {
|
|||||||
siteSubtitle: settings.subtitle ?? env.SITE_SUBTITLE,
|
siteSubtitle: settings.subtitle ?? env.SITE_SUBTITLE,
|
||||||
seo: {
|
seo: {
|
||||||
title: settings.seo?.title || settings.title || env.SITE_TITLE || 'Gallery',
|
title: settings.seo?.title || settings.title || env.SITE_TITLE || 'Gallery',
|
||||||
description: settings.seo?.description || settings.subtitle || env.SITE_SUBTITLE || 'A curated photography portfolio',
|
description:
|
||||||
|
settings.seo?.description ||
|
||||||
|
settings.subtitle ||
|
||||||
|
env.SITE_SUBTITLE ||
|
||||||
|
'A curated photography portfolio',
|
||||||
noIndex: settings.seo?.noIndex === true,
|
noIndex: settings.seo?.noIndex === true,
|
||||||
noFollow: settings.seo?.noFollow === true,
|
noFollow: settings.seo?.noFollow === true,
|
||||||
},
|
},
|
||||||
heroImages: gallery.hero
|
heroImages: gallery.hero
|
||||||
? (Array.isArray(gallery.hero) ? gallery.hero : [gallery.hero]).map((id) =>
|
? (Array.isArray(gallery.hero) ? gallery.hero : [gallery.hero]).map((id) =>
|
||||||
validateUuid(id, 'gallery.yaml hero'),
|
validateUuid(id, 'gallery.yaml hero'),
|
||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
exifOnHover: settings.exifOnHover !== false,
|
exifOnHover: settings.exifOnHover !== false,
|
||||||
grid: {
|
grid: {
|
||||||
@@ -507,11 +521,11 @@ export function getConfig(): AppConfig {
|
|||||||
theme,
|
theme,
|
||||||
footer: settings.footer
|
footer: settings.footer
|
||||||
? {
|
? {
|
||||||
name: settings.footer.name,
|
name: settings.footer.name,
|
||||||
instagram: settings.footer.instagram,
|
instagram: settings.footer.instagram,
|
||||||
email: settings.footer.email,
|
email: settings.footer.email,
|
||||||
website: settings.footer.website,
|
website: settings.footer.website,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
legal: {
|
legal: {
|
||||||
enabled: settings.legal?.enabled === true,
|
enabled: settings.legal?.enabled === true,
|
||||||
|
|||||||
@@ -77,11 +77,13 @@ export interface SubpageSummary {
|
|||||||
coverAssetId: string | null;
|
coverAssetId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module-level flag so the album summary is only logged once even if the
|
||||||
|
// ImmichClient singleton is replaced in hot-reload scenarios.
|
||||||
|
let _hasLoggedAlbums = false;
|
||||||
|
|
||||||
// ── API Client ─────────────────────────────────────────────────
|
// ── API Client ─────────────────────────────────────────────────
|
||||||
|
|
||||||
class ImmichClient {
|
class ImmichClient {
|
||||||
private hasLoggedAlbums = false;
|
|
||||||
|
|
||||||
private get config() {
|
private get config() {
|
||||||
return getConfig();
|
return getConfig();
|
||||||
}
|
}
|
||||||
@@ -176,8 +178,8 @@ class ImmichClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Log album summary on first load so admins can see what's published
|
// Log album summary on first load so admins can see what's published
|
||||||
if (!this.hasLoggedAlbums) {
|
if (!_hasLoggedAlbums) {
|
||||||
this.hasLoggedAlbums = true;
|
_hasLoggedAlbums = true;
|
||||||
console.log('\n[Lightbox] Published albums:');
|
console.log('\n[Lightbox] Published albums:');
|
||||||
console.log('─'.repeat(80));
|
console.log('─'.repeat(80));
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ function getKey(): Buffer {
|
|||||||
*/
|
*/
|
||||||
export function encodeAssetId(assetId: string): string {
|
export function encodeAssetId(assetId: string): string {
|
||||||
const key = getKey();
|
const key = getKey();
|
||||||
// Deterministic IV from asset ID (same input → same token)
|
// Deterministic IV from asset ID (same input → same token).
|
||||||
const iv = crypto.createHash('md5').update(assetId).digest();
|
// SHA-256 slice → first 16 bytes. MD5 was deprecated for cryptographic use.
|
||||||
|
const iv = crypto.createHash('sha256').update(assetId).digest().subarray(0, 16);
|
||||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||||
const encrypted = Buffer.concat([cipher.update(assetId, 'utf8'), cipher.final()]);
|
const encrypted = Buffer.concat([cipher.update(assetId, 'utf8'), cipher.final()]);
|
||||||
// Compact: base64url(iv + ciphertext)
|
// Compact: base64url(iv + ciphertext)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { NextConfig } from 'next';
|
|||||||
// Locks down the public-facing site to only trusted sources.
|
// Locks down the public-facing site to only trusted sources.
|
||||||
const cspDirectives = [
|
const cspDirectives = [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Added unsafe-eval for some client-side libs if needed
|
"script-src 'self' 'unsafe-inline'",
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://unpkg.com",
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://unpkg.com",
|
||||||
"font-src 'self' https://fonts.gstatic.com",
|
"font-src 'self' https://fonts.gstatic.com",
|
||||||
"img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://*.tile.openstreetmap.org https://unpkg.com", // Added blob: and osm
|
"img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://*.tile.openstreetmap.org https://unpkg.com", // Added blob: and osm
|
||||||
|
|||||||
Reference in New Issue
Block a user