forked from SteamWar/SteamWar
Add Backend to Monorepo
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import de.steamwar.sql.Token
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Serializable
|
||||
data class AuthLoginRequest(val username: String, val password: String)
|
||||
|
||||
@Serializable
|
||||
data class AuthTokenResponse(val token: String)
|
||||
|
||||
@Serializable
|
||||
data class ResponseToken(val id: Int, val name: String, val created: String) {
|
||||
constructor(token: Token) : this(token.id, token.name, token.created.toLocalDateTime().toString())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CreateTokenRequest(val name: String, val password: String)
|
||||
|
||||
fun Route.configureAuthRoutes() {
|
||||
route("/auth") {
|
||||
post("/login") {
|
||||
if (call.principal<SWAuthPrincipal>() != null) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Already logged in", "already_logged_in"))
|
||||
return@post
|
||||
}
|
||||
|
||||
val request = call.receive<AuthLoginRequest>()
|
||||
|
||||
val user = SteamwarUser.get(request.username)
|
||||
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid"))
|
||||
return@post
|
||||
}
|
||||
|
||||
if (!user.verifyPassword(request.password)) {
|
||||
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid"))
|
||||
return@post
|
||||
}
|
||||
|
||||
val code = Token.createToken("Website: ${DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now())}", user)
|
||||
call.respond(AuthTokenResponse(code))
|
||||
}
|
||||
route("/tokens") {
|
||||
install(SWPermissionCheck) {
|
||||
mustAuth = true
|
||||
}
|
||||
|
||||
get {
|
||||
val auth = call.principal<SWAuthPrincipal>()
|
||||
|
||||
if(auth == null) {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(Token.listUser(auth.user).map { ResponseToken(it) })
|
||||
}
|
||||
|
||||
post {
|
||||
val auth = call.principal<SWAuthPrincipal>()
|
||||
|
||||
if(auth == null) {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@post
|
||||
}
|
||||
|
||||
val request = call.receive<CreateTokenRequest>()
|
||||
|
||||
if(request.name.length > 32) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Name too long", "name_too_long"))
|
||||
return@post
|
||||
}
|
||||
|
||||
if(request.name.length < 3) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Name too short", "name_too_short"))
|
||||
return@post
|
||||
}
|
||||
|
||||
if(!auth.user.verifyPassword(request.password)) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid password", "invalid_password"))
|
||||
return@post
|
||||
}
|
||||
|
||||
val token = Token.createToken(request.name, auth.user)
|
||||
|
||||
call.respond(AuthTokenResponse(token))
|
||||
}
|
||||
|
||||
route("/{id}") {
|
||||
delete {
|
||||
val auth = call.principal<SWAuthPrincipal>()
|
||||
|
||||
if(auth == null) {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@delete
|
||||
}
|
||||
|
||||
val id = call.parameters["id"]?.toIntOrNull()
|
||||
|
||||
if(id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@delete
|
||||
}
|
||||
|
||||
val token = Token.get(id)
|
||||
|
||||
if(token == null) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@delete
|
||||
}
|
||||
|
||||
if(token.owner != auth.user) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
return@delete
|
||||
}
|
||||
|
||||
token.delete()
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
|
||||
post("/logout") {
|
||||
val auth = call.principal<SWAuthPrincipal>()
|
||||
|
||||
if(auth == null) {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@post
|
||||
}
|
||||
|
||||
auth.token.delete()
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.data.Groups
|
||||
import de.steamwar.data.getCachedSkin
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.SchematicType
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import de.steamwar.sql.UserPerm
|
||||
import de.steamwar.sql.loadSchematicTypes
|
||||
import de.steamwar.util.fetchData
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.bspfsystems.yamlconfiguration.file.YamlConfiguration
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class ResponseSchematicType(val name: String, val db: String)
|
||||
|
||||
@Serializable
|
||||
data class ResponseUser(val id: Int, val name: String, val uuid: String, val prefix: String, val perms: List<String>) {
|
||||
constructor(user: SteamwarUser) : this(user.id, user.userName, user.uuid.toString(), user.prefix().chatPrefix, user.perms().map { it.name }) {
|
||||
synchronized(cache) {
|
||||
cache[id] = this
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val cache = mutableMapOf<Int, ResponseUser>()
|
||||
|
||||
fun get(id: Int): ResponseUser {
|
||||
synchronized(cache) {
|
||||
return cache[id] ?: ResponseUser(SteamwarUser.get(id)).also { cache[id] = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
synchronized(cache) {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.configureDataRoutes() {
|
||||
route("/data") {
|
||||
get {
|
||||
call.respondText("Hello World!")
|
||||
}
|
||||
get("/schematicTypes") {
|
||||
val types = mutableListOf<SchematicType>()
|
||||
loadSchematicTypes(types, mutableMapOf())
|
||||
call.respond(types.filter { !it.check() }.map { ResponseSchematicType(it.name(), it.toDB()) })
|
||||
}
|
||||
get("/gamemodes") {
|
||||
call.respond(
|
||||
File("/configs/GameModes/").listFiles()!!
|
||||
.filter { it.name.endsWith(".yml") && !it.name.endsWith(".kits.yml") }
|
||||
.map { it.nameWithoutExtension })
|
||||
}
|
||||
get("/gamemodes/{gamemode}/maps") {
|
||||
val gamemode = call.parameters["gamemode"]
|
||||
if (gamemode == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid gamemode"))
|
||||
return@get
|
||||
}
|
||||
val file = File("/configs/GameModes/$gamemode.yml")
|
||||
if (!file.exists()) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Gamemode not found"))
|
||||
return@get
|
||||
}
|
||||
call.respond(YamlConfiguration.loadConfiguration(file).getStringList("Server.Maps"))
|
||||
}
|
||||
get("/users") {
|
||||
call.respond(SteamwarUser.getAll().map { ResponseUser(it) })
|
||||
}
|
||||
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(it) } }
|
||||
)
|
||||
}
|
||||
get("/groups") {
|
||||
call.respond(Groups.getAllGroups())
|
||||
}
|
||||
get("/server") {
|
||||
try {
|
||||
val server = fetchData(InetSocketAddress("steamwar.de", 25565), 100)
|
||||
call.respond(server)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
call.respond(HttpStatusCode.InternalServerError, ResponseError(e.message ?: "Unknown error"))
|
||||
return@get
|
||||
}
|
||||
}
|
||||
get("/skin/{uuid}") {
|
||||
val uuid = call.parameters["uuid"]
|
||||
if (uuid == null || catchException { UUID.fromString(uuid) } == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid UUID"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val skin = getCachedSkin(uuid)
|
||||
call.response.header("X-Cache", if (skin.second) "HIT" else "MISS")
|
||||
call.response.header("Cache-Control", "public, max-age=604800")
|
||||
call.respondFile(skin.first)
|
||||
}
|
||||
|
||||
route("/me") {
|
||||
install(SWPermissionCheck)
|
||||
get {
|
||||
call.respond(ResponseUser(call.principal<SWAuthPrincipal>()!!.user))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> catchException(yield: () -> T): T? = try {
|
||||
yield()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.data.Groups
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.EventFight
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import de.steamwar.sql.Team
|
||||
import de.steamwar.sql.UserPerm
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.sql.Timestamp
|
||||
import java.time.Instant
|
||||
|
||||
@Serializable
|
||||
data class ResponseEventFight(
|
||||
val id: Int,
|
||||
val spielmodus: String,
|
||||
val map: String,
|
||||
val blueTeam: ResponseTeam,
|
||||
val redTeam: ResponseTeam,
|
||||
val start: Long,
|
||||
val ergebnis: Int,
|
||||
val spectatePort: Int?,
|
||||
val group: String?
|
||||
) {
|
||||
constructor(eventFight: EventFight) : this(
|
||||
eventFight.fightID,
|
||||
eventFight.spielmodus,
|
||||
eventFight.map,
|
||||
ResponseTeam(Team.get(eventFight.teamBlue)),
|
||||
ResponseTeam(Team.get(eventFight.teamRed)),
|
||||
eventFight.startTime.time,
|
||||
eventFight.ergebnis,
|
||||
eventFight.spectatePort,
|
||||
Groups.getGroup(eventFight.fightID)?.name
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResponseTeam(val id: Int, val name: String, val kuerzel: String, val color: String) {
|
||||
constructor(team: Team) : this(team.teamId, team.teamName, team.teamKuerzel, team.teamColor)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UpdateEventFight(
|
||||
val blueTeam: Int? = null,
|
||||
val redTeam: Int? = null,
|
||||
val start: Long? = null,
|
||||
val spielmodus: String? = null,
|
||||
val map: String? = null,
|
||||
val group: String? = null,
|
||||
val spectatePort: Int? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateEventFight(
|
||||
val event: Int,
|
||||
val spielmodus: String,
|
||||
val map: String,
|
||||
val blueTeam: Int,
|
||||
val redTeam: Int,
|
||||
val start: Long,
|
||||
val spectatePort: Int? = null,
|
||||
val group: String? = null
|
||||
)
|
||||
|
||||
fun Route.configureEventFightRoutes() {
|
||||
route("/fights") {
|
||||
install(SWPermissionCheck) {
|
||||
allowMethod(HttpMethod.Get)
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
post {
|
||||
val fight = call.receiveNullable<CreateEventFight>()
|
||||
if (fight == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body"))
|
||||
return@post
|
||||
}
|
||||
val eventFight = EventFight.create(
|
||||
fight.event,
|
||||
Timestamp.from(Instant.ofEpochMilli(fight.start)),
|
||||
fight.spielmodus,
|
||||
fight.map,
|
||||
fight.blueTeam,
|
||||
fight.redTeam,
|
||||
fight.spectatePort
|
||||
)
|
||||
if (fight.group != null) {
|
||||
if (fight.group != "null") {
|
||||
Groups.setGroup(eventFight.fightID, fight.group)
|
||||
}
|
||||
}
|
||||
call.respond(HttpStatusCode.Created, ResponseEventFight(eventFight))
|
||||
}
|
||||
route("/{fight}") {
|
||||
put {
|
||||
val fightId = call.parameters["fight"]?.toIntOrNull()
|
||||
if (fightId == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID"))
|
||||
return@put
|
||||
}
|
||||
val fight = EventFight.get(fightId)
|
||||
if (fight == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Fight not found"))
|
||||
return@put
|
||||
}
|
||||
val updateFight = call.receiveNullable<UpdateEventFight>()
|
||||
if (updateFight == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body"))
|
||||
return@put
|
||||
}
|
||||
|
||||
val teamBlue = updateFight.blueTeam ?: fight.teamBlue
|
||||
val teamRed = updateFight.redTeam ?: fight.teamRed
|
||||
val start = updateFight.start?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: fight.startTime
|
||||
val spielmodus = updateFight.spielmodus ?: fight.spielmodus
|
||||
val map = updateFight.map ?: fight.map
|
||||
val spectatePort = updateFight.spectatePort ?: fight.spectatePort
|
||||
|
||||
if (updateFight.group != null) {
|
||||
if (updateFight.group == "null") {
|
||||
Groups.resetGroup(fightId, true)
|
||||
} else {
|
||||
Groups.setGroup(fightId, updateFight.group)
|
||||
}
|
||||
}
|
||||
fight.update(start, spielmodus, map, teamBlue, teamRed, spectatePort)
|
||||
call.respond(HttpStatusCode.OK, ResponseEventFight(fight))
|
||||
}
|
||||
delete {
|
||||
val fightId = call.parameters["fight"]?.toIntOrNull()
|
||||
if (fightId == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid fight ID"))
|
||||
return@delete
|
||||
}
|
||||
val fight = EventFight.get(fightId)
|
||||
if (fight == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Fight not found"))
|
||||
return@delete
|
||||
}
|
||||
fight.delete()
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.ResponseError
|
||||
import de.steamwar.data.Groups
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.lang.StringBuilder
|
||||
import java.sql.Timestamp
|
||||
import java.time.Instant
|
||||
|
||||
@Serializable
|
||||
data class ShortEvent(val id: Int, val name: String, val start: Long, val end: Long) {
|
||||
constructor(event: Event) : this(event.eventID, event.eventName, event.start.time, event.end.time)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResponseEvent(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val deadline: Long,
|
||||
val start: Long,
|
||||
val end: Long,
|
||||
val maxTeamMembers: Int,
|
||||
val schemType: String?,
|
||||
val publicSchemsOnly: Boolean,
|
||||
val referees: List<ResponseUser>,
|
||||
) {
|
||||
constructor(event: Event) : this(
|
||||
event.eventID,
|
||||
event.eventName,
|
||||
event.deadline.time,
|
||||
event.start.time,
|
||||
event.end.time,
|
||||
event.maximumTeamMembers,
|
||||
event.schematicType?.toDB(),
|
||||
event.publicSchemsOnly(),
|
||||
Referee.get(event.eventID).map { ResponseUser(SteamwarUser.get(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ExtendedResponseEvent(
|
||||
val event: ResponseEvent,
|
||||
val teams: List<ResponseTeam>,
|
||||
val fights: List<ResponseEventFight>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateEvent(val name: String, val start: Long, val end: Long)
|
||||
|
||||
@Serializable
|
||||
data class UpdateEvent(
|
||||
val name: String? = null,
|
||||
val deadline: Long? = null,
|
||||
val start: Long? = null,
|
||||
val end: Long? = null,
|
||||
val maxTeamMembers: Int? = null,
|
||||
val schemType: String? = null,
|
||||
val publicSchemsOnly: Boolean? = null,
|
||||
val addReferee: Set<Int>? = null,
|
||||
val removeReferee: Set<Int>? = null,
|
||||
)
|
||||
|
||||
fun Route.configureEventsRoute() {
|
||||
route("/events") {
|
||||
install(SWPermissionCheck) {
|
||||
allowMethod(HttpMethod.Get)
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
get {
|
||||
call.respond(Event.getAll().map { ShortEvent(it) })
|
||||
}
|
||||
post {
|
||||
val createEvent = call.receiveNullable<CreateEvent>()
|
||||
if (createEvent == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body"))
|
||||
return@post
|
||||
}
|
||||
val event = Event.create(
|
||||
createEvent.name,
|
||||
Timestamp.from(Instant.ofEpochMilli(createEvent.start)),
|
||||
Timestamp.from(Instant.ofEpochMilli(createEvent.end))
|
||||
)
|
||||
call.respond(HttpStatusCode.Created, ResponseEvent(event))
|
||||
}
|
||||
route("/{id}") {
|
||||
get {
|
||||
val id = call.parameters["id"]?.toIntOrNull()
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID"))
|
||||
return@get
|
||||
}
|
||||
val event = Event.get(id)
|
||||
if (event == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
|
||||
return@get
|
||||
}
|
||||
call.respond(
|
||||
ExtendedResponseEvent(
|
||||
ResponseEvent(event),
|
||||
TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) },
|
||||
EventFight.getEvent(event.eventID).map { ResponseEventFight(it) })
|
||||
)
|
||||
}
|
||||
get("/teams") {
|
||||
val id = call.parameters["id"]?.toIntOrNull()
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID"))
|
||||
return@get
|
||||
}
|
||||
val event = Event.get(id)
|
||||
if (event == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
|
||||
return@get
|
||||
}
|
||||
call.respond(TeamTeilnahme.getTeams(event.eventID).map { ResponseTeam(it) })
|
||||
}
|
||||
get("/fights") {
|
||||
val id = call.parameters["id"]?.toIntOrNull()
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID"))
|
||||
return@get
|
||||
}
|
||||
val event = Event.get(id)
|
||||
if (event == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
|
||||
return@get
|
||||
}
|
||||
call.respond(EventFight.getEvent(event.eventID).map { ResponseEventFight(it) })
|
||||
}
|
||||
get("/csv") {
|
||||
val id = call.parameters["id"]?.toIntOrNull()
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID"))
|
||||
return@get
|
||||
}
|
||||
val event = Event.get(id)
|
||||
if (event == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
|
||||
return@get
|
||||
}
|
||||
|
||||
val fights = EventFight.getEvent(event.eventID)
|
||||
val csv = StringBuilder();
|
||||
csv.append(arrayOf("Start", "BlueTeam", "RedTeam", "WinnerTeam", "Group").joinToString(","))
|
||||
fights.forEach {
|
||||
csv.appendLine()
|
||||
val blue = Team.get(it.teamBlue)
|
||||
val red = Team.get(it.teamRed)
|
||||
val winner = when(it.ergebnis) {
|
||||
1 -> blue.teamName
|
||||
2 -> red.teamName
|
||||
3 -> "Tie"
|
||||
else -> "Unknown"
|
||||
}
|
||||
csv.append(
|
||||
arrayOf(
|
||||
it.startTime.toString(),
|
||||
Team.get(it.teamBlue).teamName,
|
||||
Team.get(it.teamRed).teamName,
|
||||
winner,
|
||||
Groups.getGroup(it.fightID)?.name ?: "Ungrouped"
|
||||
).joinToString(",")
|
||||
)
|
||||
}
|
||||
call.response.header("Content-Disposition", "attachment; filename=\"${event.eventName}.csv\"")
|
||||
call.response.header("Content-Type", "text/csv")
|
||||
call.response.header("Content-Transfer-Encoding", "binary")
|
||||
call.response.header("Pragma", "no-cache")
|
||||
call.respondText(csv.toString())
|
||||
}
|
||||
put {
|
||||
val id = call.parameters["id"]?.toIntOrNull()
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID"))
|
||||
return@put
|
||||
}
|
||||
val event = Event.get(id)
|
||||
if (event == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
|
||||
return@put
|
||||
}
|
||||
val updateEvent = call.receiveNullable<UpdateEvent>()
|
||||
if (updateEvent == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid body"))
|
||||
return@put
|
||||
}
|
||||
val eventName = updateEvent.name ?: event.eventName
|
||||
val deadline = updateEvent.deadline?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.deadline
|
||||
val start = updateEvent.start?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.start
|
||||
val end = updateEvent.end?.let { Timestamp.from(Instant.ofEpochMilli(it)) } ?: event.end
|
||||
val maxTeamMembers = updateEvent.maxTeamMembers ?: event.maximumTeamMembers
|
||||
|
||||
val schemType = if (updateEvent.schemType == "null") null else updateEvent.schemType?.let { SchematicType.fromDB(it) } ?: event.schematicType
|
||||
val publicSchemsOnly = updateEvent.publicSchemsOnly ?: event.publicSchemsOnly()
|
||||
|
||||
if (updateEvent.addReferee != null) {
|
||||
updateEvent.addReferee.forEach {
|
||||
Referee.add(event.eventID, it)
|
||||
}
|
||||
}
|
||||
|
||||
if (updateEvent.removeReferee != null) {
|
||||
updateEvent.removeReferee.forEach {
|
||||
Referee.remove(event.eventID, it)
|
||||
}
|
||||
}
|
||||
event.update(eventName, deadline, start, end, schemType, maxTeamMembers, publicSchemsOnly)
|
||||
call.respond(ResponseEvent(event))
|
||||
}
|
||||
delete {
|
||||
val id = call.parameters["id"]?.toIntOrNull()
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid ID"))
|
||||
return@delete
|
||||
}
|
||||
val event = Event.get(id)
|
||||
if (event == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ResponseError("Event not found"))
|
||||
return@delete
|
||||
}
|
||||
event.delete()
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.config
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.UserPerm
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.java.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.util.reflect.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import java.util.Base64
|
||||
|
||||
val pathPageIdMap = mutableMapOf<String, Int>()
|
||||
var pageId = 1
|
||||
|
||||
@Serializable
|
||||
data class Identity(val name: String, val email: String)
|
||||
|
||||
@Serializable
|
||||
data class PageResponseList(
|
||||
val path: String,
|
||||
val name: String,
|
||||
val sha: String,
|
||||
val downloadUrl: String,
|
||||
val id: Int
|
||||
) {
|
||||
constructor(res: JsonObject, id: Int) : this(
|
||||
res["path"]?.jsonPrimitive?.content!!,
|
||||
res["name"]?.jsonPrimitive?.content!!,
|
||||
res["sha"]?.jsonPrimitive?.content!!,
|
||||
res["download_url"]?.jsonPrimitive?.content!!,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PageResponse(
|
||||
val path: String,
|
||||
val name: String,
|
||||
val sha: String,
|
||||
val downloadUrl: String,
|
||||
val content: String,
|
||||
val size: Int,
|
||||
val id: Int,
|
||||
) {
|
||||
constructor(res: JsonObject, id: Int) : this(
|
||||
res["path"]?.jsonPrimitive?.content!!,
|
||||
res["name"]?.jsonPrimitive?.content!!,
|
||||
res["sha"]?.jsonPrimitive?.content!!,
|
||||
res["download_url"]?.jsonPrimitive?.content!!,
|
||||
res["content"]?.jsonPrimitive?.content!!,
|
||||
res["size"]?.jsonPrimitive?.int!!,
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CreatePageRequest(val path: String, val slug: String?, val title: String?)
|
||||
|
||||
@Serializable
|
||||
data class CreateBranchRequest(val branch: String)
|
||||
|
||||
@Serializable
|
||||
data class UpdatePageRequest(val content: String, val sha: String, val message: String)
|
||||
|
||||
@Serializable
|
||||
data class MergeBranchRequest(val branch: String, val message: String)
|
||||
|
||||
@Serializable
|
||||
data class DeletePageRequest(val sha: String, val message: String)
|
||||
|
||||
fun Route.configurePage() {
|
||||
val client = HttpClient(Java) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
defaultRequest {
|
||||
url("https://steamwar.de/devlabs/api/v1/")
|
||||
header("Authorization", "token " + config.giteaToken)
|
||||
}
|
||||
}
|
||||
|
||||
route("page") {
|
||||
install(SWPermissionCheck) {
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
get {
|
||||
val branch = call.request.queryParameters["branch"] ?: "master"
|
||||
val filesToCheck = mutableListOf("src/content")
|
||||
val files = mutableListOf<PageResponseList>()
|
||||
|
||||
while (filesToCheck.isNotEmpty()) {
|
||||
val path = filesToCheck.removeAt(0)
|
||||
val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch")
|
||||
val fileJson = Json.parseToJsonElement(res.bodyAsText())
|
||||
|
||||
if (fileJson is JsonArray) {
|
||||
fileJson.forEach {
|
||||
val obj = it.jsonObject
|
||||
if (obj["type"]?.jsonPrimitive?.content == "dir") {
|
||||
filesToCheck.add(obj["path"]?.jsonPrimitive?.content!!)
|
||||
} else if (obj["type"]?.jsonPrimitive?.content == "file" && (obj["name"]?.jsonPrimitive?.content?.endsWith(".md") == true || obj["name"]?.jsonPrimitive?.content?.endsWith(".json") == true)) {
|
||||
files.add(PageResponseList(obj, pathPageIdMap.computeIfAbsent(obj["path"]?.jsonPrimitive?.content!!) { pageId++ }))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files.add(PageResponseList(fileJson.jsonObject, pathPageIdMap.computeIfAbsent(fileJson.jsonObject["path"]?.jsonPrimitive?.content!!) { pageId++ }))
|
||||
}
|
||||
}
|
||||
|
||||
call.respond(files)
|
||||
}
|
||||
get("branch") {
|
||||
val res = client.get("repos/SteamWar/Website/branches")
|
||||
call.respond(res.status, Json.parseToJsonElement(res.bodyAsText()).jsonArray.map { it.jsonObject["name"]?.jsonPrimitive?.content!! })
|
||||
}
|
||||
post("branch") {
|
||||
@Serializable
|
||||
data class CreateGiteaBranchRequest(val new_branch_name: String, val old_branch_name: String)
|
||||
|
||||
val branch = call.receive<CreateBranchRequest>().branch
|
||||
val res = client.post("repos/SteamWar/Website/branches") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CreateGiteaBranchRequest(branch, "master"))
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class CreateGiteaMergeRequest(val base: String, val head: String, val title: String)
|
||||
|
||||
client.post("repos/SteamWar/Website/pulls") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CreateGiteaMergeRequest("master", branch, "Merge branch $branch"))
|
||||
}
|
||||
|
||||
call.respond(res.status)
|
||||
}
|
||||
delete("branch") {
|
||||
val branch = call.receive<CreateBranchRequest>().branch
|
||||
val res = client.delete("repos/SteamWar/Website/branches/$branch")
|
||||
call.respond(res.status)
|
||||
}
|
||||
post {
|
||||
@Serializable
|
||||
data class CreateGiteaPageRequest(val message: String, val content: String, val branch: String, val author: Identity)
|
||||
|
||||
val req = call.receive<CreatePageRequest>()
|
||||
if(req.path.startsWith("src/content/")) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Invalid path")
|
||||
return@post
|
||||
}
|
||||
val res = client.post("repos/SteamWar/Website/contents/src/content/${req.path}") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CreateGiteaPageRequest(
|
||||
"Create page ${req.path}",
|
||||
Base64.getEncoder().encodeToString("""
|
||||
---
|
||||
title: ${req.title ?: "[Enter Title]"}
|
||||
description: [Enter Description]
|
||||
slug: ${req.slug ?: "[Enter Slug]"}
|
||||
---
|
||||
|
||||
# ${req.path}
|
||||
""".trimIndent().toByteArray()),
|
||||
call.request.queryParameters["branch"] ?: "master",
|
||||
Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de"
|
||||
)))
|
||||
}
|
||||
call.respond(res.status)
|
||||
}
|
||||
get("{id}") {
|
||||
val id = call.parameters["id"]?.toIntOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
|
||||
val path = pathPageIdMap.entries.find { it.value == id }?.key ?: return@get call.respond(HttpStatusCode.NotFound, "Page not found")
|
||||
|
||||
val branch = call.request.queryParameters["branch"] ?: "master"
|
||||
val res = client.get("repos/SteamWar/Website/contents/$path?ref=$branch")
|
||||
val fileJson = Json.parseToJsonElement(res.bodyAsText())
|
||||
if (fileJson is JsonArray) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, "Invalid id")
|
||||
}
|
||||
|
||||
val file = PageResponse(fileJson.jsonObject, id)
|
||||
call.respond(file)
|
||||
}
|
||||
|
||||
delete("{id}") {
|
||||
val data = call.receive<DeletePageRequest>()
|
||||
|
||||
val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@delete call.respond(HttpStatusCode.NotFound, "Page not found")
|
||||
val branch = call.request.queryParameters["branch"] ?: "master"
|
||||
|
||||
@Serializable
|
||||
data class DeleteGiteaPageRequest(val sha: String, val message: String, val branch: String, val author: Identity)
|
||||
|
||||
val res = client.delete("repos/SteamWar/Website/contents/$path") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(DeleteGiteaPageRequest(data.sha, data.message, branch, Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de")))
|
||||
}
|
||||
|
||||
call.respond(res.status)
|
||||
}
|
||||
|
||||
put("{id}") {
|
||||
@Serializable
|
||||
data class UpdateGiteaPageRequest(val content: String, val sha: String, val message: String, val branch: String, val author: Identity)
|
||||
|
||||
val data = call.receive<UpdatePageRequest>()
|
||||
val path = pathPageIdMap.entries.find { it.value == call.parameters["id"]?.toIntOrNull() }?.key ?: return@put call.respond(HttpStatusCode.NotFound, "Page not found")
|
||||
|
||||
val res = client.put("repos/SteamWar/Website/contents/$path") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(UpdateGiteaPageRequest(data.content, data.sha, data.message, (call.request.queryParameters["branch"] ?: "master"), Identity(call.principal<SWAuthPrincipal>()!!.user.userName, "admin-tool@steamwar.de")))
|
||||
}
|
||||
|
||||
call.respond(res.status)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Application.configureRoutes() {
|
||||
routing {
|
||||
authenticate("sw-auth", optional = true) {
|
||||
configureEventsRoute()
|
||||
configureDataRoutes()
|
||||
configureEventFightRoutes()
|
||||
configureUserPerms()
|
||||
configureStats()
|
||||
configurePage()
|
||||
configureSchematic()
|
||||
configureAuthRoutes()
|
||||
configureUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.sql.NodeData
|
||||
import de.steamwar.sql.NodeDownload
|
||||
import de.steamwar.sql.NodeMember
|
||||
import de.steamwar.sql.SWException
|
||||
import de.steamwar.sql.SchematicNode
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.security.MessageDigest
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
data class ResponseSchematic(val name: String, val id: Int, val type: String?, val owner: Int, val item: String, val lastUpdate: Long, val rank: Int, val replaceColor: Boolean, val allowReplay: Boolean) {
|
||||
constructor(node: SchematicNode) : this(node.name, node.id, node.schemtype?.name(), node.owner, node.item, node.lastUpdate.time, node.rank, node.replaceColor(), node.allowReplay())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResponseSchematicLong(val members: List<ResponseUser>, val path: String, val schem: ResponseSchematic) {
|
||||
constructor(node: SchematicNode, path: String): this(NodeMember.getNodeMembers(node.id).map { ResponseUser.get(it.member) }, path, ResponseSchematic(node))
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResponseSchematicList(val breadcrumbs: List<ResponseBreadcrumb>, val schematics: List<ResponseSchematic>, val players: Map<Int, ResponseUser>) {
|
||||
constructor(schematics: List<ResponseSchematic>, breadcrumbs: List<ResponseBreadcrumb>) : this(breadcrumbs, schematics, schematics.map { it.owner }.distinct().map { ResponseUser.get(it) }.associateBy { it.id })
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResponseBreadcrumb(val name: String, val id: Int)
|
||||
|
||||
fun generateCode(): String {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val random = ByteArray(64).map { (0..255).random().toByte() }.toByteArray()
|
||||
val code = md.digest(random)
|
||||
|
||||
return code.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SchematicCode(val id: Int, val code: String, val expires: Long)
|
||||
|
||||
@Serializable
|
||||
data class UploadSchematic(val name: String, val content: String)
|
||||
|
||||
fun Route.configureSchematic() {
|
||||
route("/download/{code}") {
|
||||
get {
|
||||
val code = call.parameters["code"] ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@get
|
||||
}
|
||||
|
||||
val dl = NodeDownload.get(code) ?: run {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
dl.delete()
|
||||
|
||||
if(dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) {
|
||||
call.respond(HttpStatusCode.Gone)
|
||||
return@get
|
||||
}
|
||||
|
||||
val node = SchematicNode.getSchematicNode(dl.nodeId) ?: run {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
val user = call.principal<SWAuthPrincipal>()?.user
|
||||
if(user != null && !node.accessibleByUser(user)) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
SWException.log("User ${user.userName} tried to download schematic ${node.name} without permission", user.id.toString())
|
||||
return@get
|
||||
}
|
||||
|
||||
val data = NodeData.get(node) ?: run {
|
||||
call.respond(HttpStatusCode.InternalServerError)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.response.header("Content-Disposition", "attachment; filename=\"${node.name}.${if (data.nodeFormat) "schem" else "schematic"}\"")
|
||||
call.respondBytes(data.schemData().readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK)
|
||||
}
|
||||
get("/info") {
|
||||
val code = call.parameters["code"] ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@get
|
||||
}
|
||||
|
||||
val dl = NodeDownload.get(code) ?: run {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
val node = SchematicNode.getSchematicNode(dl.nodeId) ?: run {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(ResponseSchematic(node))
|
||||
}
|
||||
}
|
||||
route("/schem") {
|
||||
install(SWPermissionCheck)
|
||||
get {
|
||||
val user = call.principal<SWAuthPrincipal>()!!.user
|
||||
call.respond(ResponseSchematicList(SchematicNode.list(user, null).filter { it.name != "//copy" }.sortedWith { o1, o2 ->
|
||||
if (o1.isDir || o2.isDir) {
|
||||
o2.isDir.compareTo(o1.isDir)
|
||||
} else {
|
||||
o1.name.compareTo(o2.name)
|
||||
}
|
||||
}.map { ResponseSchematic(it) }, listOf()))
|
||||
}
|
||||
|
||||
post {
|
||||
val file = call.receive<UploadSchematic>()
|
||||
val schemName = file.name.substringBeforeLast(".")
|
||||
val schemType = file.name.substringAfterLast(".")
|
||||
|
||||
if (schemType != "schem" && schemType != "schematic") {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@post
|
||||
}
|
||||
|
||||
val user = call.principal<SWAuthPrincipal>()!!.user
|
||||
|
||||
val content = Base64.getDecoder().decode(file.content)
|
||||
var node = SchematicNode.getSchematicNode(user.id, schemName, 0)
|
||||
if (node == null) {
|
||||
node = SchematicNode.createSchematic(user.id, schemName, 0)
|
||||
}
|
||||
|
||||
val data = NodeData(node.id, false)
|
||||
data.saveFromStream(content.inputStream(), schemType == "schem")
|
||||
|
||||
call.respond(ResponseSchematic(node))
|
||||
}
|
||||
|
||||
route("/{id}") {
|
||||
get {
|
||||
val user = call.principal<SWAuthPrincipal>()!!.user
|
||||
val parentId = call.parameters["id"]?.toIntOrNull()
|
||||
if(parentId == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@get
|
||||
}
|
||||
|
||||
val parent = SchematicNode.getSchematicNode(parentId)
|
||||
|
||||
if(parent == null) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
if(!parent.accessibleByUser(user)) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(ResponseSchematicLong(parent, parent.generateBreadcrumbs(user)))
|
||||
}
|
||||
|
||||
get("/list") {
|
||||
val user = call.principal<SWAuthPrincipal>()!!.user
|
||||
val parentId = call.parameters["id"]?.toIntOrNull()
|
||||
if(parentId == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@get
|
||||
}
|
||||
|
||||
val parent = SchematicNode.getSchematicNode(parentId)
|
||||
|
||||
if(parent == null) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
return@get
|
||||
}
|
||||
|
||||
if(!parent.isDir) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@get
|
||||
}
|
||||
|
||||
if(!parent.accessibleByUser(user)) {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(ResponseSchematicList(SchematicNode.list(user, parent.id).filter { it.name != "//copy" }.sortedWith { o1, o2 ->
|
||||
if (o1.isDir || o2.isDir) {
|
||||
o2.isDir.compareTo(o1.isDir)
|
||||
} else {
|
||||
o1.name.compareTo(o2.name)
|
||||
}
|
||||
}.map { ResponseSchematic(it) }, parent.generateBreadcrumbsMap(user).map { ResponseBreadcrumb(it.key, it.value) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.plugins.SWAuthPrincipal
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.plugins.getUser
|
||||
import de.steamwar.sql.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStats(val eventFightParticipation: Int, val eventParticipation: Int, val acceptedSchematics: Int, val fights: Int, val playtime: Double) {
|
||||
constructor(user: SteamwarUser): this(
|
||||
getEventFightParticipation(user) ?: 0,
|
||||
getEventParticipation(user) ?: 0,
|
||||
getAcceptedSchematics(user) ?: 0,
|
||||
getFightCount(user) ?: 0,
|
||||
user.onlinetime / 3600.0
|
||||
)
|
||||
}
|
||||
|
||||
fun Route.configureStats() {
|
||||
route("/stats") {
|
||||
get("/ranked/{gamemode}") {
|
||||
val gamemode = call.parameters["gamemode"] ?: return@get call.respond(HttpStatusCode.NotFound)
|
||||
|
||||
@Serializable
|
||||
data class RankedUser(val name: String, val elo: Int)
|
||||
|
||||
call.respond(getRankedList(gamemode).map { RankedUser(it.first, it.second) })
|
||||
}
|
||||
get("/fights") {
|
||||
val list = getFightList()
|
||||
|
||||
@Serializable
|
||||
data class Fight(val date: String, val gamemode: String, val count: Int)
|
||||
|
||||
call.respond(list.map { Fight(it.first, it.second, it.third) })
|
||||
}
|
||||
route("/user/{id}") {
|
||||
install(SWPermissionCheck) {
|
||||
userCheck {
|
||||
val user = it.call.request.getUser()
|
||||
val auth = it.call.principal<SWAuthPrincipal>()
|
||||
|
||||
if (user == null || auth == null) {
|
||||
return@userCheck false
|
||||
}
|
||||
|
||||
return@userCheck user.id == auth.user.id || auth.user.hasPerm(UserPerm.MODERATION)
|
||||
}
|
||||
}
|
||||
get {
|
||||
val user = call.request.getUser()
|
||||
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.NotFound, "User not found")
|
||||
return@get
|
||||
}
|
||||
|
||||
call.respond(UserStats(user))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.sql.Team
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.configureTeamRoutes() {
|
||||
route("/team") {
|
||||
get {
|
||||
call.respond(Team.getAll().map { ResponseTeam(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.plugins.getUser
|
||||
import de.steamwar.sql.UserPerm
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.configureUser() {
|
||||
route("/user") {
|
||||
route("/{id}") {
|
||||
route("/admin") {
|
||||
install(SWPermissionCheck) {
|
||||
permission = UserPerm.ADMINISTRATION
|
||||
}
|
||||
|
||||
put("/password") {
|
||||
val user = call.request.getUser() ?: return@put call.respond(HttpStatusCode.NotFound)
|
||||
val password = call.receiveText()
|
||||
|
||||
if (password.isEmpty()) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Password too short")
|
||||
return@put
|
||||
}
|
||||
|
||||
user.setPassword(password)
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.routes
|
||||
|
||||
import de.steamwar.plugins.SWPermissionCheck
|
||||
import de.steamwar.plugins.getUser
|
||||
import de.steamwar.sql.SteamwarUser
|
||||
import de.steamwar.sql.UserPerm
|
||||
import de.steamwar.sql.UserPerm.Prefix
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RespondPrefix(val name: String, val colorCode: String, val chatPrefix: String)
|
||||
|
||||
@Serializable
|
||||
data class RespondUserPerms(val prefixes: Map<String, RespondPrefix>, val perms: List<String>)
|
||||
|
||||
@Serializable
|
||||
data class RespondUserPermsPrefix(val prefix: RespondPrefix, val perms: List<String>)
|
||||
|
||||
fun Route.configureUserPerms() {
|
||||
route("/perms") {
|
||||
install(SWPermissionCheck) {
|
||||
allowMethod(HttpMethod.Get)
|
||||
permission = UserPerm.MODERATION
|
||||
}
|
||||
get {
|
||||
val perms = mutableListOf<String>()
|
||||
val prefixes = mutableMapOf<String, RespondPrefix>()
|
||||
UserPerm.entries.forEach {
|
||||
if (it.name.startsWith("PREFIX_")) {
|
||||
val prefix = UserPerm.prefixes[it]!!
|
||||
prefixes[it.name] = RespondPrefix(it.name, prefix.colorCode, prefix.chatPrefix)
|
||||
} else {
|
||||
perms.add(it.name)
|
||||
}
|
||||
}
|
||||
|
||||
call.respond(RespondUserPerms(prefixes, perms))
|
||||
}
|
||||
route("/user/{id}") {
|
||||
install(SWPermissionCheck) {
|
||||
allowMethod(HttpMethod.Get)
|
||||
permission = UserPerm.MODERATION
|
||||
mustAuth = true
|
||||
}
|
||||
get {
|
||||
val user = call.request.getUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@get
|
||||
}
|
||||
val perms = mutableListOf<String>()
|
||||
var prefix = UserPerm.PREFIX_NONE
|
||||
user.perms().forEach {
|
||||
if (it.name.startsWith("PREFIX_")) {
|
||||
prefix = it
|
||||
} else {
|
||||
perms.add(it.name)
|
||||
}
|
||||
}
|
||||
|
||||
val prefixs = UserPerm.prefixes[prefix]!!
|
||||
|
||||
call.respond(RespondUserPermsPrefix(RespondPrefix(prefix.name, prefixs.colorCode, prefixs.chatPrefix), perms))
|
||||
}
|
||||
put("/prefix/{prefix}") {
|
||||
val user = call.request.getUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@put
|
||||
}
|
||||
|
||||
val prefix = call.parameters["prefix"]
|
||||
if (prefix == null || UserPerm.values().find { it.name == prefix } == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@put
|
||||
}
|
||||
user.perms().filter { it.name.startsWith("PREFIX_") }.forEach {
|
||||
UserPerm.removePerm(user, it)
|
||||
}
|
||||
|
||||
UserPerm.addPerm(user, UserPerm.values().find { it.name == prefix }!!)
|
||||
call.respond(HttpStatusCode.Accepted)
|
||||
}
|
||||
put("/{perm}") {
|
||||
val user = call.request.getUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@put
|
||||
}
|
||||
|
||||
|
||||
val perm = call.parameters["perm"]
|
||||
val permission = UserPerm.values().find { it.name == perm }
|
||||
if (perm == null || perm.startsWith("PREFIX_") || permission == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@put
|
||||
}
|
||||
|
||||
if (!user.hasPerm(permission)) {
|
||||
UserPerm.addPerm(user, permission)
|
||||
call.respond(HttpStatusCode.Accepted)
|
||||
return@put
|
||||
}
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
delete("/{perm}") {
|
||||
val user = call.request.getUser()
|
||||
if (user == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@delete
|
||||
}
|
||||
|
||||
val perm = call.parameters["perm"]
|
||||
val permission = UserPerm.values().find { it.name == perm }
|
||||
if (perm == null || perm.startsWith("PREFIX_") || permission == null) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
return@delete
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (user.hasPerm(permission)) {
|
||||
UserPerm.removePerm(user, permission)
|
||||
call.respond(HttpStatusCode.Accepted)
|
||||
return@delete
|
||||
}
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user