v3: search engine, responsive header with compact user menu
This commit is contained in:
@@ -7,14 +7,14 @@ import {
|
||||
isAuthPayload,
|
||||
isInvitePayload,
|
||||
} from "../model/interfaces.ts";
|
||||
import { JWT_SECRET } from "../config.ts";
|
||||
|
||||
const jwtSecret = Deno.env.get("GERBEUR_JWT_SECRET");
|
||||
if (!jwtSecret) {
|
||||
if (!JWT_SECRET) {
|
||||
throw new Error(
|
||||
"GERBEUR_JWT_SECRET environment variable is required. Generate one with: openssl rand -hex 32",
|
||||
);
|
||||
}
|
||||
const JWT_KEY = new TextEncoder().encode(jwtSecret);
|
||||
const JWT_KEY = new TextEncoder().encode(JWT_SECRET);
|
||||
|
||||
// ── Invite tokens ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
PAGINATION_DEFAULT_LIMIT,
|
||||
PAGINATION_MAX_LIMIT,
|
||||
} from "../config.ts";
|
||||
|
||||
/**
|
||||
* Parses page/limit query parameters with sensible defaults and bounds.
|
||||
* page: clamped to [1, ∞)
|
||||
@@ -5,7 +10,7 @@
|
||||
*/
|
||||
export function parsePagination(
|
||||
params: URLSearchParams,
|
||||
defaultLimit = 20,
|
||||
defaultLimit = PAGINATION_DEFAULT_LIMIT,
|
||||
): { page: number; limit: number } {
|
||||
const page = Math.max(
|
||||
1,
|
||||
@@ -16,7 +21,7 @@ export function parsePagination(
|
||||
1,
|
||||
parseInt(params.get("limit") ?? String(defaultLimit)) || defaultLimit,
|
||||
),
|
||||
100,
|
||||
PAGINATION_MAX_LIMIT,
|
||||
);
|
||||
return { page, limit };
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import type { Context } from "@oak/oak";
|
||||
import { APIErrorCode, APIException } from "../model/interfaces.ts";
|
||||
import {
|
||||
ALLOWED_IMAGE_MIMES,
|
||||
ATTACHMENTS_DIR,
|
||||
AVATARS_DIR,
|
||||
DUMPS_DIR,
|
||||
MAX_IMAGE_SIZE_BYTES,
|
||||
PLAYLIST_IMAGES_DIR,
|
||||
UPLOADS_DIR,
|
||||
} from "../config.ts";
|
||||
|
||||
export const UPLOADS_DIR = "api/uploads";
|
||||
export const DUMPS_DIR = `${UPLOADS_DIR}/dumps`;
|
||||
export const AVATARS_DIR = `${UPLOADS_DIR}/avatars`;
|
||||
export const PLAYLIST_IMAGES_DIR = `${UPLOADS_DIR}/playlist-images`;
|
||||
export const ATTACHMENTS_DIR = `${UPLOADS_DIR}/attachments`;
|
||||
|
||||
export const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
export const ALLOWED_IMAGE_MIMES = new Set([
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
]);
|
||||
export {
|
||||
ALLOWED_IMAGE_MIMES,
|
||||
ATTACHMENTS_DIR,
|
||||
AVATARS_DIR,
|
||||
DUMPS_DIR,
|
||||
PLAYLIST_IMAGES_DIR,
|
||||
UPLOADS_DIR,
|
||||
};
|
||||
|
||||
/** Detect image MIME type from magic bytes, ignoring the browser-declared type. */
|
||||
export function detectImageMime(data: Uint8Array): string | null {
|
||||
@@ -39,7 +42,7 @@ export function detectImageMime(data: Uint8Array): string | null {
|
||||
|
||||
/** Validates image upload data: checks size and MIME. Returns the detected MIME type or throws APIException. */
|
||||
export function validateImageUpload(data: Uint8Array): string {
|
||||
if (data.length > MAX_IMAGE_SIZE) {
|
||||
if (data.length > MAX_IMAGE_SIZE_BYTES) {
|
||||
throw new APIException(
|
||||
APIErrorCode.BAD_REQUEST,
|
||||
400,
|
||||
|
||||
Reference in New Issue
Block a user