feat(auth): add Discord OAuth sign-in and user timezones

Add persistent user and session storage, protected profile requests, and React sign-in controls. Capture and preserve each user's timezone, and load development and production configuration before migrations and server startup.
This commit is contained in:
syntaxbullet
2026-09-04 08:29:48 +02:00
parent 29bf75760b
commit d3cbf2f7dc
22 changed files with 997 additions and 24 deletions

View File

@@ -0,0 +1,58 @@
import { afterEach, beforeEach, expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
let directory: string;
const project = new URL("../", import.meta.url);
beforeEach(() => {
directory = mkdtempSync(join(tmpdir(), "minabot-env-test-"));
// Run the real package commands with disposable entrypoints that report only fixture values.
writeFileSync(join(directory, "package.json"), readFileSync(new URL("package.json", project)));
writeFileSync(join(directory, "bunfig.toml"), readFileSync(new URL("bunfig.toml", project)));
mkdirSync(join(directory, "scripts"));
mkdirSync(join(directory, "src"));
for (const [file, stage] of [["scripts/migrate.ts", "migration"], ["scripts/start.ts", "server"], ["src/index.ts", "server"]]) {
writeFileSync(join(directory, file!), `console.log(JSON.stringify({ stage: ${JSON.stringify(stage)}, mode: process.env.NODE_ENV, origin: process.env.APP_ORIGIN, database: process.env.DATABASE_PATH, shared: process.env.MINABOT_ENV_TEST_SHARED })); process.exit(0);`);
}
writeFileSync(join(directory, ".env"), "APP_ORIGIN=https://shared.example\nDATABASE_PATH=shared.sqlite\nMINABOT_ENV_TEST_SHARED=shared\n");
writeFileSync(join(directory, ".env.development"), "APP_ORIGIN=http://127.0.0.1:4000\nDATABASE_PATH=development.sqlite\n");
writeFileSync(join(directory, ".env.production"), "APP_ORIGIN=https://production.example\nDATABASE_PATH=production.sqlite\n");
});
afterEach(() => rmSync(directory, { recursive: true, force: true }));
function run(command: string, overrides: Record<string, string> = {}) {
const env = { ...process.env };
for (const key of ["NODE_ENV", "APP_ORIGIN", "DATABASE_PATH", "MINABOT_ENV_TEST_SHARED", "BUN_OPTIONS"]) delete env[key];
const result = Bun.spawnSync([process.execPath, "run", command], {
cwd: directory, env: { ...env, ...overrides }, timeout: 10_000,
stdout: "pipe", stderr: "pipe",
});
expect(result.exitCode).toBe(0);
return result.stdout.toString().trim().split("\n").map(line => JSON.parse(line));
}
test.each(["development", "production"])("%s loads the same environment before migration and server startup", mode => {
const records = run(mode === "production" ? "start" : "dev");
expect(records).toEqual(["migration", "server"].map(stage => ({
stage, mode,
origin: mode === "production" ? "https://production.example" : "http://127.0.0.1:4000",
database: `${mode}.sqlite`, shared: "shared",
})));
});
test("database commands select their corresponding environment", () => {
expect(run("db:migrate")[0].database).toBe("development.sqlite");
expect(run("db:migrate:production")[0].database).toBe("production.sqlite");
});
test("exported deployment variables override environment files", () => {
const records = run("start", { APP_ORIGIN: "https://deployed.example", DATABASE_PATH: "deployed.sqlite" });
expect(records.every(record => record.origin === "https://deployed.example" && record.database === "deployed.sqlite")).toBe(true);
});
test("production never preloads development values when its file is absent", () => {
rmSync(join(directory, ".env.production"));
const records = run("start");
expect(records.every(record => record.origin === "https://shared.example" && record.database === "shared.sqlite")).toBe(true);
});