- Remove production DB dump (db_backup_*.sql) and update.log from git tracking - Add DB backups to .gitignore - Replace all console.log/console.error with structured logger module - Translate Dutch error messages to English (link-discord.ts) - Remove dead code blocks (register-form.tsx false && pattern) - Add ESLint flat config with TypeScript, React, Next.js, jsx-a11y, and security plugins - Add Prettier config - Add eslint-plugin-security for security-aware linting - Fix all 119+ ESLint warnings across the codebase: - Resolve security/detect-object-injection with safe access patterns - Resolve security/detect-non-literal-fs-filename with path traversal validation - Replace <img> with next/image <Image> component - Remove unused variables and imports - Replace non-null assertions with proper type guards - Replace <a> with <Link> for internal navigation - Use next/script Script component for external scripts - Fix setState-in-useEffect anti-patterns (navbar-color-picker, logo-generator, theme-switcher) - Add lint and format scripts to package.json All checks: typecheck ✓, tests 58/58 ✓, lint 0 errors 0 warnings ✓
82 lines
2.1 KiB
TypeScript
82 lines
2.1 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
|
import { redis } from "@/lib/redis";
|
|
|
|
const DEFAULTS: Record<string, string> = {
|
|
hotel_name: "Atom",
|
|
habbo_imaging_url: "https://www.habbo.com/habbo-imaging/avatarimage",
|
|
logo_url: "",
|
|
nitro_client_url: "",
|
|
};
|
|
|
|
const CACHE_TTL_MS = 300_000;
|
|
const REDIS_CACHE_KEY = "site_settings";
|
|
|
|
class SiteSettings {
|
|
private cache: Map<string, string> | null = null;
|
|
|
|
private async loadFromDb(): Promise<Map<string, string>> {
|
|
try {
|
|
const rows = await prisma.websiteSetting.findMany({
|
|
select: { key: true, value: true },
|
|
});
|
|
return new Map(rows.map((r) => [r.key, r.value]));
|
|
} catch {
|
|
return new Map(Object.entries(DEFAULTS));
|
|
}
|
|
}
|
|
|
|
private async load(): Promise<Map<string, string>> {
|
|
if (redis) {
|
|
try {
|
|
const cached = await redis.get(REDIS_CACHE_KEY);
|
|
if (cached) {
|
|
const parsed = JSON.parse(cached) as Record<string, string>;
|
|
return new Map(Object.entries(parsed));
|
|
}
|
|
} catch {
|
|
// Redis unavailable — fall through
|
|
}
|
|
}
|
|
|
|
if (this.cache !== null) return this.cache;
|
|
|
|
const map = await this.loadFromDb();
|
|
this.cache = map;
|
|
|
|
if (redis) {
|
|
try {
|
|
const obj = Object.fromEntries(map.entries());
|
|
await redis.setex(REDIS_CACHE_KEY, Math.ceil(CACHE_TTL_MS / 1000), JSON.stringify(obj));
|
|
} catch {
|
|
// non-critical
|
|
}
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
async get(key: string, fallback: string | null = null): Promise<string | null> {
|
|
const map = await this.load();
|
|
if (map.has(key)) return map.get(key) as string;
|
|
// eslint-disable-next-line security/detect-object-injection -- guarded by `key in DEFAULTS`
|
|
if (key in DEFAULTS) return DEFAULTS[key] as string;
|
|
return fallback;
|
|
}
|
|
|
|
async getBool(key: string, fallback = false): Promise<boolean> {
|
|
const v = await this.get(key, null);
|
|
if (v === null) return fallback;
|
|
const s = v.toLowerCase();
|
|
return s === "1" || s === "true";
|
|
}
|
|
|
|
reload(): void {
|
|
this.cache = null;
|
|
if (redis) {
|
|
redis.del(REDIS_CACHE_KEY).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
export const siteSettings = new SiteSettings();
|