mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
- Added support for self-hosted S3 video uploads with new environment variables: OPENFRAME_ENABLE_S3_VIDEO_UPLOADS and OPENFRAME_MAX_VIDEO_UPLOAD_BYTES. - Updated .env.example and .env.docker.example to reflect new configuration options. - Enhanced Content Security Policy to include origins for S3-compatible storage. - Updated dependencies for AWS SDK to support new features. - Refactored upload logic to accommodate both Bunny and S3 upload providers. - Updated documentation to clarify the usage of direct uploads and S3 configurations. - Closes #11
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import crypto from 'crypto';
|
|
|
|
const R2_UPLOAD_TOKEN_TYPE = 'r2-upload';
|
|
const DEFAULT_TOKEN_TTL_SECONDS = 60 * 60;
|
|
|
|
interface R2UploadTokenPayload {
|
|
typ: typeof R2_UPLOAD_TOKEN_TYPE;
|
|
uid: string;
|
|
pid: string;
|
|
key: string;
|
|
sid: string;
|
|
jti: string;
|
|
tkey: string;
|
|
iat: number;
|
|
exp: number;
|
|
}
|
|
|
|
export interface R2UploadTokenSubject {
|
|
userId: string;
|
|
projectId: string;
|
|
objectKey: string;
|
|
sessionId?: string;
|
|
tokenId?: string;
|
|
thumbnailObjectKey?: string;
|
|
}
|
|
|
|
function getR2UploadTokenSecret(): string {
|
|
const secret = process.env.R2_UPLOAD_TOKEN_SECRET || process.env.NEXTAUTH_SECRET;
|
|
if (!secret) {
|
|
throw new Error('Missing R2_UPLOAD_TOKEN_SECRET or NEXTAUTH_SECRET.');
|
|
}
|
|
return secret;
|
|
}
|
|
|
|
function signPayload(payload: string, secret: string): string {
|
|
return crypto.createHmac('sha256', secret).update(payload).digest('base64url');
|
|
}
|
|
|
|
function isValidPayload(value: unknown): value is R2UploadTokenPayload {
|
|
if (!value || typeof value !== 'object') return false;
|
|
const payload = value as Partial<R2UploadTokenPayload>;
|
|
return (
|
|
payload.typ === R2_UPLOAD_TOKEN_TYPE &&
|
|
typeof payload.uid === 'string' &&
|
|
typeof payload.pid === 'string' &&
|
|
typeof payload.key === 'string' &&
|
|
typeof payload.sid === 'string' &&
|
|
typeof payload.jti === 'string' &&
|
|
typeof payload.tkey === 'string' &&
|
|
typeof payload.iat === 'number' &&
|
|
Number.isFinite(payload.iat) &&
|
|
typeof payload.exp === 'number' &&
|
|
Number.isFinite(payload.exp)
|
|
);
|
|
}
|
|
|
|
export function createR2UploadToken(
|
|
subject: R2UploadTokenSubject & {
|
|
sessionId: string;
|
|
tokenId: string;
|
|
thumbnailObjectKey: string;
|
|
},
|
|
ttlSeconds = DEFAULT_TOKEN_TTL_SECONDS
|
|
): string {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const payload: R2UploadTokenPayload = {
|
|
typ: R2_UPLOAD_TOKEN_TYPE,
|
|
uid: subject.userId,
|
|
pid: subject.projectId,
|
|
key: subject.objectKey,
|
|
sid: subject.sessionId,
|
|
jti: subject.tokenId,
|
|
tkey: subject.thumbnailObjectKey,
|
|
iat: now,
|
|
exp: now + ttlSeconds,
|
|
};
|
|
|
|
const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
const signature = signPayload(encodedPayload, getR2UploadTokenSecret());
|
|
return `${encodedPayload}.${signature}`;
|
|
}
|
|
|
|
export function verifyR2UploadToken(token: string, subject: R2UploadTokenSubject): boolean {
|
|
const payload = parseR2UploadToken(token);
|
|
if (!payload) return false;
|
|
|
|
if (
|
|
payload.uid !== subject.userId ||
|
|
payload.pid !== subject.projectId ||
|
|
payload.key !== subject.objectKey
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (subject.sessionId && payload.sid !== subject.sessionId) return false;
|
|
if (subject.tokenId && payload.jti !== subject.tokenId) return false;
|
|
if (subject.thumbnailObjectKey && payload.tkey !== subject.thumbnailObjectKey) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
export function parseR2UploadToken(token: string): R2UploadTokenPayload | null {
|
|
try {
|
|
const parts = token.split('.');
|
|
if (parts.length !== 2) return null;
|
|
|
|
const [encodedPayload, providedSignature] = parts;
|
|
if (!encodedPayload || !providedSignature) return null;
|
|
|
|
const expectedSignature = signPayload(encodedPayload, getR2UploadTokenSecret());
|
|
const providedBuffer = Buffer.from(providedSignature, 'utf8');
|
|
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
|
|
|
|
if (providedBuffer.length !== expectedBuffer.length) return null;
|
|
if (!crypto.timingSafeEqual(providedBuffer, expectedBuffer)) return null;
|
|
|
|
const payloadJson = Buffer.from(encodedPayload, 'base64url').toString('utf8');
|
|
const payloadUnknown: unknown = JSON.parse(payloadJson);
|
|
|
|
if (!isValidPayload(payloadUnknown)) return null;
|
|
|
|
const payload = payloadUnknown;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
if (payload.exp < now) return null;
|
|
return payload;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|