diff --git a/backend/src/app/routes.ts b/backend/src/app/routes.ts index 2a130cb..9a85a87 100644 --- a/backend/src/app/routes.ts +++ b/backend/src/app/routes.ts @@ -11,7 +11,7 @@ import { usersRouter } from "../modules/users/users.routes.js"; import { rolesRouter } from "../modules/roles/roles.routes.js"; import { permissionsRouter } from "../modules/permissions/permissions.routes.js"; import { connectionsRouter } from "../modules/connections/connections.routes.js"; -import { logsRouter } from "../modules/logs/logs.routes.js"; +import { logViewerRouter } from "../modules/log-viewer/log-viewer.routes.js"; import { requireAuth } from "../middleware/auth.js"; export const apiRouter = Router(); @@ -30,4 +30,4 @@ apiRouter.use("/users", usersRouter); apiRouter.use("/roles", rolesRouter); apiRouter.use("/permissions", permissionsRouter); apiRouter.use("/connections", connectionsRouter); -apiRouter.use("/logs", logsRouter); +apiRouter.use("/logs", logViewerRouter); diff --git a/backend/src/modules/log-viewer/log-viewer.routes.ts b/backend/src/modules/log-viewer/log-viewer.routes.ts new file mode 100644 index 0000000..4bfa7d1 --- /dev/null +++ b/backend/src/modules/log-viewer/log-viewer.routes.ts @@ -0,0 +1,29 @@ +import { Router } from "express"; +import { ok } from "../../lib/api-response.js"; +import { requirePermission } from "../../middleware/permission.js"; +import { validateQuery } from "../../middleware/validate.js"; +import { logViewerQuerySchema } from "./log-viewer.schemas.js"; +import { listLogEntries } from "./log-viewer.service.js"; + +export const logViewerRouter = Router(); + +logViewerRouter.get( + "/", + requirePermission("logs", "view_logs"), + validateQuery(logViewerQuerySchema), + async (req, res, next) => { + try { + const query = req.query as unknown as { q: string; severity: string; page: number; limit: number }; + const result = await listLogEntries(query); + res.json( + ok(result.data, { + total: result.total, + page: result.page, + limit: result.limit + }) + ); + } catch (error) { + next(error); + } + } +); diff --git a/backend/src/modules/log-viewer/log-viewer.schemas.ts b/backend/src/modules/log-viewer/log-viewer.schemas.ts new file mode 100644 index 0000000..92243ae --- /dev/null +++ b/backend/src/modules/log-viewer/log-viewer.schemas.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const logViewerQuerySchema = z.object({ + q: z.string().optional().default(""), + severity: z.string().optional().default(""), + page: z.coerce.number().default(1), + limit: z.coerce.number().default(100) +}); diff --git a/backend/src/modules/log-viewer/log-viewer.service.ts b/backend/src/modules/log-viewer/log-viewer.service.ts new file mode 100644 index 0000000..3dd18a7 --- /dev/null +++ b/backend/src/modules/log-viewer/log-viewer.service.ts @@ -0,0 +1,21 @@ +import { readFile } from "node:fs/promises"; +import { env } from "../../config/env.js"; +import { getPagination } from "../../lib/pagination.js"; + +export async function listLogEntries(query: { q: string; severity: string; page: number; limit: number }) { + const raw = await readFile(env.LOG_SOURCE_PATH, "utf8").catch(() => ""); + const lines = raw.split(/\r?\n/).filter(Boolean); + const filtered = lines.filter((line) => { + const matchesText = !query.q || line.toLowerCase().includes(query.q.toLowerCase()); + const matchesSeverity = !query.severity || line.toLowerCase().includes(query.severity.toLowerCase()); + return matchesText && matchesSeverity; + }); + const pagination = getPagination(query.page, query.limit); + + return { + data: filtered.slice(pagination.offset, pagination.offset + pagination.limit), + total: filtered.length, + page: pagination.page, + limit: pagination.limit + }; +}