/* * 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.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.serialization.Serializable import org.jetbrains.exposed.v1.core.* import org.jetbrains.exposed.v1.jdbc.Query import org.jetbrains.exposed.v1.jdbc.andWhere import org.jetbrains.exposed.v1.jdbc.select import org.jetbrains.exposed.v1.jdbc.selectAll import java.time.Instant fun Route.configureAuditLog() { route("/auditlog") { install(SWPermissionCheck) { mustAuth = true 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")?.map { it.toInt() }?.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")?.map { it.toInt() }?.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() ) PagedAuditLog( AuditLogTable.selectAll().addAuditLogFilters( actionText, serverText, fullText, actor, actionType, timeGreater, timeLess, serverOwner, velocity ).count(), query .addAuditLogFilters( actionText, serverText, fullText, actor, actionType, timeGreater, timeLess, serverOwner, velocity ) .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] ) }, ) } private fun Query.addAuditLogFilters( 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, ): Query { actionText?.let { andWhere { (AuditLogTable.actionText like "%$it%") } } serverText?.let { andWhere { (AuditLogTable.server like "%$it%") } } fullText?.let { andWhere { (AuditLogTable.actionText like "%$it%") or (AuditLogTable.server like "%$it%") } } actor?.let { andWhere { AuditLogTable.actor inList actor } } actionType?.let { andWhere { AuditLogTable.action inList actionType } } timeGreater?.let { andWhere { AuditLogTable.time greater timeGreater } } timeLess?.let { andWhere { AuditLogTable.time less timeLess } } serverOwner?.let { andWhere { AuditLogTable.serverOwner inList serverOwner } } if (velocity == true) andWhere { AuditLogTable.serverOwner.isNull() } return this } } } @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 )