From 7c5a927d0f260e738ea668b5689a676ab86a0cf9 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Mon, 1 Dec 2025 18:36:36 +0100 Subject: [PATCH] Add AuditLog to Backend Signed-off-by: Chaoscaot --- .../src/de/steamwar/routes/AuditLog.kt | 185 ++++++++++++++++++ .../src/de/steamwar/routes/Routes.kt | 1 + 2 files changed, 186 insertions(+) create mode 100644 WebsiteBackend/src/de/steamwar/routes/AuditLog.kt diff --git a/WebsiteBackend/src/de/steamwar/routes/AuditLog.kt b/WebsiteBackend/src/de/steamwar/routes/AuditLog.kt new file mode 100644 index 00000000..27a59c72 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/AuditLog.kt @@ -0,0 +1,185 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2025 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.routes + +import de.steamwar.plugins.SWPermissionCheck +import de.steamwar.sql.AuditLog +import de.steamwar.sql.AuditLogTable +import de.steamwar.sql.SteamwarUserTable +import de.steamwar.sql.UserPerm +import de.steamwar.sql.internal.useDb +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.routing.route +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.v1.core.JoinType +import org.jetbrains.exposed.v1.core.SortOrder +import org.jetbrains.exposed.v1.core.alias +import org.jetbrains.exposed.v1.core.greater +import org.jetbrains.exposed.v1.core.inList +import org.jetbrains.exposed.v1.core.isNull +import org.jetbrains.exposed.v1.core.less +import org.jetbrains.exposed.v1.core.like +import org.jetbrains.exposed.v1.core.or +import org.jetbrains.exposed.v1.jdbc.andWhere +import org.jetbrains.exposed.v1.jdbc.select +import java.time.Instant + +fun Route.configureAuditLog() { + route("/auditlog") { + install(SWPermissionCheck) { + permission = UserPerm.MODERATION + } + + get { + val text = call.request.queryParameters["fullText"] + val actionText = call.request.queryParameters["actionText"] + val serverText = call.request.queryParameters["server"] + val actor = call.request.queryParameters.getAll("actor")?.toSet() + val actionType = + call.request.queryParameters.getAll("actionType")?.map { AuditLog.Type.valueOf(it) }?.toSet() + val timeGreater = call.request.queryParameters["timeGreater"]?.let { Instant.ofEpochMilli(it.toLong()) } + val timeLess = call.request.queryParameters["timeLess"]?.let { Instant.ofEpochMilli(it.toLong()) } + + val serverOwner = call.request.queryParameters.getAll("serverOwner")?.toSet() + val velocity = call.request.queryParameters["velocity"]?.toBoolean() + + val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0 + val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100 + val sorting = call.request.queryParameters["sorting"] ?: "DESC" + + call.respond( + PagedAuditLog.filter( + actionText, + serverText, + text, + actor, + actionType, + timeGreater, + timeLess, + serverOwner, + velocity, + page, + limit, + sorting + ) + ) + } + } +} + +@Serializable +data class PagedAuditLog(val rows: Long, val entries: List) { + + companion object { + fun filter( + actionText: String? = null, + serverText: String? = null, + fullText: String? = null, + actor: Set? = null, + actionType: Set? = null, + timeGreater: Instant? = null, + timeLess: Instant? = null, + serverOwner: Set? = null, + velocity: Boolean? = null, + page: Int = 0, + limit: Int = 100, + sorting: String = "DESC" + ) = useDb { + val actorTable = SteamwarUserTable.alias("actor") + val serverOwnerTable = SteamwarUserTable.alias("serverOwner") + + val query = AuditLogTable.join( + actorTable, + JoinType.INNER, + onColumn = actorTable[SteamwarUserTable.id], + otherColumn = AuditLogTable.actor + ) + .join( + serverOwnerTable, + JoinType.LEFT, + onColumn = serverOwnerTable[SteamwarUserTable.id], + otherColumn = AuditLogTable.serverOwner + ) + .select( + actorTable[SteamwarUserTable.username], serverOwnerTable[SteamwarUserTable.username], + *AuditLogTable.columns.toTypedArray() + ) + + + actionText?.let { + query.andWhere { (AuditLogTable.actionText like "%$it%") } + } + + serverText?.let { + query.andWhere { (AuditLogTable.server like "%$it%") } + } + + fullText?.let { + query.andWhere { (AuditLogTable.actionText like "%$it%") or (AuditLogTable.server like "%$it%") } + } + + actor?.let { query.andWhere { actorTable[SteamwarUserTable.uuid] inList actor } } + + actionType?.let { query.andWhere { AuditLogTable.action inList actionType } } + + timeGreater?.let { query.andWhere { AuditLogTable.time greater timeGreater } } + + timeLess?.let { query.andWhere { AuditLogTable.time less timeLess } } + + serverOwner?.let { query.andWhere { serverOwnerTable[SteamwarUserTable.uuid] inList serverOwner } } + + if (velocity == true) query.andWhere { AuditLogTable.serverOwner.isNull() } + + PagedAuditLog( + query.count(), + query + .limit(limit) + .offset((page * limit).toLong()) + .orderBy(AuditLogTable.time, SortOrder.valueOf(sorting.uppercase())) + .map { + AuditLogEntry( + it[AuditLogTable.id].value, + it[AuditLogTable.time].toEpochMilli(), + it[AuditLogTable.server], + it[serverOwnerTable[SteamwarUserTable.username]], + it[actorTable[SteamwarUserTable.username]], + it[AuditLogTable.action], + it[AuditLogTable.actionText] + ) + }, + ) + } + } +} + +@Serializable +data class AuditLogEntry( + val id: Int, + val time: Long, + val server: String, + val serverOwner: String?, + val actor: String, + val actionType: AuditLog.Type, + val actionText: String +) \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Routes.kt b/WebsiteBackend/src/de/steamwar/routes/Routes.kt index eb5444af..84288513 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Routes.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Routes.kt @@ -33,6 +33,7 @@ fun Application.configureRoutes() { configurePage() configureSchematic() configureAuth() + configureAuditLog() } } } \ No newline at end of file