/* * 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) { 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() ) 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 { AuditLogTable.actor 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 { AuditLogTable.serverOwner 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 )