1111
This commit is contained in:
34
backend/src/modules/sql-console/sql-console.routes.ts
Normal file
34
backend/src/modules/sql-console/sql-console.routes.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
5
backend/src/modules/sql-console/sql-console.schemas.ts
Normal file
5
backend/src/modules/sql-console/sql-console.schemas.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const executeSqlSchema = z.object({
|
||||
sql: z.string().min(1)
|
||||
});
|
||||
58
backend/src/modules/sql-console/sql-console.service.ts
Normal file
58
backend/src/modules/sql-console/sql-console.service.ts
Normal 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"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user