Improvements: remove dead config, fix ESM, add URL validation, unify types, add missing logging
- Remove .prettierrc (dead config, Biome replaces Prettier) - Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat - Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit - Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts - Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars - Replace barrel export src/types/index.ts with direct @/types/common imports - Make trustHost conditional (development only) in auth.ts - Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations - Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
This commit is contained in:
1 parent
b922f6d49f
commit
17847545dd
292 files changed
+69195
-67915
No files matched your search
+104
-98
@@ -4,105 +4,111 @@ import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
|
||||
// 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(10),
|
||||
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(FALLBACK_HOTEL_NAME),
|
||||
APP_URL: z.string().url().default("http://localhost:3000"),
|
||||
NEXT_PUBLIC_APP_URL: z.string().url().default("http://localhost:3000"),
|
||||
// Public imager URL — overrides the default /imaging relative path.
|
||||
NEXT_PUBLIC_IMAGER_URL: z.string().optional(),
|
||||
// Upstream avatar imager proxy (defaults to Habbo's public imager).
|
||||
IMAGING_UPSTREAM_URL: z.string().optional(),
|
||||
// Resend API key (preferred — simpler HTTP API, always works).
|
||||
RESEND_API_KEY: z.string().optional(),
|
||||
// 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(),
|
||||
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(10),
|
||||
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(FALLBACK_HOTEL_NAME),
|
||||
APP_URL: z.string().url().default("http://localhost:3000"),
|
||||
NEXT_PUBLIC_APP_URL: z.string().url().default("http://localhost:3000"),
|
||||
// Public imager URL — overrides the default /imaging relative path.
|
||||
NEXT_PUBLIC_IMAGER_URL: z.string().optional(),
|
||||
// Upstream avatar imager proxy (defaults to Habbo's public imager).
|
||||
IMAGING_UPSTREAM_URL: z.string().optional(),
|
||||
// Resend API key (preferred — simpler HTTP API, always works).
|
||||
RESEND_API_KEY: z.string().optional(),
|
||||
// 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(),
|
||||
|
||||
// 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(),
|
||||
// Argon2id parameters (mirrors config/hashing.php).
|
||||
ARGON2_PARALLELISM: z.coerce.number().int().positive().default(1),
|
||||
ARGON2_ITERATIONS: z.coerce.number().int().positive().default(4),
|
||||
ARGON2_MEMORY_SIZE: z.coerce.number().int().positive().default(65536),
|
||||
// Bcrypt cost factor (rounds).
|
||||
BCRYPT_ROUNDS: z.coerce.number().int().positive().default(12),
|
||||
// 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(),
|
||||
// Emulator JAR backup job (jobs-worker, host-side); no-op unless both set.
|
||||
EMULATOR_JAR_PATH: z.string().optional(),
|
||||
EMULATOR_BACKUP_DIR: z.string().optional(),
|
||||
EMULATOR_BACKUP_KEEP: z.coerce.number().int().positive().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(),
|
||||
TELEGRAM_BOT_TOKEN: z.string().optional(),
|
||||
TELEGRAM_CHAT_ID: z.string().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(),
|
||||
PAYPAL_CURRENCY: z.string().default("USD"),
|
||||
PAYPAL_CREDITS_PER_USD: z.coerce.number().positive().default(100),
|
||||
// Redis — strongly recommended in production (required for multi-instance).
|
||||
// Without it, rate limits / shared caches are in-process only.
|
||||
REDIS_URL: z.string().optional(),
|
||||
// Logging level.
|
||||
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).optional(),
|
||||
// Optional Sentry — no-op when unset.
|
||||
SENTRY_DSN: z.string().url().optional(),
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
|
||||
SENTRY_ORG: z.string().optional(),
|
||||
SENTRY_PROJECT: z.string().optional(),
|
||||
SENTRY_AUTH_TOKEN: z.string().optional(),
|
||||
APP_VERSION: z.string().optional(),
|
||||
NEXT_PUBLIC_APP_VERSION: z.string().optional(),
|
||||
}).superRefine((data, ctx) => {
|
||||
if (data.NODE_ENV !== "production") return;
|
||||
if (!data.AUTH_SECRET || data.AUTH_SECRET.length < 32) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"AUTH_SECRET (>=32 characters) is required when NODE_ENV=production",
|
||||
path: ["AUTH_SECRET"],
|
||||
});
|
||||
}
|
||||
});
|
||||
// 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(),
|
||||
// Argon2id parameters (mirrors config/hashing.php).
|
||||
ARGON2_PARALLELISM: z.coerce.number().int().positive().default(1),
|
||||
ARGON2_ITERATIONS: z.coerce.number().int().positive().default(4),
|
||||
ARGON2_MEMORY_SIZE: z.coerce.number().int().positive().default(65536),
|
||||
// Bcrypt cost factor (rounds).
|
||||
BCRYPT_ROUNDS: z.coerce.number().int().positive().default(12),
|
||||
// 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(),
|
||||
// Emulator JAR backup job (jobs-worker, host-side); no-op unless both set.
|
||||
EMULATOR_JAR_PATH: z.string().optional(),
|
||||
EMULATOR_BACKUP_DIR: z.string().optional(),
|
||||
EMULATOR_BACKUP_KEEP: z.coerce.number().int().positive().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(),
|
||||
TELEGRAM_BOT_TOKEN: z.string().optional(),
|
||||
TELEGRAM_CHAT_ID: z.string().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(),
|
||||
PAYPAL_CURRENCY: z.string().default("USD"),
|
||||
PAYPAL_CREDITS_PER_USD: z.coerce.number().positive().default(100),
|
||||
// Redis — strongly recommended in production (required for multi-instance).
|
||||
// Without it, rate limits / shared caches are in-process only.
|
||||
REDIS_URL: z.string().optional(),
|
||||
// Logging level.
|
||||
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).optional(),
|
||||
// Optional Sentry — no-op when unset.
|
||||
SENTRY_DSN: z.string().url().optional(),
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
|
||||
SENTRY_ORG: z.string().optional(),
|
||||
SENTRY_PROJECT: z.string().optional(),
|
||||
SENTRY_AUTH_TOKEN: z.string().optional(),
|
||||
APP_VERSION: z.string().optional(),
|
||||
NEXT_PUBLIC_APP_VERSION: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.NODE_ENV !== "production") return;
|
||||
if (!data.AUTH_SECRET || data.AUTH_SECRET.length < 32) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"AUTH_SECRET (>=32 characters) is required when NODE_ENV=production",
|
||||
path: ["AUTH_SECRET"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type Env = z.infer<typeof schema>;
|
||||
|
||||
|
||||
Reference in new issue
Block a user