diff --git a/.vscode/settings.json b/.vscode/settings.json index f5e10c6d1..0856db23e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" diff --git a/biome.jsonc b/biome.json similarity index 100% rename from biome.jsonc rename to biome.json diff --git a/bun.lock b/bun.lock index 82026c3ed..927626cb3 100644 --- a/bun.lock +++ b/bun.lock @@ -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:", diff --git a/docs/docs/codebase/setup/config.md b/docs/docs/codebase/setup/config.md index d910c0bb4..e290c30a6 100644 --- a/docs/docs/codebase/setup/config.md +++ b/docs/docs/codebase/setup/config.md @@ -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. \ No newline at end of file diff --git a/typescript/bot/tsconfig.json b/typescript/bot/tsconfig.json index 5d92ff168..ed8dc2ee2 100644 --- a/typescript/bot/tsconfig.json +++ b/typescript/bot/tsconfig.json @@ -4,6 +4,7 @@ "module": "Preserve", "moduleResolution": "bundler", "paths": { + "#*": ["./src/*"], // lolololOLOloLOlooLOOOOOL // LOLOLOLOLOLOLOL "@discordjs/builders": ["../../node_modules/@discordjs/builders/dist/index"], diff --git a/typescript/client/tsconfig.json b/typescript/client/tsconfig.json index 8cd5ac66d..d0e2f5ccb 100644 --- a/typescript/client/tsconfig.json +++ b/typescript/client/tsconfig.json @@ -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"], diff --git a/typescript/common/tsconfig.json b/typescript/common/tsconfig.json index c3cbf4fc2..e8eb41596 100644 --- a/typescript/common/tsconfig.json +++ b/typescript/common/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "Preserve", - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "paths": { + "#*": ["./src/*"] + } }, "include": ["src"], "exclude": ["node_modules"] diff --git a/typescript/db-cli/tsconfig.json b/typescript/db-cli/tsconfig.json index 7e3770571..c66fdaf77 100644 --- a/typescript/db-cli/tsconfig.json +++ b/typescript/db-cli/tsconfig.json @@ -4,6 +4,7 @@ "module": "Preserve", "moduleResolution": "bundler", "paths": { + "#*": ["./src/*"], "tachi-db-migration-engine": ["../db-migration-engine/src/index.ts"] } }, diff --git a/typescript/db-migration-engine/tsconfig.json b/typescript/db-migration-engine/tsconfig.json index 922670453..6b76f5d95 100644 --- a/typescript/db-migration-engine/tsconfig.json +++ b/typescript/db-migration-engine/tsconfig.json @@ -8,7 +8,10 @@ "composite": true, "declaration": true, "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "paths": { + "#*": ["./src/*"] + } }, "include": ["src"], "exclude": ["node_modules"] diff --git a/typescript/db/tsconfig.json b/typescript/db/tsconfig.json index a38033b72..852200861 100644 --- a/typescript/db/tsconfig.json +++ b/typescript/db/tsconfig.json @@ -5,7 +5,10 @@ "moduleResolution": "bundler", "lib": ["ES2022", "DOM"], "outDir": "js", - "rootDir": "src" + "rootDir": "src", + "paths": { + "#*": ["./src/*"] + } }, "include": ["src"], "exclude": ["node_modules"] diff --git a/typescript/docs-autogen-scripts/tsconfig.json b/typescript/docs-autogen-scripts/tsconfig.json index c3cbf4fc2..e8eb41596 100644 --- a/typescript/docs-autogen-scripts/tsconfig.json +++ b/typescript/docs-autogen-scripts/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "Preserve", - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "paths": { + "#*": ["./src/*"] + } }, "include": ["src"], "exclude": ["node_modules"] diff --git a/typescript/github-bot/tsconfig.json b/typescript/github-bot/tsconfig.json index a38033b72..852200861 100644 --- a/typescript/github-bot/tsconfig.json +++ b/typescript/github-bot/tsconfig.json @@ -5,7 +5,10 @@ "moduleResolution": "bundler", "lib": ["ES2022", "DOM"], "outDir": "js", - "rootDir": "src" + "rootDir": "src", + "paths": { + "#*": ["./src/*"] + } }, "include": ["src"], "exclude": ["node_modules"] diff --git a/typescript/seeds-scripts/tsconfig.json b/typescript/seeds-scripts/tsconfig.json index 5231153ba..70dfcbafb 100644 --- a/typescript/seeds-scripts/tsconfig.json +++ b/typescript/seeds-scripts/tsconfig.json @@ -6,7 +6,10 @@ "noImplicitAny": false, "strictNullChecks": true, "allowJs": true, - "strict": false + "strict": false, + "paths": { + "#*": ["./src/*"] + } }, "exclude": ["node_modules"] } diff --git a/typescript/server/package.json b/typescript/server/package.json index 28c58f476..8bbd41e33 100644 --- a/typescript/server/package.json +++ b/typescript/server/package.json @@ -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:" diff --git a/typescript/server/src/lib/cdn/cdn.test.ts b/typescript/server/src/lib/cdn/cdn.test.ts index da1c21e02..aad4b4e73 100644 --- a/typescript/server/src/lib/cdn/cdn.test.ts +++ b/typescript/server/src/lib/cdn/cdn.test.ts @@ -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.`, ); diff --git a/typescript/server/src/lib/constants/version.ts b/typescript/server/src/lib/constants/version.ts index fc04f9cc6..1ce8b0e36 100644 --- a/typescript/server/src/lib/constants/version.ts +++ b/typescript/server/src/lib/constants/version.ts @@ -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. diff --git a/typescript/server/src/lib/email/client.ts b/typescript/server/src/lib/email/client.ts index 7828021af..005396bb5 100644 --- a/typescript/server/src/lib/email/client.ts +++ b/typescript/server/src/lib/email/client.ts @@ -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 | undefined { - if (Environment.nodeEnv === "test") { + if (Env.NODE_ENV === "test") { logger.debug(`Stubbed out SendEmail as env was test.`); return; } diff --git a/typescript/server/src/lib/handlers/sigterm.ts b/typescript/server/src/lib/handlers/sigterm.ts index 68acf1ebf..b605ae985 100644 --- a/typescript/server/src/lib/handlers/sigterm.ts +++ b/typescript/server/src/lib/handlers/sigterm.ts @@ -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(); diff --git a/typescript/server/src/lib/logger/logger.test.ts b/typescript/server/src/lib/logger/logger.test.ts index f7a4dc053..dae8e4de5 100644 --- a/typescript/server/src/lib/logger/logger.test.ts +++ b/typescript/server/src/lib/logger/logger.test.ts @@ -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); diff --git a/typescript/server/src/lib/logger/logger.ts b/typescript/server/src/lib/logger/logger.ts index a2f5ba329..9f05a32e5 100644 --- a/typescript/server/src/lib/logger/logger.ts +++ b/typescript/server/src/lib/logger/logger.ts @@ -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 = [ }), ]; -if (Environment.seqUrl) { - const levelMap: Record = { - 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 ); } diff --git a/typescript/server/src/lib/migration/migrations.ts b/typescript/server/src/lib/migration/migrations.ts index 20ca9656d..5c22849b5 100644 --- a/typescript/server/src/lib/migration/migrations.ts +++ b/typescript/server/src/lib/migration/migrations.ts @@ -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 = - Environment.nodeEnv === "test" + Env.NODE_ENV === "test" ? [FAKE_MIGRATION] : [ UGPTRivalsMigration, @@ -66,7 +66,7 @@ const REGISTERED_MIGRATIONS: Array = ]; // 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."); } diff --git a/typescript/server/src/lib/score-import/worker/queue.ts b/typescript/server/src/lib/score-import/worker/queue.ts index d2efd6f34..2a91a0a5a 100644 --- a/typescript/server/src/lib/score-import/worker/queue.ts +++ b/typescript/server/src/lib/score-import/worker/queue.ts @@ -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() { diff --git a/typescript/server/src/lib/score-import/worker/worker.ts b/typescript/server/src/lib/score-import/worker/worker.ts index 9c5ee193f..98f171c1f 100644 --- a/typescript/server/src/lib/score-import/worker/worker.ts +++ b/typescript/server/src/lib/score-import/worker/worker.ts @@ -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, }, }, ); diff --git a/typescript/server/src/lib/seeds/repo.ts b/typescript/server/src/lib/seeds/repo.ts index 56f62fa96..0e82fda5a 100644 --- a/typescript/server/src/lib/seeds/repo.ts +++ b/typescript/server/src/lib/seeds/repo.ts @@ -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. diff --git a/typescript/server/src/lib/setup/config.ts b/typescript/server/src/lib/setup/config.ts index ecdc9a456..95df1848e 100644 --- a/typescript/server/src/lib/setup/config.ts +++ b/typescript/server/src/lib/setup/config.ts @@ -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; +export type CGConfig = z.infer; +export type TachiServerConfig = z.infer; + +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", }; diff --git a/typescript/server/src/lib/v3/migration-tools.ts b/typescript/server/src/lib/v3/migration-tools.ts index 364ce09fd..d26bc5587 100644 --- a/typescript/server/src/lib/v3/migration-tools.ts +++ b/typescript/server/src/lib/v3/migration-tools.ts @@ -1,8 +1,5 @@ -import { GPT_SERVER_IMPLEMENTATIONS } from "#game-implementations/game-implementations"; - import { type DerivedMetrics, - GAME_PT_CONFIGS, GetGPTConfig, type GPTString, type OptionalMetrics, diff --git a/typescript/server/src/main.ts b/typescript/server/src/main.ts index 24fb7341f..b3dd95363 100644 --- a/typescript/server/src/main.ts +++ b/typescript/server/src/main.ts @@ -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 }, diff --git a/typescript/server/src/scripts/migrate-to-postgres.ts b/typescript/server/src/scripts/migrate-to-postgres.ts index 84b980bdb..1563a7492 100644 --- a/typescript/server/src/scripts/migrate-to-postgres.ts +++ b/typescript/server/src/scripts/migrate-to-postgres.ts @@ -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."); diff --git a/typescript/server/src/server/middleware/rate-limiter.ts b/typescript/server/src/server/middleware/rate-limiter.ts index e0d4b54d6..9def016dd 100644 --- a/typescript/server/src/server/middleware/rate-limiter.ts +++ b/typescript/server/src/server/middleware/rate-limiter.ts @@ -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)); diff --git a/typescript/server/src/server/middleware/type-require.ts b/typescript/server/src/server/middleware/type-require.ts index fab08ddca..cbcbf94e2 100644 --- a/typescript/server/src/server/middleware/type-require.ts +++ b/typescript/server/src/server/middleware/type-require.ts @@ -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; } diff --git a/typescript/server/src/server/router/api/v1/admin/router.test.ts b/typescript/server/src/server/router/api/v1/admin/router.test.ts index 32f92374f..da2f3dfac 100644 --- a/typescript/server/src/server/router/api/v1/admin/router.test.ts +++ b/typescript/server/src/server/router/api/v1/admin/router.test.ts @@ -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 () => { diff --git a/typescript/server/src/server/router/api/v1/admin/router.ts b/typescript/server/src/server/router/api/v1/admin/router.ts index 6741dd231..3e06cf08a 100644 --- a/typescript/server/src/server/router/api/v1/admin/router.ts +++ b/typescript/server/src/server/router/api/v1/admin/router.ts @@ -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); diff --git a/typescript/server/src/server/router/api/v1/auth/auth.ts b/typescript/server/src/server/router/api/v1/auth/auth.ts index f66ae0660..a0a0e93c4 100644 --- a/typescript/server/src/server/router/api/v1/auth/auth.ts +++ b/typescript/server/src/server/router/api/v1/auth/auth.ts @@ -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; } diff --git a/typescript/server/src/server/router/api/v1/auth/router.ts b/typescript/server/src/server/router/api/v1/auth/router.ts index 00146ad01..8022fe7dc 100644 --- a/typescript/server/src/server/router/api/v1/auth/router.ts +++ b/typescript/server/src/server/router/api/v1/auth/router.ts @@ -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.`, diff --git a/typescript/server/src/server/router/api/v1/clients/middleware.ts b/typescript/server/src/server/router/api/v1/clients/middleware.ts index 511bc76a6..0c8dc122c 100644 --- a/typescript/server/src/server/router/api/v1/clients/middleware.ts +++ b/typescript/server/src/server/router/api/v1/clients/middleware.ts @@ -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 diff --git a/typescript/server/src/server/router/api/v1/seeds/router.ts b/typescript/server/src/server/router/api/v1/seeds/router.ts index 4d5db58da..f996512ae 100644 --- a/typescript/server/src/server/router/api/v1/seeds/router.ts +++ b/typescript/server/src/server/router/api/v1/seeds/router.ts @@ -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}. diff --git a/typescript/server/src/server/router/router.ts b/typescript/server/src/server/router/router.ts index 49a2cffd1..7314f57c1 100644 --- a/typescript/server/src/server/router/router.ts +++ b/typescript/server/src/server/router/router.ts @@ -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!
diff --git a/typescript/server/src/server/server.ts b/typescript/server/src/server/server.ts index a7a0f53c6..4aa682086 100644 --- a/typescript/server/src/server/server.ts +++ b/typescript/server/src/server/server.ts @@ -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 }, diff --git a/typescript/server/src/services/mongo/db.ts b/typescript/server/src/services/mongo/db.ts index 2c0b2b749..4ec76b676 100644 --- a/typescript/server/src/services/mongo/db.ts +++ b/typescript/server/src/services/mongo/db.ts @@ -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 */ diff --git a/typescript/server/src/services/pg/db.ts b/typescript/server/src/services/pg/db.ts index 1e5efaea5..852414688 100644 --- a/typescript/server/src/services/pg/db.ts +++ b/typescript/server/src/services/pg/db.ts @@ -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({ dialect: new PostgresDialect({ pool }), diff --git a/typescript/server/src/services/redis/redis.ts b/typescript/server/src/services/redis/redis.ts index 0de98a2f5..07cdd3960 100644 --- a/typescript/server/src/services/redis/redis.ts +++ b/typescript/server/src/services/redis/redis.ts @@ -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(); diff --git a/typescript/server/src/test-utils/resets.ts b/typescript/server/src/test-utils/resets.ts index aaa107da9..9cfc6c6fd 100644 --- a/typescript/server/src/test-utils/resets.ts +++ b/typescript/server/src/test-utils/resets.ts @@ -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}`); diff --git a/typescript/server/src/utils/fetch.ts b/typescript/server/src/utils/fetch.ts index e6bda16ec..fefb79fe9 100644 --- a/typescript/server/src/utils/fetch.ts +++ b/typescript/server/src/utils/fetch.ts @@ -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!"); } diff --git a/typescript/server/tsconfig.json b/typescript/server/tsconfig.json index 81e8fe11c..53a3044a1 100644 --- a/typescript/server/tsconfig.json +++ b/typescript/server/tsconfig.json @@ -3,7 +3,10 @@ "compilerOptions": { "module": "Preserve", "moduleResolution": "bundler", - "lib": ["ES2022"] + "lib": ["ES2022"], + "paths": { + "#*": ["./src/*"] + } }, "include": ["src"], "exclude": ["node_modules"] diff --git a/typescript/sieglinde/tsconfig.json b/typescript/sieglinde/tsconfig.json index c3cbf4fc2..e8eb41596 100644 --- a/typescript/sieglinde/tsconfig.json +++ b/typescript/sieglinde/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "Preserve", - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "paths": { + "#*": ["./src/*"] + } }, "include": ["src"], "exclude": ["node_modules"]