From b7d6476a700a75b18d562cc33d424bc21d5b2b62 Mon Sep 17 00:00:00 2001 From: Lars Westermann Date: Sun, 12 May 2019 16:40:21 +0200 Subject: [PATCH] Add repositories - Make model classes imutable - Move model classes to common module - Add repositories for backend and frontend - Backend repositories communicates with sqlite - Frontend repositories commuincates with rest api - Add rest api - Authentication via cookies - Add coroutines to forntend - Add change listener to repositories - Transmit change events to frontend repositories via websocket - Switch to server side sessions - Move setup from ui to cli --- .gitignore | 1 + build.gradle | 9 +- .../model => de/kif/common}/Constants.kt | 2 +- .../kotlin/de/kif/common/Message.kt | 35 ++ .../kotlin/de/kif/common/Repository.kt | 22 + .../kotlin/{ => de}/kif/common/model/Color.kt | 25 +- .../kotlin/de/kif/common/model/Language.kt | 6 + .../kotlin/de/kif/common/model/Model.kt | 3 + .../kotlin/de/kif/common}/model/Permission.kt | 2 +- .../kotlin/de/kif/common/model/Room.kt | 11 + .../kotlin/de/kif/common/model/Schedule.kt | 12 + .../kotlin/de/kif/common/model/Track.kt | 10 + .../kotlin/de/kif/common/model/User.kt | 16 + .../kotlin/de/kif/common/model/WorkGroup.kt | 15 + .../kotlin/kif/common/model/Message.kt | 58 --- .../kotlin/de/kif/frontend/WebSocketClient.kt | 76 ++++ .../de/kif/frontend/calendar/Calendar.kt | 11 - .../kotlin/de/kif/frontend/extensions.kt | 39 ++ src/jsMain/kotlin/de/kif/frontend/main.kt | 376 +----------------- .../kotlin/de/kif/frontend/repository/Ajax.kt | 106 +++++ .../kif/frontend/repository/RoomRepository.kt | 52 +++ .../frontend/repository/ScheduleRepository.kt | 52 +++ .../frontend/repository/TrackRepository.kt | 52 +++ .../kif/frontend/repository/UserRepository.kt | 52 +++ .../repository/WorkGroupRepository.kt | 52 +++ .../kotlin/de/kif/frontend/views/Calendar.kt | 359 +++++++++++++++++ .../de/westermann/kwebview/ClassList.kt | 18 + .../de/westermann/kwebview/Dimension.kt | 19 +- .../de/westermann/kwebview/extensions.kt | 42 +- .../kotlin/de/kif/backend/Application.kt | 201 ++-------- src/jvmMain/kotlin/de/kif/backend/Main.kt | 37 ++ src/jvmMain/kotlin/de/kif/backend/Security.kt | 125 ++++++ .../de/kif/backend/database/Connection.kt | 4 +- .../kotlin/de/kif/backend/database/Schema.kt | 77 +--- .../kotlin/de/kif/backend/model/Person.kt | 89 ----- .../de/kif/backend/model/PersonConstraint.kt | 132 ------ .../kotlin/de/kif/backend/model/Room.kt | 85 ---- .../kotlin/de/kif/backend/model/Schedule.kt | 99 ----- .../kotlin/de/kif/backend/model/Track.kt | 63 --- .../kotlin/de/kif/backend/model/User.kt | 110 ----- .../kotlin/de/kif/backend/model/WorkGroup.kt | 121 ------ .../kif/backend/model/WorkGroupConstraint.kt | 132 ------ .../kif/backend/repository/RoomRepository.kt | 93 +++++ .../backend/repository/ScheduleRepository.kt | 177 +++++++++ .../kif/backend/repository/TrackRepository.kt | 93 +++++ .../kif/backend/repository/UserRepository.kt | 120 ++++++ .../backend/repository/WorkGroupRepository.kt | 148 +++++++ .../kotlin/de/kif/backend/route/Account.kt | 19 +- .../kotlin/de/kif/backend/route/Calendar.kt | 164 ++++---- .../kotlin/de/kif/backend/route/Dashboard.kt | 5 +- .../kotlin/de/kif/backend/route/Login.kt | 43 +- .../kotlin/de/kif/backend/route/Person.kt | 258 ------------ .../kotlin/de/kif/backend/route/Room.kt | 101 +++-- .../kotlin/de/kif/backend/route/Setup.kt | 101 ----- .../kotlin/de/kif/backend/route/Track.kt | 100 ++--- .../kotlin/de/kif/backend/route/User.kt | 118 +++--- .../kotlin/de/kif/backend/route/WorkGroup.kt | 149 ++++--- .../de/kif/backend/route/api/Authenticate.kt | 61 +++ .../kotlin/de/kif/backend/route/api/Room.kt | 92 +++++ .../de/kif/backend/route/api/Schedule.kt | 121 ++++++ .../kotlin/de/kif/backend/route/api/Track.kt | 92 +++++ .../kotlin/de/kif/backend/route/api/User.kt | 99 +++++ .../de/kif/backend/route/api/WorkGroup.kt | 92 +++++ .../backend/{route => util}/PushService.kt | 20 +- .../de/kif/backend/view/MenuTemplate.kt | 78 ++-- src/jvmTest/resources/test.http | 38 ++ 66 files changed, 2854 insertions(+), 2336 deletions(-) rename src/commonMain/kotlin/{kif/common/model => de/kif/common}/Constants.kt (58%) create mode 100644 src/commonMain/kotlin/de/kif/common/Message.kt create mode 100644 src/commonMain/kotlin/de/kif/common/Repository.kt rename src/commonMain/kotlin/{ => de}/kif/common/model/Color.kt (83%) create mode 100644 src/commonMain/kotlin/de/kif/common/model/Language.kt create mode 100644 src/commonMain/kotlin/de/kif/common/model/Model.kt rename src/{jvmMain/kotlin/de/kif/backend => commonMain/kotlin/de/kif/common}/model/Permission.kt (73%) create mode 100644 src/commonMain/kotlin/de/kif/common/model/Room.kt create mode 100644 src/commonMain/kotlin/de/kif/common/model/Schedule.kt create mode 100644 src/commonMain/kotlin/de/kif/common/model/Track.kt create mode 100644 src/commonMain/kotlin/de/kif/common/model/User.kt create mode 100644 src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt delete mode 100644 src/commonMain/kotlin/kif/common/model/Message.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt delete mode 100644 src/jsMain/kotlin/de/kif/frontend/calendar/Calendar.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/extensions.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/views/Calendar.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/Security.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/model/Person.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/model/PersonConstraint.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/model/Room.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/model/Schedule.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/model/Track.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/model/User.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/model/WorkGroup.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/model/WorkGroupConstraint.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/repository/RoomRepository.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/repository/ScheduleRepository.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/repository/TrackRepository.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/repository/UserRepository.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/route/Person.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/route/Setup.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/api/Authenticate.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/api/Room.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/api/Schedule.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/api/Track.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/api/User.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/api/WorkGroup.kt rename src/jvmMain/kotlin/de/kif/backend/{route => util}/PushService.kt (66%) create mode 100644 src/jvmTest/resources/test.http diff --git a/.gitignore b/.gitignore index 77c01ba..43c1e65 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .idea/ build/ web/ +.sessions/ *.swp *.swo diff --git a/build.gradle b/build.gradle index fda923c..58d694d 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,6 @@ kotlin { compilations.all { kotlinOptions { freeCompilerArgs += [ - "-Xuse-experimental=io.ktor.locations.KtorExperimentalLocationsAPI", "-Xuse-experimental=io.ktor.util.KtorExperimentalAPI" ] } @@ -54,6 +53,7 @@ kotlin { commonMain { dependencies { implementation kotlin('stdlib-common') + implementation "de.westermann:KObserve-metadata:0.9.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version" } @@ -72,8 +72,9 @@ kotlin { implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "io.ktor:ktor-auth:$ktor_version" - implementation "io.ktor:ktor-locations:$ktor_version" + implementation "io.ktor:ktor-server-sessions:$ktor_version" implementation "io.ktor:ktor-websockets:$ktor_version" + implementation "io.ktor:ktor-jackson:$ktor_version" implementation 'org.jetbrains:kotlin-css-jvm:1.0.0-pre.70-kotlin-1.3.21' implementation "io.ktor:ktor-html-builder:$ktor_version" @@ -83,6 +84,8 @@ kotlin { implementation 'org.mindrot:jbcrypt:0.4' + implementation "de.westermann:KObserve-jvm:0.9.1" + api 'io.github.microutils:kotlin-logging:1.6.23' api 'ch.qos.logback:logback-classic:1.2.3' api 'org.fusesource.jansi:jansi:1.8' @@ -155,10 +158,12 @@ task run(type: JavaExec, dependsOn: [jvmMainClasses, jsJar]) { ] } args = [] + standardInput = System.in } clean.doFirst { delete webFolder + delete ".sessions" } task jar(type: ShadowJar, dependsOn: [jvmMainClasses, jsMainClasses, sass]) { diff --git a/src/commonMain/kotlin/kif/common/model/Constants.kt b/src/commonMain/kotlin/de/kif/common/Constants.kt similarity index 58% rename from src/commonMain/kotlin/kif/common/model/Constants.kt rename to src/commonMain/kotlin/de/kif/common/Constants.kt index 2c87c39..02ebb40 100644 --- a/src/commonMain/kotlin/kif/common/model/Constants.kt +++ b/src/commonMain/kotlin/de/kif/common/Constants.kt @@ -1,3 +1,3 @@ -package kif.common.model +package de.kif.common const val CALENDAR_GRID_WIDTH = 15 \ No newline at end of file diff --git a/src/commonMain/kotlin/de/kif/common/Message.kt b/src/commonMain/kotlin/de/kif/common/Message.kt new file mode 100644 index 0000000..5eb4276 --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/Message.kt @@ -0,0 +1,35 @@ +package de.kif.common + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule + +@Serializable +data class Message( + val type: MessageType, + val repository: RepositoryType, + val id: Long +) { + + fun stringify(): String { + return json.stringify(serializer(), this) + } + + companion object { + private val jsonContext = SerializersModule {} + + val json = Json(context = jsonContext) + + fun parse(data: String): Message { + return json.parse(serializer(), data) + } + } +} + +enum class MessageType { + CREATE, UPDATE, DELETE +} + +enum class RepositoryType { + ROOM, SCHEDULE, TRACK, USER, WORK_GROUP +} diff --git a/src/commonMain/kotlin/de/kif/common/Repository.kt b/src/commonMain/kotlin/de/kif/common/Repository.kt new file mode 100644 index 0000000..ca85efa --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/Repository.kt @@ -0,0 +1,22 @@ +package de.kif.common + +import de.kif.common.model.Model +import de.westermann.kobserve.event.EventHandler + + +interface Repository { + + suspend fun get(id: Long): T? + + suspend fun create(model: T): Long + + suspend fun update(model: T) + + suspend fun delete(id: Long) + + suspend fun all(): List + + val onCreate: EventHandler + val onUpdate: EventHandler + val onDelete: EventHandler +} diff --git a/src/commonMain/kotlin/kif/common/model/Color.kt b/src/commonMain/kotlin/de/kif/common/model/Color.kt similarity index 83% rename from src/commonMain/kotlin/kif/common/model/Color.kt rename to src/commonMain/kotlin/de/kif/common/model/Color.kt index cc073e4..8b45070 100644 --- a/src/commonMain/kotlin/kif/common/model/Color.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Color.kt @@ -1,7 +1,6 @@ -package kif.common.model +package de.kif.common.model import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient @Serializable data class Color( @@ -21,25 +20,15 @@ data class Color( } } - @Transient - val redDouble - get() = red / 255.0 + fun calcRedDouble() = red / 255.0 - @Transient - val greenDouble - get() = green / 255.0 + fun calcGreenDouble() = green / 255.0 - @Transient - val blueDouble - get() = blue / 255.0 + fun calcBlueDouble() = blue / 255.0 - @Transient - val luminance: Double - get() = 0.2126 * redDouble + 0.7152 * greenDouble + 0.0722 * blueDouble + fun calcLuminance(): Double = 0.2126 * calcRedDouble() + 0.7152 * calcGreenDouble() + 0.0722 * calcBlueDouble() - @Transient - val textColor: Color - get() = if (luminance < 0.7) WHITE else BLACK + fun calcTextColor(): Color = if (calcLuminance() < 0.7) WHITE else BLACK companion object { fun parse(color: String): Color { @@ -94,3 +83,5 @@ data class Color( ) } } + +fun String.parseColor() = Color.parse(this) diff --git a/src/commonMain/kotlin/de/kif/common/model/Language.kt b/src/commonMain/kotlin/de/kif/common/model/Language.kt new file mode 100644 index 0000000..cd7707e --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/Language.kt @@ -0,0 +1,6 @@ +package de.kif.common.model + +enum class Language(val code: String, val localeName: String) { + GERMAN("DE", "Deutsch"), + ENGLISH("EN", "English") +} \ No newline at end of file diff --git a/src/commonMain/kotlin/de/kif/common/model/Model.kt b/src/commonMain/kotlin/de/kif/common/model/Model.kt new file mode 100644 index 0000000..779afd2 --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/Model.kt @@ -0,0 +1,3 @@ +package de.kif.common.model + +interface Model \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Permission.kt b/src/commonMain/kotlin/de/kif/common/model/Permission.kt similarity index 73% rename from src/jvmMain/kotlin/de/kif/backend/model/Permission.kt rename to src/commonMain/kotlin/de/kif/common/model/Permission.kt index 67b0ca6..ac5cedd 100644 --- a/src/jvmMain/kotlin/de/kif/backend/model/Permission.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Permission.kt @@ -1,4 +1,4 @@ -package de.kif.backend.model +package de.kif.common.model enum class Permission { USER, SCHEDULE, WORK_GROUP, ROOM, PERSON, ADMIN diff --git a/src/commonMain/kotlin/de/kif/common/model/Room.kt b/src/commonMain/kotlin/de/kif/common/model/Room.kt new file mode 100644 index 0000000..dda4e61 --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/Room.kt @@ -0,0 +1,11 @@ +package de.kif.common.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Room( + val id: Long? = null, + val name: String, + val places: Int, + val projector: Boolean +) : Model diff --git a/src/commonMain/kotlin/de/kif/common/model/Schedule.kt b/src/commonMain/kotlin/de/kif/common/model/Schedule.kt new file mode 100644 index 0000000..ec568cf --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/Schedule.kt @@ -0,0 +1,12 @@ +package de.kif.common.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Schedule( + val id: Long?, + val workGroup: WorkGroup, + val room: Room, + val day: Int, + val time: Int +) : Model diff --git a/src/commonMain/kotlin/de/kif/common/model/Track.kt b/src/commonMain/kotlin/de/kif/common/model/Track.kt new file mode 100644 index 0000000..a671ef6 --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/Track.kt @@ -0,0 +1,10 @@ +package de.kif.common.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Track( + val id: Long?, + var name: String, + var color: Color +) : Model diff --git a/src/commonMain/kotlin/de/kif/common/model/User.kt b/src/commonMain/kotlin/de/kif/common/model/User.kt new file mode 100644 index 0000000..5d86bd8 --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/User.kt @@ -0,0 +1,16 @@ +package de.kif.common.model + +import kotlinx.serialization.Serializable + +@Serializable +data class User( + val id: Long?, + val username: String, + val password: String, + val permissions: Set +) : Model { + + fun checkPermission(permission: Permission): Boolean { + return permission in permissions || Permission.ADMIN in permissions + } +} diff --git a/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt b/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt new file mode 100644 index 0000000..c4767fa --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt @@ -0,0 +1,15 @@ +package de.kif.common.model + +import kotlinx.serialization.Serializable + +@Serializable +data class WorkGroup( + val id: Long?, + val name: String, + val interested: Int, + val track: Track?, + val projector: Boolean, + val resolution: Boolean, + val length: Int, + val language: Language +) : Model diff --git a/src/commonMain/kotlin/kif/common/model/Message.kt b/src/commonMain/kotlin/kif/common/model/Message.kt deleted file mode 100644 index 1e87a84..0000000 --- a/src/commonMain/kotlin/kif/common/model/Message.kt +++ /dev/null @@ -1,58 +0,0 @@ -package kif.common.model - -import kotlinx.serialization.Polymorphic -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.SerializersModule - -@Serializable -data class Message( - @Polymorphic - val message: MessageType -) { - - fun stringify(): String = Companion.stringify(this) - - companion object { - - private val module = SerializersModule { - polymorphic(MessageType::class) { - MessageCreateCalendarEntry::class with MessageCreateCalendarEntry.serializer() - MessageDeleteCalendarEntry::class with MessageDeleteCalendarEntry.serializer() - } - } - - private val json = Json(context = module) - - fun stringify(message: Message): String { - return json.stringify(serializer(), message) - } - - fun parse(message: String): Message { - return json.parse(serializer(), message) - } - } -} - -abstract class MessageType() - -@Serializable -data class MessageCreateCalendarEntry( - val day: Int, - val time: Int, - val cellTime: Int, - val room: Int, - val workGroupId: Int, - val workGroupName: String, - val workGroupLength: Int, - val workGroupLanguage: String, - val workGroupColor: Color? = null -) : MessageType() - -@Serializable -data class MessageDeleteCalendarEntry( - val day: Int, - val time: Int, - val roomId: Int, - val workGroupId: Int -) : MessageType() diff --git a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt new file mode 100644 index 0000000..5839f85 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt @@ -0,0 +1,76 @@ +package de.kif.frontend + +import de.kif.common.Message +import de.kif.common.MessageType +import de.kif.common.RepositoryType +import de.kif.frontend.repository.* +import de.westermann.kwebview.async +import org.w3c.dom.MessageEvent +import org.w3c.dom.WebSocket +import org.w3c.dom.events.Event +import kotlin.browser.window + +class WebSocketClient() { + private val url = "ws://${window.location.host}/" + + private lateinit var ws: WebSocket + + @Suppress("UNUSED_PARAMETER") + private fun onOpen(event: Event) { + console.log("Connected!") + } + + private fun onMessage(messageEvent: MessageEvent) { + val message = Message.parse(messageEvent.data?.toString() ?: "") + + for (handler in messageHandlers) { + if (handler.repository == message.repository) { + when (message.type) { + MessageType.CREATE -> handler.onCreate(message.id) + MessageType.UPDATE -> handler.onUpdate(message.id) + MessageType.DELETE -> handler.onDelete(message.id) + } + } + } + } + + @Suppress("UNUSED_PARAMETER") + private fun onClose(event: Event) { + console.log("Disconnected!") + async(1000) { + connect() + } + } + + @Suppress("UNUSED_PARAMETER") + private fun onError(event: Event) { + console.log("An error occurred!") + } + + private fun connect() { + ws = WebSocket(url) + + ws.onopen = this::onOpen + ws.onmessage = this::onMessage + ws.onclose = this::onClose + ws.onerror = this::onError + } + + private val messageHandlers: List = listOf( + RoomRepository.handler, + ScheduleRepository.handler, + TrackRepository.handler, + UserRepository.handler, + WorkGroupRepository.handler + ) + + init { + connect() + } +} + +abstract class MessageHandler(val repository: RepositoryType) { + abstract fun onCreate(id: Long) + abstract fun onUpdate(id: Long) + abstract fun onDelete(id: Long) +} diff --git a/src/jsMain/kotlin/de/kif/frontend/calendar/Calendar.kt b/src/jsMain/kotlin/de/kif/frontend/calendar/Calendar.kt deleted file mode 100644 index 3a3f790..0000000 --- a/src/jsMain/kotlin/de/kif/frontend/calendar/Calendar.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.kif.frontend.calendar - -import de.westermann.kwebview.View -import de.westermann.kwebview.ViewCollection -import de.westermann.kwebview.createHtmlView - -class Calendar : ViewCollection(createHtmlView()) { - init { - - } -} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/extensions.kt b/src/jsMain/kotlin/de/kif/frontend/extensions.kt new file mode 100644 index 0000000..d07fa12 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/extensions.kt @@ -0,0 +1,39 @@ +package de.kif.frontend + +import org.w3c.dom.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.startCoroutine + + +operator fun HTMLCollection.iterator() = object : Iterator { + private var index = 0 + override fun hasNext(): Boolean { + return index < this@iterator.length + } + + override fun next(): HTMLElement { + return this@iterator.get(index++) as HTMLElement + } + +} + +operator fun NodeList.iterator() = object : Iterator { + private var index = 0 + override fun hasNext(): Boolean { + return index < this@iterator.length + } + + override fun next(): Node { + return this@iterator.get(index++)!! + } + +} + +fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) = + block.startCoroutine(Continuation(context) { result -> + result.onFailure { exception -> + console.error(exception) + } + }) \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt index 6a99087..8207573 100644 --- a/src/jsMain/kotlin/de/kif/frontend/main.kt +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -1,381 +1,13 @@ package de.kif.frontend -import de.westermann.kobserve.property.mapBinding -import de.westermann.kwebview.* -import de.westermann.kwebview.components.Body +import de.kif.frontend.views.initCalendar import de.westermann.kwebview.components.init -import kif.common.model.* -import org.w3c.dom.* -import org.w3c.dom.events.EventListener -import org.w3c.dom.events.MouseEvent import kotlin.browser.document -import kotlin.browser.window -import kotlin.collections.Iterator -import kotlin.collections.List -import kotlin.collections.emptyList -import kotlin.collections.filter -import kotlin.collections.find -import kotlin.collections.forEach -import kotlin.collections.listOf -import kotlin.collections.minus -import kotlin.collections.plus -import kotlin.dom.appendText - -class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) { - - init { - var linkM10: HTMLAnchorElement? = null - var linkM5: HTMLAnchorElement? = null - var linkReset: HTMLAnchorElement? = null - var linkP5: HTMLAnchorElement? = null - var linkP10: HTMLAnchorElement? = null - var linkDel: HTMLAnchorElement? = null - - for (element in html.children) { - when { - element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement - element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement - } - } - - linkM10 = linkM10 ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-m10") - link.textContent = "-10" - html.appendChild(link) - link - } - linkM10.removeAttribute("href") - linkM10.addEventListener("click", EventListener { - classList += "pending" - get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") { - get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 10}/${entry.workGroup}") { - println("success") - } - } - }) - - linkM5 = linkM5 ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-m5") - link.textContent = "-5" - html.appendChild(link) - link - } - linkM5.removeAttribute("href") - linkM5.addEventListener("click", EventListener { - classList += "pending" - get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") { - get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 5}/${entry.workGroup}") { - println("success") - } - } - }) - - linkReset = linkReset ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-reset") - link.textContent = "reset" - html.appendChild(link) - link - } - linkReset.removeAttribute("href") - linkReset.addEventListener("click", EventListener { - classList += "pending" - get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") { - get("/calendar/${entry.day}/${entry.room}/${entry.cellTime}/${entry.workGroup}") { - println("success") - } - } - }) - - linkP5 = linkP5 ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-p5") - link.textContent = "+5" - html.appendChild(link) - link - } - linkP5.removeAttribute("href") - linkP5.addEventListener("click", EventListener { - classList += "pending" - get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") { - get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 5}/${entry.workGroup}") { - println("success") - } - } - }) - - linkP10 = linkP10 ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-p10") - link.textContent = "+10" - html.appendChild(link) - link - } - linkP10.removeAttribute("href") - linkP10.addEventListener("click", EventListener { - classList += "pending" - get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") { - get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 10}/${entry.workGroup}") { - println("success") - } - } - }) - - linkDel = linkDel ?: run { - val link = createHtmlView() - link.classList.add("calendar-tools-del") - link.textContent = "del" - html.appendChild(link) - link - } - linkDel.removeAttribute("href") - linkDel.addEventListener("click", EventListener { - classList += "pending" - get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") { - println("success") - } - }) - } -} - -class CalendarEntry(view: HTMLElement) : View(view) { - - private lateinit var mouseDelta: Point - private var newCell: CalendarCell? = null - - - var day by dataset.property("day") - val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 } - - var time by dataset.property("time") - val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 } - - var room by dataset.property("room") - val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 } - - var cellTime by dataset.property("cellTime") - var language by dataset.property("language") - - var workGroup by dataset.property("workgroup") - val workGroupId by dataset.property("workgroup").mapBinding { it?.toIntOrNull() ?: 0 } - private fun onMove(event: MouseEvent) { - val position = event.toPoint() - mouseDelta - - newCell?.classList?.remove("drag") - - val cell = calendarCells.find { - position in it.dimension - } - - if (cell != null) { - cell.classList.add("drag") - cell += this - newCell = cell - } - - event.preventDefault() - event.stopPropagation() - } - - private fun onFinishMove(event: MouseEvent) { - classList -= "drag" - - newCell?.let { cell -> - cell.classList -= "drag" - - val newTime = cell.time - val newRoom = cell.room - val day = - (document.getElementsByClassName("calendar")[0] as? HTMLElement)?.dataset?.get("day")?.toIntOrNull() - ?: 0 - - classList += "pending" - get("/calendar/$day/$room/$time/-1") { - get("/calendar/$day/$newRoom/$newTime/$workGroup") { - println("success") - } - } - } - newCell = null - - for (it in listeners) { - it.detach() - } - listeners = emptyList() - - event.preventDefault() - event.stopPropagation() - } - - private var listeners: List> = emptyList() - - init { - onMouseDown { event -> - if (event.target != html || "pending" in classList) { - event.stopPropagation() - return@onMouseDown - } - - classList += "drag" - - mouseDelta = event.toPoint() - point - - listeners = listOf( - Body.onMouseMove.reference(this::onMove), - Body.onMouseUp.reference(this::onFinishMove), - Body.onMouseLeave.reference(this::onFinishMove) - ) - - event.preventDefault() - event.stopPropagation() - } - - var calendarTools: CalendarTools? = null - for (item in html.children) { - if (item.classList.contains("calendar-tools")) { - calendarTools = CalendarTools(this, item) - break - } - } - if (calendarTools == null) { - calendarTools = CalendarTools(this, createHtmlView()) - html.appendChild(calendarTools.html) - } - } - - companion object { - fun create( - day: Int, - time: Int, - cellTime: Int, - room: Int, - workGroupId: Int, - workGroupName: String, - workGroupLength: Int, - workGroupLanguage: String, - workGroupColor: Color? - ): CalendarEntry { - val entry = CalendarEntry(createHtmlView()) - - entry.day = day.toString() - entry.time = time.toString() - entry.cellTime = cellTime.toString() - entry.room = room.toString() - entry.workGroup = workGroupId.toString() - entry.language = workGroupLanguage - if (workGroupColor != null) { - - entry.style { - val size = workGroupLength / CALENDAR_GRID_WIDTH.toDouble() - val pos = (time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble() - - val pct = "${pos * 100}%" - val sz = "${size * 100}%" - - left = pct - top = "calc($pct + 0.1rem)" - - width = sz - height = "calc($sz - 0.2rem)" - - backgroundColor = workGroupColor.toString() - color = workGroupColor.textColor.toString() - } - } - entry.html.appendText(workGroupName) - - return entry - } - } -} - -class CalendarCell(view: HTMLElement) : ViewCollection(view) { - var day by dataset.property("day") - val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 } - - var time by dataset.property("time") - val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 } - - var room by dataset.property("room") - val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 } -} - -var calendarEntries: List = emptyList() -var calendarCells: List = emptyList() fun main() = init { + WebSocketClient() - val ws = WebSocket("ws://${window.location.host}/".also { println(it) }) - - ws.onmessage = { - val messageWrapper = Message.parse(it.data?.toString() ?: "") - val message = messageWrapper.message - - println(message) - - when (message) { - is MessageCreateCalendarEntry -> { - val entry = CalendarEntry.create( - message.day, - message.time, - message.cellTime, - message.room, - message.workGroupId, - message.workGroupName, - message.workGroupLength, - message.workGroupLanguage, - message.workGroupColor - ) - for (cell in calendarCells) { - if (cell.dayId == message.day && cell.timeId == message.cellTime && cell.roomId == message.room) { - cell.html.appendChild(entry.html) - calendarEntries += entry - break - } - } - } - is MessageDeleteCalendarEntry -> { - val remove = calendarEntries.filter { entry -> - entry.dayId == message.day && - entry.timeId == message.time && - entry.roomId == message.roomId && - entry.workGroupId == message.workGroupId - } - calendarEntries -= remove - remove.forEach { - it.html.remove() - } - } - else -> { - } - } + if (document.getElementsByClassName("calendar").length > 0) { + initCalendar() } - - ws.onopen = { - console.log("yes!") - } - - calendarEntries = document.getElementsByClassName("calendar-entry") - .iterator().asSequence().map(::CalendarEntry).toList() - - calendarCells = document.getElementsByClassName("calendar-cell") - .iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList() } - -operator fun HTMLCollection.iterator() = object : Iterator { - private var index = 0 - override fun hasNext(): Boolean { - return index < this@iterator.length - } - - override fun next(): HTMLElement { - return this@iterator.get(index++) as HTMLElement - } - -} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt b/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt new file mode 100644 index 0000000..f113d8c --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt @@ -0,0 +1,106 @@ +package de.kif.frontend.repository + +import de.kif.common.Repository +import de.kif.common.model.Model +import org.w3c.xhr.XMLHttpRequest +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.js.Promise + +suspend fun repositoryGet( + url: String +): dynamic { + console.log("GET: $url") + val promise = Promise { resolve, reject -> + val xhttp = XMLHttpRequest() + + xhttp.onreadystatechange = { + if (xhttp.readyState == 4.toShort()) { + if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) { + resolve(JSON.parse(xhttp.responseText)) + } else { + reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}")) + } + } + } + xhttp.open("GET", url, true) + + xhttp.send() + } + + try { + val d = promise.await() + + return if (d.OK) { + d.data + } else { + null + } + } catch (e: NoSuchElementException) { + console.error(e) + + return null + } +} + +suspend fun repositoryPost( + url: String, + data: String? = null +): dynamic { + console.log("POST: $url", data) + val promise = Promise { resolve, reject -> + val xhttp = XMLHttpRequest() + + xhttp.onreadystatechange = { + if (xhttp.readyState == 4.toShort()) { + if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) { + resolve(JSON.parse(xhttp.responseText)) + } else { + reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}")) + } + } + } + xhttp.open("POST", url, true) + xhttp.setRequestHeader("Content-type", "application/json"); + + xhttp.send(data) + } + + try { + val d = promise.await() + + return if (d.OK) { + d.data + } else { + null + } + } catch (e: NoSuchElementException) { + console.error(e) + + return null + } +} + +suspend fun Promise.await(): T = suspendCoroutine { cont -> + then({ cont.resume(it) }, { cont.resumeWithException(it) }) +} + +class RepositoryDelegate( + private val repository: Repository, + private val id: Long +) { + + private var backing: T? = null + + suspend fun get(): T { + if (backing == null) { + backing = repository.get(id) ?: throw NoSuchElementException() + } + return backing!! + } + + fun set(value: T) { + backing = value + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt new file mode 100644 index 0000000..6920c3b --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt @@ -0,0 +1,52 @@ +package de.kif.frontend.repository + +import de.kif.common.Message +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.Room +import de.kif.frontend.MessageHandler +import de.westermann.kobserve.event.EventHandler +import kotlinx.serialization.DynamicObjectParser +import kotlinx.serialization.list + +object RoomRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private val parser = DynamicObjectParser() + + override suspend fun get(id: Long): Room? { + val json = repositoryGet("/api/room/$id") ?: return null + return parser.parse(json, Room.serializer()) + } + + override suspend fun create(model: Room): Long { + return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))?.toLong() + ?: throw IllegalStateException("Cannot create model!") + } + + override suspend fun update(model: Room) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + repositoryPost("/api/room/${model.id}", Message.json.stringify(Room.serializer(), model)) + } + + override suspend fun delete(id: Long) { + repositoryPost("/api/room/$id/delete") + } + + override suspend fun all(): List { + val json = repositoryGet("/api/rooms") ?: return emptyList() + return parser.parse(json, Room.serializer().list) + } + + val handler = object : MessageHandler(RepositoryType.ROOM) { + + override fun onCreate(id: Long) = onCreate.emit(id) + + override fun onUpdate(id: Long) = onUpdate.emit(id) + + override fun onDelete(id: Long) = onDelete.emit(id) + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt new file mode 100644 index 0000000..6217fcc --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt @@ -0,0 +1,52 @@ +package de.kif.frontend.repository + +import de.kif.common.Message +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.Schedule +import de.kif.frontend.MessageHandler +import de.westermann.kobserve.event.EventHandler +import kotlinx.serialization.DynamicObjectParser +import kotlinx.serialization.list + +object ScheduleRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private val parser = DynamicObjectParser() + + override suspend fun get(id: Long): Schedule? { + val json = repositoryGet("/api/schedule/$id") ?: return null + return parser.parse(json, Schedule.serializer()) + } + + override suspend fun create(model: Schedule): Long { + return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))?.toLong() + ?: throw IllegalStateException("Cannot create model!") + } + + override suspend fun update(model: Schedule) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + repositoryPost("/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model)) + } + + override suspend fun delete(id: Long) { + repositoryPost("/api/schedule/$id/delete") + } + + override suspend fun all(): List { + val json = repositoryGet("/api/schedules") ?: return emptyList() + return parser.parse(json, Schedule.serializer().list) + } + + val handler = object : MessageHandler(RepositoryType.SCHEDULE) { + + override fun onCreate(id: Long) = onCreate.emit(id) + + override fun onUpdate(id: Long) = onUpdate.emit(id) + + override fun onDelete(id: Long) = onDelete.emit(id) + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt new file mode 100644 index 0000000..5adaa61 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt @@ -0,0 +1,52 @@ +package de.kif.frontend.repository + +import de.kif.common.Message +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.Track +import de.kif.frontend.MessageHandler +import de.westermann.kobserve.event.EventHandler +import kotlinx.serialization.DynamicObjectParser +import kotlinx.serialization.list + +object TrackRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private val parser = DynamicObjectParser() + + override suspend fun get(id: Long): Track? { + val json = repositoryGet("/api/track/$id") ?: return null + return parser.parse(json, Track.serializer()) + } + + override suspend fun create(model: Track): Long { + return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))?.toLong() + ?: throw IllegalStateException("Cannot create model!") + } + + override suspend fun update(model: Track) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + repositoryPost("/api/track/${model.id}", Message.json.stringify(Track.serializer(), model)) + } + + override suspend fun delete(id: Long) { + repositoryPost("/api/track/$id/delete") + } + + override suspend fun all(): List { + val json = repositoryGet("/api/tracks") ?: return emptyList() + return parser.parse(json, Track.serializer().list) + } + + val handler = object : MessageHandler(RepositoryType.TRACK) { + + override fun onCreate(id: Long) = onCreate.emit(id) + + override fun onUpdate(id: Long) = onUpdate.emit(id) + + override fun onDelete(id: Long) = onDelete.emit(id) + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt new file mode 100644 index 0000000..b837141 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt @@ -0,0 +1,52 @@ +package de.kif.frontend.repository + +import de.kif.common.Message +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.User +import de.kif.frontend.MessageHandler +import de.westermann.kobserve.event.EventHandler +import kotlinx.serialization.DynamicObjectParser +import kotlinx.serialization.list + +object UserRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private val parser = DynamicObjectParser() + + override suspend fun get(id: Long): User? { + val json = repositoryGet("/api/user/$id") ?: return null + return parser.parse(json, User.serializer()) + } + + override suspend fun create(model: User): Long { + return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))?.toLong() + ?: throw IllegalStateException("Cannot create model!") + } + + override suspend fun update(model: User) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + repositoryPost("/api/user/${model.id}", Message.json.stringify(User.serializer(), model)) + } + + override suspend fun delete(id: Long) { + repositoryPost("/api/user/$id/delete") + } + + override suspend fun all(): List { + val json = repositoryGet("/api/users") ?: return emptyList() + return parser.parse(json, User.serializer().list) + } + + val handler = object : MessageHandler(RepositoryType.USER) { + + override fun onCreate(id: Long) = onCreate.emit(id) + + override fun onUpdate(id: Long) = onUpdate.emit(id) + + override fun onDelete(id: Long) = onDelete.emit(id) + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt new file mode 100644 index 0000000..1b85517 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt @@ -0,0 +1,52 @@ +package de.kif.frontend.repository + +import de.kif.common.Message +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.WorkGroup +import de.kif.frontend.MessageHandler +import de.westermann.kobserve.event.EventHandler +import kotlinx.serialization.DynamicObjectParser +import kotlinx.serialization.list + +object WorkGroupRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private val parser = DynamicObjectParser() + + override suspend fun get(id: Long): WorkGroup? { + val json = repositoryGet("/api/workgroup/$id") ?: return null + return parser.parse(json, WorkGroup.serializer()) + } + + override suspend fun create(model: WorkGroup): Long { + return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))?.toLong() + ?: throw IllegalStateException("Cannot create model!") + } + + override suspend fun update(model: WorkGroup) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + repositoryPost("/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model)) + } + + override suspend fun delete(id: Long) { + repositoryPost("/api/workgroup/$id/delete") + } + + override suspend fun all(): List { + val json = repositoryGet("/api/workgroups") ?: return emptyList() + return parser.parse(json, WorkGroup.serializer().list) + } + + val handler = object : MessageHandler(RepositoryType.WORK_GROUP) { + + override fun onCreate(id: Long) = onCreate.emit(id) + + override fun onUpdate(id: Long) = onUpdate.emit(id) + + override fun onDelete(id: Long) = onDelete.emit(id) + } +} diff --git a/src/jsMain/kotlin/de/kif/frontend/views/Calendar.kt b/src/jsMain/kotlin/de/kif/frontend/views/Calendar.kt new file mode 100644 index 0000000..380cd17 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/Calendar.kt @@ -0,0 +1,359 @@ +package de.kif.frontend.views + +import de.kif.common.CALENDAR_GRID_WIDTH +import de.kif.common.model.Room +import de.kif.common.model.Schedule +import de.kif.frontend.iterator +import de.kif.frontend.launch +import de.kif.frontend.repository.RepositoryDelegate +import de.kif.frontend.repository.RoomRepository +import de.kif.frontend.repository.ScheduleRepository +import de.westermann.kwebview.* +import de.westermann.kwebview.components.Body +import org.w3c.dom.HTMLAnchorElement +import org.w3c.dom.HTMLElement +import org.w3c.dom.events.EventListener +import org.w3c.dom.events.MouseEvent +import org.w3c.dom.get +import kotlin.browser.document +import kotlin.dom.appendText +import kotlin.dom.isText + +class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) { + + init { + var linkM10: HTMLAnchorElement? = null + var linkM5: HTMLAnchorElement? = null + var linkReset: HTMLAnchorElement? = null + var linkP5: HTMLAnchorElement? = null + var linkP10: HTMLAnchorElement? = null + var linkDel: HTMLAnchorElement? = null + + for (element in html.children) { + when { + element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement + element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement + } + } + + linkM10 = linkM10 ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-m10") + link.textContent = "-10" + html.appendChild(link) + link + } + linkM10.removeAttribute("href") + linkM10.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time - 10)) + } + }) + + linkM5 = linkM5 ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-m5") + link.textContent = "-5" + html.appendChild(link) + link + } + linkM5.removeAttribute("href") + linkM5.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time - 5)) + } + }) + + linkReset = linkReset ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-reset") + link.textContent = "reset" + html.appendChild(link) + link + } + linkReset.removeAttribute("href") + linkReset.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH)) + } + }) + + linkP5 = linkP5 ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-p5") + link.textContent = "+5" + html.appendChild(link) + link + } + linkP5.removeAttribute("href") + linkP5.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time + 5)) + } + }) + + linkP10 = linkP10 ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-p10") + link.textContent = "+10" + html.appendChild(link) + link + } + linkP10.removeAttribute("href") + linkP10.addEventListener("click", EventListener { + entry.pending = true + launch { + val s = entry.schedule.get() + ScheduleRepository.update(s.copy(time = s.time + 10)) + } + }) + + linkDel = linkDel ?: run { + val link = createHtmlView() + link.classList.add("calendar-tools-del") + link.textContent = "del" + html.appendChild(link) + link + } + linkDel.removeAttribute("href") + linkDel.addEventListener("click", EventListener { + entry.pending = true + launch { + ScheduleRepository.delete(entry.scheduleId) + } + }) + } +} + +class CalendarEntry(view: HTMLElement) : View(view) { + + private lateinit var mouseDelta: Point + private var newCell: CalendarCell? = null + + private var language by dataset.property("language") + + val scheduleId = dataset["id"]?.toLongOrNull() ?: 0 + + val schedule = RepositoryDelegate(ScheduleRepository, scheduleId) + + var pending by classList.property("pending") + + private fun onMove(event: MouseEvent) { + val position = event.toPoint() - mouseDelta + + val cell = calendarCells.find { + position in it.dimension + } + + + if (cell != null) { + cell += this + + if (newCell == null) { + style { + left = "0" + top = "0.1rem" + } + } + + newCell = cell + } + + event.preventDefault() + event.stopPropagation() + } + + private fun onFinishMove(event: MouseEvent) { + classList -= "drag" + + newCell?.let { cell -> + launch { + val newTime = cell.time + val newRoom = cell.getRoom() + + pending = true + + val s = schedule.get().copy(room = newRoom, time = newTime) + + ScheduleRepository.update(s) + } + } + newCell = null + + for (it in listeners) { + it.detach() + } + listeners = emptyList() + + event.preventDefault() + event.stopPropagation() + } + + private var listeners: List> = emptyList() + + init { + onMouseDown { event -> + if (event.target != html || "pending" in classList) { + event.stopPropagation() + return@onMouseDown + } + + launch { + classList += "drag" + + val s = schedule.get() + val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH + + val p = calendarCells.find { + it.day == s.day && it.time == time && it.roomId == s.room.id + }?.dimension?.center ?: dimension.center + + mouseDelta = event.toPoint() - p + + listeners = listOf( + Body.onMouseMove.reference(this::onMove), + Body.onMouseUp.reference(this::onFinishMove), + Body.onMouseLeave.reference(this::onFinishMove) + ) + } + event.preventDefault() + event.stopPropagation() + } + + var calendarTools: CalendarTools? = null + for (item in html.children) { + if (item.classList.contains("calendar-tools")) { + calendarTools = CalendarTools(this, item) + break + } + } + if (calendarTools == null) { + calendarTools = CalendarTools(this, createHtmlView()) + html.appendChild(calendarTools.html) + } + } + + fun load(schedule: Schedule) { + pending = false + + language = schedule.workGroup.language.code + + this.schedule.set(schedule) + + style { + val size = schedule.workGroup.length / CALENDAR_GRID_WIDTH.toDouble() + val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble() + + val ps = "${pos * 100}%" + val sz = "${size * 100}%" + + left = ps + top = "calc($ps + 0.1rem)" + + width = sz + height = "calc($sz - 0.2rem)" + + if (schedule.workGroup.track?.color != null) { + backgroundColor = schedule.workGroup.track.color.toString() + color = schedule.workGroup.track.color.calcTextColor().toString() + } + } + + for (element in html.childNodes) { + if (element.isText) { + html.removeChild(element) + } + } + + html.appendText(schedule.workGroup.name) + + + val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH + val cell = calendarCells.find { + it.day == schedule.day && it.time == time && it.roomId == schedule.room.id + } + + if (cell != null && cell.html != html.parentElement) { + cell += this + } + } + + companion object { + fun create(schedule: Schedule): CalendarEntry { + val entry = CalendarEntry(createHtmlView()) + + entry.load(schedule) + + return entry + } + } +} + +class CalendarCell(view: HTMLElement) : ViewCollection(view) { + val day = dataset["day"]?.toIntOrNull() ?: 0 + val time = dataset["time"]?.toIntOrNull() ?: 0 + val roomId = dataset["room"]?.toLongOrNull() ?: 0 + + private lateinit var room: Room + + suspend fun getRoom(): Room { + if (this::room.isInitialized) { + return room + } + + room = RoomRepository.get(roomId) ?: throw NoSuchElementException() + return room + } +} + +var calendarEntries: List = emptyList() +var calendarCells: List = emptyList() + +fun initCalendar() { + calendarEntries = document.getElementsByClassName("calendar-entry") + .iterator().asSequence().map(::CalendarEntry).toList() + + calendarCells = document.getElementsByClassName("calendar-cell") + .iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList() + + ScheduleRepository.onCreate { + launch { + val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException() + CalendarEntry.create(schedule) + } + } + ScheduleRepository.onUpdate { + launch { + val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException() + var found = false + for (entry in calendarEntries) { + if (entry.scheduleId == it) { + entry.load(schedule) + found = true + } + } + if (!found) { + CalendarEntry.create(schedule) + } + } + } + ScheduleRepository.onDelete { + for (entry in calendarEntries) { + if (entry.scheduleId == it) { + entry.html.remove() + } + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt b/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt index 27d0491..19aa695 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/ClassList.kt @@ -3,6 +3,7 @@ package de.westermann.kwebview import de.westermann.kobserve.event.EventListener import de.westermann.kobserve.Property import de.westermann.kobserve.ReadOnlyProperty +import de.westermann.kobserve.property.property import org.w3c.dom.DOMTokenList /** @@ -97,6 +98,23 @@ class ClassList( ) } + fun property(clazz: String): Property { + if (clazz in bound) { + throw IllegalArgumentException("Class is already bound!") + } + + val property = property(get(clazz)) + + bound[clazz] = Bound(property, + property.onChange.reference { + list.toggle(clazz, property.value) + } + ) + + return property + } + + fun unbind(clazz: String) { if (clazz !in bound) { throw IllegalArgumentException("Class is not bound!") diff --git a/src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt b/src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt index 72c39fb..50cac28 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/Dimension.kt @@ -7,10 +7,10 @@ import kotlin.math.min * @author lars */ data class Dimension( - val left: Double, - val top: Double, - val width: Double = 0.0, - val height: Double = 0.0 + val left: Double, + val top: Double, + val width: Double = 0.0, + val height: Double = 0.0 ) { constructor(position: Point, size: Point = Point.ZERO) : this(position.x, position.y, size.x, size.y) @@ -27,12 +27,15 @@ data class Dimension( val bottom: Double get() = top + height + val center: Point + get() = Point(left + width / 2.0, top + height / 2.0) + val edges: Set get() = setOf( - Point(left, top), - Point(right, top), - Point(left, bottom), - Point(right, bottom) + Point(left, top), + Point(right, top), + Point(left, bottom), + Point(right, bottom) ) val normalized: Dimension diff --git a/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt b/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt index fb745f9..7750584 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/extensions.kt @@ -110,7 +110,7 @@ fun get( } } -fun post( +fun postForm( url: String, data: Map = emptyMap(), onError: (Int) -> Unit = {}, @@ -141,3 +141,43 @@ fun post( xhttp.send() } } + +fun postJson( + url: String, + data: dynamic, + onError: (Int) -> Unit = {}, + onSuccess: (String) -> Unit = {} +) { + val xhttp = XMLHttpRequest() + + xhttp.onreadystatechange = { + if (xhttp.readyState == 4.toShort()) { + console.log(xhttp.status) + if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) { + onSuccess(xhttp.responseText) + } else { + onError(xhttp.status.toInt()) + } + } + } + xhttp.open("POST", url, true) + + if (data.isNotEmpty()) { + xhttp.setRequestHeader("Content-type", "application/json"); + xhttp.send(data) + } else { + xhttp.send() + } +} + +fun jsonObject(block: (dynamic) -> Unit): dynamic { + val json = js("{}") + block(json) + return json +} + +fun jsonArray(block: (dynamic) -> Unit): dynamic { + val json = js("[]") + block(json) + return json +} diff --git a/src/jvmMain/kotlin/de/kif/backend/Application.kt b/src/jvmMain/kotlin/de/kif/backend/Application.kt index 7eb747e..7a18bae 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Application.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Application.kt @@ -1,203 +1,60 @@ package de.kif.backend -import de.kif.backend.database.Connection -import de.kif.backend.model.User +import com.fasterxml.jackson.databind.SerializationFeature import de.kif.backend.route.* +import de.kif.backend.route.api.* +import de.kif.backend.util.pushService import io.ktor.application.Application -import io.ktor.application.ApplicationCall import io.ktor.application.install -import io.ktor.application.log -import io.ktor.auth.Authentication -import io.ktor.auth.FormAuthChallenge -import io.ktor.auth.UserPasswordCredential -import io.ktor.auth.form import io.ktor.features.* import io.ktor.http.content.files import io.ktor.http.content.static -import io.ktor.locations.Location -import io.ktor.locations.Locations -import io.ktor.response.respondRedirect +import io.ktor.jackson.jackson import io.ktor.routing.routing -import io.ktor.sessions.* -import io.ktor.util.hex import io.ktor.websocket.WebSockets -import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json - -data class PortalSession(val id: Int, val name: String) { - suspend fun getUser(call: ApplicationCall): User { - val user = User.find(name) - if (user == null || user.id != id) { - call.sessions.clear() - call.respondRedirect("/login?error") - throw IllegalAccessException() - } - return user - } -} - -@Location("/") -class LocationDashboard() - -@Location("/calendar/{day}") -data class LocationCalendar(val day: Int) { - @Location("/{room}/{time}") - data class LocationCalendarEdit( - val calendar: LocationCalendar, - val room: Int, - val time: Int, - val search: String = "" - ) { - val day: Int get() = calendar.day - } - - @Location("/{room}/{time}/{workGroup}") - data class LocationCalendarSet( - val calendar: LocationCalendar, - val room: Int, - val time: Int, - val workGroup: Int, - val next: String? = null - ) { - val day: Int get() = calendar.day - } - - @Location("/time-to-room") - data class LocationCalendarTimeToRoom(val calendar: LocationCalendar){ - val day: Int get() = calendar.day - } - - @Location("/room-to-time") - data class LocationCalendarRoomToTime(val calendar: LocationCalendar){ - val day: Int get() = calendar.day - } -} - -@Location("/login") -data class LocationLogin(val username: String = "", val password: String = "", val next: String = "/") - -@Location("/logout") -class LocationLogout() - -@Location("/account") -class LocationAccount() - -@Location("/user") -data class LocationUser(val search: String = "") { - @Location("/{id}") - data class Edit(val id: Int) - - @Location("/new") - class New() - - @Location("/{id}/delete") - data class Delete(val id: Int) -} - -@Location("/workgroup") -data class LocationWorkGroup(val search: String = "") { - @Location("/{id}") - data class Edit(val id: Int) - - @Location("/new") - class New() - - @Location("/{id}/delete") - data class Delete(val id: Int) -} - -@Location("/track") -data class LocationTrack(val search: String = "") { - @Location("/{id}") - data class Edit(val id: Int) - - @Location("/new") - class New() - - @Location("/{id}/delete") - data class Delete(val id: Int) -} - -@Location("/room") -data class LocationRoom(val search: String = "") { - @Location("/{id}") - data class Edit(val id: Int) - - @Location("/new") - class New() - - @Location("/{id}/delete") - data class Delete(val id: Int) -} - -@Location("/person") -data class LocationPerson(val search: String = "") { - @Location("/{id}") - data class Edit(val id: Int) - - @Location("/new") - class New() - - @Location("/{id}/delete") - data class Delete(val id: Int) -} fun Application.main() { - Connection.init() - install(DefaultHeaders) install(CallLogging) install(ConditionalHeaders) install(Compression) install(DataConversion) - install(Locations) install(WebSockets) - install(Authentication) { - form { - userParamName = LocationLogin::username.name - passwordParamName = LocationLogin::password.name - challenge = FormAuthChallenge.Redirect { _ -> - "/login?error" - } - validate { credential: UserPasswordCredential -> - val user = User.find(credential.name) ?: return@validate null - if (user.checkPassword(credential.password)) user else null - } + install(ContentNegotiation) { + jackson { + enable(SerializationFeature.INDENT_OUTPUT) // Pretty Prints the JSON } } - val sessionKey = hex("1234567890abcdef") //TODO - install(Sessions) { - cookie("SESSION") { - transform(SessionTransportTransformerMessageAuthentication(sessionKey)) - } - } + security() routing { static("/static") { files(Resources.directory) } - launch { - val firstStart = User.exists() - if (firstStart) { - log.info("Please create the first user and restart the server!") - setup() - } else { - dashboard() - calendar() - login() - account() + // UI routes + dashboard() + calendar() + login() + account() - workGroup() - track() - room() - person() - user() + workGroup() + track() + room() + user() - pushService() - } - } + // RESTful routes + authenticateApi() + + roomApi() + scheduleApi() + trackApi() + userApi() + workGroupApi() + + // Web socket push notifications + pushService() } -} \ No newline at end of file +} diff --git a/src/jvmMain/kotlin/de/kif/backend/Main.kt b/src/jvmMain/kotlin/de/kif/backend/Main.kt index 1d5a852..38c4abb 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Main.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Main.kt @@ -1,13 +1,50 @@ package de.kif.backend +import de.kif.backend.database.Connection +import de.kif.backend.repository.UserRepository +import de.kif.common.model.Permission +import de.kif.common.model.User import io.ktor.application.Application import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import kotlin.coroutines.suspendCoroutine object Main { @Suppress("UnusedMainParameter") @JvmStatic fun main(args: Array) { + Connection.init() + + runBlocking { + if (UserRepository.all().isEmpty()) { + + println("Please create a root user") + + var username: String? = null + while (username == null) { + print("Username: ") + username = readLine() + } + + var password: String? = null + while (password == null) { + print("Password: ") + password = System.console()?.readPassword()?.toString() ?: readLine() + } + + UserRepository.create( + User( + null, + username, + hashPassword(password), + setOf(Permission.ADMIN) + ) + ) + } + } + embeddedServer( factory = Netty, port = 8080, diff --git a/src/jvmMain/kotlin/de/kif/backend/Security.kt b/src/jvmMain/kotlin/de/kif/backend/Security.kt new file mode 100644 index 0000000..92222a7 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/Security.kt @@ -0,0 +1,125 @@ +package de.kif.backend + +import de.kif.backend.repository.UserRepository +import de.kif.common.model.Permission +import de.kif.common.model.User +import io.ktor.application.Application +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.application.install +import io.ktor.auth.* +import io.ktor.request.path +import io.ktor.response.respondRedirect +import io.ktor.sessions.* +import io.ktor.util.hex +import io.ktor.util.pipeline.PipelineContext +import org.mindrot.jbcrypt.BCrypt +import java.io.File + +interface ErrorContext { + suspend infix fun onFailure(block: suspend () -> Unit) +} + +object ErrorContextIgnore : ErrorContext { + override suspend fun onFailure(block: suspend () -> Unit) { + // do nothing + } +} + +class ErrorContextOccur() : ErrorContext { + override suspend fun onFailure(block: suspend () -> Unit) { + block() + } +} + +suspend inline fun PipelineContext.authenticate( + vararg permissions: Permission, + block: (user: User) -> Unit +): ErrorContext { + val user = call.sessions.get()?.getUser(call) + return if (user == null || permissions.any { !user.checkPermission(it) }) { + ErrorContextOccur() + } else { + block(user) + ErrorContextIgnore + } +} + +suspend inline fun PipelineContext.authenticateOrRedirect( + vararg permissions: Permission, + block: (user: User) -> Unit +) { + authenticate(*permissions, block = block) onFailure { + call.respondRedirect("/login?redirect=${call.request.path()}}") + } +} + +suspend fun PipelineContext.isAuthenticated(vararg permissions: Permission): User? { + val user = call.sessions.get()?.getUser(call) + return if (user == null || permissions.any { !user.checkPermission(it) }) { + null + } else { + user + } +} + +data class PortalSession(val userId: Long) { + suspend fun getUser(call: ApplicationCall): User { + val user = UserRepository.get(userId) + + if (user == null) { + call.sessions.clear() + call.respondRedirect("/login?onFailure") + throw IllegalAccessException() + } + + return user + } +} + +data class UserPrinciple(val user: User) : Principal + +fun checkPassword(password: String, hash: String): Boolean { + return BCrypt.checkpw(password, hash) +} + +fun hashPassword(password: String): String { + return BCrypt.hashpw(password, BCrypt.gensalt()) +} + +fun Application.security() { + + install(Authentication) { + form { + userParamName = "username" + passwordParamName = "password" + challenge = FormAuthChallenge.Redirect { _ -> + "/login?onFailure" + } + validate { credential: UserPasswordCredential -> + val user = UserRepository.find(credential.name) ?: return@validate null + if (checkPassword(credential.password, user.password)) UserPrinciple(user) else null + } + } + } + + val encryptionKey = + hex("80 51 b8 13 b4 73 a9 69 c7 b0 10 ad 08 06 11 e3".replace(" ", "")) + val signKey = + hex( + "d1 20 23 8c 01 f8 f0 0d 9d 7c ff 68 21 97 75 31 38 3f fb 91 20 3a 8d 86 d4 e9 d8 50 f8 71 f1 dc".replace( + " ", + "" + ) + ) + + install(Sessions) { + cookie( + "SESSION", + directorySessionStorage(File(".sessions"), cached = false) + ) { + cookie.path = "/" + transform(SessionTransportTransformerMessageAuthentication(signKey)) + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt b/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt index e2270b4..c0f7168 100644 --- a/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt +++ b/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt @@ -16,9 +16,7 @@ object Connection { transaction { SchemaUtils.create( - DbPerson, DbPersonConstraint, - DbTrack, DbWorkGroup, DbWorkGroupConstraint, - DbLeader, DbWorkGroupOrder, + DbTrack, DbWorkGroup, DbRoom, DbSchedule, DbUser, DbUserPermission ) diff --git a/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt b/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt index 181ca5a..cab06c7 100644 --- a/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt +++ b/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt @@ -1,71 +1,30 @@ package de.kif.backend.database -import de.kif.backend.model.Permission +import de.kif.common.model.Language +import de.kif.common.model.Permission import org.jetbrains.exposed.sql.Table -object DbPerson : Table() { - val id = integer("id").autoIncrement().primaryKey() - val firstName = varchar("first_name", 64) - val lastName = varchar("last_name", 64) - - val arrival = long("arrival").nullable() - val departure = long("departure").nullable() -} - -object DbPersonConstraint : Table() { - val id = integer("id").autoIncrement().primaryKey() - val personId = integer("person_id") - - val type = enumeration("type", DbConstraintType::class) - val time = long("time") - val duration = integer("duration").default(0) - val day = integer("day") -} - object DbTrack : Table() { - val id = integer("id").autoIncrement().primaryKey() + val id = long("id").autoIncrement().primaryKey() val name = varchar("name", 64) val color = varchar("color", 32) } object DbWorkGroup : Table() { - val id = integer("id").autoIncrement().primaryKey() + val id = long("id").autoIncrement().primaryKey() val name = varchar("first_name", 64) val interested = integer("interested") - val trackId = integer("track_id").nullable() + val trackId = long("track_id").nullable() val projector = bool("projector") val resolution = bool("resolution") val language = enumeration("language", Language::class) val length = integer("length") - - val start = long("start").nullable() - val end = long("end").nullable() -} - -object DbWorkGroupConstraint : Table() { - val id = integer("id").autoIncrement().primaryKey() - val workGroupId = integer("work_group_id") - - val type = enumeration("type", DbConstraintType::class) - val time = long("time") - val duration = integer("duration").default(0) - val day = integer("day") -} - -object DbLeader : Table() { - val workGroupId = integer("work_group_id").primaryKey(0) - val personId = integer("person_id").primaryKey(1) -} - -object DbWorkGroupOrder : Table() { - val beforeWorkGroupId = integer("before_work_group_id").primaryKey(0) - val afterWorkGroupId = integer("after_work_group_id").primaryKey(1) } object DbRoom : Table() { - val id = integer("id").autoIncrement().primaryKey() + val id = long("id").autoIncrement().primaryKey() val name = varchar("name", 64) val places = integer("places") @@ -73,30 +32,20 @@ object DbRoom : Table() { } object DbSchedule : Table() { - val workGroupId = integer("work_group_id").primaryKey(0) - val day = integer("day").primaryKey(1) - val time = integer("time_slot").primaryKey(2) - val roomId = integer("room_id").primaryKey(3) -} - -enum class DbConstraintType { - BEGIN, END, BLOCKED + val id = long("id").autoIncrement().primaryKey() + val workGroupId = long("work_group_id").index() + val roomId = long("room_id").index() + val day = integer("day").index() + val time = integer("time_slot") } object DbUser : Table() { - val userId = integer("id").autoIncrement().primaryKey() + val id = long("id").autoIncrement().primaryKey() val username = varchar("username", 64).uniqueIndex() val password = varchar("password", 64) } object DbUserPermission : Table() { - val userId = integer("id").primaryKey(0) + val userId = long("id").primaryKey(0) val permission = enumeration("permission", Permission::class).primaryKey(1) } - -enum class Language(val value: String) { - GERMAN("Deutsch"), ENGLISH("English"); - - override fun toString() = value - val code = value.take(2).toLowerCase() -} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Person.kt b/src/jvmMain/kotlin/de/kif/backend/model/Person.kt deleted file mode 100644 index efe6fd7..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/model/Person.kt +++ /dev/null @@ -1,89 +0,0 @@ -package de.kif.backend.model - -import de.kif.backend.database.DbPerson -import de.kif.backend.database.DbPersonConstraint -import de.kif.backend.database.dbQuery -import org.jetbrains.exposed.sql.* - -class Person( - var id: Int = -1, - var firstName: String = "", - var lastName: String = "", - var arrival: Long? = null, - var departure: Long? = null -) { - var constraints: Set = emptySet() - - suspend fun save() { - if (id < 0) { - dbQuery { - val newId = DbPerson.insert { - it[firstName] = this@Person.firstName - it[lastName] = this@Person.lastName - it[arrival] = this@Person.arrival - it[departure] = this@Person.departure - }[DbPerson.id]!! - this@Person.id = newId - } - for (constraint in constraints) { - constraint.save(this@Person.id) - } - } else { - dbQuery { - DbPerson.update({ DbPerson.id eq id }) { - it[firstName] = this@Person.firstName - it[lastName] = this@Person.lastName - it[arrival] = this@Person.arrival - it[departure] = this@Person.departure - } - - DbPersonConstraint.deleteWhere { DbPersonConstraint.personId eq id } - } - for (constraint in constraints) { - constraint.save(this@Person.id) - } - } - } - - suspend fun delete() { - val id = id - if (id >= 0) { - dbQuery { - DbPersonConstraint.deleteWhere { DbPersonConstraint.personId eq id } - DbPerson.deleteWhere { DbPerson.id eq id } - } - } - } - - suspend fun loadConstraints() { - if (id >= 0) { - constraints = PersonConstraint.get(id) - } - } - - companion object { - suspend fun get(personId: Int): Person? = dbQuery { - val result = DbPerson.select { DbPerson.id eq personId }.firstOrNull() ?: return@dbQuery null - Person( - result[DbPerson.id], - result[DbPerson.firstName], - result[DbPerson.lastName], - result[DbPerson.arrival], - result[DbPerson.departure] - ) - }?.apply { loadConstraints() } - - suspend fun list(): List = dbQuery { - val query = DbPerson.selectAll() - query.map { result -> - Person( - result[DbPerson.id], - result[DbPerson.firstName], - result[DbPerson.lastName], - result[DbPerson.arrival], - result[DbPerson.departure] - ) - } - }.onEach { it.loadConstraints() } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/PersonConstraint.kt b/src/jvmMain/kotlin/de/kif/backend/model/PersonConstraint.kt deleted file mode 100644 index 6754e89..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/model/PersonConstraint.kt +++ /dev/null @@ -1,132 +0,0 @@ -package de.kif.backend.model - -import de.kif.backend.database.DbConstraintType -import de.kif.backend.database.DbPersonConstraint -import de.kif.backend.database.dbQuery -import org.jetbrains.exposed.sql.deleteWhere -import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.update - -sealed class PersonConstraint( - var id: Int = -1 -) { - - abstract suspend fun save(personId: Int) - - suspend fun delete() { - val id = id - if (id >= 0) { - dbQuery { - DbPersonConstraint.deleteWhere { DbPersonConstraint.id eq id } - } - } - } - - class BeginOnDay( - var time: Long = 0, - var day: Int = 0 - ) : PersonConstraint() { - override suspend fun save(personId: Int) { - if (id < 0) { - dbQuery { - val newId = DbPersonConstraint.insert { - it[this@insert.personId] = personId - it[type] = DbConstraintType.BEGIN - it[time] = this@BeginOnDay.time - it[day] = this@BeginOnDay.day - }[DbPersonConstraint.id]!! - this@BeginOnDay.id = newId - } - } else { - dbQuery { - DbPersonConstraint.update({ DbPersonConstraint.id eq id }) { - it[this@update.personId] = personId - it[type] = DbConstraintType.BEGIN - it[time] = this@BeginOnDay.time - it[day] = this@BeginOnDay.day - } - } - } - } - } - - class EndOnDay( - var time: Long = 0, - var day: Int = 0 - ) : PersonConstraint() { - override suspend fun save(personId: Int) { - if (id < 0) { - dbQuery { - val newId = DbPersonConstraint.insert { - it[this@insert.personId] = personId - it[type] = DbConstraintType.END - it[time] = this@EndOnDay.time - it[day] = this@EndOnDay.day - }[DbPersonConstraint.id]!! - this@EndOnDay.id = newId - } - } else { - dbQuery { - DbPersonConstraint.update({ DbPersonConstraint.id eq id }) { - it[this@update.personId] = personId - it[type] = DbConstraintType.END - it[time] = this@EndOnDay.time - it[day] = this@EndOnDay.day - } - } - } - } - } - - class BlockedOnDay( - var time: Long = 0, - var duration: Int = 0, - var day: Int = 0 - ) : PersonConstraint() { - - override suspend fun save(personId: Int) { - if (id < 0) { - dbQuery { - val newId = DbPersonConstraint.insert { - it[this@insert.personId] = personId - it[type] = DbConstraintType.BLOCKED - it[time] = this@BlockedOnDay.time - it[duration] = this@BlockedOnDay.duration - it[day] = this@BlockedOnDay.day - }[DbPersonConstraint.id]!! - this@BlockedOnDay.id = newId - } - } else { - dbQuery { - DbPersonConstraint.update({ DbPersonConstraint.id eq id }) { - it[this@update.personId] = personId - it[type] = DbConstraintType.BLOCKED - it[time] = this@BlockedOnDay.time - it[duration] = this@BlockedOnDay.duration - it[day] = this@BlockedOnDay.day - } - } - } - } - } - - companion object { - suspend fun get(personId: Int): Set = dbQuery { - val result = DbPersonConstraint.select { DbPersonConstraint.personId eq personId } - result.map { - val id = it[DbPersonConstraint.id] - val type = it[DbPersonConstraint.type] - val time = it[DbPersonConstraint.time] - val duration = it[DbPersonConstraint.duration] - val day = it[DbPersonConstraint.day] - - when (type) { - DbConstraintType.BEGIN -> PersonConstraint.BeginOnDay(time, day) - DbConstraintType.END -> PersonConstraint.EndOnDay(time, day) - DbConstraintType.BLOCKED -> PersonConstraint.BlockedOnDay(time, duration, day) - }.also { it.id = id } - }.toSet() - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Room.kt b/src/jvmMain/kotlin/de/kif/backend/model/Room.kt deleted file mode 100644 index c9ee9fb..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/model/Room.kt +++ /dev/null @@ -1,85 +0,0 @@ -package de.kif.backend.model - -import de.kif.backend.database.DbRoom -import de.kif.backend.database.dbQuery -import org.jetbrains.exposed.sql.* - -class Room( - var id: Int = -1, - var name: String = "", - var places: Int = 0, - var projector: Boolean = false -) { - suspend fun save() { - if (id < 0) { - dbQuery { - val newId = DbRoom.insert { - it[name] = this@Room.name - it[places] = this@Room.places - it[projector] = this@Room.projector - }[DbRoom.id]!! - this@Room.id = newId - } - } else { - dbQuery { - DbRoom.update({ DbRoom.id eq id }) { - it[name] = this@Room.name - it[places] = this@Room.places - it[projector] = this@Room.projector - } - } - } - } - - suspend fun delete() { - val id = id - if (id >= 0) { - for (it in Schedule.getByRoom(id)) { - it.delete() - } - dbQuery { - DbRoom.deleteWhere { DbRoom.id eq id } - } - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Room - - if (id != other.id) return false - - return true - } - - override fun hashCode(): Int { - return id - } - - - companion object { - suspend fun get(roomId: Int): Room = dbQuery { - val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: throw IllegalArgumentException() - Room( - result[DbRoom.id], - result[DbRoom.name], - result[DbRoom.places], - result[DbRoom.projector] - ) - } - - suspend fun list(): List = dbQuery { - val query = DbRoom.selectAll() - query.map { result -> - Room( - result[DbRoom.id], - result[DbRoom.name], - result[DbRoom.places], - result[DbRoom.projector] - ) - } - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Schedule.kt b/src/jvmMain/kotlin/de/kif/backend/model/Schedule.kt deleted file mode 100644 index 172408b..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/model/Schedule.kt +++ /dev/null @@ -1,99 +0,0 @@ -package de.kif.backend.model - -import de.kif.backend.database.DbSchedule -import de.kif.backend.database.dbQuery -import org.jetbrains.exposed.sql.* - -data class Schedule( - val workGroupId: Int, - val day: Int, - val time: Int, - val roomId: Int -) { - lateinit var workGroup: WorkGroup - lateinit var room: Room - - suspend fun save() { - delete() - dbQuery { - DbSchedule.insert { - it[workGroupId] = this@Schedule.workGroupId - it[day] = this@Schedule.day - it[time] = this@Schedule.time - it[roomId] = this@Schedule.roomId - } - } - } - - suspend fun delete() { - dbQuery { - DbSchedule.deleteWhere { - (DbSchedule.workGroupId eq workGroupId) and - (DbSchedule.day eq day) and - (DbSchedule.time eq time) and - (DbSchedule.roomId eq roomId) - } - } - } - - suspend fun loadConstraints() { - try { - workGroup = WorkGroup.get(workGroupId) - room = Room.get(roomId) - } catch (e: IllegalArgumentException) { - delete() - throw e - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Schedule - - if (workGroupId != other.workGroupId) return false - - return true - } - - override fun hashCode(): Int { - return workGroupId - } - - companion object { - - private suspend fun parseQuery(block: () -> Query): List = - dbQuery { - val query = block() - query.map { result -> - Schedule( - result[DbSchedule.workGroupId], - result[DbSchedule.day], - result[DbSchedule.time], - result[DbSchedule.roomId] - ) - } - }.onEach { it.loadConstraints() } - - suspend fun getByRoom(roomId: Int): List = parseQuery { - DbSchedule.select { DbSchedule.roomId eq roomId } - } - - suspend fun getByRoom(roomId: Int, day: Int, time: Int): List = parseQuery { - DbSchedule.select { (DbSchedule.roomId eq roomId) and (DbSchedule.day eq day) and (DbSchedule.time eq time) } - } - - suspend fun getByWorkGroup(workGroupId: Int): List = parseQuery { - DbSchedule.select { DbSchedule.workGroupId eq workGroupId } - } - - suspend fun getByDay(day: Int): List = parseQuery { - DbSchedule.select { DbSchedule.day eq day } - } - - suspend fun list(): List = parseQuery { - DbSchedule.selectAll() - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Track.kt b/src/jvmMain/kotlin/de/kif/backend/model/Track.kt deleted file mode 100644 index 5c0ea16..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/model/Track.kt +++ /dev/null @@ -1,63 +0,0 @@ -package de.kif.backend.model - -import de.kif.backend.database.DbTrack -import de.kif.backend.database.dbQuery -import kif.common.model.Color -import org.jetbrains.exposed.sql.* -import java.lang.IllegalArgumentException - -class Track( - var id: Int = -1, - var name: String = "", - var color: Color -) { - suspend fun save() { - if (id < 0) { - dbQuery { - val newId = DbTrack.insert { - it[name] = this@Track.name - it[color] = this@Track.color.toString() - }[DbTrack.id]!! - this@Track.id = newId - } - } else { - dbQuery { - DbTrack.update({ DbTrack.id eq id }) { - it[name] = this@Track.name - it[color] = this@Track.color.toString() - } - } - } - } - - suspend fun delete() { - val id = id - if (id >= 0) { - dbQuery { - DbTrack.deleteWhere { DbTrack.id eq id } - } - } - } - - companion object { - suspend fun get(TrackId: Int): Track = dbQuery { - val result = DbTrack.select { DbTrack.id eq TrackId }.firstOrNull() ?: throw IllegalArgumentException() - Track( - result[DbTrack.id], - result[DbTrack.name], - Color.parse(result[DbTrack.color]) - ) - } - - suspend fun list(): List = dbQuery { - val query = DbTrack.selectAll() - query.map { result -> - Track( - result[DbTrack.id], - result[DbTrack.name], - Color.parse(result[DbTrack.color]) - ) - } - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/User.kt b/src/jvmMain/kotlin/de/kif/backend/model/User.kt deleted file mode 100644 index 85aee20..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/model/User.kt +++ /dev/null @@ -1,110 +0,0 @@ -package de.kif.backend.model - -import de.kif.backend.database.DbUser -import de.kif.backend.database.DbUserPermission -import de.kif.backend.database.dbQuery -import io.ktor.auth.Principal -import org.jetbrains.exposed.sql.* -import org.mindrot.jbcrypt.BCrypt - -class User( - var id: Int = -1, - var username: String = "", - private var password: String = "" -) : Principal { - var permissions: Set = emptySet() - - fun checkPassword(password: String): Boolean { - return BCrypt.checkpw(password, this.password) - } - - fun hashPassword(newPassword: String) { - password = BCrypt.hashpw(newPassword, BCrypt.gensalt()) - } - - fun checkPermission(permission: Permission): Boolean { - return permission in permissions || Permission.ADMIN in permissions - } - - suspend fun loadPermissions() = dbQuery { - permissions = DbUserPermission.slice(DbUserPermission.permission).select { - DbUserPermission.userId eq id - }.map { it[DbUserPermission.permission] }.toSet() - } - - suspend fun save() { - if (id < 0) { - dbQuery { - val newId = DbUser.insert { - it[username] = this@User.username - it[password] = this@User.password - }[DbUser.userId]!! - this@User.id = newId - - for (permission in permissions) { - DbUserPermission.insert { - it[userId] = newId - it[this.permission] = permission - } - } - } - } else { - dbQuery { - DbUser.update({ DbUser.userId eq id }) { - it[username] = this@User.username - it[password] = this@User.password - } - - DbUserPermission.deleteWhere { DbUserPermission.userId eq id } - - for (permission in permissions) { - DbUserPermission.insert { - it[userId] = id - it[this.permission] = permission - } - } - } - } - } - - suspend fun delete() { - val id = id - if (id >= 0) { - dbQuery { - DbUserPermission.deleteWhere { DbUserPermission.userId eq id } - DbUser.deleteWhere { DbUser.userId eq id } - } - } - } - - companion object { - suspend fun create(username: String, password: String, permissions: Set) { - val user = User(username = username) - user.hashPassword(password) - user.permissions = permissions - user.save() - } - - suspend fun find(username: String): User? = dbQuery { - val result = DbUser.select { DbUser.username eq username }.firstOrNull() ?: return@dbQuery null - User(result[DbUser.userId], result[DbUser.username], result[DbUser.password]) - - }?.apply { loadPermissions() } - - suspend fun get(userId: Int): User? = dbQuery { - val result = DbUser.select { DbUser.userId eq userId }.firstOrNull() ?: return@dbQuery null - User(result[DbUser.userId], result[DbUser.username], result[DbUser.password]) - }?.apply { loadPermissions() } - - suspend fun list(): List = dbQuery { - val query = DbUser.selectAll() - query.map { result -> - User(result[DbUser.userId], result[DbUser.username], result[DbUser.password]) - } - }.onEach { it.loadPermissions() } - - suspend fun exists(): Boolean = dbQuery { - DbUser.selectAll().count() == 0 - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/model/WorkGroup.kt deleted file mode 100644 index 6564e63..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/model/WorkGroup.kt +++ /dev/null @@ -1,121 +0,0 @@ -package de.kif.backend.model - -import de.kif.backend.database.DbWorkGroup -import de.kif.backend.database.DbWorkGroupConstraint -import de.kif.backend.database.Language -import de.kif.backend.database.dbQuery -import io.ktor.features.NotFoundException -import org.jetbrains.exposed.sql.* - -class WorkGroup( - var id: Int = -1, - var name: String = "", - var interested: Int = 0, - var trackId: Int? = null, - var projector: Boolean = false, - var resolution: Boolean = false, - var length: Int = 0, - var language: Language = Language.GERMAN, - var start: Long? = null, - var end: Long? = null -) { - var constraints: Set = emptySet() - var track: Track? = null - - suspend fun save() { - if (id < 0) { - dbQuery { - val newId = DbWorkGroup.insert { - it[name] = this@WorkGroup.name - it[interested] = this@WorkGroup.interested - it[trackId] = this@WorkGroup.trackId - it[projector] = this@WorkGroup.projector - it[resolution] = this@WorkGroup.resolution - it[length] = this@WorkGroup.length - it[language] = this@WorkGroup.language - it[start] = this@WorkGroup.start - it[end] = this@WorkGroup.end - }[DbWorkGroup.id]!! - this@WorkGroup.id = newId - } - for (constraint in constraints) { - constraint.save(this@WorkGroup.id) - } - } else { - dbQuery { - DbWorkGroup.update({ DbWorkGroup.id eq id }) { - it[name] = this@WorkGroup.name - it[interested] = this@WorkGroup.interested - it[trackId] = this@WorkGroup.trackId - it[projector] = this@WorkGroup.projector - it[resolution] = this@WorkGroup.resolution - it[length] = this@WorkGroup.length - it[language] = this@WorkGroup.language - it[start] = this@WorkGroup.start - it[end] = this@WorkGroup.end - } - - DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id } - } - for (constraint in constraints) { - constraint.save(this@WorkGroup.id) - } - } - } - - suspend fun delete() { - val id = id - if (id >= 0) { - for (it in Schedule.getByWorkGroup(id)) { - it.delete() - } - dbQuery { - DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id } - DbWorkGroup.deleteWhere { DbWorkGroup.id eq id } - } - } - } - - suspend fun loadConstraints() { - if (id >= 0) { - constraints = WorkGroupConstraint.get(id) - track = trackId?.let { if (it < 0) null else Track.get(it) } - } - } - - companion object { - suspend fun get(workGroupId: Int): WorkGroup = dbQuery { - val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: throw IllegalArgumentException() - WorkGroup( - result[DbWorkGroup.id], - result[DbWorkGroup.name], - result[DbWorkGroup.interested], - result[DbWorkGroup.trackId], - result[DbWorkGroup.projector], - result[DbWorkGroup.resolution], - result[DbWorkGroup.length], - result[DbWorkGroup.language], - result[DbWorkGroup.start], - result[DbWorkGroup.end] - ) - }.apply { loadConstraints() } - - suspend fun list(): List = dbQuery { - val query = DbWorkGroup.selectAll() - query.map { result -> - WorkGroup( - result[DbWorkGroup.id], - result[DbWorkGroup.name], - result[DbWorkGroup.interested], - result[DbWorkGroup.trackId], - result[DbWorkGroup.projector], - result[DbWorkGroup.resolution], - result[DbWorkGroup.length], - result[DbWorkGroup.language], - result[DbWorkGroup.start], - result[DbWorkGroup.end] - ) - } - }.onEach { it.loadConstraints() } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/WorkGroupConstraint.kt b/src/jvmMain/kotlin/de/kif/backend/model/WorkGroupConstraint.kt deleted file mode 100644 index ab7dde5..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/model/WorkGroupConstraint.kt +++ /dev/null @@ -1,132 +0,0 @@ -package de.kif.backend.model - -import de.kif.backend.database.DbConstraintType -import de.kif.backend.database.DbWorkGroupConstraint -import de.kif.backend.database.dbQuery -import org.jetbrains.exposed.sql.deleteWhere -import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.update - -sealed class WorkGroupConstraint( - var id: Int = -1 -) { - - abstract suspend fun save(workGroupId: Int) - - suspend fun delete() { - val id = id - if (id >= 0) { - dbQuery { - DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.id eq id } - } - } - } - - class BeginOnDay( - var time: Long = 0, - var day: Int = 0 - ) : WorkGroupConstraint() { - override suspend fun save(workGroupId: Int) { - if (id < 0) { - dbQuery { - val newId = DbWorkGroupConstraint.insert { - it[this@insert.workGroupId] = workGroupId - it[type] = DbConstraintType.BEGIN - it[time] = this@BeginOnDay.time - it[day] = this@BeginOnDay.day - }[DbWorkGroupConstraint.id]!! - this@BeginOnDay.id = newId - } - } else { - dbQuery { - DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) { - it[this@update.workGroupId] = workGroupId - it[type] = DbConstraintType.BEGIN - it[time] = this@BeginOnDay.time - it[day] = this@BeginOnDay.day - } - } - } - } - } - - class EndOnDay( - var time: Long = 0, - var day: Int = 0 - ) : WorkGroupConstraint() { - override suspend fun save(workGroupId: Int) { - if (id < 0) { - dbQuery { - val newId = DbWorkGroupConstraint.insert { - it[this@insert.workGroupId] = workGroupId - it[type] = DbConstraintType.END - it[time] = this@EndOnDay.time - it[day] = this@EndOnDay.day - }[DbWorkGroupConstraint.id]!! - this@EndOnDay.id = newId - } - } else { - dbQuery { - DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) { - it[this@update.workGroupId] = workGroupId - it[type] = DbConstraintType.END - it[time] = this@EndOnDay.time - it[day] = this@EndOnDay.day - } - } - } - } - } - - class BlockedOnDay( - var time: Long = 0, - var duration: Int = 0, - var day: Int = 0 - ) : WorkGroupConstraint() { - - override suspend fun save(workGroupId: Int) { - if (id < 0) { - dbQuery { - val newId = DbWorkGroupConstraint.insert { - it[this@insert.workGroupId] = workGroupId - it[type] = DbConstraintType.BLOCKED - it[time] = this@BlockedOnDay.time - it[duration] = this@BlockedOnDay.duration - it[day] = this@BlockedOnDay.day - }[DbWorkGroupConstraint.id]!! - this@BlockedOnDay.id = newId - } - } else { - dbQuery { - DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) { - it[this@update.workGroupId] = workGroupId - it[type] = DbConstraintType.BLOCKED - it[time] = this@BlockedOnDay.time - it[duration] = this@BlockedOnDay.duration - it[day] = this@BlockedOnDay.day - } - } - } - } - } - - companion object { - suspend fun get(workGroupId: Int): Set = dbQuery { - val result = DbWorkGroupConstraint.select { DbWorkGroupConstraint.workGroupId eq workGroupId } - result.map { - val id = it[DbWorkGroupConstraint.id] - val type = it[DbWorkGroupConstraint.type] - val time = it[DbWorkGroupConstraint.time] - val duration = it[DbWorkGroupConstraint.duration] - val day = it[DbWorkGroupConstraint.day] - - when (type) { - DbConstraintType.BEGIN -> WorkGroupConstraint.BeginOnDay(time, day) - DbConstraintType.END -> WorkGroupConstraint.EndOnDay(time, day) - DbConstraintType.BLOCKED -> WorkGroupConstraint.BlockedOnDay(time, duration, day) - }.also { it.id = id } - }.toSet() - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/RoomRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/RoomRepository.kt new file mode 100644 index 0000000..a83b3b2 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/repository/RoomRepository.kt @@ -0,0 +1,93 @@ +package de.kif.backend.repository + +import de.kif.backend.database.DbRoom +import de.kif.backend.database.dbQuery +import de.kif.backend.util.PushService +import de.kif.common.* +import de.kif.common.model.Room +import de.westermann.kobserve.event.EventHandler +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.* + +object RoomRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private fun rowToModel(row: ResultRow): Room { + val id = row[DbRoom.id] + val name = row[DbRoom.name] + val places = row[DbRoom.places] + val projector = row[DbRoom.projector] + + return Room(id, name, places, projector) + } + + override suspend fun get(id: Long): Room? { + return dbQuery { + rowToModel(DbRoom.select { DbRoom.id eq id }.firstOrNull() ?: return@dbQuery null) + } + } + + override suspend fun create(model: Room): Long { + return dbQuery { + val id = DbRoom.insert { + it[name] = model.name + it[places] = model.places + it[projector] = model.projector + }[DbRoom.id] ?: throw IllegalStateException("Cannot create model!") + + onCreate.emit(id) + + id + } + } + + override suspend fun update(model: Room) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + dbQuery { + DbRoom.update({ DbRoom.id eq model.id }) { + it[name] = model.name + it[places] = model.places + it[projector] = model.projector + } + + onUpdate.emit(model.id) + } + } + + override suspend fun delete(id: Long) { + onDelete.emit(id) + + dbQuery { + DbRoom.deleteWhere { DbRoom.id eq id } + } + } + + override suspend fun all(): List { + return dbQuery { + val result = DbRoom.selectAll() + + result.map(this::rowToModel) + } + } + + fun registerPushService() { + onCreate { + runBlocking { + PushService.notify(MessageType.CREATE, RepositoryType.ROOM, it) + } + } + onUpdate { + runBlocking { + PushService.notify(MessageType.UPDATE, RepositoryType.ROOM, it) + } + } + onDelete { + runBlocking { + PushService.notify(MessageType.DELETE, RepositoryType.ROOM, it) + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/ScheduleRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/ScheduleRepository.kt new file mode 100644 index 0000000..618e67b --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/repository/ScheduleRepository.kt @@ -0,0 +1,177 @@ +package de.kif.backend.repository + +import de.kif.backend.database.DbSchedule +import de.kif.backend.database.dbQuery +import de.kif.backend.util.PushService +import de.kif.common.* +import de.kif.common.model.Schedule +import de.westermann.kobserve.event.EventHandler +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.* + +object ScheduleRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private suspend fun rowToModel(row: ResultRow): Schedule { + val id = row[DbSchedule.id] + val workGroupId = row[DbSchedule.workGroupId] + val roomId = row[DbSchedule.roomId] + val day = row[DbSchedule.day] + val time = row[DbSchedule.time] + + val workGroup = WorkGroupRepository.get(workGroupId) + ?: throw IllegalStateException("Work group for schedule does not exist!") + val room = RoomRepository.get(roomId) + ?: throw IllegalStateException("Room for schedule does not exist!") + + return Schedule(id, workGroup, room, day, time) + } + + override suspend fun get(id: Long): Schedule? { + return dbQuery { + val row = DbSchedule.select { DbSchedule.id eq id }.firstOrNull() ?: return@dbQuery null + + runBlocking { + rowToModel(row) + } + } + } + + override suspend fun create(model: Schedule): Long { + return dbQuery { + val id = DbSchedule.insert { + it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!") + it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!") + it[day] = model.day + it[time] = model.time + }[DbSchedule.id] ?: throw IllegalStateException("Cannot create model!") + + onCreate.emit(id) + + id + } + } + + override suspend fun update(model: Schedule) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + dbQuery { + DbSchedule.update({ DbSchedule.id eq model.id }) { + it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!") + it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!") + it[day] = model.day + it[time] = model.time + } + + onUpdate.emit(model.id) + } + } + + override suspend fun delete(id: Long) { + onDelete.emit(id) + + dbQuery { + DbSchedule.deleteWhere { DbSchedule.id eq id } + } + } + + override suspend fun all(): List { + return dbQuery { + val result = DbSchedule.selectAll() + + runBlocking { + result.map { + rowToModel(it) + } + } + } + } + + suspend fun getByDay(day: Int): List { + return dbQuery { + val result = DbSchedule.select { DbSchedule.day eq day } + + runBlocking { + result.map { + rowToModel(it) + } + } + } + } + + suspend fun getByWorkGroup(workGroupId: Long): List { + return dbQuery { + val result = DbSchedule.select { DbSchedule.workGroupId eq workGroupId } + + runBlocking { + result.map { + rowToModel(it) + } + } + } + } + + suspend fun getByRoom(roomId: Long): List { + return dbQuery { + val result = DbSchedule.select { DbSchedule.roomId eq roomId } + + runBlocking { + result.map { + rowToModel(it) + } + } + } + } + + init { + RoomRepository.onUpdate { roomId -> + runBlocking { + getByRoom(roomId).forEach { schedule -> + if (schedule.id != null) onUpdate.emit(schedule.id) + } + } + } + RoomRepository.onDelete { roomId -> + runBlocking { + getByRoom(roomId).forEach { schedule -> + if (schedule.id != null) delete(schedule.id) + } + } + } + + WorkGroupRepository.onUpdate { workGroupId -> + runBlocking { + getByWorkGroup(workGroupId).forEach { schedule -> + if (schedule.id != null) onUpdate.emit(schedule.id) + } + } + } + WorkGroupRepository.onDelete { workGroupId -> + runBlocking { + getByWorkGroup(workGroupId).forEach { schedule -> + if (schedule.id != null) delete(schedule.id) + } + } + } + } + + fun registerPushService() { + onCreate { + runBlocking { + PushService.notify(MessageType.CREATE, RepositoryType.SCHEDULE, it) + } + } + onUpdate { + runBlocking { + PushService.notify(MessageType.UPDATE, RepositoryType.SCHEDULE, it) + } + } + onDelete { + runBlocking { + PushService.notify(MessageType.DELETE, RepositoryType.SCHEDULE, it) + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/TrackRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/TrackRepository.kt new file mode 100644 index 0000000..4de9743 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/repository/TrackRepository.kt @@ -0,0 +1,93 @@ +package de.kif.backend.repository + +import de.kif.backend.database.DbTrack +import de.kif.backend.database.dbQuery +import de.kif.backend.util.PushService +import de.kif.common.MessageType +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.Track +import de.kif.common.model.parseColor +import de.westermann.kobserve.event.EventHandler +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.* + +object TrackRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private fun rowToModel(row: ResultRow): Track { + val id = row[DbTrack.id] + val name = row[DbTrack.name] + val color = row[DbTrack.color].parseColor() + + return Track(id, name, color) + } + + override suspend fun get(id: Long): Track? { + return dbQuery { + rowToModel(DbTrack.select { DbTrack.id eq id }.firstOrNull() ?: return@dbQuery null) + } + } + + override suspend fun create(model: Track): Long { + return dbQuery { + val id = DbTrack.insert { + it[name] = model.name + it[color] = model.color.toString() + }[DbTrack.id] ?: throw IllegalStateException("Cannot create model!") + + onCreate.emit(id) + + id + } + } + + override suspend fun update(model: Track) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + dbQuery { + DbTrack.update({ DbTrack.id eq model.id }) { + it[name] = model.name + it[color] = model.color.toString() + } + + onUpdate.emit(model.id) + } + } + + override suspend fun delete(id: Long) { + onDelete.emit(id) + + dbQuery { + DbTrack.deleteWhere { DbTrack.id eq id } + } + } + + override suspend fun all(): List { + return dbQuery { + val result = DbTrack.selectAll() + + result.map(this::rowToModel) + } + } + + fun registerPushService() { + onCreate { + runBlocking { + PushService.notify(MessageType.CREATE, RepositoryType.TRACK, it) + } + } + onUpdate { + runBlocking { + PushService.notify(MessageType.UPDATE, RepositoryType.TRACK, it) + } + } + onDelete { + runBlocking { + PushService.notify(MessageType.DELETE, RepositoryType.TRACK, it) + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/UserRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/UserRepository.kt new file mode 100644 index 0000000..f851f2d --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/repository/UserRepository.kt @@ -0,0 +1,120 @@ +package de.kif.backend.repository + +import de.kif.backend.database.DbUser +import de.kif.backend.database.DbUserPermission +import de.kif.backend.database.dbQuery +import de.kif.backend.util.PushService +import de.kif.common.MessageType +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.User +import de.westermann.kobserve.event.EventHandler +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.* + +object UserRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private fun rowToModel(row: ResultRow): User { + val id = row[DbUser.id] + val username = row[DbUser.username] + val password = row[DbUser.password] + + val permissions = DbUserPermission.slice(DbUserPermission.permission).select { + DbUserPermission.userId eq id + }.map { it[DbUserPermission.permission] }.toSet() + + return User(id, username, password, permissions) + } + + override suspend fun get(id: Long): User? { + return dbQuery { + rowToModel(DbUser.select { DbUser.id eq id }.firstOrNull() ?: return@dbQuery null) + } + } + + override suspend fun create(model: User): Long { + return dbQuery { + val id = DbUser.insert { + it[username] = model.username + it[password] = model.password + }[DbUser.id] ?: throw IllegalStateException("Cannot create model!") + + for (permission in model.permissions) { + DbUserPermission.insert { + it[userId] = id + it[this.permission] = permission + } + } + + onCreate.emit(id) + + id + } + } + + override suspend fun update(model: User) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + dbQuery { + DbUser.update({ DbUser.id eq model.id }) { + it[username] = model.username + it[password] = model.password + } + + DbUserPermission.deleteWhere { DbUserPermission.userId eq model.id } + + for (permission in model.permissions) { + DbUserPermission.insert { + it[userId] = model.id + it[this.permission] = permission + } + } + + onUpdate.emit(model.id) + } + } + + override suspend fun delete(id: Long) { + onDelete.emit(id) + + dbQuery { + DbUserPermission.deleteWhere { DbUserPermission.userId eq id } + DbUser.deleteWhere { DbUser.id eq id } + } + } + + override suspend fun all(): List { + return dbQuery { + val result = DbUser.selectAll() + + result.map(this::rowToModel) + } + } + + suspend fun find(username: String): User? { + return dbQuery { + rowToModel(DbUser.select { DbUser.username eq username }.firstOrNull() ?: return@dbQuery null) + } + } + + fun registerPushService() { + onCreate { + runBlocking { + PushService.notify(MessageType.CREATE, RepositoryType.USER, it) + } + } + onUpdate { + runBlocking { + PushService.notify(MessageType.UPDATE, RepositoryType.USER, it) + } + } + onDelete { + runBlocking { + PushService.notify(MessageType.DELETE, RepositoryType.USER, it) + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt new file mode 100644 index 0000000..7117854 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt @@ -0,0 +1,148 @@ +package de.kif.backend.repository + +import de.kif.backend.database.DbWorkGroup +import de.kif.backend.database.dbQuery +import de.kif.backend.util.PushService +import de.kif.common.MessageType +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.WorkGroup +import de.westermann.kobserve.event.EventHandler +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.* + +object WorkGroupRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private suspend fun rowToModel(row: ResultRow): WorkGroup { + val id = row[DbWorkGroup.id] + val name = row[DbWorkGroup.name] + val interested = row[DbWorkGroup.interested] + val trackId = row[DbWorkGroup.trackId] + val projector = row[DbWorkGroup.projector] + val resolution = row[DbWorkGroup.resolution] + val length = row[DbWorkGroup.length] + val language = row[DbWorkGroup.language] + + val track = trackId?.let { TrackRepository.get(it) } + + return WorkGroup(id, name, interested, track, projector, resolution, length, language) + } + + override suspend fun get(id: Long): WorkGroup? { + return dbQuery { + val row = DbWorkGroup.select { DbWorkGroup.id eq id }.firstOrNull() ?: return@dbQuery null + + runBlocking { + rowToModel(row) + } + } + } + + override suspend fun create(model: WorkGroup): Long { + return dbQuery { + val id = DbWorkGroup.insert { + it[name] = model.name + it[interested] = model.interested + it[trackId] = model.track?.id + it[projector] = model.projector + it[resolution] = model.resolution + it[length] = model.length + it[language] = model.language + }[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!") + + onCreate.emit(id) + + id + } + } + + override suspend fun update(model: WorkGroup) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + dbQuery { + DbWorkGroup.update({ DbWorkGroup.id eq model.id }) { + it[name] = model.name + it[interested] = model.interested + it[trackId] = model.track?.id + it[projector] = model.projector + it[resolution] = model.resolution + it[length] = model.length + it[language] = model.language + } + + onUpdate.emit(model.id) + } + } + + override suspend fun delete(id: Long) { + onDelete.emit(id) + + dbQuery { + DbWorkGroup.deleteWhere { DbWorkGroup.id eq id } + } + } + + override suspend fun all(): List { + return dbQuery { + val result = DbWorkGroup.selectAll() + + result.map { + runBlocking { + rowToModel(it) + } + } + } + } + + suspend fun getByTrack(trackId: Long?): List { + return dbQuery { + val result = DbWorkGroup.select { DbWorkGroup.trackId eq trackId } + + result.map { + runBlocking { + rowToModel(it) + } + } + } + } + + init { + TrackRepository.onUpdate { roomId -> + runBlocking { + getByTrack(roomId).forEach { workGroup -> + if (workGroup.id != null) onUpdate.emit(workGroup.id) + } + } + } + TrackRepository.onDelete { roomId -> + runBlocking { + getByTrack(roomId).forEach { workGroup -> + if (workGroup.id != null) { + update(workGroup.copy(track = null)) + } + } + } + } + } + + fun registerPushService() { + onCreate { + runBlocking { + PushService.notify(MessageType.CREATE, RepositoryType.WORK_GROUP, it) + } + } + onUpdate { + runBlocking { + PushService.notify(MessageType.UPDATE, RepositoryType.WORK_GROUP, it) + } + } + onDelete { + runBlocking { + PushService.notify(MessageType.DELETE, RepositoryType.WORK_GROUP, it) + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt index ee8ad87..3fa2f5a 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt @@ -1,26 +1,17 @@ package de.kif.backend.route -import de.kif.backend.LocationAccount -import de.kif.backend.LocationLogin -import de.kif.backend.PortalSession +import de.kif.backend.authenticateOrRedirect import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import io.ktor.application.call import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.get -import io.ktor.request.path -import io.ktor.response.respondRedirect import io.ktor.routing.Route -import io.ktor.sessions.get -import io.ktor.sessions.sessions +import io.ktor.routing.get import kotlinx.html.* fun Route.account() { - get { - val user = call.sessions.get()?.getUser(call) - if (user == null) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + get("/account") { + authenticateOrRedirect { user -> call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -34,7 +25,7 @@ fun Route.account() { +"You have the following rights: ${user.permissions}" br {} a("/logout") { - button(classes="form-btn") { + button(classes = "form-btn") { +"Logout" } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt index 650716b..2a58521 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt @@ -1,40 +1,38 @@ package de.kif.backend.route -import de.kif.backend.LocationCalendar -import de.kif.backend.LocationLogin -import de.kif.backend.PortalSession -import de.kif.backend.model.Permission -import de.kif.backend.model.Room -import de.kif.backend.model.Schedule -import de.kif.backend.model.WorkGroup +import de.kif.backend.authenticateOrRedirect +import de.kif.backend.isAuthenticated +import de.kif.backend.repository.RoomRepository +import de.kif.backend.repository.ScheduleRepository +import de.kif.backend.repository.WorkGroupRepository import de.kif.backend.util.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate +import de.kif.common.CALENDAR_GRID_WIDTH +import de.kif.common.model.Permission +import de.kif.common.model.Room +import de.kif.common.model.Schedule import io.ktor.application.call import io.ktor.html.insert import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.get -import io.ktor.request.path import io.ktor.response.respondRedirect import io.ktor.routing.Route import io.ktor.routing.get -import io.ktor.sessions.get -import io.ktor.sessions.sessions -import kif.common.model.CALENDAR_GRID_WIDTH -import kif.common.model.MessageCreateCalendarEntry -import kif.common.model.MessageDeleteCalendarEntry import kotlinx.css.CSSBuilder import kotlinx.css.Color import kotlinx.css.pct import kotlinx.css.rem import kotlinx.html.* +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set import kotlin.math.max import kotlin.math.min const val MINUTES_OF_DAY = 24 * 60 -private fun DIV.calendarCell(schedule: Schedule?, day: Int, time: Int) { +private fun DIV.calendarCell(schedule: Schedule?) { if (schedule != null) { span("calendar-entry") { attributes["style"] = CSSBuilder().apply { @@ -50,42 +48,38 @@ private fun DIV.calendarCell(schedule: Schedule?, day: Int, time: Int) { val c = schedule.workGroup.track?.color if (c != null) { backgroundColor = Color(c.toString()) - color = Color(c.textColor.toString()) + color = Color(c.calcTextColor().toString()) } }.toString() attributes["data-language"] = schedule.workGroup.language.code - attributes["data-day"] = schedule.day.toString() - attributes["data-room"] = schedule.room.id.toString() - attributes["data-time"] = schedule.time.toString() - attributes["data-cell-time"] = time.toString() - attributes["data-workgroup"] = schedule.workGroup.id.toString() + attributes["data-id"] = schedule.id.toString() +schedule.workGroup.name div("calendar-tools") { a( classes = "calendar-tools-m10", - href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time - 10}/${schedule.workGroupId}" + href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time - 10}/${schedule.workGroup.id}" ) { +"-10" } a( classes = "calendar-tools-m5", - href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time - 5}/${schedule.workGroupId}" + href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time - 5}/${schedule.workGroup.id}" ) { +"-05" } a( classes = "calendar-tools-reset", - href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/$time/${schedule.workGroupId}" + href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH}/${schedule.workGroup.id}" ) { +"reset" } a( classes = "calendar-tools-p5", - href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time + 5}/${schedule.workGroupId}" + href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time + 5}/${schedule.workGroup.id}" ) { +"+05" } a( classes = "calendar-tools-p10", - href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time + 10}/${schedule.workGroupId}" + href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time + 10}/${schedule.workGroup.id}" ) { +"+10" } a( classes = "calendar-tools-del", - href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1" + href = "/calendar/${schedule.day}/${schedule.id}/delete" ) { +"del" } } } @@ -148,7 +142,7 @@ private fun DIV.renderTimeToRoom( val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() - calendarCell(schedule, day, time) + calendarCell(schedule) val href = if (allowEdit) "/calendar/$day/${room.id}/$start" else null a(href, classes = "calendar-link") @@ -216,7 +210,7 @@ private fun DIV.renderRoomToTime( val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() - calendarCell(schedule, day, time) + calendarCell(schedule) val href = if (allowEdit) "/calendar/$day/${room.id}/$start" else null a(href, classes = "calendar-link") @@ -233,37 +227,40 @@ fun Route.calendar() { call.respondRedirect("/calendar/0", true) } - get { param -> + get("/calendar/{day}/rtt") { call.response.cookies.append( "orientation", CalendarOrientation.ROOM_TO_TIME.name, maxAge = Int.MAX_VALUE, path = "/" ) - call.respondRedirect("/calendar/${param.day}") + val day = call.parameters["day"]?.toIntOrNull() ?: 0 + call.respondRedirect("/calendar/$day") } - get { param -> + get("/calendar/{day}/ttr") { call.response.cookies.append( "orientation", CalendarOrientation.TIME_TO_ROOM.name, maxAge = Int.MAX_VALUE, path = "/" ) - call.respondRedirect("/calendar/${param.day}") + val day = call.parameters["day"]?.toIntOrNull() ?: 0 + call.respondRedirect("/calendar/$day") } - get { param -> - val user = call.sessions.get()?.getUser(call) - val allowEdit = user?.checkPermission(Permission.SCHEDULE) ?: false - val rooms = Room.list() + get("/calendar/{day}") { + val user = isAuthenticated(Permission.SCHEDULE) + + val day = call.parameters["day"]?.toIntOrNull() ?: return@get + + val rooms = RoomRepository.all() val orientation = call.request.cookies["orientation"]?.let { name -> CalendarOrientation.values().find { it.name == name } } ?: CalendarOrientation.ROOM_TO_TIME - val day = param.day - val h = Schedule.getByDay(day) + val h = ScheduleRepository.getByDay(day) val schedules = h.groupBy { it.room }.mapValues { (_, it) -> it.associateBy { it.time @@ -327,7 +324,7 @@ fun Route.calendar() { max, rooms, schedules, - allowEdit + user != null ) CalendarOrientation.TIME_TO_ROOM -> renderTimeToRoom( day, @@ -335,7 +332,7 @@ fun Route.calendar() { max, rooms, schedules, - allowEdit + user != null ) } } @@ -345,15 +342,15 @@ fun Route.calendar() { } } - get { param -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.SCHEDULE)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val list = WorkGroup.list() - val room = Room.get(param.room) - val day = param.day - val time = param.time + get("/calendar/{day}/{room}/{time}") { + authenticateOrRedirect(Permission.SCHEDULE) { user -> + val day = call.parameters["day"]?.toIntOrNull() ?: return@get + val time = call.parameters["time"]?.toIntOrNull() ?: return@get + val roomId = call.parameters["room"]?.toLongOrNull() ?: return@get + val search = call.parameters["search"] ?: "" + + val list = WorkGroupRepository.all() + val room = RoomRepository.get(roomId) ?: return@get call.respondHtmlTemplate(MainTemplate()) { menuTemplate { @@ -363,12 +360,12 @@ fun Route.calendar() { content { h1 { +"Select work groups" } insert(TableTemplate()) { - searchValue = param.search + searchValue = search action { - a("/calendar/$day/${room.id}/$time/-1") { + a("/calendar/$day") { button(classes = "form-btn btn-primary") { - +"Delete" + +"Cancel" } } } @@ -395,7 +392,7 @@ fun Route.calendar() { } for (u in list) { - if (Search.match(param.search, u.name)) { + if (Search.match(search, u.name)) { val href = "/calendar/$day/${room.id}/$time/${u.id}" entry { attributes["data-search"] = Search.pack(u.name) @@ -427,46 +424,33 @@ fun Route.calendar() { } } } + get("/calendar/{day}/{room}/{time}/{workgroup}") { + authenticateOrRedirect(Permission.SCHEDULE) { user -> + val day = call.parameters["day"]?.toIntOrNull() ?: return@get + val time = call.parameters["time"]?.toIntOrNull() ?: return@get + val roomId = call.parameters["room"]?.toLongOrNull() ?: return@get + val workGroupId = call.parameters["workgroup"]?.toLongOrNull() ?: return@get - get { param -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.SCHEDULE)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - for (it in Schedule.getByRoom(param.room, param.day, param.time)) { - PushService.notify( - MessageDeleteCalendarEntry( - it.day, - it.time, - it.room.id, - it.workGroup.id - ) - ) - it.delete() - } - if (param.workGroup >= 0) { - val schedule = Schedule(param.workGroup, param.day, param.time, param.room) - schedule.save() - schedule.loadConstraints() + val room = RoomRepository.get(roomId) ?: return@get + val workGroup = WorkGroupRepository.get(workGroupId) ?: return@get - val cellTime = (schedule.time / 15) * 15 + val schedule = Schedule(null, workGroup, room, day, time) + ScheduleRepository.create(schedule) - PushService.notify( - MessageCreateCalendarEntry( - schedule.day, - schedule.time, - cellTime, - schedule.room.id, - schedule.workGroup.id, - schedule.workGroup.name, - schedule.workGroup.length, - schedule.workGroup.language.code, - schedule.workGroup.track?.color - ) - ) - } + val redirect = call.parameters["redirect"] + call.respondRedirect(redirect ?: "/calendar/$day") + } + } - call.respondRedirect(param.next ?: "/calendar/${param.day}") + get("/calendar/{day}/{schedule}/delete") { + authenticateOrRedirect(Permission.SCHEDULE) { user -> + val day = call.parameters["day"]?.toIntOrNull() ?: return@get + val scheduleId = call.parameters["schedule"]?.toLongOrNull() ?: return@get + + ScheduleRepository.delete(scheduleId) + + val redirect = call.parameters["redirect"] + call.respondRedirect(redirect ?: "/calendar/$day") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt b/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt index a71934a..c157043 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt @@ -1,19 +1,18 @@ package de.kif.backend.route -import de.kif.backend.LocationDashboard import de.kif.backend.PortalSession import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import io.ktor.application.call import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.get import io.ktor.routing.Route +import io.ktor.routing.get import io.ktor.sessions.get import io.ktor.sessions.sessions import kotlinx.html.h1 fun Route.dashboard() { - get { + get("") { val user = call.sessions.get()?.getUser(call) call.respondHtmlTemplate(MainTemplate()) { menuTemplate { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Login.kt b/src/jvmMain/kotlin/de/kif/backend/route/Login.kt index 7f4533a..6f7cf92 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Login.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Login.kt @@ -1,19 +1,17 @@ package de.kif.backend.route -import de.kif.backend.LocationLogin -import de.kif.backend.LocationLogout import de.kif.backend.PortalSession -import de.kif.backend.model.User +import de.kif.backend.UserPrinciple import de.kif.backend.view.MainTemplate import io.ktor.application.call import io.ktor.auth.authenticate import io.ktor.auth.principal import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.location import io.ktor.response.respondRedirect import io.ktor.routing.Route import io.ktor.routing.get import io.ktor.routing.post +import io.ktor.routing.route import io.ktor.sessions.clear import io.ktor.sessions.get import io.ktor.sessions.sessions @@ -21,12 +19,13 @@ import io.ktor.sessions.set import kotlinx.html.* fun Route.login() { - location { + route("login") { authenticate { post { - val principal = call.principal() ?: return@post - call.sessions.set(PortalSession(principal.id, principal.username)) - call.respondRedirect(call.parameters[LocationLogin::next.name] ?: "/") + val principal = call.principal() ?: return@post + if (principal.user.id == null) return@post + call.sessions.set(PortalSession(principal.user.id)) + call.respondRedirect(call.parameters["redirect"] ?: "/") } } @@ -41,43 +40,43 @@ fun Route.login() { form("/login", method = FormMethod.post) { div("form-group") { label { - htmlFor = LocationLogin::username.name + htmlFor = "username" +"Username" } input( - name = LocationLogin::username.name, + name = "username", classes = "form-control" ) { - id = LocationLogin::username.name + id = "username" placeholder = "Username" } } div("form-group") { label { - htmlFor = LocationLogin::password.name + htmlFor = "password" +"Password" } input( - name = LocationLogin::password.name, + name = "password", classes = "form-control", type = InputType.password ) { - id = LocationLogin::password.name + id = "password" placeholder = "Password" } } input( - name = LocationLogin::next.name, + name = "redirect", type = InputType.hidden ) { - value = call.parameters[LocationLogin::next.name] ?: "/" + value = call.parameters["redirect"] ?: "/" } button(type = ButtonType.submit, classes = "form-btn btn-primary") { +"Login" } } - if ("error" in call.parameters) { + if ("onFailure" in call.parameters) { br { } div("alert alert-danger") { +"Username or password incorrect!" @@ -88,15 +87,13 @@ fun Route.login() { } } } else { - call.respondRedirect(call.parameters[LocationLogin::next.name] ?: "/") + call.respondRedirect(call.parameters["redirect"] ?: "/") } } } - location { - get { - call.sessions.clear() - call.respondRedirect("/") - } + get("logout") { + call.sessions.clear() + call.respondRedirect("/") } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Person.kt b/src/jvmMain/kotlin/de/kif/backend/route/Person.kt deleted file mode 100644 index 77803d3..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/route/Person.kt +++ /dev/null @@ -1,258 +0,0 @@ -package de.kif.backend.route - -import de.kif.backend.LocationLogin -import de.kif.backend.LocationPerson -import de.kif.backend.PortalSession -import de.kif.backend.model.Permission -import de.kif.backend.model.Person -import de.kif.backend.util.Search -import de.kif.backend.view.MainTemplate -import de.kif.backend.view.MenuTemplate -import de.kif.backend.view.TableTemplate -import io.ktor.application.call -import io.ktor.html.insert -import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.get -import io.ktor.locations.post -import io.ktor.request.path -import io.ktor.request.receiveParameters -import io.ktor.response.respondRedirect -import io.ktor.routing.Route -import io.ktor.sessions.get -import io.ktor.sessions.sessions -import io.ktor.util.toMap -import kotlinx.html.* - -fun Route.person() { - get { param -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.PERSON)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val list = Person.list() - call.respondHtmlTemplate(MainTemplate()) { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.PERSON - } - content { - h1 { +"Persons" } - insert(TableTemplate()) { - searchValue = param.search - - action { - a("/person/new") { - button(classes="form-btn btn-primary") { - +"Add person" - } - } - } - - header { - th { - +"First name" - } - th { - +"Last name" - } - th(classes = "action") { - +"Action" - } - } - - for (u in list) { - if (Search.match(param.search, u.firstName, u.lastName)) { - entry { - attributes["data-search"] = Search.pack(u.firstName, u.lastName) - td { - +u.firstName - } - td { - +u.lastName - } - td(classes = "action") { - a("/person/${u.id}") { - i("material-icons") { +"edit" } - } - } - } - } - } - } - } - } - } - } - - get { personId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.PERSON)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val editPerson = Person.get(personId.id) ?: return@get - call.respondHtmlTemplate(MainTemplate()) { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.PERSON - } - content { - h1 { +"Edit person" } - form(method = FormMethod.post) { - div("form-group") { - label { - htmlFor = "first-name" - +"First name" - } - input( - name = "first-name", - classes = "form-control" - ) { - id = "first-name" - placeholder = "First name" - value = editPerson.firstName - } - } - div("form-group") { - label { - htmlFor = "last-name" - +"Last name" - } - input( - name = "last-name", - classes = "form-control" - ) { - id = "last-name" - placeholder = "Last name" - value = editPerson.lastName - } - } - - div("form-group") { - a("/person") { - button(classes = "form-btn") { - +"Cancel" - } - } - button(type = ButtonType.submit, classes = "form-btn btn-primary") { - +"Save" - } - } - } - a("/person/${editPerson.id}/delete") { - button(classes = "form-btn btn-danger") { - +"Delete" - } - } - } - } - } - } - - post { personId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.PERSON)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val params = call.receiveParameters().toMap().mapValues { (_, list) -> - list.firstOrNull() - } - val editPerson = Person.get(personId.id) ?: return@post - - params["first-name"]?.let { editPerson.firstName = it } - params["last-name"]?.let { editPerson.lastName = it } - - editPerson.save() - - call.respondRedirect("/person") - } - } - - get { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.PERSON)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - call.respondHtmlTemplate(MainTemplate()) { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.PERSON - } - content { - h1 { +"Create person" } - form(method = FormMethod.post) { - div("form-group") { - label { - htmlFor = "first-name" - +"First name" - } - input( - name = "first-name", - classes = "form-control" - ) { - id = "first-name" - placeholder = "First name" - value = "" - } - } - div("form-group") { - label { - htmlFor = "last-name" - +"Last name" - } - input( - name = "last-name", - classes = "form-control" - ) { - id = "last-name" - placeholder = "Last name" - value = "" - } - } - - div("form-group") { - a("/person") { - button(classes = "form-btn") { - +"Cancel" - } - } - button(type = ButtonType.submit, classes = "form-btn btn-primary") { - +"Create" - } - } - } - } - } - } - } - - post { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.PERSON)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val params = call.receiveParameters().toMap().mapValues { (_, list) -> - list.firstOrNull() - } - - val firstName = params["first-name"] ?: return@post - val lastName = params["last-name"] ?: return@post - - Person(firstName = firstName, lastName = lastName).save() - - call.respondRedirect("/person") - } - } - - get { personId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.PERSON)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val deletePerson = Person.get(personId.id) ?: return@get - - deletePerson.delete() - - call.respondRedirect("/person") - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt index fc887bb..d6c5ee4 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt @@ -1,35 +1,35 @@ package de.kif.backend.route -import de.kif.backend.LocationLogin -import de.kif.backend.LocationRoom -import de.kif.backend.PortalSession -import de.kif.backend.model.Permission -import de.kif.backend.model.Room +import de.kif.backend.authenticateOrRedirect +import de.kif.backend.repository.RoomRepository import de.kif.backend.util.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate +import de.kif.common.model.Permission +import de.kif.common.model.Room import io.ktor.application.call import io.ktor.html.insert import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.get -import io.ktor.locations.post -import io.ktor.request.path import io.ktor.request.receiveParameters import io.ktor.response.respondRedirect import io.ktor.routing.Route -import io.ktor.sessions.get -import io.ktor.sessions.sessions +import io.ktor.routing.get +import io.ktor.routing.post import io.ktor.util.toMap import kotlinx.html.* +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.firstOrNull +import kotlin.collections.mapValues +import kotlin.collections.set fun Route.room() { - get { param -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.ROOM)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val list = Room.list() + + get("/rooms") { + authenticateOrRedirect(Permission.ROOM) { user -> + val search = call.parameters["search"] ?: "" + val list = RoomRepository.all() call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -38,11 +38,11 @@ fun Route.room() { content { h1 { +"Rooms" } insert(TableTemplate()) { - searchValue = param.search + searchValue = search action { a("/room/new") { - button(classes="form-btn btn-primary") { + button(classes = "form-btn btn-primary") { +"Add room" } } @@ -64,7 +64,7 @@ fun Route.room() { } for (u in list) { - if (Search.match(param.search, u.name)) { + if (Search.match(search, u.name)) { entry { attributes["data-search"] = Search.pack(u.name) td { @@ -90,12 +90,10 @@ fun Route.room() { } } - get { roomId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.ROOM)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val editRoom = Room.get(roomId.id) + get("/room/{id}") { + authenticateOrRedirect(Permission.ROOM) { user -> + val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get + val editRoom = RoomRepository.get(roomId) ?: return@get call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -175,31 +173,26 @@ fun Route.room() { } } - post { roomId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.ROOM)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + post("/room/{id}") { + authenticateOrRedirect(Permission.ROOM) { user -> + val roomId = call.parameters["id"]?.toLongOrNull() ?: return@post val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } - val editRoom = Room.get(roomId.id) + var room = RoomRepository.get(roomId) ?: return@post - params["name"]?.let { editRoom.name = it } - params["places"]?.let { editRoom.places = it.toIntOrNull() ?: 0 } - params["projector"]?.let { editRoom.projector = it == "on" } + params["name"]?.let { room = room.copy(name = it) } + params["places"]?.let { room = room.copy(places = it.toIntOrNull() ?: 0) } + params["projector"]?.let { room = room.copy(projector = it == "on") } - editRoom.save() + RoomRepository.update(room) - call.respondRedirect("/room") + call.respondRedirect("/rooms") } } - get { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.ROOM)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + get("/room/new") { + authenticateOrRedirect(Permission.ROOM) { user -> call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -274,11 +267,8 @@ fun Route.room() { } } - post { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.ROOM)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + post("/room/new") { + authenticateOrRedirect(Permission.ROOM) { user -> val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } @@ -287,22 +277,21 @@ fun Route.room() { val places = (params["places"] ?: return@post).toIntOrNull() ?: 0 val projector = params["projector"] == "on" - Room(name = name, places = places, projector = projector).save() + val room = Room(null, name, places, projector) - call.respondRedirect("/room") + RoomRepository.create(room) + + call.respondRedirect("/rooms") } } - get { roomId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.ROOM)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val deleteRoom = Room.get(roomId.id) + get("/room/{id}/delete") { + authenticateOrRedirect(Permission.ROOM) { user -> + val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get - deleteRoom.delete() + RoomRepository.delete(roomId) - call.respondRedirect("/room") + call.respondRedirect("/rooms") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Setup.kt b/src/jvmMain/kotlin/de/kif/backend/route/Setup.kt deleted file mode 100644 index 73029c7..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/route/Setup.kt +++ /dev/null @@ -1,101 +0,0 @@ -package de.kif.backend.route - -import de.kif.backend.LocationLogin -import de.kif.backend.database.DbUser -import de.kif.backend.model.Permission -import de.kif.backend.model.User -import de.kif.backend.view.MainTemplate -import io.ktor.application.call -import io.ktor.html.respondHtmlTemplate -import io.ktor.request.receiveParameters -import io.ktor.response.respondRedirect -import io.ktor.routing.Route -import io.ktor.routing.get -import io.ktor.routing.post -import io.ktor.routing.route -import kotlinx.html.* -import org.jetbrains.exposed.sql.selectAll -import org.jetbrains.exposed.sql.transactions.transaction - -fun Route.setup() { - route("/") { - get { - call.respondHtmlTemplate(MainTemplate()) { - transaction { - val firstStart = DbUser.selectAll().count() == 0 - - menuTemplate { - setup = true - } - - if (firstStart) { - content { - div { - h1 { +"Create account" } - form("/", method = FormMethod.post) { - div("form-group") { - label { - htmlFor = LocationLogin::username.name - +"Username" - } - input( - name = LocationLogin::username.name, - classes = "form-control" - ) { - id = LocationLogin::username.name - placeholder = "Username" - } - } - div("form-group") { - label { - htmlFor = LocationLogin::password.name - +"Password" - } - input( - name = LocationLogin::password.name, - classes = "form-control", - type = InputType.password - ) { - id = LocationLogin::password.name - placeholder = "Password" - } - } - button(type = ButtonType.submit, classes = "btn btn-primary") { - +"Create" - } - } - } - } - } else { - content { - div { - h1 { +"Setup complete" } - p { - +"Please restart the server!" - } - } - } - } - } - } - } - - post { - val parameters = call.receiveParameters() - val username = parameters[LocationLogin::username.name] - val password = parameters[LocationLogin::password.name] - - if (username == null || password == null) { - call.respondRedirect("/") - return@post - } - - User.create(username, password, Permission.values().toSet()) - call.respondRedirect("/") - } - } - - get("*") { - call.respondRedirect("/") - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt index fa7cbb7..50e8f2e 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt @@ -1,29 +1,27 @@ package de.kif.backend.route -import de.kif.backend.LocationLogin -import de.kif.backend.LocationTrack -import de.kif.backend.PortalSession -import de.kif.backend.model.Permission -import de.kif.backend.model.Track + +import de.kif.backend.authenticateOrRedirect +import de.kif.backend.repository.TrackRepository import de.kif.backend.util.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate +import de.kif.common.model.Color +import de.kif.common.model.Permission +import de.kif.common.model.Track import io.ktor.application.call import io.ktor.html.insert import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.get -import io.ktor.locations.post -import io.ktor.request.path import io.ktor.request.receiveParameters import io.ktor.response.respondRedirect import io.ktor.routing.Route -import io.ktor.sessions.get -import io.ktor.sessions.sessions +import io.ktor.routing.get +import io.ktor.routing.post import io.ktor.util.toMap -import kif.common.model.Color import kotlinx.css.CSSBuilder import kotlinx.html.* +import kotlin.collections.set import kotlin.random.Random fun DIV.colorPicker(color: Color?) { @@ -85,12 +83,10 @@ fun DIV.colorPicker(color: Color?) { } fun Route.track() { - get { param -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val list = Track.list() + get("/tracks") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val search = call.parameters["search"] ?: "" + val list = TrackRepository.all() call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -99,7 +95,7 @@ fun Route.track() { content { h1 { +"Tracks" } insert(TableTemplate()) { - searchValue = param.search + searchValue = search action { a("/track/new") { @@ -122,7 +118,7 @@ fun Route.track() { } for (u in list) { - if (Search.match(param.search, u.name)) { + if (Search.match(search, u.name)) { entry { attributes["data-search"] = Search.pack(u.name) td { @@ -145,12 +141,10 @@ fun Route.track() { } } - get { trackId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val editTrack = Track.get(trackId.id) + get("/track/{id}") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get + val editTrack = TrackRepository.get(trackId) ?: return@get call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -198,33 +192,28 @@ fun Route.track() { } } - post { trackId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + post("/track/{id}") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val trackId = call.parameters["id"]?.toLongOrNull() ?: return@post val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } - val editTrack = Track.get(trackId.id) + var editTrack = TrackRepository.get(trackId) ?: return@post - params["name"]?.let { editTrack.name = it } + params["name"]?.let { editTrack = editTrack.copy(name = it) } - editTrack.color = (params["color"] ?: return@post).let { c -> + editTrack = editTrack.copy(color = (params["color"] ?: return@post).let { c -> Color.default.find { it.first == c } - }?.second ?: (params["color-input"] ?: return@post).let(Color.Companion::parse) + }?.second ?: (params["color-input"] ?: return@post).let(Color.Companion::parse)) - editTrack.save() + TrackRepository.update(editTrack) - call.respondRedirect("/track") + call.respondRedirect("/tracks") } } - get { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + get("/track/new") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -266,11 +255,8 @@ fun Route.track() { } } - post { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + post("/track/new") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } @@ -280,25 +266,21 @@ fun Route.track() { Color.default.find { it.first == c } }?.second ?: (params["color-input"] ?: return@post).let(Color.Companion::parse) - Track( - name = name, - color = color - ).save() + val track = Track(null, name, color) - call.respondRedirect("/track") + TrackRepository.create(track) + + call.respondRedirect("/tracks") } } - get { trackId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val deleteTrack = Track.get(trackId.id) + get("track/{id}/delete") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get - deleteTrack.delete() + TrackRepository.delete(trackId) - call.respondRedirect("/track") + call.respondRedirect("/tracks") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/User.kt index 4dc6b4c..0fe31f0 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/User.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/User.kt @@ -1,35 +1,40 @@ package de.kif.backend.route -import de.kif.backend.LocationLogin -import de.kif.backend.LocationUser -import de.kif.backend.PortalSession -import de.kif.backend.model.Permission -import de.kif.backend.model.User + +import de.kif.backend.authenticateOrRedirect +import de.kif.backend.hashPassword +import de.kif.backend.repository.UserRepository import de.kif.backend.util.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate +import de.kif.common.model.Permission +import de.kif.common.model.User import io.ktor.application.call import io.ktor.html.insert import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.get -import io.ktor.locations.post -import io.ktor.request.path import io.ktor.request.receiveParameters import io.ktor.response.respondRedirect import io.ktor.routing.Route -import io.ktor.sessions.get -import io.ktor.sessions.sessions +import io.ktor.routing.get +import io.ktor.routing.post import io.ktor.util.toMap import kotlinx.html.* +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.filter +import kotlin.collections.firstOrNull +import kotlin.collections.joinToString +import kotlin.collections.mapNotNull +import kotlin.collections.mapValues +import kotlin.collections.set +import kotlin.collections.toSet fun Route.user() { - get { param -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.USER)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val list = User.list() + get("/users") { param -> + authenticateOrRedirect(Permission.USER) { user -> + val search = call.parameters["search"] ?: "" + val list = UserRepository.all() call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -38,11 +43,11 @@ fun Route.user() { content { h1 { +"Users" } insert(TableTemplate()) { - searchValue = param.search + searchValue = search action { a("/user/new") { - button(classes="form-btn btn-primary") { + button(classes = "form-btn btn-primary") { +"Add user" } } @@ -61,7 +66,7 @@ fun Route.user() { } for (u in list) { - if (Search.match(param.search, u.username)) { + if (Search.match(search, u.username)) { entry { attributes["data-search"] = Search.pack(u.username) td { @@ -84,12 +89,10 @@ fun Route.user() { } } - get { userId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.USER)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val editUser = User.get(userId.id) ?: return@get + get("/user/{id}") { + authenticateOrRedirect(Permission.USER) { user -> + val userId = call.parameters["id"]?.toLongOrNull() ?: return@get + val editUser = UserRepository.get(userId) ?: return@get call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -158,39 +161,31 @@ fun Route.user() { } } - post { userId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.USER)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + post("/user/{id}") { + authenticateOrRedirect(Permission.USER) { user -> + val userId = call.parameters["id"]?.toLongOrNull() ?: return@post val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } - val editUser = User.get(userId.id) ?: return@post - params["username"]?.let { editUser.username = it } + var editUser = UserRepository.get(userId) ?: return@post - for (permission in Permission.values()) { + params["username"]?.let { editUser = editUser.copy(username = it) } + + val permissions = Permission.values().filter { permission -> val name = permission.toString().toLowerCase() - if (user.checkPermission(permission)) { - if (params["permission-$name"] == "on") { - editUser.permissions += permission - } else { - editUser.permissions -= permission - } - } - } - editUser.save() + user.checkPermission(permission) && params["permission-$name"] == "on" + }.toSet() + editUser = editUser.copy(permissions = permissions) - call.respondRedirect("/user") + UserRepository.update(user) + + call.respondRedirect("/users") } } - get { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.USER)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + get("/user/new") { + authenticateOrRedirect(Permission.USER) { user -> call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -269,11 +264,8 @@ fun Route.user() { } } - post { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.USER)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + post("/user/new") { + authenticateOrRedirect(Permission.USER) { user -> val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } @@ -288,26 +280,26 @@ fun Route.user() { null }.toSet() - User.create(username, password, permissions) + val newUser = User(null, username, hashPassword(password), permissions) - call.respondRedirect("/user") + UserRepository.create(newUser) + + call.respondRedirect("/users") } } - get { userId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.USER)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val deleteUser = User.get(userId.id) ?: return@get + get("/user/{id}/delete") { + authenticateOrRedirect(Permission.USER) { user -> + val userId = call.parameters["id"]?.toLongOrNull() ?: return@get + val deleteUser = UserRepository.get(userId) ?: return@get if (user.checkPermission(Permission.USER) && (Permission.ADMIN !in deleteUser.permissions || Permission.ADMIN in user.permissions) ) { - deleteUser.delete() + UserRepository.delete(userId) } - call.respondRedirect("/user") + call.respondRedirect("/users") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index c4226f3..7f4efea 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -1,37 +1,38 @@ package de.kif.backend.route -import de.kif.backend.LocationLogin -import de.kif.backend.LocationWorkGroup -import de.kif.backend.PortalSession -import de.kif.backend.database.Language -import de.kif.backend.model.Permission -import de.kif.backend.model.Track -import de.kif.backend.model.WorkGroup + +import de.kif.backend.authenticateOrRedirect +import de.kif.backend.repository.TrackRepository +import de.kif.backend.repository.WorkGroupRepository import de.kif.backend.util.Search import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate +import de.kif.common.model.Language +import de.kif.common.model.Permission +import de.kif.common.model.WorkGroup import io.ktor.application.call import io.ktor.html.insert import io.ktor.html.respondHtmlTemplate -import io.ktor.locations.get -import io.ktor.locations.post -import io.ktor.request.path import io.ktor.request.receiveParameters import io.ktor.response.respondRedirect import io.ktor.routing.Route -import io.ktor.sessions.get -import io.ktor.sessions.sessions +import io.ktor.routing.get +import io.ktor.routing.post import io.ktor.util.toMap import kotlinx.html.* +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.find +import kotlin.collections.firstOrNull +import kotlin.collections.mapValues +import kotlin.collections.set fun Route.workGroup() { - get { param -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val list = WorkGroup.list() + get("workgroups") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val search = call.parameters["search"] ?: "" + val list = WorkGroupRepository.all() call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -40,10 +41,10 @@ fun Route.workGroup() { content { h1 { +"Work groups" } insert(TableTemplate()) { - searchValue = param.search + searchValue = search action { - a("/track") { + a("/tracks") { button(classes = "form-btn") { +"Edit tracks" } @@ -83,7 +84,7 @@ fun Route.workGroup() { } for (u in list) { - if (Search.match(param.search, u.name)) { + if (Search.match(search, u.name)) { entry { attributes["data-search"] = Search.pack(u.name) td { @@ -121,13 +122,11 @@ fun Route.workGroup() { } } - get { workGroupId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val editWorkGroup = WorkGroup.get(workGroupId.id) - val tracks = Track.list() + get("/workgroup/{id}") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get + val editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@get + val tracks = TrackRepository.all() call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -179,20 +178,20 @@ fun Route.workGroup() { name = "track" option { - selected = (editWorkGroup.trackId ?: -1) < 0 + selected = (editWorkGroup.track?.id ?: -1) < 0 value = "-1" +"None" } for (track in tracks) { option { - selected = editWorkGroup.trackId == track.id + selected = editWorkGroup.track?.id == track.id value = track.id.toString() +track.name } } } - a("/track", classes = "form-btn") { + a("/tracks", classes = "form-btn") { i("material-icons") { +"edit" } } } @@ -228,8 +227,8 @@ fun Route.workGroup() { for (language in Language.values()) { option { selected = editWorkGroup.language == language - value = language.name - +language.toString() + value = language.code + +language.localeName } } } @@ -287,36 +286,37 @@ fun Route.workGroup() { } } - post { workGroupId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + post("/workgroup/{id}") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@post val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } - val editWorkGroup = WorkGroup.get(workGroupId.id) + var editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@post - params["name"]?.let { editWorkGroup.name = it } - params["interested"]?.toIntOrNull()?.let { editWorkGroup.interested = it } - params["track"]?.toIntOrNull()?.let { editWorkGroup.trackId = it } - params["projector"]?.let { editWorkGroup.projector = it == "on" } - params["resolution"]?.let { editWorkGroup.resolution = it == "on" } - params["length"]?.toIntOrNull()?.let { editWorkGroup.length = it } - params["language"]?.let { editWorkGroup.language = Language.values().find { l -> l.name == it } ?: Language.GERMAN } + params["name"]?.let { editWorkGroup = editWorkGroup.copy(name = it) } + params["interested"]?.toIntOrNull()?.let { editWorkGroup = editWorkGroup.copy(interested = it) } + params["track"]?.toLongOrNull()?.let { + val track = TrackRepository.get(it) + editWorkGroup = editWorkGroup.copy(track = track) + } + params["projector"]?.let { editWorkGroup = editWorkGroup.copy(projector = it == "on") } + params["resolution"]?.let { editWorkGroup = editWorkGroup.copy(resolution = it == "on") } + params["length"]?.toIntOrNull()?.let { editWorkGroup = editWorkGroup.copy(length = it) } + params["language"]?.let { + editWorkGroup = + editWorkGroup.copy(language = Language.values().find { l -> l.code == it } ?: Language.GERMAN) + } - editWorkGroup.save() + WorkGroupRepository.update(editWorkGroup) - call.respondRedirect("/workgroup") + call.respondRedirect("/workgroups") } } - get { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val tracks = Track.list() + get("/workgroup/new") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val tracks = TrackRepository.all() call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -410,8 +410,8 @@ fun Route.workGroup() { for (language in Language.values()) { option { selected = language == Language.GERMAN - value = language.name - +language.toString() + value = language.code + +language.localeName } } } @@ -464,47 +464,46 @@ fun Route.workGroup() { } } - post { - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { + post("/workgroup/new") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } val name = params["name"] ?: return@post val interested = (params["interested"] ?: return@post).toIntOrNull() ?: 0 - val trackId = (params["track"] ?: return@post).toIntOrNull() + val track = (params["track"] ?: return@post).toLongOrNull()?.let { TrackRepository.get(it) } val projector = params["projector"] == "on" val resolution = params["resolution"] == "on" val length = (params["length"] ?: return@post).toIntOrNull() ?: 0 - val language = (params["language"] ?: return@post).let { Language.values().find { l -> l.name == it } ?: Language.GERMAN } + val language = (params["language"] ?: return@post).let { + Language.values().find { l -> l.code == it } ?: Language.GERMAN + } - WorkGroup( + val workGroup = WorkGroup( + null, name = name, interested = interested, - trackId = trackId, + track = track, projector = projector, resolution = resolution, length = length, language = language - ).save() + ) - call.respondRedirect("/workgroup") + WorkGroupRepository.create(workGroup) + + call.respondRedirect("/workgroups") } } - get { workGroupId -> - val user = call.sessions.get()?.getUser(call) - if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { - call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") - } else { - val deleteWorkGroup = WorkGroup.get(workGroupId.id) + get("/workgroup/{id}/delete") { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> + val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get - deleteWorkGroup.delete() + WorkGroupRepository.delete(workGroupId) - call.respondRedirect("/workgroup") + call.respondRedirect("/workgroups") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/Authenticate.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/Authenticate.kt new file mode 100644 index 0000000..abe8d46 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/Authenticate.kt @@ -0,0 +1,61 @@ +package de.kif.backend.route.api + +import de.kif.backend.PortalSession +import de.kif.backend.checkPassword +import de.kif.backend.repository.UserRepository +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.response.respond +import io.ktor.routing.Route +import io.ktor.routing.post +import io.ktor.sessions.sessions +import io.ktor.sessions.set + +data class Credentials( + val username: String, + val password: String +) + +fun Route.authenticateApi() { + post("/api/authenticate") { + val credentials = call.receive() + + val user = UserRepository.find(credentials.username) + + if (user?.id == null || !checkPassword(credentials.password, user.password)) { + call.error(HttpStatusCode.Unauthorized) + return@post + } + + call.sessions.set(PortalSession(user.id)) + call.respond(HttpStatusCode.OK, mapOf("OK" to true)) + } +} + +suspend fun ApplicationCall.success(data: Any? = null) { + val map: Map + + if (data == null) { + map = mapOf("OK" to true) + } else { + map = mapOf( + "OK" to true, + "data" to data + ) + } + + respond(HttpStatusCode.OK, map) +} + +suspend fun ApplicationCall.error(code: HttpStatusCode) { + respond( + code, + mapOf( + "OK" to false, + "errorCode" to code.value, + "errorDescription" to code.description + ) + ) +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/Room.kt new file mode 100644 index 0000000..58c8665 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/Room.kt @@ -0,0 +1,92 @@ +package de.kif.backend.route.api + +import de.kif.backend.authenticate +import de.kif.backend.repository.RoomRepository +import de.kif.common.model.Permission +import de.kif.common.model.Room +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.routing.Route +import io.ktor.routing.get +import io.ktor.routing.post + +fun Route.roomApi() { + get("/api/rooms") { + try { + val rooms = RoomRepository.all() + call.success(rooms) + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/rooms") { + try { + authenticate(Permission.ROOM) { + val room = call.receive() + + val id = RoomRepository.create(room) + + call.success(mapOf("id" to id)) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + get("/api/room/{id}") { + try { + val id = call.parameters["id"]?.toLongOrNull() + val room = id?.let { RoomRepository.get(it) } + + if (room != null) { + call.success(room) + } else { + call.error(HttpStatusCode.NotFound) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/room/{id}") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + val room = call.receive().copy(id = id) + + if (room.id != null) { + RoomRepository.update(room) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + post("/api/room/{id}/delete") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + + if (id != null) { + RoomRepository.delete(id) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/Schedule.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/Schedule.kt new file mode 100644 index 0000000..63df478 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/Schedule.kt @@ -0,0 +1,121 @@ +package de.kif.backend.route.api + +import de.kif.backend.authenticate +import de.kif.backend.repository.RoomRepository +import de.kif.backend.repository.ScheduleRepository +import de.kif.common.model.Permission +import de.kif.common.model.Schedule +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.routing.Route +import io.ktor.routing.get +import io.ktor.routing.post + +data class ScheduleMove(val day: Int, val time: Int, val roomId: Long) + +fun Route.scheduleApi() { + get("/api/schedules") { + try { + val schedules = ScheduleRepository.all() + call.success(schedules) + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/schedules") { + try { + authenticate(Permission.SCHEDULE) { + val schedule = call.receive() + + val id = ScheduleRepository.create(schedule) + + call.success(mapOf("id" to id)) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + get("/api/schedule/{id}") { + try { + val id = call.parameters["id"]?.toLongOrNull() + val schedule = id?.let { ScheduleRepository.get(it) } + + if (schedule != null) { + call.success(schedule) + } else { + call.error(HttpStatusCode.NotFound) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/schedule/{id}") { + try { + authenticate(Permission.SCHEDULE) { + val id = call.parameters["id"]?.toLongOrNull() + val schedule = call.receive().copy(id = id) + + if (schedule.id != null) { + ScheduleRepository.update(schedule) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/schedule/{id}/delete") { + try { + authenticate(Permission.SCHEDULE) { + val id = call.parameters["id"]?.toLongOrNull() + + if (id != null) { + ScheduleRepository.delete(id) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/schedule/{id}/move") { + try { + authenticate(Permission.SCHEDULE) { + val id = call.parameters["id"]?.toLongOrNull() + val scheduleMove = call.receive() + var schedule = id?.let { ScheduleRepository.get(it) } + val room = RoomRepository.get(scheduleMove.roomId) + + if (schedule != null && room != null) { + schedule = schedule.copy(day = scheduleMove.day, time = scheduleMove.time, room = room) + + ScheduleRepository.update(schedule) + + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/Track.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/Track.kt new file mode 100644 index 0000000..4a7d7d1 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/Track.kt @@ -0,0 +1,92 @@ +package de.kif.backend.route.api + +import de.kif.backend.authenticate +import de.kif.backend.repository.TrackRepository +import de.kif.common.model.Permission +import de.kif.common.model.Track +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.routing.Route +import io.ktor.routing.get +import io.ktor.routing.post + +fun Route.trackApi() { + get("/api/tracks") { + try { + val tracks = TrackRepository.all() + call.success(tracks) + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/tracks") { + try { + authenticate(Permission.WORK_GROUP) { + val track = call.receive() + + val id = TrackRepository.create(track) + + call.success(mapOf("id" to id)) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + get("/api/track/{id}") { + try { + val id = call.parameters["id"]?.toLongOrNull() + val track = id?.let { TrackRepository.get(it) } + + if (track != null) { + TrackRepository.update(track) + call.success(track) + } else { + call.error(HttpStatusCode.NotFound) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/track/{id}") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + val track = call.receive().copy(id = id) + + if (track.id != null) { + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + post("/api/track/{id}/delete") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + + if (id != null) { + TrackRepository.delete(id) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/User.kt new file mode 100644 index 0000000..d485fad --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/User.kt @@ -0,0 +1,99 @@ +package de.kif.backend.route.api + +import de.kif.backend.authenticate +import de.kif.backend.repository.UserRepository +import de.kif.common.model.Permission +import de.kif.common.model.User +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.routing.Route +import io.ktor.routing.get +import io.ktor.routing.post + +fun Route.userApi() { + get("/api/users") { + try { + authenticate(Permission.USER) { + val users = UserRepository.all() + call.success(users) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/users") { + try { + authenticate(Permission.USER) { + val user = call.receive() + + val id = UserRepository.create(user) + + call.success(mapOf("id" to id)) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + get("/api/user/{id}") { + try { + authenticate(Permission.USER) { + val id = call.parameters["id"]?.toLongOrNull() + val user = id?.let { UserRepository.get(it) } + + if (user != null) { + call.success(user) + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/user/{id}") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + val user = call.receive().copy(id = id) + + if (user.id != null) { + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + post("/api/user/{id}/delete") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + + if (id != null) { + UserRepository.delete(id) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/WorkGroup.kt new file mode 100644 index 0000000..5260b8c --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/WorkGroup.kt @@ -0,0 +1,92 @@ +package de.kif.backend.route.api + +import de.kif.backend.authenticate +import de.kif.backend.repository.WorkGroupRepository +import de.kif.common.model.Permission +import de.kif.common.model.WorkGroup +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.routing.Route +import io.ktor.routing.get +import io.ktor.routing.post + +fun Route.workGroupApi() { + get("/api/workgroups") { + try { + val workGroups = WorkGroupRepository.all() + call.success(workGroups) + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/workgroups") { + try { + authenticate(Permission.WORK_GROUP) { + val workGroup = call.receive() + + val id = WorkGroupRepository.create(workGroup) + + call.success(mapOf("id" to id)) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + get("/api/workgroup/{id}") { + try { + val id = call.parameters["id"]?.toLongOrNull() + val workGroup = id?.let { WorkGroupRepository.get(it) } + + if (workGroup != null) { + call.success(workGroup) + } else { + call.error(HttpStatusCode.NotFound) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + post("/api/workgroup/{id}") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + val workGroup = call.receive().copy(id = id) + + if (workGroup.id != null) { + WorkGroupRepository.update(workGroup) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + post("/api/workgroup/{id}/delete") { + try { + authenticate { + val id = call.parameters["id"]?.toLongOrNull() + + if (id != null) { + WorkGroupRepository.delete(id) + call.success() + } else { + call.error(HttpStatusCode.NotFound) + } + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/PushService.kt b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt similarity index 66% rename from src/jvmMain/kotlin/de/kif/backend/route/PushService.kt rename to src/jvmMain/kotlin/de/kif/backend/util/PushService.kt index ea56c29..8977931 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/PushService.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt @@ -1,22 +1,22 @@ -package de.kif.backend.route +package de.kif.backend.util +import de.kif.backend.repository.* +import de.kif.common.* +import de.kif.common.RepositoryType import io.ktor.http.cio.websocket.Frame import io.ktor.http.cio.websocket.readText import io.ktor.routing.Route import io.ktor.websocket.WebSocketServerSession import io.ktor.websocket.webSocket -import kif.common.model.Message -import kif.common.model.MessageType import kotlinx.coroutines.channels.ClosedReceiveChannelException -import java.lang.Exception object PushService { var clients: List = emptyList() - suspend fun notify(messageType: MessageType) { + suspend fun notify(type: MessageType, repository: RepositoryType, id: Long) { try { - val data = Message(messageType).stringify() + val data = Message(type, repository, id).stringify() for (client in clients) { client.outgoing.send(Frame.Text(data)) } @@ -39,9 +39,15 @@ fun Route.pushService() { } catch (_: ClosedReceiveChannelException) { PushService.clients -= this } catch (e: Throwable) { - println("onError ${closeReason.await()}") + println("onFailure ${closeReason.await()}") e.printStackTrace() PushService.clients -= this } } + + RoomRepository.registerPushService() + ScheduleRepository.registerPushService() + TrackRepository.registerPushService() + UserRepository.registerPushService() + WorkGroupRepository.registerPushService() } \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt index 89e91a0..e0fe5f4 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt @@ -1,13 +1,12 @@ package de.kif.backend.view -import de.kif.backend.model.Permission -import de.kif.backend.model.User +import de.kif.common.model.Permission +import de.kif.common.model.User import io.ktor.html.Template import kotlinx.html.* class MenuTemplate() : Template { - var setup = false var active: Tab = Tab.DASHBOARD var user: User? = null @@ -15,55 +14,42 @@ class MenuTemplate() : Template { nav("menu") { div("container") { div("menu-left") { - if (setup) { - a(classes = "active") { - +"Setup" - } - } else { - a("/", classes = if (active == Tab.DASHBOARD) "active" else null) { - +"Dashboard" - } - a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) { - +"Calendar" - } + a("/", classes = if (active == Tab.DASHBOARD) "active" else null) { + +"Dashboard" + } + a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) { + +"Calendar" } } - if (!setup) { - div("menu-right") { - span("menu-icon") { - i("material-icons") { +"menu" } - } - val user = user - div("menu-content") { - if (user == null) { - a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { - +"Login" + div("menu-right") { + span("menu-icon") { + i("material-icons") { +"menu" } + } + val user = user + div("menu-content") { + if (user == null) { + a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { + +"Login" + } + } else { + if (user.checkPermission(Permission.WORK_GROUP)) { + a("/workgroups", classes = if (active == Tab.WORK_GROUP) "active" else null) { + +"Work groups" } - } else { - if (user.checkPermission(Permission.WORK_GROUP)) { - a("/workgroup", classes = if (active == Tab.WORK_GROUP) "active" else null) { - +"Work groups" - } + } + if (user.checkPermission(Permission.ROOM)) { + a("/rooms", classes = if (active == Tab.ROOM) "active" else null) { + +"Rooms" } - if (user.checkPermission(Permission.ROOM)) { - a("/room", classes = if (active == Tab.ROOM) "active" else null) { - +"Rooms" - } - } - if (user.checkPermission(Permission.PERSON)) { - a("/person", classes = if (active == Tab.PERSON) "active" else null) { - +"Persons" - } - } - if (user.checkPermission(Permission.PERSON)) { - a("/user", classes = if (active == Tab.USER) "active" else null) { - +"Users" - } - } - a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { - +user.username + } + if (user.checkPermission(Permission.PERSON)) { + a("/users", classes = if (active == Tab.USER) "active" else null) { + +"Users" } } + a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { + +user.username + } } } } diff --git a/src/jvmTest/resources/test.http b/src/jvmTest/resources/test.http new file mode 100644 index 0000000..acede2e --- /dev/null +++ b/src/jvmTest/resources/test.http @@ -0,0 +1,38 @@ +GET http://localhost:8080/api/rooms + +### + +POST http://localhost:8080/api/authenticate +Content-Type: application/json + +{ + "username": "admin", + "password": "admin" +} + + +> {% + client.assert(response.body.OK === true, "Login failed!"); + client.global.set("auth_token", response.headers.valueOf("Set-Cookie")); + %} + +### + +POST http://localhost:8080/api/rooms +Content-Type: application/json +Cookie: {{auth_token}} + +{ + "name": "E007", + "places": 20, + "projector": true +} + +### + + +POST http://localhost:8080/api/room/2/delete +Content-Type: application/json +Cookie: {{auth_token}} + +### \ No newline at end of file