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