Built the admin tools previously listed as missing: - Badge upload (/admin/badges): uploads a <code>.gif into the emulator's badge dir via BADGE_UPLOAD_DIR (node:fs); validated code/type/size, logged. Made configurable rather than skipped. - Radio tools: /admin/radio/api-keys (CRUD, server-generated keys), /admin/radio/autodj (Auto-DJ playlist CRUD), /admin/radio/embed (embed snippet generator), /admin/radio/points (points settings), /admin/radio/monitoring (live stream/now-playing/listeners status). radio_api_keys + radio_auto_dj_playlist already had real columns. - /admin/vpn: VPN/proxy detection config (block toggle + provider + key), complementing /admin/ip's raw blacklist. - Writeable boxes: new website_writeable_boxes table (model + migration 0006) + /admin/writeable-boxes CRUD; active boxes render on the public home page. env: BADGE_UPLOAD_DIR. Verified live (prod, amx_test): all 8 pages render with real data; a test writeable box appeared on the public home and was reverted. tsc 0, vitest 49/49, next build 0 (7 new admin routes).
67 lines
3.1 KiB
TypeScript
67 lines
3.1 KiB
TypeScript
import { z } from "zod";
|
|
|
|
// Minimal validated env for the foundation. When the Next.js app is added this
|
|
// will move to @t3-oss/env-nextjs (the habbo-next pattern), but the data layer
|
|
// only needs the DB connection + a couple of values today.
|
|
const schema = z.object({
|
|
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
DATABASE_URL: z.string().url(),
|
|
DATABASE_POOL_SIZE: z.coerce.number().int().positive().default(40),
|
|
DATABASE_IDLE_TIMEOUT_MS: z.coerce.number().int().positive().default(300_000),
|
|
DATABASE_CONNECT_TIMEOUT_MS: z.coerce.number().int().positive().default(10_000),
|
|
HOTEL_NAME: z.string().default("Atom"),
|
|
APP_URL: z.string().url().default("http://localhost:3000"),
|
|
// SMTP (password reset / notifications). Email features no-op if unset.
|
|
SMTP_HOST: z.string().optional(),
|
|
SMTP_PORT: z.coerce.number().int().positive().optional(),
|
|
SMTP_SECURE: z
|
|
.string()
|
|
.optional()
|
|
.transform((v) => v === "true" || v === "1"),
|
|
SMTP_USER: z.string().optional(),
|
|
SMTP_PASSWORD: z.string().optional(),
|
|
SMTP_FROM: z.string().optional(),
|
|
// RCON link to the Arcturus emulator (raw-JSON TCP protocol).
|
|
RCON_HOST: z.string().default("127.0.0.1"),
|
|
RCON_PORT: z.coerce.number().int().positive().default(3001),
|
|
RCON_TIMEOUT_MS: z.coerce.number().int().positive().default(10_000),
|
|
RCON_MAX_RETRIES: z.coerce.number().int().positive().default(3),
|
|
// NextAuth v5 reads AUTH_SECRET itself; declared here for documentation/typing.
|
|
AUTH_SECRET: z.string().min(1).optional(),
|
|
// Laravel APP_KEY (base64:...) — needed to read existing 2FA secrets.
|
|
APP_KEY: z.string().optional(),
|
|
// Optional OAuth providers (enabled only when both id+secret are set).
|
|
DISCORD_CLIENT_ID: z.string().optional(),
|
|
DISCORD_CLIENT_SECRET: z.string().optional(),
|
|
GOOGLE_CLIENT_ID: z.string().optional(),
|
|
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
|
// Mirrors Laravel config('habbo.site.convert_passwords') — enables md5->argon2id.
|
|
CONVERT_PASSWORDS: z
|
|
.string()
|
|
.optional()
|
|
.transform((v) => v === "true" || v === "1"),
|
|
// Hashing driver for NEW passwords: bcrypt (default, fits varchar(64)) | argon2id.
|
|
PASSWORD_HASH: z.enum(["bcrypt", "argon2id"]).optional(),
|
|
// Filesystem dir the badge uploader writes <code>.gif into (the emulator's
|
|
// badge image folder, e.g. .../assets/c_images/album1584). Upload is disabled
|
|
// when unset.
|
|
BADGE_UPLOAD_DIR: z.string().optional(),
|
|
// Optional AI content moderation (comments / guestbook).
|
|
OPENAI_API_KEY: z.string().optional(),
|
|
// Optional alerting (jobs worker / alert service).
|
|
DISCORD_WEBHOOK_URL: z.string().url().optional(),
|
|
ALERT_EMAIL: z.string().optional(),
|
|
// Optional PayPal top-up.
|
|
PAYPAL_CLIENT_ID: z.string().optional(),
|
|
PAYPAL_SECRET: z.string().optional(),
|
|
PAYPAL_API: z.string().url().optional(),
|
|
});
|
|
|
|
type Env = z.infer<typeof schema>;
|
|
|
|
// SKIP_ENV_VALIDATION lets tooling (typecheck, tests that don't touch the DB)
|
|
// import modules transitively without a populated .env.
|
|
export const env: Env = process.env.SKIP_ENV_VALIDATION
|
|
? (process.env as unknown as Env)
|
|
: schema.parse(process.env);
|