feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
||||
REDIS_URL: z.string().min(1, 'REDIS_URL is required for the BullMQ worker'),
|
||||
DATABASE_URL: z.string().optional(),
|
||||
PRIVATE_DATABASE_URL: z.string().optional(),
|
||||
|
||||
/** Object storage (mirrors apps/api/src/config/env). Workers stream object bytes. */
|
||||
OBJECT_STORAGE_ENDPOINT: z.string().optional(),
|
||||
OBJECT_STORAGE_REGION: z.string().optional(),
|
||||
OBJECT_STORAGE_ACCESS_KEY_ID: z.string().optional(),
|
||||
OBJECT_STORAGE_SECRET_ACCESS_KEY: z.string().optional(),
|
||||
OBJECT_STORAGE_FORCE_PATH_STYLE: z.string().optional(),
|
||||
OBJECT_STORAGE_BUCKET_VIDEOS: z.string().optional(),
|
||||
OBJECT_STORAGE_BUCKET_DOCUMENTS: z.string().optional(),
|
||||
OBJECT_STORAGE_BUCKET_MEDIA: z.string().optional(),
|
||||
OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL: z.string().optional(),
|
||||
OBJECT_STORAGE_PUBLIC_ENDPOINT: z.string().optional(),
|
||||
|
||||
/** Cloudflare R2 fallback. */
|
||||
CLOUDFLARE_ACCOUNT_ID: z.string().optional(),
|
||||
CLOUDFLARE_ACCESS_KEY: z.string().optional(),
|
||||
CLOUDFLARE_SECRET_ACCESS_KEY: z.string().optional(),
|
||||
CLOUDFLARE_IMAGE_BUCKET_DOMAIN: z.string().optional(),
|
||||
|
||||
/** Concurrency tuning per worker process. */
|
||||
MEDIA_WORKER_CONCURRENCY: z.string().optional(),
|
||||
TRANSCRIBE_WORKER_CONCURRENCY: z.string().optional(),
|
||||
EMAIL_WORKER_CONCURRENCY: z.string().optional(),
|
||||
|
||||
/** Optional OpenAI Whisper key — when unset, transcribe-audio jobs no-op. */
|
||||
OPENAI_API_KEY: z.string().optional()
|
||||
});
|
||||
|
||||
export const env = envSchema.parse(process.env);
|
||||
|
||||
/** Worker DB connection string (DATABASE_URL preferred, falls back to PRIVATE_DATABASE_URL). */
|
||||
export function resolveDatabaseUrl(): string {
|
||||
return env.DATABASE_URL ?? env.PRIVATE_DATABASE_URL ?? '';
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.getStorageConfig = getStorageConfig;
|
||||
exports.getS3Client = getS3Client;
|
||||
var client_s3_1 = require('@aws-sdk/client-s3');
|
||||
var env_1 = require('./env');
|
||||
/**
|
||||
* Worker-side S3 client. Mirrors apps/api/src/config/storage.ts so workers
|
||||
* can stream the same buckets the API uploads to. Kept self-contained so
|
||||
* apps/jobs has no runtime dependency on apps/api.
|
||||
*/
|
||||
var BUCKET_DEFAULTS = {
|
||||
VIDEOS: 'videos',
|
||||
DOCUMENTS: 'documents',
|
||||
MEDIA: 'media'
|
||||
};
|
||||
function useS3CompatibleConfig() {
|
||||
return Boolean(
|
||||
env_1.env.OBJECT_STORAGE_ENDPOINT &&
|
||||
env_1.env.OBJECT_STORAGE_ACCESS_KEY_ID &&
|
||||
env_1.env.OBJECT_STORAGE_SECRET_ACCESS_KEY
|
||||
);
|
||||
}
|
||||
function isCloudflareConfigured() {
|
||||
return Boolean(
|
||||
env_1.env.CLOUDFLARE_ACCOUNT_ID && env_1.env.CLOUDFLARE_ACCESS_KEY && env_1.env.CLOUDFLARE_SECRET_ACCESS_KEY
|
||||
);
|
||||
}
|
||||
var cachedConfig = null;
|
||||
function getStorageConfig() {
|
||||
if (cachedConfig) return cachedConfig;
|
||||
if (useS3CompatibleConfig()) {
|
||||
cachedConfig = {
|
||||
endpoint: env_1.env.OBJECT_STORAGE_ENDPOINT,
|
||||
region: env_1.env.OBJECT_STORAGE_REGION || 'us-east-1',
|
||||
accessKeyId: env_1.env.OBJECT_STORAGE_ACCESS_KEY_ID,
|
||||
secretAccessKey: env_1.env.OBJECT_STORAGE_SECRET_ACCESS_KEY,
|
||||
forcePathStyle:
|
||||
env_1.env.OBJECT_STORAGE_FORCE_PATH_STYLE === 'true' || env_1.env.OBJECT_STORAGE_FORCE_PATH_STYLE === '1',
|
||||
bucketVideos: env_1.env.OBJECT_STORAGE_BUCKET_VIDEOS || BUCKET_DEFAULTS.VIDEOS,
|
||||
bucketDocuments: env_1.env.OBJECT_STORAGE_BUCKET_DOCUMENTS || BUCKET_DEFAULTS.DOCUMENTS,
|
||||
bucketMedia: env_1.env.OBJECT_STORAGE_BUCKET_MEDIA || BUCKET_DEFAULTS.MEDIA,
|
||||
mediaPublicBaseUrl: env_1.env.OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL || null
|
||||
};
|
||||
} else if (isCloudflareConfigured()) {
|
||||
cachedConfig = {
|
||||
endpoint: 'https://'.concat(env_1.env.CLOUDFLARE_ACCOUNT_ID, '.r2.cloudflarestorage.com'),
|
||||
region: 'auto',
|
||||
accessKeyId: env_1.env.CLOUDFLARE_ACCESS_KEY,
|
||||
secretAccessKey: env_1.env.CLOUDFLARE_SECRET_ACCESS_KEY,
|
||||
forcePathStyle: false,
|
||||
bucketVideos: BUCKET_DEFAULTS.VIDEOS,
|
||||
bucketDocuments: BUCKET_DEFAULTS.DOCUMENTS,
|
||||
bucketMedia: BUCKET_DEFAULTS.MEDIA,
|
||||
mediaPublicBaseUrl: env_1.env.CLOUDFLARE_IMAGE_BUCKET_DOMAIN || null
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
'Object storage not configured. Set OBJECT_STORAGE_* (MinIO/S3) or CLOUDFLARE_* (R2) env vars on apps/jobs.'
|
||||
);
|
||||
}
|
||||
return cachedConfig;
|
||||
}
|
||||
var s3Client = null;
|
||||
function getS3Client() {
|
||||
if (s3Client) return s3Client;
|
||||
var config = getStorageConfig();
|
||||
s3Client = new client_s3_1.S3Client({
|
||||
region: config.region,
|
||||
endpoint: config.endpoint,
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey
|
||||
},
|
||||
forcePathStyle: config.forcePathStyle
|
||||
});
|
||||
return s3Client;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { S3Client } from '@aws-sdk/client-s3';
|
||||
|
||||
import { env } from './env';
|
||||
|
||||
/**
|
||||
* Worker-side S3 client. Mirrors apps/api/src/config/storage.ts so workers
|
||||
* can stream the same buckets the API uploads to. Kept self-contained so
|
||||
* apps/jobs has no runtime dependency on apps/api.
|
||||
*/
|
||||
|
||||
const BUCKET_DEFAULTS = {
|
||||
VIDEOS: 'videos',
|
||||
DOCUMENTS: 'documents',
|
||||
MEDIA: 'media'
|
||||
} as const;
|
||||
|
||||
export interface StorageConfig {
|
||||
endpoint: string;
|
||||
region: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
forcePathStyle: boolean;
|
||||
bucketVideos: string;
|
||||
bucketDocuments: string;
|
||||
bucketMedia: string;
|
||||
mediaPublicBaseUrl: string | null;
|
||||
}
|
||||
|
||||
function useS3CompatibleConfig(): boolean {
|
||||
return Boolean(
|
||||
env.OBJECT_STORAGE_ENDPOINT && env.OBJECT_STORAGE_ACCESS_KEY_ID && env.OBJECT_STORAGE_SECRET_ACCESS_KEY
|
||||
);
|
||||
}
|
||||
|
||||
function isCloudflareConfigured(): boolean {
|
||||
return Boolean(env.CLOUDFLARE_ACCOUNT_ID && env.CLOUDFLARE_ACCESS_KEY && env.CLOUDFLARE_SECRET_ACCESS_KEY);
|
||||
}
|
||||
|
||||
let cachedConfig: StorageConfig | null = null;
|
||||
|
||||
export function getStorageConfig(): StorageConfig {
|
||||
if (cachedConfig) return cachedConfig;
|
||||
|
||||
if (useS3CompatibleConfig()) {
|
||||
cachedConfig = {
|
||||
endpoint: env.OBJECT_STORAGE_ENDPOINT!,
|
||||
region: env.OBJECT_STORAGE_REGION || 'us-east-1',
|
||||
accessKeyId: env.OBJECT_STORAGE_ACCESS_KEY_ID!,
|
||||
secretAccessKey: env.OBJECT_STORAGE_SECRET_ACCESS_KEY!,
|
||||
forcePathStyle: env.OBJECT_STORAGE_FORCE_PATH_STYLE === 'true' || env.OBJECT_STORAGE_FORCE_PATH_STYLE === '1',
|
||||
bucketVideos: env.OBJECT_STORAGE_BUCKET_VIDEOS || BUCKET_DEFAULTS.VIDEOS,
|
||||
bucketDocuments: env.OBJECT_STORAGE_BUCKET_DOCUMENTS || BUCKET_DEFAULTS.DOCUMENTS,
|
||||
bucketMedia: env.OBJECT_STORAGE_BUCKET_MEDIA || BUCKET_DEFAULTS.MEDIA,
|
||||
mediaPublicBaseUrl: env.OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL || null
|
||||
};
|
||||
} else if (isCloudflareConfigured()) {
|
||||
cachedConfig = {
|
||||
endpoint: `https://${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
region: 'auto',
|
||||
accessKeyId: env.CLOUDFLARE_ACCESS_KEY!,
|
||||
secretAccessKey: env.CLOUDFLARE_SECRET_ACCESS_KEY!,
|
||||
forcePathStyle: false,
|
||||
bucketVideos: BUCKET_DEFAULTS.VIDEOS,
|
||||
bucketDocuments: BUCKET_DEFAULTS.DOCUMENTS,
|
||||
bucketMedia: BUCKET_DEFAULTS.MEDIA,
|
||||
mediaPublicBaseUrl: env.CLOUDFLARE_IMAGE_BUCKET_DOMAIN || null
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
'Object storage not configured. Set OBJECT_STORAGE_* (MinIO/S3) or CLOUDFLARE_* (R2) env vars on apps/jobs.'
|
||||
);
|
||||
}
|
||||
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
let s3Client: S3Client | null = null;
|
||||
|
||||
export function getS3Client(): S3Client {
|
||||
if (s3Client) return s3Client;
|
||||
|
||||
const config = getStorageConfig();
|
||||
s3Client = new S3Client({
|
||||
region: config.region,
|
||||
endpoint: config.endpoint,
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey
|
||||
},
|
||||
forcePathStyle: config.forcePathStyle
|
||||
});
|
||||
|
||||
return s3Client;
|
||||
}
|
||||
Reference in New Issue
Block a user