v3: search engine, responsive header with compact user menu

This commit is contained in:
khannurien
2026-03-29 11:56:31 +00:00
parent f0f6472db6
commit cbb3505139
31 changed files with 1206 additions and 178 deletions

View File

@@ -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 ─────────────────────────────────────────────────────────────

View File

@@ -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 };
}

View File

@@ -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,