mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(api): Implement API response Cache-Control
- Introduce `withCacheControl` utility function for API responses. - Apply `private, no-store` to authentication and data modification (POST, PATCH, DELETE) routes. - Apply `private, no-cache` to sensitive data retrieval (GET) routes. - Enhance security by preventing caching of private user data. - Ensure fresh data is always fetched for authenticated API responses.
This commit is contained in:
@@ -126,6 +126,11 @@ export function successResponse<T>(
|
||||
return NextResponse.json(body, { status });
|
||||
}
|
||||
|
||||
export function withCacheControl<T>(response: NextResponse<T>, value: string): NextResponse<T> {
|
||||
response.headers.set('Cache-Control', value);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common error response helpers
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { VideoMetadata } from './types';
|
||||
|
||||
type CacheEntry = {
|
||||
value: VideoMetadata;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
const MAX_ENTRIES = 500;
|
||||
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const cache = new Map<string, CacheEntry>();
|
||||
|
||||
function pruneIfNeeded(): void {
|
||||
if (cache.size <= MAX_ENTRIES) return;
|
||||
const overflow = cache.size - MAX_ENTRIES;
|
||||
for (let i = 0; i < overflow; i += 1) {
|
||||
const oldestKey = cache.keys().next().value as string | undefined;
|
||||
if (!oldestKey) return;
|
||||
cache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCachedMetadata(key: string): VideoMetadata | null {
|
||||
const entry = cache.get(key);
|
||||
if (!entry) return null;
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
cache.delete(key);
|
||||
cache.set(key, entry);
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
export function setCachedMetadata(key: string, value: VideoMetadata, ttlMs: number = DEFAULT_TTL_MS): void {
|
||||
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
|
||||
pruneIfNeeded();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { VideoProvider, VideoMetadata, EmbedOptions, ThumbnailSize } from './types';
|
||||
import { getCachedMetadata, setCachedMetadata } from './metadata-cache';
|
||||
|
||||
// Vimeo URL patterns
|
||||
const VIMEO_PATTERNS = [
|
||||
@@ -60,6 +61,9 @@ export const vimeoProvider: VideoProvider = {
|
||||
},
|
||||
|
||||
async getMetadata(videoId: string): Promise<VideoMetadata> {
|
||||
const cacheKey = `vimeo:${videoId}`;
|
||||
const cached = getCachedMetadata(cacheKey);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://vimeo.com/api/oembed.json?url=https://vimeo.com/${videoId}`
|
||||
@@ -71,7 +75,7 @@ export const vimeoProvider: VideoProvider = {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
const metadata: VideoMetadata = {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
thumbnailUrl: data.thumbnail_url,
|
||||
@@ -80,11 +84,16 @@ export const vimeoProvider: VideoProvider = {
|
||||
authorUrl: data.author_url,
|
||||
uploadDate: data.upload_date ? new Date(data.upload_date) : undefined,
|
||||
};
|
||||
|
||||
setCachedMetadata(cacheKey, metadata);
|
||||
return metadata;
|
||||
} catch (error) {
|
||||
return {
|
||||
const fallback: VideoMetadata = {
|
||||
title: 'Vimeo Video',
|
||||
thumbnailUrl: this.getThumbnailUrl(videoId, 'large'),
|
||||
};
|
||||
setCachedMetadata(cacheKey, fallback);
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { VideoProvider, VideoMetadata, EmbedOptions, ThumbnailSize } from './types';
|
||||
import { getCachedMetadata, setCachedMetadata } from './metadata-cache';
|
||||
|
||||
// YouTube URL patterns
|
||||
const YOUTUBE_PATTERNS = [
|
||||
@@ -57,6 +58,9 @@ export const youtubeProvider: VideoProvider = {
|
||||
},
|
||||
|
||||
async getMetadata(videoId: string): Promise<VideoMetadata> {
|
||||
const cacheKey = `youtube:${videoId}`;
|
||||
const cached = getCachedMetadata(cacheKey);
|
||||
if (cached) return cached;
|
||||
// Using oEmbed API - no API key required
|
||||
// For production, you might want to use YouTube Data API for more data
|
||||
try {
|
||||
@@ -70,18 +74,23 @@ export const youtubeProvider: VideoProvider = {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
const metadata: VideoMetadata = {
|
||||
title: data.title,
|
||||
thumbnailUrl: data.thumbnail_url,
|
||||
author: data.author_name,
|
||||
authorUrl: data.author_url,
|
||||
};
|
||||
|
||||
setCachedMetadata(cacheKey, metadata);
|
||||
return metadata;
|
||||
} catch (error) {
|
||||
// Fallback with minimal data
|
||||
return {
|
||||
const fallback: VideoMetadata = {
|
||||
title: 'YouTube Video',
|
||||
thumbnailUrl: this.getThumbnailUrl(videoId, 'large'),
|
||||
};
|
||||
setCachedMetadata(cacheKey, fallback);
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user