mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-04 21:08:09 +03:00
refactor: config becomes zod¬ shit
This commit is contained in:
Vendored
+21
-5
@@ -4,25 +4,41 @@
|
||||
},
|
||||
"eslint.validate": ["javascript", "javascriptreact", "html", "typescriptreact", "typescript"],
|
||||
"search.useIgnoreFiles": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
}
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
}
|
||||
},
|
||||
"[json]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
}
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
}
|
||||
},
|
||||
"files.associations": {
|
||||
"Justfile-*": "makefile"
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
"node-fetch": "catalog:",
|
||||
"prudence": "catalog:",
|
||||
"seq-logging": "catalog:",
|
||||
"sync-fetch": "catalog:",
|
||||
"tachi-common": "workspace:*",
|
||||
"ts-node": "catalog:",
|
||||
"tsconfig-paths": "catalog:",
|
||||
@@ -381,7 +380,6 @@
|
||||
"connect-redis": "catalog:",
|
||||
"csv-parse": "catalog:",
|
||||
"deepmerge": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"express": "catalog:",
|
||||
"express-async-errors": "catalog:",
|
||||
"express-prom-bundle": "catalog:",
|
||||
@@ -417,6 +415,7 @@
|
||||
"winston": "catalog:",
|
||||
"winston-loki": "catalog:",
|
||||
"winston-transport": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grpc/grpc-js": "catalog:",
|
||||
|
||||
@@ -430,7 +430,3 @@ this will log a warning, and nothing will be sent to Seq.
|
||||
### NODE_ENV
|
||||
|
||||
Expected to be either "dev", "production", "staging" or "test". If not set, this will terminate the process.
|
||||
|
||||
### REPLICA_IDENTITY
|
||||
|
||||
Optional. If present, this declares the identity of this server as a replica.
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"#*": ["./src/*"],
|
||||
// lolololOLOloLOlooLOOOOOL
|
||||
// LOLOLOLOLOLOLOL
|
||||
"@discordjs/builders": ["../../node_modules/@discordjs/builders/dist/index"],
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true
|
||||
"allowJs": true,
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"#*": ["./src/*"],
|
||||
"tachi-db-migration-engine": ["../db-migration-engine/src/index.ts"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"outDir": "js",
|
||||
"rootDir": "src"
|
||||
"rootDir": "src",
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"outDir": "js",
|
||||
"rootDir": "src"
|
||||
"rootDir": "src",
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": true,
|
||||
"allowJs": true,
|
||||
"strict": false
|
||||
"strict": false,
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"connect-redis": "catalog:",
|
||||
"csv-parse": "catalog:",
|
||||
"deepmerge": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"express": "catalog:",
|
||||
"express-async-errors": "catalog:",
|
||||
"express-prom-bundle": "catalog:",
|
||||
@@ -108,6 +107,7 @@
|
||||
"ts-node": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"uuidv7": "catalog:",
|
||||
"zod": "catalog:",
|
||||
"winston": "catalog:",
|
||||
"winston-loki": "catalog:",
|
||||
"winston-transport": "catalog:"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import { expressRequestMock } from "#test-utils/mock-request";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -17,7 +17,7 @@ function getTestTxt() {
|
||||
}
|
||||
|
||||
const ResetFileRoot = () => {
|
||||
if (Environment.nodeEnv !== "test") {
|
||||
if (Env.NODE_ENV !== "test") {
|
||||
throw new Error(
|
||||
`Not in test, yet CDN.test.ts was triggered, which could rm -rf something important.`,
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import fs from "fs";
|
||||
// viz. boku and kamai share versioning. This is because they
|
||||
// essentially share codebases.
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment } from "#lib/setup/config";
|
||||
import { Env } from "#lib/setup/config";
|
||||
import path from "path";
|
||||
import semver from "semver";
|
||||
|
||||
@@ -26,8 +26,8 @@ if (!semverInfo) {
|
||||
let commit: string | null = null;
|
||||
|
||||
try {
|
||||
if (Environment.commitHash) {
|
||||
commit = Environment.commitHash;
|
||||
if (Env.COMMIT_HASH) {
|
||||
commit = Env.COMMIT_HASH;
|
||||
} else {
|
||||
// This fetches the HEAD of our current running branch. This is useful for debugging.
|
||||
// Note that git returns this with a trailing newline, so we have to trim that off.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import bunyan from "bunyan";
|
||||
import nodemailer, { type SentMessageInfo, type Transporter } from "nodemailer";
|
||||
|
||||
@@ -45,7 +45,7 @@ export function SendEmail(
|
||||
htmlContent: string,
|
||||
textContent: string,
|
||||
): Promise<SentMessageInfo> | undefined {
|
||||
if (Environment.nodeEnv === "test") {
|
||||
if (Env.NODE_ENV === "test") {
|
||||
logger.debug(`Stubbed out SendEmail as env was test.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type https from "https";
|
||||
import { rootLogger } from "#lib/logger/logger";
|
||||
import { CloseScoreImportQueue } from "#lib/score-import/worker/queue";
|
||||
import { monkDB } from "#services/mongo/db";
|
||||
import { ClosePgConnection } from "#services/pg/db.js";
|
||||
import { CloseRedisConnection } from "#services/redis/redis";
|
||||
|
||||
const logger = rootLogger;
|
||||
@@ -22,6 +23,9 @@ async function CloseEverythingElse() {
|
||||
logger.info("Closing Mongo Database.", { shutdownInfo: true });
|
||||
await monkDB.close();
|
||||
|
||||
logger.info("Closing database...", {shutdownInfo: true});
|
||||
await ClosePgConnection();
|
||||
|
||||
logger.info("Closing Redis Connection.", { shutdownInfo: true });
|
||||
await CloseRedisConnection();
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import t from "tap";
|
||||
|
||||
import CreateLogCtx, { ChangeRootLogLevel, GetLogLevel, rootLogger, Transports } from "./logger";
|
||||
|
||||
const LOG_LEVEL = Environment.logLevel;
|
||||
const LOG_LEVEL = Env.LOG_LEVEL;
|
||||
|
||||
t.test("Logger Tests", (t) => {
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, TachiConfig } from "#lib/setup/config";
|
||||
import { EscapeStringRegexp } from "#utils/misc";
|
||||
import { Transport as SeqTransport } from "@valuabletouch/winston-seq";
|
||||
import SafeJSONStringify from "safe-json-stringify";
|
||||
import winston, { format, type LeveledLogMethod, type Logger, transports } from "winston";
|
||||
|
||||
@@ -66,18 +65,16 @@ const formatExcessPropertiesNoStack = (
|
||||
return ` ${limit ? StrCap(content) : content}`;
|
||||
};
|
||||
|
||||
const replicaInfo = Environment.replicaIdentity ? ` (${Environment.replicaIdentity})` : "";
|
||||
|
||||
const tachiPrintf = format.printf(
|
||||
({ level, message, context = "tachi-root", timestamp, ...meta }) =>
|
||||
`${timestamp}${replicaInfo} [${
|
||||
`${timestamp} [${
|
||||
Array.isArray(context) ? context.join(" | ") : context
|
||||
}] ${level}: ${message}${formatExcessProperties(meta)}`,
|
||||
);
|
||||
|
||||
const tachiConsolePrintf = format.printf(
|
||||
({ level, message, context = "tachi-root", timestamp, hideFromConsole, ...meta }) =>
|
||||
`${timestamp}${replicaInfo} [${
|
||||
`${timestamp} [${
|
||||
Array.isArray(context) ? context.join(" | ") : context
|
||||
}] ${level}: ${message}${formatExcessPropertiesNoStack(
|
||||
meta,
|
||||
@@ -123,30 +120,6 @@ const tports: Array<winston.transport> = [
|
||||
}),
|
||||
];
|
||||
|
||||
if (Environment.seqUrl) {
|
||||
const levelMap: Record<string, any> = {
|
||||
crit: "Fatal",
|
||||
severe: "Error",
|
||||
error: "Error",
|
||||
warn: "Warning",
|
||||
info: "Information",
|
||||
verbose: "Verbose",
|
||||
debug: "Debug",
|
||||
};
|
||||
|
||||
tports.push(
|
||||
new SeqTransport({
|
||||
apiKey: Environment.seqApiKey,
|
||||
serverUrl: Environment.seqUrl,
|
||||
onError: (err) => {
|
||||
console.error(`Failed to send seq message: ${err.message}.`);
|
||||
},
|
||||
levelMapper(level = "") {
|
||||
return levelMap[level] ?? "Information";
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export const rootLogger = winston.createLogger({
|
||||
levels: {
|
||||
@@ -177,7 +150,6 @@ export const rootLogger = winston.createLogger({
|
||||
defaultMeta: {
|
||||
__ServerName: TachiConfig.NAME,
|
||||
__Worker: !!process.env.IS_WORKER,
|
||||
__ReplicaID: Environment.replicaIdentity,
|
||||
},
|
||||
}) as KtLogger;
|
||||
|
||||
@@ -217,7 +189,7 @@ export function ChangeRootLogLevel(
|
||||
export function GetLogLevel() {
|
||||
return (
|
||||
rootLogger.transports.map((e) => e.level).find((e) => typeof e === "string") ??
|
||||
Environment.logLevel
|
||||
Env.LOG_LEVEL
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Migration } from "#utils/types";
|
||||
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, TachiConfig } from "#lib/setup/config";
|
||||
import db from "#services/mongo/db";
|
||||
|
||||
import UserFollowersMigration from "./migrations/add-following-to-users";
|
||||
@@ -51,7 +51,7 @@ export const FAKE_MIGRATION: Migration = {
|
||||
// If we're testing, we should pull fake migrations instead to ensure the tests
|
||||
// stay consistent
|
||||
const REGISTERED_MIGRATIONS: Array<Migration> =
|
||||
Environment.nodeEnv === "test"
|
||||
Env.NODE_ENV === "test"
|
||||
? [FAKE_MIGRATION]
|
||||
: [
|
||||
UGPTRivalsMigration,
|
||||
@@ -66,7 +66,7 @@ const REGISTERED_MIGRATIONS: Array<Migration> =
|
||||
];
|
||||
|
||||
// only apply type-specific migrations if we're not in testing
|
||||
if (Environment.nodeEnv !== "test") {
|
||||
if (Env.NODE_ENV !== "test") {
|
||||
// kamaitachi specific migrations
|
||||
if (TachiConfig.TYPE !== "boku") {
|
||||
REGISTERED_MIGRATIONS.push(
|
||||
@@ -269,7 +269,7 @@ export async function ApplyMigration(migration: Migration) {
|
||||
// remove the stale migration so it can be re-ran in the future.
|
||||
await db.migrations.findOneAndDelete({ migrationID });
|
||||
|
||||
if (Environment.nodeEnv === "test") {
|
||||
if (Env.NODE_ENV === "test") {
|
||||
throw new Error("Was going to exit with statusCode 1, but we're in testing.");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Environment, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, TachiConfig } from "#lib/setup/config";
|
||||
import { Queue, QueueEvents } from "bullmq";
|
||||
|
||||
const ScoreImportQueue = new Queue(`${TachiConfig.NAME} Score Import Queue`, {
|
||||
connection: { host: Environment.redisUrl, port: 6379 },
|
||||
connection: { host: Env.REDIS_URL, port: 6379 },
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 10, // keep the last 10 failed jobs, but start pruning beyond that.
|
||||
@@ -12,7 +12,7 @@ const ScoreImportQueue = new Queue(`${TachiConfig.NAME} Score Import Queue`, {
|
||||
export default ScoreImportQueue;
|
||||
|
||||
export const ScoreImportQueueEvents = new QueueEvents(ScoreImportQueue.name, {
|
||||
connection: { host: Environment.redisUrl, port: 6379 },
|
||||
connection: { host: Env.REDIS_URL, port: 6379 },
|
||||
});
|
||||
|
||||
export async function CloseScoreImportQueue() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HandleSIGTERMGracefully } from "#lib/handlers/sigterm";
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import { FormatUserDoc, GetUserWithID } from "#utils/user";
|
||||
import { Worker } from "bullmq";
|
||||
import { EventEmitter } from "events";
|
||||
@@ -130,7 +130,7 @@ export const worker = new Worker(
|
||||
concurrency: ServerConfig.EXTERNAL_SCORE_IMPORT_WORKER_CONCURRENCY ?? 10,
|
||||
connection: {
|
||||
port: 6379,
|
||||
host: Environment.redisUrl,
|
||||
host: Env.REDIS_URL,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import fs from "fs/promises";
|
||||
import type { GameGroup } from "../../../../common/src";
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import { asyncExec } from "#utils/misc";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
@@ -189,7 +189,7 @@ export class DatabaseSeedsRepo {
|
||||
* Pull any seeds changes in this repository.
|
||||
*/
|
||||
pull() {
|
||||
if (Environment.nodeEnv === "dev") {
|
||||
if (Env.NODE_ENV === "dev") {
|
||||
// prevent an awful interaction where a user edits stuff on their disk
|
||||
// and tries to run pnpm load-seeds
|
||||
// but it fails because pull can't rebase with changes.
|
||||
|
||||
@@ -1,326 +1,187 @@
|
||||
import type { SendMailOptions } from "nodemailer";
|
||||
|
||||
import { FormatPrError } from "#utils/prudence";
|
||||
|
||||
import {
|
||||
allSupportedGameGroups,
|
||||
type integer,
|
||||
type TachiServerCoreConfig,
|
||||
} from "../../../../common/src";
|
||||
// barrel file for re-exporting env variables.
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
import JSON5 from "json5";
|
||||
import { p } from "prudence";
|
||||
import { URL } from "url";
|
||||
|
||||
import { allImportTypes } from "../../../../common/src/constants/import-types";
|
||||
|
||||
// imports things like NODE_ENV from a local .env file if one is present.
|
||||
dotenv.config();
|
||||
import { allSupportedGameGroups, type GameGroup , type ImportTypes } from "tachi-common";
|
||||
import { allImportTypes } from "tachi-common/constants/import-types";
|
||||
import { z } from "zod";
|
||||
|
||||
// stub - having a real logger here creates a circular dependency.
|
||||
const logger = console;
|
||||
|
||||
const confLocation = process.env.TCHIS_CONF_LOCATION ?? "./conf.json5";
|
||||
|
||||
// reads from $pwd/conf.json5, unless an override is set
|
||||
let confFile;
|
||||
|
||||
try {
|
||||
confFile = fs.readFileSync(confLocation, "utf-8");
|
||||
} catch (err) {
|
||||
logger.error("Error while trying to open conf.json5. Is one present?", {
|
||||
err,
|
||||
});
|
||||
const rawConf = process.env.TCHIS_CONF;
|
||||
|
||||
if (!rawConf) {
|
||||
logger.error("TCHIS_CONF environment variable is not set. Terminating.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config: unknown = JSON5.parse(confFile);
|
||||
let config: unknown;
|
||||
|
||||
function isValidURL(self: unknown) {
|
||||
if (typeof self !== "string") {
|
||||
return `Expected URL, received type ${typeof self}`;
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(self);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return `Invalid URL ${self} (${(err as Error).message}).`;
|
||||
}
|
||||
try {
|
||||
config = JSON5.parse(rawConf);
|
||||
} catch (err) {
|
||||
logger.error("Failed to parse TCHIS_CONF as JSON5.", { err });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export interface OAuth2Info {
|
||||
CLIENT_ID: string;
|
||||
CLIENT_SECRET: string;
|
||||
REDIRECT_URI: string;
|
||||
}
|
||||
|
||||
export interface CGConfig {
|
||||
API_KEY: string;
|
||||
URL: string;
|
||||
}
|
||||
|
||||
export interface TachiServerConfig {
|
||||
MONGO_DATABASE_NAME: string;
|
||||
CAPTCHA_SECRET_KEY: string;
|
||||
SESSION_SECRET: string;
|
||||
FLO_API_URL?: string;
|
||||
EAG_API_URL?: string;
|
||||
MIN_API_URL?: string;
|
||||
ARC_API_URL?: string;
|
||||
MYT_API_HOST?: string;
|
||||
|
||||
CG_DEV_CONFIG?: CGConfig;
|
||||
CG_NAG_CONFIG?: CGConfig;
|
||||
CG_GAN_CONFIG?: CGConfig;
|
||||
|
||||
FLO_OAUTH2_INFO?: OAuth2Info;
|
||||
EAG_OAUTH2_INFO?: OAuth2Info;
|
||||
MIN_OAUTH2_INFO?: OAuth2Info;
|
||||
ARC_AUTH_TOKEN?: string;
|
||||
MYT_AUTH_TOKEN?: string;
|
||||
ENABLE_SERVER_HTTPS?: boolean;
|
||||
CLIENT_DEV_SERVER?: string | null;
|
||||
RATE_LIMIT: integer;
|
||||
OAUTH_CLIENT_CAP: integer;
|
||||
OPTIONS_ALWAYS_SUCCEEDS?: boolean;
|
||||
USE_EXTERNAL_SCORE_IMPORT_WORKER: boolean;
|
||||
EXTERNAL_SCORE_IMPORT_WORKER_CONCURRENCY?: integer;
|
||||
ENABLE_METRICS: boolean;
|
||||
SEEDS_CONFIG?:
|
||||
| {
|
||||
BRANCH?: string;
|
||||
REPO_URL: string;
|
||||
TYPE: "GIT_REPO";
|
||||
USER_EMAIL: string | null;
|
||||
USER_NAME: string | null;
|
||||
}
|
||||
| {
|
||||
PATH: string;
|
||||
TYPE: "LOCAL_FILES";
|
||||
};
|
||||
EMAIL_CONFIG?: {
|
||||
DKIM?: SendMailOptions["dkim"];
|
||||
FROM: string;
|
||||
|
||||
// @warning This is explicitly allowed to be any
|
||||
// As nodemailer doesnt properly export the types we care about
|
||||
// This should be set to SMTPTransport.Options, but it is
|
||||
// inaccessible.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
TRANSPORT_OPS: any;
|
||||
};
|
||||
USC_QUEUE_SIZE: integer;
|
||||
BEATORAJA_QUEUE_SIZE: integer;
|
||||
MAX_GOAL_SUBSCRIPTIONS: integer;
|
||||
MAX_QUEST_SUBSCRIPTIONS: integer;
|
||||
MAX_FOLLOWING_AMOUNT: integer;
|
||||
MAX_RIVALS: integer;
|
||||
OUR_URL: string;
|
||||
ALLOW_RUNNING_OFFLINE?: boolean;
|
||||
INVITE_CODE_CONFIG?: {
|
||||
BATCH_SIZE: integer;
|
||||
BETA_USER_BONUS: integer;
|
||||
INVITE_CAP: integer;
|
||||
};
|
||||
TACHI_CONFIG: TachiServerCoreConfig;
|
||||
CDN_CONFIG: {
|
||||
SAVE_LOCATION:
|
||||
| {
|
||||
ACCESS_KEY_ID: string;
|
||||
BUCKET: string;
|
||||
ENDPOINT: string;
|
||||
KEY_PREFIX?: string;
|
||||
REGION?: string;
|
||||
SECRET_ACCESS_KEY: string;
|
||||
TYPE: "S3_BUCKET";
|
||||
}
|
||||
| { LOCATION: string; SERVE_OWN_CDN?: boolean; TYPE: "LOCAL_FILESYSTEM" };
|
||||
WEB_LOCATION: string;
|
||||
};
|
||||
}
|
||||
|
||||
const isValidOauth2 = p.optional({
|
||||
CLIENT_ID: "string",
|
||||
CLIENT_SECRET: "string",
|
||||
REDIRECT_URI: "string",
|
||||
const oauth2Schema = z.object({
|
||||
CLIENT_ID: z.string(),
|
||||
CLIENT_SECRET: z.string(),
|
||||
REDIRECT_URI: z.string(),
|
||||
});
|
||||
|
||||
const isValidCGConfig = p.optional({
|
||||
API_KEY: "string",
|
||||
URL: "string",
|
||||
const cgConfigSchema = z.object({
|
||||
API_KEY: z.string(),
|
||||
URL: z.string(),
|
||||
});
|
||||
|
||||
const err = p(config, {
|
||||
MONGO_DATABASE_NAME: "string",
|
||||
CAPTCHA_SECRET_KEY: "string",
|
||||
SESSION_SECRET: "string",
|
||||
FLO_API_URL: p.optional(isValidURL),
|
||||
EAG_API_URL: p.optional(isValidURL),
|
||||
MIN_API_URL: p.optional(isValidURL),
|
||||
ARC_API_URL: p.optional(isValidURL),
|
||||
MYT_API_HOST: "*string",
|
||||
const configSchema = z.object({
|
||||
MONGO_DATABASE_NAME: z.string(),
|
||||
CAPTCHA_SECRET_KEY: z.string(),
|
||||
SESSION_SECRET: z.string(),
|
||||
FLO_API_URL: z.url().optional(),
|
||||
EAG_API_URL: z.url().optional(),
|
||||
MIN_API_URL: z.url().optional(),
|
||||
ARC_API_URL: z.url().optional(),
|
||||
MYT_API_HOST: z.string().optional(),
|
||||
|
||||
CG_DEV_CONFIG: isValidCGConfig,
|
||||
CG_NAG_CONFIG: isValidCGConfig,
|
||||
CG_GAN_CONFIG: isValidCGConfig,
|
||||
CG_DEV_CONFIG: cgConfigSchema.optional(),
|
||||
CG_NAG_CONFIG: cgConfigSchema.optional(),
|
||||
CG_GAN_CONFIG: cgConfigSchema.optional(),
|
||||
|
||||
FLO_OAUTH2_INFO: isValidOauth2,
|
||||
EAG_OAUTH2_INFO: isValidOauth2,
|
||||
MIN_OAUTH2_INFO: isValidOauth2,
|
||||
ARC_AUTH_TOKEN: "*string",
|
||||
MYT_AUTH_TOKEN: "*string",
|
||||
ENABLE_SERVER_HTTPS: "*boolean",
|
||||
CLIENT_DEV_SERVER: "*?string",
|
||||
RATE_LIMIT: p.optional(p.isPositiveInteger),
|
||||
OAUTH_CLIENT_CAP: p.optional(p.isPositiveInteger),
|
||||
OPTIONS_ALWAYS_SUCCEEDS: "*boolean",
|
||||
USE_EXTERNAL_SCORE_IMPORT_WORKER: "*boolean",
|
||||
EXTERNAL_SCORE_IMPORT_WORKER_CONCURRENCY: p.optional(p.isPositiveInteger),
|
||||
ALLOW_RUNNING_OFFLINE: "*boolean",
|
||||
ENABLE_METRICS: "*boolean",
|
||||
EMAIL_CONFIG: p.optional({
|
||||
FROM: "string",
|
||||
DKIM: "*object",
|
||||
|
||||
// WARN: This validation is improper and lazy.
|
||||
// The actual content is just some wacky options object.
|
||||
// I'm not going to assert this properly.
|
||||
TRANSPORT_OPS: "*object",
|
||||
FLO_OAUTH2_INFO: oauth2Schema.optional(),
|
||||
EAG_OAUTH2_INFO: oauth2Schema.optional(),
|
||||
MIN_OAUTH2_INFO: oauth2Schema.optional(),
|
||||
ARC_AUTH_TOKEN: z.string().optional(),
|
||||
MYT_AUTH_TOKEN: z.string().optional(),
|
||||
ENABLE_SERVER_HTTPS: z.boolean().optional(),
|
||||
CLIENT_DEV_SERVER: z.string().nullable().optional(),
|
||||
RATE_LIMIT: z.number().int().positive().default(500),
|
||||
OAUTH_CLIENT_CAP: z.number().int().positive().default(15),
|
||||
OPTIONS_ALWAYS_SUCCEEDS: z.boolean().optional(),
|
||||
USE_EXTERNAL_SCORE_IMPORT_WORKER: z.boolean().default(false),
|
||||
EXTERNAL_SCORE_IMPORT_WORKER_CONCURRENCY: z.number().int().positive().optional(),
|
||||
ALLOW_RUNNING_OFFLINE: z.boolean().optional(),
|
||||
ENABLE_METRICS: z.boolean().default(false),
|
||||
EMAIL_CONFIG: z
|
||||
.object({
|
||||
FROM: z.string(),
|
||||
// Nodemailer does not export the DKIM type properly, so we accept any object.
|
||||
DKIM: z.any().optional(),
|
||||
// The actual content is just a wacky options object — not worth asserting precisely.
|
||||
TRANSPORT_OPS: z.any().optional(),
|
||||
})
|
||||
.optional(),
|
||||
USC_QUEUE_SIZE: z.number().int().gte(2).default(3),
|
||||
BEATORAJA_QUEUE_SIZE: z.number().int().gte(2).default(3),
|
||||
MAX_GOAL_SUBSCRIPTIONS: z.number().int().positive().default(1_000),
|
||||
MAX_QUEST_SUBSCRIPTIONS: z.number().int().positive().default(100),
|
||||
MAX_FOLLOWING_AMOUNT: z.number().int().positive().default(1_000),
|
||||
MAX_RIVALS: z.number().int().positive().default(5),
|
||||
OUR_URL: z.string().refine((s) => !s.endsWith("/"), {
|
||||
message: "OUR_URL must not end with a trailing slash.",
|
||||
}),
|
||||
USC_QUEUE_SIZE: p.optional(p.gteInt(2)),
|
||||
BEATORAJA_QUEUE_SIZE: p.optional(p.gteInt(2)),
|
||||
MAX_GOAL_SUBSCRIPTIONS: p.optional(p.isPositiveInteger),
|
||||
MAX_QUEST_SUBSCRIPTIONS: p.optional(p.isPositiveInteger),
|
||||
MAX_FOLLOWING_AMOUNT: p.optional(p.isPositiveInteger),
|
||||
MAX_RIVALS: p.optional(p.isPositiveInteger),
|
||||
OUR_URL: (self) => {
|
||||
if (typeof self !== "string") {
|
||||
return "Expected a string.";
|
||||
}
|
||||
|
||||
if (self.endsWith("/")) {
|
||||
return `OUR_URL should not end with a trailing slash. Use ${self.substring(
|
||||
0,
|
||||
self.length - 1,
|
||||
)} instead.`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
INVITE_CODE_CONFIG: p.optional({
|
||||
BATCH_SIZE: p.isPositiveInteger,
|
||||
INVITE_CAP: p.isPositiveInteger,
|
||||
BETA_USER_BONUS: p.isPositiveInteger,
|
||||
INVITE_CODE_CONFIG: z
|
||||
.object({
|
||||
BATCH_SIZE: z.number().int().positive(),
|
||||
INVITE_CAP: z.number().int().positive(),
|
||||
BETA_USER_BONUS: z.number().int().positive(),
|
||||
})
|
||||
.optional(),
|
||||
TACHI_CONFIG: z.object({
|
||||
NAME: z.string(),
|
||||
TYPE: z.enum(["kamai", "boku", "omni"]),
|
||||
GAMES: z.array(z.enum(allSupportedGameGroups as [GameGroup, ...GameGroup[]])),
|
||||
IMPORT_TYPES: z.array(z.enum(allImportTypes as [ImportTypes, ...ImportTypes[]])),
|
||||
SIGNUPS_ENABLED: z.boolean().default(true),
|
||||
}),
|
||||
TACHI_CONFIG: {
|
||||
NAME: "string",
|
||||
TYPE: p.isIn("kamai", "boku", "omni"),
|
||||
GAMES: [p.isIn(allSupportedGameGroups)],
|
||||
IMPORT_TYPES: [p.isIn(allImportTypes)],
|
||||
SIGNUPS_ENABLED: p.optional("boolean"),
|
||||
},
|
||||
CDN_CONFIG: {
|
||||
WEB_LOCATION: "string",
|
||||
SAVE_LOCATION: p.or(
|
||||
{
|
||||
TYPE: p.is("LOCAL_FILESYSTEM"),
|
||||
SERVE_OWN_CDN: "*boolean",
|
||||
LOCATION: "string",
|
||||
},
|
||||
{
|
||||
TYPE: p.is("S3_BUCKET"),
|
||||
ENDPOINT: "string",
|
||||
ACCESS_KEY_ID: "string",
|
||||
SECRET_ACCESS_KEY: "string",
|
||||
BUCKET: "string",
|
||||
KEY_PREFIX: "*string",
|
||||
REGION: "*string",
|
||||
},
|
||||
),
|
||||
},
|
||||
SEEDS_CONFIG: p.optional(
|
||||
p.or(
|
||||
{
|
||||
TYPE: p.is("GIT_REPO"),
|
||||
REPO_URL: "string",
|
||||
USER_NAME: "?string",
|
||||
USER_EMAIL: "?string",
|
||||
BRANCH: "*string",
|
||||
},
|
||||
{
|
||||
TYPE: p.is("LOCAL_FILES"),
|
||||
PATH: "string",
|
||||
},
|
||||
),
|
||||
),
|
||||
CDN_CONFIG: z.object({
|
||||
WEB_LOCATION: z.string(),
|
||||
SAVE_LOCATION: z.union([
|
||||
z.object({
|
||||
TYPE: z.literal("LOCAL_FILESYSTEM"),
|
||||
SERVE_OWN_CDN: z.boolean().optional(),
|
||||
LOCATION: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
TYPE: z.literal("S3_BUCKET"),
|
||||
ENDPOINT: z.string(),
|
||||
ACCESS_KEY_ID: z.string(),
|
||||
SECRET_ACCESS_KEY: z.string(),
|
||||
BUCKET: z.string(),
|
||||
KEY_PREFIX: z.string().optional(),
|
||||
REGION: z.string().optional(),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
SEEDS_CONFIG: z
|
||||
.union([
|
||||
z.object({
|
||||
TYPE: z.literal("GIT_REPO"),
|
||||
REPO_URL: z.string(),
|
||||
USER_NAME: z.string().nullable(),
|
||||
USER_EMAIL: z.string().nullable(),
|
||||
BRANCH: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
TYPE: z.literal("LOCAL_FILES"),
|
||||
PATH: z.string(),
|
||||
}),
|
||||
])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
if (err) {
|
||||
throw new Error(FormatPrError(err, "Invalid conf.json5 file."));
|
||||
export type OAuth2Info = z.infer<typeof oauth2Schema>;
|
||||
export type CGConfig = z.infer<typeof cgConfigSchema>;
|
||||
export type TachiServerConfig = z.infer<typeof configSchema>;
|
||||
|
||||
const result = configSchema.safeParse(config);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid TCHIS_CONF: ${result.error.message}`);
|
||||
}
|
||||
|
||||
const tachiServerConfig = config as TachiServerConfig;
|
||||
|
||||
// default rate limit 500
|
||||
tachiServerConfig.RATE_LIMIT ??= 500;
|
||||
tachiServerConfig.OAUTH_CLIENT_CAP ??= 15;
|
||||
tachiServerConfig.USC_QUEUE_SIZE ??= 3;
|
||||
tachiServerConfig.BEATORAJA_QUEUE_SIZE ??= 3;
|
||||
tachiServerConfig.MAX_GOAL_SUBSCRIPTIONS ??= 1_000;
|
||||
tachiServerConfig.MAX_QUEST_SUBSCRIPTIONS ??= 100;
|
||||
tachiServerConfig.MAX_RIVALS ??= 5;
|
||||
tachiServerConfig.MAX_FOLLOWING_AMOUNT ??= 1_000;
|
||||
tachiServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER ??= false;
|
||||
tachiServerConfig.TACHI_CONFIG.SIGNUPS_ENABLED ??= true;
|
||||
tachiServerConfig.ENABLE_METRICS ??= false;
|
||||
|
||||
export const TachiConfig = tachiServerConfig.TACHI_CONFIG;
|
||||
export const ServerConfig = tachiServerConfig;
|
||||
export const TachiConfig = result.data.TACHI_CONFIG;
|
||||
export const ServerConfig: TachiServerConfig = result.data;
|
||||
|
||||
// Environment Variable Validation
|
||||
|
||||
let port = Number(process.env.PORT);
|
||||
let PORT = Number(process.env.PORT);
|
||||
|
||||
if (Number.isNaN(port) && process.env.IS_SERVER) {
|
||||
if (Number.isNaN(PORT) && process.env.IS_SERVER) {
|
||||
logger.warn(`No/invalid PORT specified in environment, defaulting to 8080.`);
|
||||
port = 8080;
|
||||
PORT = 8080;
|
||||
}
|
||||
|
||||
const redisUrl = process.env.REDIS_URL ?? "";
|
||||
const REDIS_URL = process.env.REDIS_URL;
|
||||
|
||||
if (!redisUrl) {
|
||||
if (!REDIS_URL) {
|
||||
// n.b. These logs should be critical level, but the logger cant actually instantiate
|
||||
// itself in this file, because this file also controlls the logger. Ouch!
|
||||
logger.error(`No REDIS_URL specified in environment. Terminating.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mongoUrl = process.env.MONGO_URL ?? "";
|
||||
const MONGO_URL = process.env.MONGO_URL;
|
||||
|
||||
if (!mongoUrl) {
|
||||
if (!MONGO_URL) {
|
||||
logger.error(`No MONGO_URL specified in environment. Terminating.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const nodeEnv = process.env.NODE_ENV ?? "";
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
|
||||
if (!nodeEnv) {
|
||||
if (!NODE_ENV) {
|
||||
logger.error(`No NODE_ENV specified in environment. Terminating.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!["dev", "production", "staging", "test"].includes(nodeEnv)) {
|
||||
if (!["dev", "production", "staging", "test"].includes(NODE_ENV)) {
|
||||
logger.error(
|
||||
`Invalid NODE_ENV set in environment. Expected dev, production, test or staging. Got ${nodeEnv}.`,
|
||||
`Invalid NODE_ENV set in environment. Expected dev, production, test or staging. Got ${NODE_ENV}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// if (bms XOR popn) is enabled
|
||||
// if (bms XOR pms) is enabled
|
||||
if (TachiConfig.GAMES.includes("bms") !== TachiConfig.GAMES.includes("pms")) {
|
||||
logger.error(
|
||||
`BMS and PMS MUST be enabled at the same time, due to how the beatoraja IR works.`,
|
||||
@@ -337,24 +198,21 @@ if (!["crit", "debug", "error", "info", "severe", "verbose", "warn"].includes(lo
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const replicaIdentity = process.env.REPLICA_IDENTITY;
|
||||
|
||||
const postgresUrl = process.env.POSTGRES_URL ?? "";
|
||||
const POSTGRES_URL = process.env.POSTGRES_URL ?? "";
|
||||
|
||||
if (!postgresUrl) {
|
||||
if (!POSTGRES_URL) {
|
||||
logger.error(`No POSTGRES_URL specified in environment. Terminating.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export const Environment = {
|
||||
port,
|
||||
redisUrl,
|
||||
mongoUrl,
|
||||
postgresUrl,
|
||||
nodeEnv: nodeEnv as "dev" | "production" | "staging" | "test",
|
||||
replicaIdentity,
|
||||
commitHash: process.env.COMMIT_HASH,
|
||||
seqUrl: process.env.SEQ_URL,
|
||||
seqApiKey: process.env.SEQ_API_KEY,
|
||||
logLevel: logLevel as "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn",
|
||||
// Typed variant of process.env
|
||||
export const Env = {
|
||||
PORT,
|
||||
REDIS_URL,
|
||||
MONGO_URL,
|
||||
POSTGRES_URL,
|
||||
NODE_ENV: NODE_ENV as "dev" | "production" | "staging" | "test",
|
||||
COMMIT_HASH: process.env.COMMIT_HASH,
|
||||
LOG_LEVEL: logLevel as "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn",
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { GPT_SERVER_IMPLEMENTATIONS } from "#game-implementations/game-implementations";
|
||||
|
||||
import {
|
||||
type DerivedMetrics,
|
||||
GAME_PT_CONFIGS,
|
||||
GetGPTConfig,
|
||||
type GPTString,
|
||||
type OptionalMetrics,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Before we run anything, set a global to indicate to the code that
|
||||
// we're running as a server, and not as a job runner or score worker.
|
||||
process.env.IS_SERVER = "true";
|
||||
|
||||
import type http from "http";
|
||||
|
||||
import { LoadDefaultClients } from "#lib/builtin-clients/builtin-clients";
|
||||
@@ -9,7 +5,7 @@ import { VERSION_PRETTY } from "#lib/constants/version";
|
||||
import { HandleSIGTERMGracefully } from "#lib/handlers/sigterm";
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { ApplyUnappliedMigrations } from "#lib/migration/migrations";
|
||||
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { AddNewUser } from "#server/router/api/v1/auth/auth";
|
||||
import server from "#server/server";
|
||||
import db, { monkDB } from "#services/mongo/db";
|
||||
@@ -26,10 +22,10 @@ import { UserAuthLevels } from "../../common/src";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
logger.info(`Booting ${TachiConfig.NAME} - ${VERSION_PRETTY} [ENV: ${Environment.nodeEnv}]`, {
|
||||
logger.info(`Booting ${TachiConfig.NAME} - ${VERSION_PRETTY} [ENV: ${Env.NODE_ENV}]`, {
|
||||
bootInfo: true,
|
||||
});
|
||||
logger.info(`Log level is set to ${Environment.logLevel}.`, { bootInfo: true });
|
||||
logger.info(`Log level is set to ${Env.LOG_LEVEL}.`, { bootInfo: true });
|
||||
|
||||
logger.info(`Loading sequence documents...`, { bootInfo: true });
|
||||
|
||||
@@ -48,7 +44,7 @@ async function RunOnInit() {
|
||||
}
|
||||
});
|
||||
|
||||
if (Environment.nodeEnv === "dev") {
|
||||
if (Env.NODE_ENV === "dev") {
|
||||
const exists = await db.users.findOne({ id: 1 });
|
||||
|
||||
if (!exists) {
|
||||
@@ -85,7 +81,7 @@ void RunOnInit();
|
||||
let instance: http.Server | https.Server;
|
||||
|
||||
if (ServerConfig.ENABLE_SERVER_HTTPS === true) {
|
||||
if (Environment.nodeEnv === "production") {
|
||||
if (Env.NODE_ENV === "production") {
|
||||
logger.warn(
|
||||
"HTTPS Mode is enabled. This should not be used in production, and you should instead run behind a reverse proxy.",
|
||||
{ bootInfo: true },
|
||||
@@ -97,11 +93,11 @@ if (ServerConfig.ENABLE_SERVER_HTTPS === true) {
|
||||
|
||||
const httpsServer = https.createServer({ key: privateKey, cert: certificate }, server);
|
||||
|
||||
instance = httpsServer.listen(Environment.port);
|
||||
logger.info(`HTTPS Listening on port ${Environment.port}`, { bootInfo: true });
|
||||
instance = httpsServer.listen(Env.PORT);
|
||||
logger.info(`HTTPS Listening on port ${Env.PORT}`, { bootInfo: true });
|
||||
} else {
|
||||
instance = server.listen(Environment.port);
|
||||
logger.info(`HTTP Listening on port ${Environment.port}`, { bootInfo: true });
|
||||
instance = server.listen(Env.PORT);
|
||||
logger.info(`HTTP Listening on port ${Env.PORT}`, { bootInfo: true });
|
||||
}
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
@@ -111,7 +107,7 @@ process.on("SIGTERM", () => {
|
||||
if (process.env.INVOKE_JOB_RUNNER) {
|
||||
logger.info(`Spawning a tachi-server job runner inline.`, { bootInfo: true });
|
||||
|
||||
if (Environment.nodeEnv === "production") {
|
||||
if (Env.NODE_ENV === "production") {
|
||||
logger.warn(
|
||||
`Spawning inline tachi-server job runner in production. This is bad for performance.`,
|
||||
{ bootInfo: true },
|
||||
|
||||
@@ -129,7 +129,7 @@ interface InviteLockDocument {
|
||||
|
||||
const MONGO_URL = process.env.MONGO_URL ?? "mongodb://mongo/tachi";
|
||||
const POSTGRES_URL = process.env.POSTGRES_URL;
|
||||
const SEEDS_DIR = process.env.SEEDS_DIR ?? path.resolve(__dirname, "../../../seeds/collections");
|
||||
const SEEDS_DIR = process.env.SEEDS_DIR ?? path.resolve(__dirname, "../../../../db/seeds");
|
||||
|
||||
if (!POSTGRES_URL) {
|
||||
console.error("[migrate] POSTGRES_URL is not set.");
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Request } from "express";
|
||||
|
||||
import { ONE_MINUTE } from "#lib/constants/time";
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { RedisClient } from "#services/redis/redis";
|
||||
import { OmitUndefinedKeys } from "#utils/misc";
|
||||
import rateLimit, { type Options } from "express-rate-limit";
|
||||
@@ -15,7 +15,7 @@ const logger = CreateLogCtx(__filename);
|
||||
function CreateStore(name: string) {
|
||||
// undefined forces a default to an in-memory store
|
||||
// So we use that when in testing or localdev.
|
||||
return Environment.nodeEnv === "production" || Environment.nodeEnv === "staging"
|
||||
return Env.NODE_ENV === "production" || Env.NODE_ENV === "staging"
|
||||
? new RateLimitRedis({ prefix: `${TachiConfig.NAME}-RL:${name}`, client: RedisClient })
|
||||
: undefined;
|
||||
}
|
||||
@@ -68,6 +68,6 @@ export const HyperAggressiveRateLimitMiddleware = rateLimit(
|
||||
// vulnerable to bursting down the server.
|
||||
// if we're in testing, disable this rate limit!
|
||||
export const ScoreImportRateLimiter =
|
||||
Environment.nodeEnv === "test"
|
||||
Env.NODE_ENV === "test"
|
||||
? rateLimit(CreateRateLimitOptions(Infinity, "ScImport", ONE_MINUTE))
|
||||
: rateLimit(CreateRateLimitOptions(5, "ScImport", ONE_MINUTE));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RequestHandler } from "express-serve-static-core";
|
||||
|
||||
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
|
||||
/**
|
||||
* Middleware that makes the route only available under Bokutachi.
|
||||
@@ -56,7 +56,7 @@ export const RequireInvitesEnabled: RequestHandler = (req, res, next) => {
|
||||
* at an expected checkout location.
|
||||
*/
|
||||
export const RequireLocalDevelopment: RequestHandler = (req, res, next) => {
|
||||
if (Environment.nodeEnv === "dev" || Environment.nodeEnv === "test") {
|
||||
if (Env.NODE_ENV === "dev" || Env.NODE_ENV === "test") {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ONE_MINUTE } from "#lib/constants/time";
|
||||
import { ChangeRootLogLevel, GetLogLevel } from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import db from "#services/mongo/db";
|
||||
import { CreateFakeAuthCookie } from "#test-utils/fake-auth";
|
||||
import mockApi from "#test-utils/mock-api";
|
||||
@@ -12,7 +12,7 @@ import t from "tap";
|
||||
|
||||
import { type ScoreDocument, UserAuthLevels } from "../../../../../../../common/src";
|
||||
|
||||
const LOG_LEVEL = Environment.logLevel;
|
||||
const LOG_LEVEL = Env.LOG_LEVEL;
|
||||
|
||||
t.test("POST /api/v1/admin/change-log-level", async (t) => {
|
||||
t.beforeEach(async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SendSiteAnnouncementNotification } from "#lib/notifications/notificatio
|
||||
import { UpdateGoalsForUser } from "#lib/score-import/framework/goals/goals";
|
||||
import { UpdateQuestsForUser } from "#lib/score-import/framework/quests/quests";
|
||||
import { DeleteMultipleScores, DeleteScore } from "#lib/score-mutation/delete-scores";
|
||||
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import prValidate from "#server/middleware/prudence-validate";
|
||||
import db from "#services/mongo/db";
|
||||
import { RecalcAllScores, UpdateAllPBs } from "#utils/calculations/recalc-scores";
|
||||
@@ -60,7 +60,7 @@ const RequireAdminLevel: RequestHandler = async (req, res, next) => {
|
||||
next();
|
||||
};
|
||||
|
||||
const LOG_LEVEL = Environment.logLevel;
|
||||
const LOG_LEVEL = Env.LOG_LEVEL;
|
||||
|
||||
router.use(RequireAdminLevel);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PrivateUserInfoDocument } from "#utils/types";
|
||||
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import db from "#services/mongo/db";
|
||||
import nodeFetch from "#utils/fetch";
|
||||
import { Random20Hex } from "#utils/misc";
|
||||
@@ -108,7 +108,7 @@ export async function AddNewUser(
|
||||
};
|
||||
|
||||
// all created users on a dev instance should be admins, for convenience.
|
||||
if (Environment.nodeEnv === "dev") {
|
||||
if (Env.NODE_ENV === "dev") {
|
||||
userDoc.authLevel = UserAuthLevels.ADMIN;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SendEmail } from "#lib/email/client";
|
||||
import { EmailFormatResetPassword, EmailFormatVerifyEmail } from "#lib/email/formats";
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import prValidate from "#server/middleware/prudence-validate";
|
||||
import {
|
||||
AggressiveRateLimitMiddleware,
|
||||
@@ -76,7 +76,7 @@ router.post(
|
||||
logger.verbose(`Received login request with username ${body.username} (${req.ip})`);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (Environment.nodeEnv === "production" || Environment.nodeEnv === "staging") {
|
||||
if (Env.NODE_ENV === "production" || Env.NODE_ENV === "staging") {
|
||||
logger.verbose("Validating captcha...");
|
||||
const validCaptcha = await ValidateCaptcha(body.captcha, req.socket.remoteAddress);
|
||||
|
||||
@@ -214,7 +214,7 @@ router.post(
|
||||
logger.verbose(`received register request with username ${body.username} (${req.ip})`);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (Environment.nodeEnv === "production" || Environment.nodeEnv === "staging") {
|
||||
if (Env.NODE_ENV === "production" || Env.NODE_ENV === "staging") {
|
||||
logger.verbose("Validating captcha...");
|
||||
const validCaptcha = await ValidateCaptcha(body.captcha, req.socket.remoteAddress);
|
||||
|
||||
@@ -463,7 +463,7 @@ router.post(
|
||||
HyperAggressiveRateLimitMiddleware,
|
||||
prValidate({ email: "string" }),
|
||||
async (req, res) => {
|
||||
if (!ServerConfig.EMAIL_CONFIG && Environment.nodeEnv !== "test") {
|
||||
if (!ServerConfig.EMAIL_CONFIG && Env.NODE_ENV !== "test") {
|
||||
return res.status(501).json({
|
||||
success: false,
|
||||
description: `This server does not support password resets.`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
import { Environment } from "#lib/setup/config";
|
||||
import { Env } from "#lib/setup/config";
|
||||
import db from "#services/mongo/db";
|
||||
import { AssignToReqTachiData, GetTachiData } from "#utils/req-tachi-data";
|
||||
|
||||
@@ -39,7 +39,7 @@ export const RequireOwnershipOfClient: RequestHandler = (req, res, next) => {
|
||||
// There's an open issue for this here: https://github.com/i-like-robots/express-request-mock/issues/19
|
||||
/* istanbul ignore next */
|
||||
if (
|
||||
Environment.nodeEnv === "test" &&
|
||||
Env.NODE_ENV === "test" &&
|
||||
(req.safeBody.__terribleHackOauth2ClientDoc as TachiAPIClientDocument | undefined)
|
||||
) {
|
||||
// obviously a glaring hack and security flaw - this only applies
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { PullDatabaseSeeds } from "#lib/seeds/repo";
|
||||
import { Environment } from "#lib/setup/config";
|
||||
import { Env } from "#lib/setup/config";
|
||||
import prValidate from "#server/middleware/prudence-validate";
|
||||
import { RequireLocalDevelopment } from "#server/middleware/type-require";
|
||||
import { GetCommit, ListGitCommitsInPath } from "#utils/git";
|
||||
@@ -37,9 +37,9 @@ const LOCAL_DEV_SEEDS_PATH = path.join(
|
||||
);
|
||||
const TEST_SEEDS_PATH = path.join(__dirname, "../../../../../test-utils/mock-db");
|
||||
|
||||
const LOCAL_SEEDS_PATH = Environment.nodeEnv === "test" ? TEST_SEEDS_PATH : LOCAL_DEV_SEEDS_PATH;
|
||||
const LOCAL_SEEDS_PATH = Env.NODE_ENV === "test" ? TEST_SEEDS_PATH : LOCAL_DEV_SEEDS_PATH;
|
||||
|
||||
if (Environment.nodeEnv === "dev" || Environment.nodeEnv === "test") {
|
||||
if (Env.NODE_ENV === "dev" || Env.NODE_ENV === "test") {
|
||||
if (!fsSync.existsSync(LOCAL_SEEDS_PATH)) {
|
||||
logger.error(
|
||||
`Failed to load seeds routes, could not find any seeds/collections checked out at ${LOCAL_SEEDS_PATH}.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Environment } from "#lib/setup/config";
|
||||
import { Env } from "#lib/setup/config";
|
||||
import { UpdateLastSeen } from "#server/middleware/update-last-seen";
|
||||
import { Router } from "express";
|
||||
|
||||
@@ -24,7 +24,7 @@ router.use("/api/v1", apiRouterV1);
|
||||
// on the root endpoint.
|
||||
// That is to say, if a user is hitting 127.0.0.1:8080/
|
||||
// instead of "cannot GET /", they should get a nice message.
|
||||
if (Environment.nodeEnv === "dev") {
|
||||
if (Env.NODE_ENV === "dev") {
|
||||
router.get("/", (req, res) =>
|
||||
res.send(
|
||||
`Server is live and running. All is good!<br />
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { integer } from "../../../common/src";
|
||||
|
||||
import { SYMBOL_TACHI_API_AUTH } from "#lib/constants/tachi";
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig, TachiConfig } from "#lib/setup/config";
|
||||
import { RedisClient } from "#services/redis/redis";
|
||||
import { IsNonEmptyString, IsRecord } from "#utils/misc";
|
||||
import ExpressPromBundle from "express-prom-bundle";
|
||||
@@ -22,7 +22,7 @@ const logger = CreateLogCtx(__filename);
|
||||
|
||||
let store;
|
||||
|
||||
if (Environment.nodeEnv !== "test") {
|
||||
if (Env.NODE_ENV !== "test") {
|
||||
logger.info("Connecting ExpressSession to Redis.", { bootInfo: true });
|
||||
const RedisStore = connectRedis(expressSession);
|
||||
|
||||
@@ -46,13 +46,13 @@ const userSessionMiddleware = expressSession({
|
||||
// the absence of Secure in combination with SameSite=None will cause issues on non-https
|
||||
// instances in newer versions of chromium. there is no workaround for this.
|
||||
secure:
|
||||
Environment.nodeEnv === "production" ||
|
||||
Environment.nodeEnv === "staging" ||
|
||||
Env.NODE_ENV === "production" ||
|
||||
Env.NODE_ENV === "staging" ||
|
||||
ServerConfig.ENABLE_SERVER_HTTPS,
|
||||
|
||||
// Very important. Without this, we're vulnerable to CSRF!
|
||||
sameSite:
|
||||
Environment.nodeEnv === "production" || Environment.nodeEnv === "staging"
|
||||
Env.NODE_ENV === "production" || Env.NODE_ENV === "staging"
|
||||
? "strict"
|
||||
: "none",
|
||||
},
|
||||
@@ -60,7 +60,7 @@ const userSessionMiddleware = expressSession({
|
||||
|
||||
const app: Express = express();
|
||||
|
||||
if (Environment.nodeEnv !== "production" && IsNonEmptyString(ServerConfig.CLIENT_DEV_SERVER)) {
|
||||
if (Env.NODE_ENV !== "production" && IsNonEmptyString(ServerConfig.CLIENT_DEV_SERVER)) {
|
||||
logger.warn(`Enabling CORS requests from ${ServerConfig.CLIENT_DEV_SERVER}.`, {
|
||||
bootInfo: true,
|
||||
});
|
||||
@@ -100,7 +100,7 @@ if (Environment.nodeEnv !== "production" && IsNonEmptyString(ServerConfig.CLIENT
|
||||
|
||||
app.options("*", (req, res) => res.send());
|
||||
|
||||
if (Environment.nodeEnv !== "test") {
|
||||
if (Env.NODE_ENV !== "test") {
|
||||
logger.info("Enabling Helmet, as no CLIENT_DEV_SERVER was set, or we are in production.", {
|
||||
bootInfo: true,
|
||||
});
|
||||
@@ -159,7 +159,7 @@ if (
|
||||
ServerConfig.CDN_CONFIG.SAVE_LOCATION.TYPE === "LOCAL_FILESYSTEM" &&
|
||||
ServerConfig.CDN_CONFIG.SAVE_LOCATION.SERVE_OWN_CDN === true
|
||||
) {
|
||||
if (Environment.nodeEnv === "production") {
|
||||
if (Env.NODE_ENV === "production") {
|
||||
logger.warn(
|
||||
`Running LOCAL_FILESYSTEM OWN_CDN in production. Consider making a separate process handle your CDN for performance.`,
|
||||
{ bootInfo: true },
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
// so we just disable these rules. I know, it sucks, but we'll live.
|
||||
import { ONE_MINUTE, ONE_SECOND } from "#lib/constants/time";
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import { GetMillisecondsSince } from "#utils/misc";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
@@ -58,21 +58,21 @@ const logger = CreateLogCtx(__filename);
|
||||
let dbName = ServerConfig.MONGO_DATABASE_NAME;
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (Environment.nodeEnv === "test") {
|
||||
if (Env.NODE_ENV === "test") {
|
||||
dbName = `testingdb`;
|
||||
}
|
||||
|
||||
logger.info(`Connecting to database ${Environment.mongoUrl}/${dbName}...`, { bootInfo: true });
|
||||
logger.info(`Connecting to database ${Env.MONGO_URL}/${dbName}...`, { bootInfo: true });
|
||||
const dbtime = process.hrtime.bigint();
|
||||
|
||||
export const monkDB = monk(`${Environment.mongoUrl}/${dbName}`, {
|
||||
export const monkDB = monk(`${Env.MONGO_URL}/${dbName}`, {
|
||||
// Various things cause bizarre issues with mongodb connections. Windows+Docker especially so.
|
||||
// 5 minutes is excessive, but believe it or not, some setups are exceeding 2 minutes!
|
||||
serverSelectionTimeoutMS: ONE_MINUTE * 5,
|
||||
|
||||
// in local dev, don't **ever** add _id onto objects you're inserting
|
||||
// in production, this might have a performance hit.
|
||||
forceServerObjectId: Environment.nodeEnv === "test",
|
||||
forceServerObjectId: Env.NODE_ENV === "test",
|
||||
});
|
||||
|
||||
/* istanbul ignore next */
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Database } from "tachi-db";
|
||||
|
||||
import { Environment } from "#lib/setup/config";
|
||||
import { Env } from "#lib/setup/config";
|
||||
import { Kysely, PostgresDialect } from "kysely";
|
||||
import { Pool } from "pg";
|
||||
|
||||
const pool = new Pool({ connectionString: Environment.postgresUrl });
|
||||
const pool = new Pool({ connectionString: Env.POSTGRES_URL });
|
||||
|
||||
const pgDb = new Kysely<Database>({
|
||||
dialect: new PostgresDialect({ pool }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ONE_MINUTE, ONE_SECOND } from "#lib/constants/time";
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment } from "#lib/setup/config";
|
||||
import { Env } from "#lib/setup/config";
|
||||
import { GetMillisecondsSince } from "#utils/misc";
|
||||
import redis from "redis";
|
||||
|
||||
@@ -9,7 +9,7 @@ const logger = CreateLogCtx(__filename);
|
||||
logger.verbose("Instantiated Redis Store", { bootInfo: true });
|
||||
|
||||
export const RedisClient = redis.createClient({
|
||||
url: `redis://${Environment.redisUrl}`,
|
||||
url: `redis://${Env.REDIS_URL}`,
|
||||
});
|
||||
|
||||
const startConnect = process.hrtime.bigint();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ICollection } from "monk";
|
||||
|
||||
import CreateLogCtx from "#lib/logger/logger";
|
||||
import { Environment, ServerConfig } from "#lib/setup/config";
|
||||
import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import { ClearTestingRateLimitCache } from "#server/middleware/rate-limiter";
|
||||
import db, { type StaticDatabases } from "#services/mongo/db";
|
||||
import { SetIndexes } from "#services/mongo/indexes";
|
||||
@@ -116,7 +116,7 @@ export function ResetCDN() {
|
||||
|
||||
export async function SetIndexesForDB() {
|
||||
await ResetDBState();
|
||||
const url = `${Environment.mongoUrl}/testingdb`;
|
||||
const url = `${Env.MONGO_URL}/testingdb`;
|
||||
|
||||
logger.info(`Setting indexes for ${url}`);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Environment } from "#lib/setup/config";
|
||||
import { Env } from "#lib/setup/config";
|
||||
import nodeFetch, { type RequestInfo, type RequestInit, type Response } from "node-fetch";
|
||||
|
||||
const fetch =
|
||||
Environment.nodeEnv === "test"
|
||||
Env.NODE_ENV === "test"
|
||||
? () => {
|
||||
throw new Error("Cannot use real fetch inside testing env!");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
"compilerOptions": {
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"]
|
||||
"lib": ["ES2022"],
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user