This commit is contained in:
2026-03-19 18:00:46 +07:00
commit f72ad2769f
98 changed files with 9299 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import { Router } from "express";
import { ok } from "../../lib/api-response.js";
import { AppError } from "../../lib/errors.js";
import { requireAuth } from "../../middleware/auth.js";
import { requirePermission } from "../../middleware/permission.js";
import { validateBody } from "../../middleware/validate.js";
import { executeSqlSchema } from "./sql-console.schemas.js";
import { executeSql } from "./sql-console.service.js";
export const sqlConsoleRouter = Router();
sqlConsoleRouter.post(
"/execute",
requireAuth,
requirePermission("sql_console", "execute_sql"),
validateBody(executeSqlSchema),
async (req, res, next) => {
try {
if (!req.user) {
throw new AppError(401, "UNAUTHORIZED", "Authentication is required");
}
res.json(
ok(
await executeSql(req.body.sql, req.user, {
ip: req.ip,
userAgent: req.headers["user-agent"] as string | undefined
})
)
);
} catch (error) {
next(error);
}
}
);

View File

@@ -0,0 +1,5 @@
import { z } from "zod";
export const executeSqlSchema = z.object({
sql: z.string().min(1)
});

View File

@@ -0,0 +1,58 @@
import { createHash } from "node:crypto";
import { getTargetPool } from "../../db/target.js";
import { AppError } from "../../lib/errors.js";
import { guardSql } from "../../lib/sql-guard.js";
import type { SessionUser } from "../../types/auth.js";
import { createAuditEvent } from "../audit/audit.service.js";
function isReadOnly(user: SessionUser) {
if (user.isRoot) {
return false;
}
const hasWrite = user.permissions.some((grant) => grant.action === "write" || grant.action === "schema_change");
return !hasWrite;
}
export async function executeSql(sql: string, user: SessionUser, context: { ip?: string; userAgent?: string }) {
if (process.env.FEATURE_SQL_CONSOLE === "false") {
throw new AppError(403, "SQL_CONSOLE_DISABLED", "SQL console feature is disabled");
}
const guard = guardSql(sql, {
allowMultiStatement: user.isRoot,
readOnly: isReadOnly(user),
allowSchemaChanges: user.isRoot || user.permissions.some((grant) => grant.action === "schema_change")
});
const pool = getTargetPool();
const startedAt = Date.now();
const result = await pool.query(guard.normalized);
const durationMs = Date.now() - startedAt;
const maskedSql = guard.normalized.slice(0, 4000);
await createAuditEvent({
actorUserId: user.id,
action: "sql.execute",
resourceType: "sql_console",
resourceName: guard.statementType,
sqlTextMasked: maskedSql,
payloadAfter: {
rowCount: result.rowCount,
durationMs,
statementHash: createHash("sha256").update(guard.normalized).digest("hex")
},
ip: context.ip ?? null,
userAgent: context.userAgent ?? null,
status: "success"
});
return {
rows: result.rows,
fields: result.fields.map((field) => ({ name: field.name, dataTypeId: field.dataTypeID })),
rowCount: result.rowCount ?? 0,
durationMs,
statementType: guard.statementType,
notice: guard.isMutating ? "Mutation executed" : "Query executed"
};
}