diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ba1b360223..bc24ba407a 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -23,6 +23,10 @@ jobs: | grep -Ei 'error|permissions|permission_ranks|permission_definitions|unknown column|doesn.t exist|digest' \ | tail -120 \ || true + echo "--- Permission page database probe ---" + cd /var/www/atom-nexst + pnpm tsx scripts/diagnose-permission-page.ts + check: if: startsWith(gitea.ref_name, 'v') == false diff --git a/scripts/diagnose-permission-page.ts b/scripts/diagnose-permission-page.ts new file mode 100644 index 0000000000..8a1852e4a1 --- /dev/null +++ b/scripts/diagnose-permission-page.ts @@ -0,0 +1,103 @@ +import "./load-env"; +import mysql, { type RowDataPacket } from "mysql2/promise"; +import { mysqlConnectionUrl } from "./db-url"; + +type DbError = Error & { code?: string }; + +const databaseUrl = process.env.DATABASE_URL; +if (!databaseUrl) throw new Error("DATABASE_URL is required"); + +const connection = await mysql.createConnection( + mysqlConnectionUrl(databaseUrl), +); +let failed = false; + +async function probe( + name: string, + statement: string, + values: Array = [], +): Promise { + try { + const [rows] = await connection.execute(statement, values); + console.log(`[permissions-diag] ${name}: ok (${rows.length} rows)`); + return rows; + } catch (error) { + failed = true; + const dbError = error as DbError; + console.error( + `[permissions-diag] ${name}: failed code=${dbError.code ?? "unknown"} message=${dbError.message}`, + ); + return null; + } +} + +try { + const rankRows = await probe( + "select rank id", + "SELECT id FROM permission_ranks ORDER BY id DESC LIMIT 1", + ); + const rankId = Number(rankRows?.[0]?.id ?? 11); + console.log(`[permissions-diag] probing rank id ${rankId}`); + + await probe( + "rank detail", + `SELECT id, rank_name, badge, level, prefix, prefix_color, hidden_rank, + log_commands, room_effect, auto_credits_amount, auto_pixels_amount, + auto_gotw_amount, auto_points_amount + FROM permission_ranks WHERE id = ?`, + [rankId], + ); + + const definitionColumns = await probe( + "permission_definitions columns", + `SELECT column_name FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'permission_definitions'`, + ); + const rankColumn = `rank_${rankId}`; + if (definitionColumns?.some((row) => row.column_name === rankColumn)) { + await probe( + "normalized permissions", + `SELECT permission_key, max_value, \`${rankColumn}\` AS value + FROM permission_definitions ORDER BY permission_key ASC`, + ); + } else { + await probe( + "legacy permissions", + "SELECT * FROM permissions WHERE id = ? LIMIT 1", + [rankId], + ); + } + + await probe( + "rank users", + "SELECT id, username, look FROM users WHERE rank = ? ORDER BY username LIMIT 200", + [rankId], + ); + await probe( + "rank user count", + "SELECT COUNT(*) AS total FROM users WHERE rank = ?", + [rankId], + ); + await probe( + "CMS permissions", + "SELECT id, slug, title FROM acl_permissions ORDER BY slug", + ); + const roles = await probe( + "CMS role", + "SELECT id FROM acl_roles WHERE slug = ? LIMIT 1", + [`rank_${rankId}`], + ); + if (roles?.[0]?.id) { + await probe( + "CMS role permissions", + `SELECT p.slug FROM acl_model_permissions mp + INNER JOIN acl_permissions p ON mp.permission_id = p.id + WHERE mp.model_id = ?`, + [roles[0].id], + ); + } +} finally { + await connection.end(); +} + +if (failed) process.exitCode = 1;