nettoyage du code

This commit is contained in:
Puechberty Arthur
2026-04-23 23:39:18 +02:00
parent 9c311b75dd
commit 0d55fe5978
160 changed files with 4165 additions and 1237 deletions
+35 -10
View File
@@ -10,12 +10,21 @@ const LOCK_CLASS_ID = 7813;
const LOCK_OBJECT_ID = 4312;
const parseBoolean = (value, fallback = false) => {
if (value === undefined || value === null || String(value).trim().length === 0) {
if (
value === undefined ||
value === null ||
String(value).trim().length === 0
) {
return fallback;
}
const normalized = String(value).trim().toLowerCase();
return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
return (
normalized === "true" ||
normalized === "1" ||
normalized === "yes" ||
normalized === "on"
);
};
const sha256 = (content) => createHash("sha256").update(content).digest("hex");
@@ -33,7 +42,9 @@ const loadMigrations = async (migrationsDir) => {
const files = await listMigrationFiles(migrationsDir);
if (files.length === 0) {
throw new Error(`[migrate] no SQL migration files found in ${migrationsDir}`);
throw new Error(
`[migrate] no SQL migration files found in ${migrationsDir}`,
);
}
const migrations = [];
@@ -63,9 +74,13 @@ const ensureMigrationTable = async (client) => {
};
const applyMigrations = async (client, migrations) => {
const applied = await client.query("SELECT version, checksum FROM schema_migrations ORDER BY version ASC");
const applied = await client.query(
"SELECT version, checksum FROM schema_migrations ORDER BY version ASC",
);
const appliedByVersion = new Map(applied.rows.map((row) => [String(row.version), String(row.checksum)]));
const appliedByVersion = new Map(
applied.rows.map((row) => [String(row.version), String(row.checksum)]),
);
for (const migration of migrations) {
const existingChecksum = appliedByVersion.get(migration.fileName);
@@ -91,7 +106,9 @@ const applyMigrations = async (client, migrations) => {
console.log(`[migrate] applied ${migration.fileName}`);
} catch (error) {
await client.query("ROLLBACK").catch(() => undefined);
throw new Error(`[migrate] failed while applying ${migration.fileName}`, { cause: error });
throw new Error(`[migrate] failed while applying ${migration.fileName}`, {
cause: error,
});
}
}
};
@@ -111,8 +128,8 @@ const main = async () => {
connectionString: process.env.DATABASE_URL,
ssl: parseBoolean(process.env.DATABASE_SSL, false)
? {
rejectUnauthorized: true,
}
rejectUnauthorized: true,
}
: undefined,
});
@@ -121,12 +138,20 @@ const main = async () => {
try {
const migrations = await loadMigrations(migrationsDir);
await client.query("SELECT pg_advisory_lock($1, $2)", [LOCK_CLASS_ID, LOCK_OBJECT_ID]);
await client.query("SELECT pg_advisory_lock($1, $2)", [
LOCK_CLASS_ID,
LOCK_OBJECT_ID,
]);
await ensureMigrationTable(client);
await applyMigrations(client, migrations);
console.log("[migrate] completed");
} finally {
await client.query("SELECT pg_advisory_unlock($1, $2)", [LOCK_CLASS_ID, LOCK_OBJECT_ID]).catch(() => undefined);
await client
.query("SELECT pg_advisory_unlock($1, $2)", [
LOCK_CLASS_ID,
LOCK_OBJECT_ID,
])
.catch(() => undefined);
client.release();
await pool.end();
}
+10 -3
View File
@@ -53,7 +53,10 @@ export const buildDiscordLoginUrl = (state: string): string => {
return `https://discord.com/oauth2/authorize?${params.toString()}`;
};
const createAvatarUrl = (discordUserId: string, avatarHash: string | null): string | null => {
const createAvatarUrl = (
discordUserId: string,
avatarHash: string | null,
): string | null => {
if (!avatarHash) {
return null;
}
@@ -61,7 +64,9 @@ const createAvatarUrl = (discordUserId: string, avatarHash: string | null): stri
return `https://cdn.discordapp.com/avatars/${discordUserId}/${avatarHash}.png`;
};
export const exchangeCodeForDiscordIdentity = async (code: string): Promise<DiscordIdentity> => {
export const exchangeCodeForDiscordIdentity = async (
code: string,
): Promise<DiscordIdentity> => {
const payload = new URLSearchParams({
client_id: env.DISCORD_CLIENT_ID,
client_secret: env.DISCORD_CLIENT_SECRET,
@@ -103,7 +108,9 @@ export const exchangeCodeForDiscordIdentity = async (code: string): Promise<Disc
};
};
export const validateDiscordBotToken = async (botToken: string): Promise<DiscordBotIdentity> => {
export const validateDiscordBotToken = async (
botToken: string,
): Promise<DiscordBotIdentity> => {
const response = await fetch(`${DISCORD_API_BASE}/users/@me`, {
headers: {
Authorization: `Bot ${botToken}`,
+6 -2
View File
@@ -12,7 +12,9 @@ interface SessionJwtPayload {
const secret = new TextEncoder().encode(env.JWT_SECRET);
export const issueSessionToken = async (session: AuthSession): Promise<string> => {
export const issueSessionToken = async (
session: AuthSession,
): Promise<string> => {
return new SignJWT({
tenantId: session.tenantId,
discordUserId: session.discordUserId,
@@ -25,7 +27,9 @@ export const issueSessionToken = async (session: AuthSession): Promise<string> =
.sign(secret);
};
export const verifySessionToken = async (token: string): Promise<AuthSession> => {
export const verifySessionToken = async (
token: string,
): Promise<AuthSession> => {
const verified = await jwtVerify<SessionJwtPayload>(token, secret, {
algorithms: ["HS256"],
});
+17 -4
View File
@@ -5,18 +5,27 @@ loadEnv();
const parseBoolean = (value: string): boolean => {
const normalized = value.trim().toLowerCase();
return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
return (
normalized === "true" ||
normalized === "1" ||
normalized === "yes" ||
normalized === "on"
);
};
const envSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
PORT: z.coerce.number().int().positive().default(4000),
DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"),
DATABASE_SSL: z.string().default("false").transform(parseBoolean),
REDIS_URL: z.string().url("REDIS_URL must be a valid URL"),
DISCORD_CLIENT_ID: z.string().min(1, "DISCORD_CLIENT_ID is required"),
DISCORD_CLIENT_SECRET: z.string().min(1, "DISCORD_CLIENT_SECRET is required"),
DISCORD_REDIRECT_URI: z.string().url("DISCORD_REDIRECT_URI must be a valid URL"),
DISCORD_REDIRECT_URI: z
.string()
.url("DISCORD_REDIRECT_URI must be a valid URL"),
WEB_URL: z.string().url("WEB_URL must be a valid URL"),
API_BASE_URL: z.string().url("API_BASE_URL must be a valid URL"),
JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters"),
@@ -25,7 +34,11 @@ const envSchema = z.object({
COOKIE_SECURE: z.string().default("false").transform(parseBoolean),
TOKEN_ENCRYPTION_KEY: z.string().min(10, "TOKEN_ENCRYPTION_KEY is required"),
TENANT_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(120),
TENANT_RATE_LIMIT_WINDOW_SECONDS: z.coerce.number().int().positive().default(60),
TENANT_RATE_LIMIT_WINDOW_SECONDS: z.coerce
.number()
.int()
.positive()
.default(60),
});
export const env = envSchema.parse(process.env);
+2 -2
View File
@@ -7,8 +7,8 @@ export const createPgPool = (): Pool => {
connectionString: env.DATABASE_URL,
ssl: env.DATABASE_SSL
? {
rejectUnauthorized: true,
}
rejectUnauthorized: true,
}
: undefined,
});
};
+8 -2
View File
@@ -151,7 +151,10 @@ export const getUserByIdAndTenant = async (
return mapUser(result.rows[0] as Record<string, unknown>);
};
export const listBotsForTenant = async (pool: Pool, tenantId: string): Promise<PublicBot[]> => {
export const listBotsForTenant = async (
pool: Pool,
tenantId: string,
): Promise<PublicBot[]> => {
const result = await pool.query(
`
SELECT id, tenant_id, discord_bot_id, display_name, status, last_error, created_at, updated_at
@@ -206,7 +209,10 @@ export const createOrUpdateBotForTenant = async (
[input.discordBotId],
);
if (existingByDiscordBot.rows[0] && existingByDiscordBot.rows[0].tenant_id !== input.tenantId) {
if (
existingByDiscordBot.rows[0] &&
existingByDiscordBot.rows[0].tenant_id !== input.tenantId
) {
throw new Error("BOT_ALREADY_CLAIMED");
}
+8 -5
View File
@@ -56,9 +56,15 @@ const bootstrap = async (): Promise<void> => {
return;
}
const user = await getUserByIdAndTenant(pgPool, req.auth.userId, req.auth.tenantId);
const user = await getUserByIdAndTenant(
pgPool,
req.auth.userId,
req.auth.tenantId,
);
if (!user) {
res.status(401).json({ error: "Session does not match an existing user" });
res
.status(401)
.json({ error: "Session does not match an existing user" });
return;
}
@@ -93,12 +99,10 @@ const bootstrap = async (): Promise<void> => {
);
const server = app.listen(env.PORT, () => {
// eslint-disable-next-line no-console
console.log(`[api] listening on :${env.PORT}`);
});
const shutdown = async (signal: string): Promise<void> => {
// eslint-disable-next-line no-console
console.log(`[api] shutdown requested (${signal})`);
server.close(async () => {
@@ -119,7 +123,6 @@ const bootstrap = async (): Promise<void> => {
};
bootstrap().catch((error) => {
// eslint-disable-next-line no-console
console.error("[api] fatal startup error", error);
process.exit(1);
});
+14 -4
View File
@@ -2,7 +2,11 @@ import type { Router } from "express";
import { Router as expressRouter } from "express";
import type { Pool } from "pg";
import { buildDiscordLoginUrl, createOauthState, exchangeCodeForDiscordIdentity } from "../auth/discordOAuth.js";
import {
buildDiscordLoginUrl,
createOauthState,
exchangeCodeForDiscordIdentity,
} from "../auth/discordOAuth.js";
import { issueSessionToken } from "../auth/jwt.js";
import { env } from "../config/env.js";
import { upsertUserFromDiscord } from "../db/repositories.js";
@@ -46,7 +50,9 @@ export const createAuthRouter = ({ pool }: AuthRouterDependencies): Router => {
const cookieState = req.cookies?.[OAUTH_STATE_COOKIE_NAME];
if (!code || !state || !cookieState || state !== cookieState) {
res.clearCookie(OAUTH_STATE_COOKIE_NAME, { path: "/auth/discord/callback" });
res.clearCookie(OAUTH_STATE_COOKIE_NAME, {
path: "/auth/discord/callback",
});
res.redirect(`${env.WEB_URL}/login?error=oauth_state_invalid`);
return;
}
@@ -66,11 +72,15 @@ export const createAuthRouter = ({ pool }: AuthRouterDependencies): Router => {
username: user.username,
});
res.clearCookie(OAUTH_STATE_COOKIE_NAME, { path: "/auth/discord/callback" });
res.clearCookie(OAUTH_STATE_COOKIE_NAME, {
path: "/auth/discord/callback",
});
res.cookie(env.SESSION_COOKIE_NAME, sessionToken, sessionCookieConfig);
res.redirect(`${env.WEB_URL}/dashboard`);
} catch {
res.clearCookie(OAUTH_STATE_COOKIE_NAME, { path: "/auth/discord/callback" });
res.clearCookie(OAUTH_STATE_COOKIE_NAME, {
path: "/auth/discord/callback",
});
res.redirect(`${env.WEB_URL}/login?error=oauth_failed`);
}
});
+41 -9
View File
@@ -50,13 +50,23 @@ const queueBotAction = (
return;
}
const bot = await getBotForTenant(deps.pool, req.auth.tenantId, parsedParams.data.botId);
const bot = await getBotForTenant(
deps.pool,
req.auth.tenantId,
parsedParams.data.botId,
);
if (!bot) {
res.status(404).json({ error: "Bot not found" });
return;
}
await setBotStatusForTenant(deps.pool, req.auth.tenantId, parsedParams.data.botId, statusDuringAction, null);
await setBotStatusForTenant(
deps.pool,
req.auth.tenantId,
parsedParams.data.botId,
statusDuringAction,
null,
);
await insertBotRuntimeEvent(deps.pool, {
tenantId: req.auth.tenantId,
@@ -105,13 +115,18 @@ export const createBotRouter = (deps: BotRouterDependencies): Router => {
const parsedBody = addBotSchema.safeParse(req.body);
if (!parsedBody.success) {
res.status(400).json({ error: parsedBody.error.issues[0]?.message ?? "Invalid body" });
res
.status(400)
.json({ error: parsedBody.error.issues[0]?.message ?? "Invalid body" });
return;
}
try {
const botIdentity = await validateDiscordBotToken(parsedBody.data.token);
const encryptedToken = encryptToken(parsedBody.data.token, deps.tokenEncryptionKey);
const encryptedToken = encryptToken(
parsedBody.data.token,
deps.tokenEncryptionKey,
);
const bot = await createOrUpdateBotForTenant(deps.pool, {
tenantId: req.auth.tenantId,
@@ -135,11 +150,16 @@ export const createBotRouter = (deps: BotRouterDependencies): Router => {
res.status(201).json({ bot });
} catch (error) {
if (error instanceof Error && error.message === "BOT_ALREADY_CLAIMED") {
res.status(409).json({ error: "This Discord bot is already claimed by another tenant" });
res.status(409).json({
error: "This Discord bot is already claimed by another tenant",
});
return;
}
if (error instanceof Error && error.message === "Invalid Discord bot token") {
if (
error instanceof Error &&
error.message === "Invalid Discord bot token"
) {
res.status(400).json({ error: error.message });
return;
}
@@ -148,9 +168,21 @@ export const createBotRouter = (deps: BotRouterDependencies): Router => {
}
});
router.post("/:botId/start", deps.tenantControlRateLimit, queueBotAction("start", "starting", deps));
router.post("/:botId/stop", deps.tenantControlRateLimit, queueBotAction("stop", "stopping", deps));
router.post("/:botId/restart", deps.tenantControlRateLimit, queueBotAction("restart", "starting", deps));
router.post(
"/:botId/start",
deps.tenantControlRateLimit,
queueBotAction("start", "starting", deps),
);
router.post(
"/:botId/stop",
deps.tenantControlRateLimit,
queueBotAction("stop", "stopping", deps),
);
router.post(
"/:botId/restart",
deps.tenantControlRateLimit,
queueBotAction("restart", "starting", deps),
);
return router;
};
+4 -1
View File
@@ -4,7 +4,10 @@
"composite": true,
"rootDir": "src",
"outDir": "dist",
"types": ["node"]
"types": ["node"],
"declaration": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src/**/*.ts"]
}