diff --git a/WebsiteBackend/src/de/steamwar/Application.kt b/WebsiteBackend/src/de/steamwar/Application.kt
index e49b9a9d..9853d4ab 100644
--- a/WebsiteBackend/src/de/steamwar/Application.kt
+++ b/WebsiteBackend/src/de/steamwar/Application.kt
@@ -20,12 +20,9 @@
package de.steamwar
import de.steamwar.plugins.configurePlugins
-import de.steamwar.routes.ResponseUser
-import de.steamwar.routes.SchematicCode
import io.ktor.server.application.*
import io.ktor.server.engine.*
import de.steamwar.routes.configureRoutes
-import de.steamwar.sql.SchematicType
import de.steamwar.sql.SteamwarUser
import io.ktor.server.netty.*
import kotlinx.serialization.ExperimentalSerializationApi
@@ -47,7 +44,6 @@ fun main() {
Thread {
while (true) {
Thread.sleep(1000 * 10)
- ResponseUser.clearCache()
SteamwarUser.clear()
}
}.start()
diff --git a/WebsiteBackend/src/de/steamwar/routes/AuditLog.kt b/WebsiteBackend/src/de/steamwar/routes/AuditLog.kt
new file mode 100644
index 00000000..7ac08d5f
--- /dev/null
+++ b/WebsiteBackend/src/de/steamwar/routes/AuditLog.kt
@@ -0,0 +1,215 @@
+/*
+ * 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.Alias
+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.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
+)
\ No newline at end of file
diff --git a/WebsiteBackend/src/de/steamwar/routes/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/Auth.kt
index 19f300f0..4710bd5b 100644
--- a/WebsiteBackend/src/de/steamwar/routes/Auth.kt
+++ b/WebsiteBackend/src/de/steamwar/routes/Auth.kt
@@ -66,7 +66,7 @@ fun Route.configureAuth() {
}
call.sessions.set(SWUserSession(user.getId()))
- call.respond(ResponseUser.get(user))
+ call.respond(ResponseUser(user))
}
delete {
@@ -100,7 +100,7 @@ fun Route.configureAuth() {
}
call.sessions.set(SWUserSession(user.getId()))
- call.respond(ResponseUser.get(user))
+ call.respond(ResponseUser(user))
}
}
}
diff --git a/WebsiteBackend/src/de/steamwar/routes/Data.kt b/WebsiteBackend/src/de/steamwar/routes/Data.kt
index 9cc7be0c..e52f72aa 100644
--- a/WebsiteBackend/src/de/steamwar/routes/Data.kt
+++ b/WebsiteBackend/src/de/steamwar/routes/Data.kt
@@ -25,6 +25,7 @@ import de.steamwar.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.sql.SchematicType
import de.steamwar.sql.SteamwarUser
+import de.steamwar.sql.SteamwarUserTable
import de.steamwar.sql.Team
import de.steamwar.sql.UserPerm
import de.steamwar.sql.internal.useDb
@@ -36,6 +37,13 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import org.bspfsystems.yamlconfiguration.file.YamlConfiguration
+import org.jetbrains.exposed.v1.core.ResultRow
+import org.jetbrains.exposed.v1.core.eq
+import org.jetbrains.exposed.v1.core.inList
+import org.jetbrains.exposed.v1.core.like
+import org.jetbrains.exposed.v1.jdbc.Query
+import org.jetbrains.exposed.v1.jdbc.andWhere
+import org.jetbrains.exposed.v1.jdbc.selectAll
import java.io.File
import java.net.InetSocketAddress
import java.util.*
@@ -44,28 +52,39 @@ import java.util.*
data class ResponseSchematicType(val name: String, val db: String)
@Serializable
-data class ResponseUser(val name: String, val uuid: String, val prefix: String, val perms: List) {
- private constructor(user: SteamwarUser) : this(user.userName, user.uuid.toString(), user.prefix().chatPrefix, user.perms().filter { !it.name.startsWith("PREFIX_") }.map { it.name })
+data class ResponseUser(
+ val name: String, val uuid: String, val id: Int?, val perms: List?, val prefix: String?
+) {
+ constructor(user: SteamwarUser, includeId: Boolean = false, includePerms: Boolean = false) : this(
+ user.userName,
+ user.uuid.toString(),
+ if (includeId) user.getId() else null,
+ if (includePerms) user.perms().map { it.name } else null,
+ if (includePerms) user.prefix().chatPrefix else null,
+ )
- companion object {
- private val cache = mutableMapOf()
+ constructor(row: ResultRow, includeId: Boolean = false, includePerms: Boolean = false, prefixes: MutableSet = mutableSetOf()) : this(
+ row[SteamwarUserTable.username],
+ row[SteamwarUserTable.uuid],
+ if (includeId) row[SteamwarUserTable.id].value else null,
+ if (includePerms) UserPerm.getPerms(row[SteamwarUserTable.id].value).also { prefixes.addAll(it) }.map { it.name } else null,
+ if (includePerms) prefixes.firstOrNull { UserPerm.prefixes.containsKey(it) }?.let { UserPerm.prefixes[it]!!.chatPrefix } else null
+ )
+}
- fun get(id: Int): ResponseUser {
- synchronized(cache) {
- return cache[id] ?: ResponseUser(SteamwarUser.byId(id)!!).also { cache[id] = it }
- }
- }
+@Serializable
+data class ResponseUserList(val entries: List, val rows: Long)
- fun get(user: SteamwarUser): ResponseUser = synchronized(cache) {
- return cache[user.getId()] ?: ResponseUser(user).also { cache[user.getId()] = it }
- }
+private fun Query.addUserFilter(
+ name: String? = null,
+ uuid: UUID? = null,
+ team: Set? = null,
+): Query {
+ name?.let { andWhere { (SteamwarUserTable.username like "%$it%") } }
+ uuid?.let { andWhere { (SteamwarUserTable.uuid eq it.toString()) } }
+ team?.let { andWhere { (SteamwarUserTable.team inList team) } }
- fun clearCache() {
- synchronized(cache) {
- cache.clear()
- }
- }
- }
+ return this
}
fun Route.configureDataRoutes() {
@@ -76,13 +95,33 @@ fun Route.configureDataRoutes() {
permission = UserPerm.MODERATION
}
get("/users") {
- call.respond(useDb { SteamwarUser.all().map { ResponseUser.get(it) } })
+ val name = call.request.queryParameters["name"]
+ val uuid = call.request.queryParameters["uuid"]?.let { catchException { UUID.fromString(it) } }
+ val team = call.request.queryParameters.getAll("team")?.map { it.toInt() }?.toSet()
+
+ val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
+ val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
+
+ val includePerms = call.request.queryParameters["includePerms"]?.toBoolean() ?: false
+ val includeId = call.request.queryParameters["includeId"]?.toBoolean() ?: false
+
+ call.respond(
+ useDb {
+ ResponseUserList(
+ SteamwarUserTable.selectAll().addUserFilter(name, uuid, team).limit(limit)
+ .offset((page * limit).toLong())
+ .map { ResponseUser(it, includeId, includePerms) },
+ SteamwarUserTable.selectAll().addUserFilter(name, uuid, team).count()
+ )
+ }
+ )
}
get("/teams") {
call.respond(Team.getAll().map { ResponseTeam(it) })
}
get("/schematicTypes") {
- call.respond(SchematicType.values().filter { !it.check() }.map { ResponseSchematicType(it.name(), it.toDB()) })
+ call.respond(SchematicType.values().filter { !it.check() }
+ .map { ResponseSchematicType(it.name(), it.toDB()) })
}
get("/gamemodes") {
call.respond(
@@ -116,11 +155,14 @@ fun Route.configureDataRoutes() {
}
get("/team") {
call.respond(
- listOf(UserPerm.PREFIX_ADMIN, UserPerm.PREFIX_DEVELOPER, UserPerm.PREFIX_MODERATOR, UserPerm.PREFIX_SUPPORTER, UserPerm.PREFIX_BUILDER)
- .associateWith { SteamwarUser.getUsersWithPerm(it) }
- .mapKeys { UserPerm.prefixes[it.key]!!.chatPrefix }
- .mapValues { it.value.map { ResponseUser.get(it) } }
- )
+ listOf(
+ UserPerm.PREFIX_ADMIN,
+ UserPerm.PREFIX_DEVELOPER,
+ UserPerm.PREFIX_MODERATOR,
+ UserPerm.PREFIX_SUPPORTER,
+ UserPerm.PREFIX_BUILDER
+ ).associateWith { SteamwarUser.getUsersWithPerm(it) }.mapKeys { UserPerm.prefixes[it.key]!!.chatPrefix }
+ .mapValues { it.value.map { ResponseUser(it) } })
}
get("/skin/{uuid}") {
val uuid = call.parameters["uuid"]
@@ -138,7 +180,7 @@ fun Route.configureDataRoutes() {
route("/me") {
install(SWPermissionCheck)
get {
- call.respond(ResponseUser.get(call.principal()!!.user))
+ call.respond(ResponseUser(call.principal()!!.user, includePerms = true))
}
}
}
diff --git a/WebsiteBackend/src/de/steamwar/routes/EventReferees.kt b/WebsiteBackend/src/de/steamwar/routes/EventReferees.kt
index 4997b1fa..0c8bbf3b 100644
--- a/WebsiteBackend/src/de/steamwar/routes/EventReferees.kt
+++ b/WebsiteBackend/src/de/steamwar/routes/EventReferees.kt
@@ -31,7 +31,7 @@ fun Route.configureEventRefereesRouting() {
route("/referees") {
get {
val event = call.receiveEvent() ?: return@get
- call.respond(Referee.get(event.eventID).map { ResponseUser.get(SteamwarUser.byId(it)!!) })
+ call.respond(Referee.get(event.eventID).map { ResponseUser(SteamwarUser.byId(it)!!) })
}
put {
val event = call.receiveEvent() ?: return@put
@@ -39,7 +39,7 @@ fun Route.configureEventRefereesRouting() {
referees.forEach {
Referee.add(event.eventID, SteamwarUser.get(UUID.fromString(it))!!.getId())
}
- call.respond(Referee.get(event.eventID).map { ResponseUser.get(SteamwarUser.byId(it)!!) })
+ call.respond(Referee.get(event.eventID).map { ResponseUser(SteamwarUser.byId(it)!!) })
}
delete {
val event = call.receiveEvent() ?: return@delete
@@ -47,7 +47,7 @@ fun Route.configureEventRefereesRouting() {
referees.forEach {
Referee.remove(event.eventID, SteamwarUser.get(UUID.fromString(it))!!.getId())
}
- call.respond(Referee.get(event.eventID).map { ResponseUser.get(SteamwarUser.byId(it)!!) })
+ call.respond(Referee.get(event.eventID).map { ResponseUser(SteamwarUser.byId(it)!!) })
}
}
}
\ No newline at end of file
diff --git a/WebsiteBackend/src/de/steamwar/routes/Events.kt b/WebsiteBackend/src/de/steamwar/routes/Events.kt
index e82fc495..2c76498b 100644
--- a/WebsiteBackend/src/de/steamwar/routes/Events.kt
+++ b/WebsiteBackend/src/de/steamwar/routes/Events.kt
@@ -118,7 +118,7 @@ data class ExtendedResponseEvent(
TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) },
EventGroup.get(event).map { ResponseGroups(it) },
EventFight.getEvent(event.eventID).map { ResponseEventFight(it) },
- Referee.get(event.eventID).map { ResponseUser.get(SteamwarUser.byId(it)!!) },
+ Referee.get(event.eventID).map { ResponseUser(SteamwarUser.byId(it)!!) },
EventRelation.get(event).map { ResponseRelation(it) }
)
}
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
diff --git a/WebsiteBackend/src/de/steamwar/routes/Schematic.kt b/WebsiteBackend/src/de/steamwar/routes/Schematic.kt
index d1ada3fa..0e8ef701 100644
--- a/WebsiteBackend/src/de/steamwar/routes/Schematic.kt
+++ b/WebsiteBackend/src/de/steamwar/routes/Schematic.kt
@@ -34,9 +34,7 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import java.io.BufferedInputStream
-import java.io.ByteArrayInputStream
import java.io.DataInputStream
-import java.io.InputStream
import java.security.MessageDigest
import java.time.Duration
import java.time.Instant
@@ -49,16 +47,6 @@ data class ResponseSchematic(val name: String, val id: Int, val type: String?, v
constructor(node: SchematicNode) : this(node.name, node.getId(), node.schemtype?.name(), node.owner, node.item, node.lastUpdate.time, node.rank, node.replaceColor(), node.allowReplay())
}
-@Serializable
-data class ResponseSchematicLong(val members: List, val path: String, val schem: ResponseSchematic) {
- constructor(node: SchematicNode, path: String): this(NodeMember.getNodeMembers(node.getId()).map { ResponseUser.get(it.member) }, path, ResponseSchematic(node))
-}
-
-@Serializable
-data class ResponseSchematicList(val breadcrumbs: List, val schematics: List, val players: Map) {
- constructor(schematics: List, breadcrumbs: List) : this(breadcrumbs, schematics, schematics.map { it.owner }.distinct().map { ResponseUser.get(it) }.associateBy { it.uuid })
-}
-
@Serializable
data class ResponseBreadcrumb(val name: String, val id: Int)
diff --git a/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt b/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt
index 6ed092a4..1c3987a7 100644
--- a/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt
+++ b/WebsiteBackend/src/de/steamwar/routes/UserPerms.kt
@@ -23,11 +23,17 @@ import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.plugins.getUser
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.UserPerm
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import kotlinx.serialization.Serializable
+import io.ktor.http.HttpStatusCode
+import io.ktor.server.application.ApplicationCall
+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.delete
+import io.ktor.server.routing.get
+import io.ktor.server.routing.put
+import io.ktor.server.routing.route
+import kotlinx.serialization.Serializable;
@Serializable
data class RespondPrefix(val name: String, val colorCode: String, val chatPrefix: String)
@@ -76,7 +82,12 @@ fun Route.configureUserPerms() {
val prefixs = UserPerm.prefixes[prefix]!!
- call.respond(RespondUserPermsPrefix(RespondPrefix(prefix.name, prefixs.colorCode, prefixs.chatPrefix), perms))
+ call.respond(
+ RespondUserPermsPrefix(
+ RespondPrefix(prefix.name, prefixs.colorCode, prefixs.chatPrefix),
+ perms
+ )
+ )
}
put("/prefix/{prefix}") {
val (user, prefix) = call.receivePermission("prefix", isPrefix = true) ?: return@put
@@ -112,7 +123,10 @@ fun Route.configureUserPerms() {
}
}
-suspend fun ApplicationCall.receivePermission(fieldName: String = "perm", isPrefix: Boolean = false): Pair? {
+suspend fun ApplicationCall.receivePermission(
+ fieldName: String = "perm",
+ isPrefix: Boolean = false
+): Pair? {
val user = request.getUser()
if (user == null) {
respond(HttpStatusCode.BadRequest)