From afbced61e3dcd88bb3d2504544ecaca19e26e817 Mon Sep 17 00:00:00 2001 From: Lars Westermann Date: Mon, 10 Jun 2019 10:09:36 +0200 Subject: [PATCH] Add path prefix --- portal.toml | 2 + .../kotlin/de/kif/frontend/WebSocketClient.kt | 3 +- .../kif/frontend/repository/PostRepository.kt | 16 +- .../kif/frontend/repository/RoomRepository.kt | 12 +- .../frontend/repository/ScheduleRepository.kt | 16 +- .../frontend/repository/TrackRepository.kt | 12 +- .../kif/frontend/repository/UserRepository.kt | 12 +- .../repository/WorkGroupRepository.kt | 12 +- .../frontend/views/WorkGroupConstraints.kt | 10 +- .../kif/frontend/views/calendar/Calendar.kt | 11 +- .../frontend/views/calendar/CalendarBody.kt | 13 +- .../frontend/views/calendar/CalendarEntry.kt | 24 ++- src/jsMain/resources/style/_config.scss | 4 + .../resources/style/components/_calendar.scss | 21 ++- .../resources/style/components/_menu.scss | 2 +- .../resources/style/components/_overview.scss | 2 +- .../resources/style/components/_wall.scss | 7 + src/jsMain/resources/style/style.scss | 6 +- .../kotlin/de/kif/backend/Application.kt | 91 +++++++----- .../kotlin/de/kif/backend/Configuration.kt | 4 + src/jvmMain/kotlin/de/kif/backend/Main.kt | 2 +- src/jvmMain/kotlin/de/kif/backend/Security.kt | 7 +- .../kotlin/de/kif/backend/route/Account.kt | 21 +-- .../kotlin/de/kif/backend/route/Board.kt | 31 ++-- .../kotlin/de/kif/backend/route/Calendar.kt | 137 ++++++++++-------- .../kotlin/de/kif/backend/route/Login.kt | 12 +- .../kotlin/de/kif/backend/route/News.kt | 17 ++- .../kotlin/de/kif/backend/route/Room.kt | 17 ++- .../kotlin/de/kif/backend/route/Track.kt | 18 +-- .../kotlin/de/kif/backend/route/User.kt | 65 ++++++--- .../kotlin/de/kif/backend/route/Wall.kt | 27 ++-- .../kotlin/de/kif/backend/route/WorkGroup.kt | 37 +++-- .../kotlin/de/kif/backend/util/PushService.kt | 3 +- .../de/kif/backend/view/MainTemplate.kt | 41 ++++-- .../de/kif/backend/view/MenuTemplate.kt | 16 +- src/jvmMain/resources/error.html | 76 +++++++++- src/jvmMain/resources/error404.html | 76 +++++++++- src/jvmMain/resources/error500.html | 76 +++++++++- src/jvmMain/resources/portal.toml | 2 + 39 files changed, 656 insertions(+), 305 deletions(-) diff --git a/portal.toml b/portal.toml index c98b36a..3221a6f 100644 --- a/portal.toml +++ b/portal.toml @@ -1,6 +1,8 @@ [server] host = "localhost" port = 8080 +prefix = "" +debug = false [schedule] reference = "2019-06-12" diff --git a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt index 71b0bd0..34916fa 100644 --- a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt +++ b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt @@ -11,7 +11,8 @@ import org.w3c.dom.events.Event import kotlin.browser.window class WebSocketClient() { - private val url = "ws://${window.location.host}/" + val prefix = js("prefix") + private val url = "ws://${window.location.host}$prefix/" private lateinit var ws: WebSocket private var reconnect = false diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt index 97e6292..aa56570 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt @@ -11,6 +11,8 @@ import kotlinx.serialization.list object PostRepository : Repository { + val prefix = js("prefix") + override val onCreate = EventHandler() override val onUpdate = EventHandler() override val onDelete = EventHandler() @@ -18,35 +20,35 @@ object PostRepository : Repository { private val parser = DynamicObjectParser() override suspend fun get(id: Long): Post? { - val json = repositoryGet("/api/post/$id") ?: return null + val json = repositoryGet("$prefix/api/post/$id") ?: return null return parser.parse(json, Post.serializer()) } override suspend fun create(model: Post): Long { - return repositoryPost("/api/posts", Message.json.stringify(Post.serializer(), model)) + return repositoryPost("$prefix/api/posts", Message.json.stringify(Post.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } override suspend fun update(model: Post) { if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") - repositoryPost("/api/post/${model.id}", Message.json.stringify(Post.serializer(), model)) + repositoryPost("$prefix/api/post/${model.id}", Message.json.stringify(Post.serializer(), model)) } override suspend fun delete(id: Long) { - repositoryPost("/api/post/$id/delete") + repositoryPost("$prefix/api/post/$id/delete") } override suspend fun all(): List { - val json = repositoryGet("/api/posts") ?: return emptyList() + val json = repositoryGet("$prefix/api/posts") ?: return emptyList() return parser.parse(json, Post.serializer().list) } suspend fun htmlByUrl(url: String): String { - return repositoryRawGet("/api/p/$url") + return repositoryRawGet("$prefix/api/p/$url") } suspend fun render(data: String): String { - return repositoryPost("/api/render", data) + return repositoryPost("$prefix/api/render", data) } val handler = object : MessageHandler(RepositoryType.POST) { diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt index 2d0b68d..1dc88ee 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt @@ -11,6 +11,8 @@ import kotlinx.serialization.list object RoomRepository : Repository { + val prefix = js("prefix") + override val onCreate = EventHandler() override val onUpdate = EventHandler() override val onDelete = EventHandler() @@ -18,26 +20,26 @@ object RoomRepository : Repository { private val parser = DynamicObjectParser() override suspend fun get(id: Long): Room? { - val json = repositoryGet("/api/room/$id") ?: return null + val json = repositoryGet("$prefix/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)) + return repositoryPost("$prefix/api/rooms", Message.json.stringify(Room.serializer(), model)) ?: 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)) + repositoryPost("$prefix/api/room/${model.id}", Message.json.stringify(Room.serializer(), model)) } override suspend fun delete(id: Long) { - repositoryPost("/api/room/$id/delete") + repositoryPost("$prefix/api/room/$id/delete") } override suspend fun all(): List { - val json = repositoryGet("/api/rooms") ?: return emptyList() + val json = repositoryGet("$prefix/api/rooms") ?: return emptyList() return parser.parse(json, Room.serializer().list) } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt index c400c9c..aaaf1b3 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt @@ -12,6 +12,8 @@ import kotlinx.serialization.list object ScheduleRepository : Repository { + val prefix = js("prefix") + override val onCreate = EventHandler() override val onUpdate = EventHandler() override val onDelete = EventHandler() @@ -19,26 +21,26 @@ object ScheduleRepository : Repository { private val parser = DynamicObjectParser() override suspend fun get(id: Long): Schedule? { - val json = repositoryGet("/api/schedule/$id") ?: return null + val json = repositoryGet("$prefix/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)) + return repositoryPost("$prefix/api/schedules", Message.json.stringify(Schedule.serializer(), model)) ?: 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)) + repositoryPost("$prefix/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model)) } override suspend fun delete(id: Long) { - repositoryPost("/api/schedule/$id/delete") + repositoryPost("$prefix/api/schedule/$id/delete") } override suspend fun all(): List { - val json = repositoryGet("/api/schedules") ?: return emptyList() + val json = repositoryGet("$prefix/api/schedules") ?: return emptyList() return parser.parse(json, Schedule.serializer().list) } @@ -52,12 +54,12 @@ object ScheduleRepository : Repository { } suspend fun checkConstraints(): ConstraintMap { - val json = repositoryGet("/api/constraints") + val json = repositoryGet("$prefix/api/constraints") return parser.parse(json, ConstraintMap.serializer()) } suspend fun checkConstraintsFor(schedule: Schedule): ConstraintMap { - val json = repositoryGet("/api/constraint/${schedule.id}") + val json = repositoryGet("$prefix/api/constraint/${schedule.id}") return parser.parse(json, ConstraintMap.serializer()) } } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt index 7e4acd2..cf5dcae 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt @@ -11,6 +11,8 @@ import kotlinx.serialization.list object TrackRepository : Repository { + val prefix = js("prefix") + override val onCreate = EventHandler() override val onUpdate = EventHandler() override val onDelete = EventHandler() @@ -18,26 +20,26 @@ object TrackRepository : Repository { private val parser = DynamicObjectParser() override suspend fun get(id: Long): Track? { - val json = repositoryGet("/api/track/$id") ?: return null + val json = repositoryGet("$prefix/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)) + return repositoryPost("$prefix/api/tracks", Message.json.stringify(Track.serializer(), model)) ?: 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)) + repositoryPost("$prefix/api/track/${model.id}", Message.json.stringify(Track.serializer(), model)) } override suspend fun delete(id: Long) { - repositoryPost("/api/track/$id/delete") + repositoryPost("$prefix/api/track/$id/delete") } override suspend fun all(): List { - val json = repositoryGet("/api/tracks") ?: return emptyList() + val json = repositoryGet("$prefix/api/tracks") ?: return emptyList() return parser.parse(json, Track.serializer().list) } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt index 83f013a..b5a42db 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt @@ -11,6 +11,8 @@ import kotlinx.serialization.list object UserRepository : Repository { + val prefix = js("prefix") + override val onCreate = EventHandler() override val onUpdate = EventHandler() override val onDelete = EventHandler() @@ -18,26 +20,26 @@ object UserRepository : Repository { private val parser = DynamicObjectParser() override suspend fun get(id: Long): User? { - val json = repositoryGet("/api/user/$id") ?: return null + val json = repositoryGet("$prefix/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)) + return repositoryPost("$prefix/api/users", Message.json.stringify(User.serializer(), model)) ?: 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)) + repositoryPost("$prefix/api/user/${model.id}", Message.json.stringify(User.serializer(), model)) } override suspend fun delete(id: Long) { - repositoryPost("/api/user/$id/delete") + repositoryPost("$prefix/api/user/$id/delete") } override suspend fun all(): List { - val json = repositoryGet("/api/users") ?: return emptyList() + val json = repositoryGet("$prefix/api/users") ?: return emptyList() return parser.parse(json, User.serializer().list) } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt index e38fbed..18c240c 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt @@ -11,6 +11,8 @@ import kotlinx.serialization.list object WorkGroupRepository : Repository { + val prefix = js("prefix") + override val onCreate = EventHandler() override val onUpdate = EventHandler() override val onDelete = EventHandler() @@ -18,26 +20,26 @@ object WorkGroupRepository : Repository { private val parser = DynamicObjectParser() override suspend fun get(id: Long): WorkGroup? { - val json = repositoryGet("/api/workgroup/$id") ?: return null + val json = repositoryGet("$prefix/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)) + return repositoryPost("$prefix/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model)) ?: 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)) + repositoryPost("$prefix/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model)) } override suspend fun delete(id: Long) { - repositoryPost("/api/workgroup/$id/delete") + repositoryPost("$prefix/api/workgroup/$id/delete") } override suspend fun all(): List { - val json = repositoryGet("/api/workgroups") ?: return emptyList() + val json = repositoryGet("$prefix/api/workgroups") ?: return emptyList() return parser.parse(json, WorkGroup.serializer().list) } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt index 2661fba..2e9615e 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt @@ -47,6 +47,7 @@ fun initWorkGroupConstraints() { html.name = "constraint-only-on-day-${index++}" min = -1337.0 max = 1337.0 + placeholder = "Tag" }.html) }.html) } @@ -64,6 +65,7 @@ fun initWorkGroupConstraints() { html.name = "constraint-not-on-day-${index++}" min = -1337.0 max = 1337.0 + placeholder = "Tag" }.html) }.html) } @@ -78,13 +80,15 @@ fun initWorkGroupConstraints() { }.html) html.appendChild(InputView(InputType.TEXT).apply { classList += "form-control" - html.name = "constraint-only-before-time-day-${index++}" + html.name = "constraint-only-before-time-day-${index}" + placeholder = "Tag (optional)" }.html) html.appendChild(InputView(InputType.NUMBER).apply { classList += "form-control" html.name = "constraint-only-before-time-${index++}" min = -1337.0 max = 133700.0 + placeholder = "Minuten" }.html) }.html) } @@ -99,13 +103,15 @@ fun initWorkGroupConstraints() { }.html) html.appendChild(InputView(InputType.TEXT).apply { classList += "form-control" - html.name = "constraint-only-after-time-day-${index++}" + html.name = "constraint-only-after-time-day-${index}" + placeholder = "Tag (optional)" }.html) html.appendChild(InputView(InputType.NUMBER).apply { classList += "form-control" html.name = "constraint-only-after-time-${index++}" min = -1337.0 max = 133700.0 + placeholder = "Minuten" }.html) }.html) } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt index 47c1121..b08d097 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt @@ -9,17 +9,20 @@ import de.westermann.kwebview.iterator import org.w3c.dom.* import kotlin.browser.document import kotlin.browser.window - +import kotlin.js.Date class Calendar(calendar: HTMLElement) : View(calendar) { var autoScroll = true - val day: Int = calendar.dataset["day"]?.toIntOrNull() ?: -1 - val calendarTable = calendar.getElementsByClassName("calendar-table")[0] as HTMLElement private val calendarTableHeader = calendar.getElementsByClassName("calendar-header")[0] as HTMLElement + val day = calendarTable.dataset["day"]?.toIntOrNull() ?: -1 + val referenceDate = calendarTable.dataset["reference"]?.toLongOrNull() ?: -1L + val nowDate = calendarTable.dataset["now"]?.toLongOrNull() ?: -1L + val timeDifference = Date.now().toLong() - nowDate + fun scrollVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) { scrollAllVerticalBy(pixel, scrollBehavior) } @@ -46,7 +49,6 @@ class Calendar(calendar: HTMLElement) : View(calendar) { Orientation.ROOM_TO_TIME } - init { scroll += calendarTable @@ -131,7 +133,6 @@ class Calendar(calendar: HTMLElement) : View(calendar) { private var scroll = listOf() private fun scrollAllVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) { - println("scroll ${scroll.size} elemenets") for (calendarTable in scroll) { calendarTable.scrollBy(ScrollToOptions(0.0, pixel, scrollBehavior)) } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt index 082b490..b6ae69a 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarBody.kt @@ -86,9 +86,14 @@ class CalendarBody(val calendar: Calendar, view: HTMLElement) : ViewCollection("calendar-entry-name") + private fun onMove(event: MouseEvent) { val position = event.toPoint() - mouseDelta + if (ignoreEditHover) { + for (element in document.elementsFromPoint(position.x, position.y)) { + if (element.classList.contains("calendar-edit")) { + return + } + } + ignoreEditHover = false + } + val cell = calendar.calendarCells.find { position in it.dimension } @@ -245,13 +260,7 @@ class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : Vie } } - for (element in html.childNodes) { - if (element.isText) { - html.removeChild(element) - } - } - - html.appendText(workGroup.name) + nameView.textContent = workGroup.name } companion object { @@ -268,6 +277,7 @@ class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : Vie entry.load(workGroup) entry.mouseDelta = Point.ZERO + entry.ignoreEditHover = true entry.startDrag() return entry diff --git a/src/jsMain/resources/style/_config.scss b/src/jsMain/resources/style/_config.scss index 3f17228..dfa6c50 100644 --- a/src/jsMain/resources/style/_config.scss +++ b/src/jsMain/resources/style/_config.scss @@ -1,6 +1,10 @@ $border-radius: 0.2rem; $transitionTime: 150ms; +$mainFont: 'Raleway', Roboto, Arial, sans-serif; +$menuFont: 'Bungee', 'Raleway', Roboto, Arial, sans-serif; +$headFont: 'Montserrat', 'Raleway', Roboto, Arial, sans-serif; + @mixin no-select() { -webkit-touch-callout: none; -webkit-user-select: none; diff --git a/src/jsMain/resources/style/components/_calendar.scss b/src/jsMain/resources/style/components/_calendar.scss index f2a505c..df3f962 100644 --- a/src/jsMain/resources/style/components/_calendar.scss +++ b/src/jsMain/resources/style/components/_calendar.scss @@ -213,6 +213,8 @@ span:first-child { color: var(--text-primary-color); width: 6rem; + overflow: hidden; + text-overflow: ellipsis; &:hover { background-color: transparent; @@ -297,6 +299,13 @@ @include no-select() } +.calendar-entry-name { + display: block; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} + .calendar-table-box { display: flex; flex-wrap: nowrap; @@ -404,7 +413,7 @@ left: 6rem; right: 0; height: 1px; - border-bottom: solid 1px $primary-color; + border-bottom: solid 1px var(--primary-color); } &.calendar-now::after { @@ -414,8 +423,8 @@ left: 6rem; transform: scale(1, 0.5) rotate(-45deg); transform-origin: bottom; - border-bottom: solid 0.4rem $primary-color; - border-right: solid 0.4rem $primary-color; + border-bottom: solid 0.4rem var(--primary-color); + border-right: solid 0.4rem var(--primary-color); border-top: solid 0.4rem transparent; border-left: solid 0.4rem transparent; margin-top: -0.55rem; @@ -498,7 +507,7 @@ top: 3rem; bottom: 0; width: 1px; - border-right: solid 1px $primary-color; + border-right: solid 1px var(--primary-color); } &.calendar-now::after { @@ -508,8 +517,8 @@ top: 3rem; transform: scale(0.5, 1) rotate(45deg); transform-origin: right; - border-bottom: solid 0.4rem $primary-color; - border-right: solid 0.4rem $primary-color; + border-bottom: solid 0.4rem var(--primary-color); + border-right: solid 0.4rem var(--primary-color); border-top: solid 0.4rem transparent; border-left: solid 0.4rem transparent; margin-top: -0.3rem; diff --git a/src/jsMain/resources/style/components/_menu.scss b/src/jsMain/resources/style/components/_menu.scss index 4227b6e..6f65816 100644 --- a/src/jsMain/resources/style/components/_menu.scss +++ b/src/jsMain/resources/style/components/_menu.scss @@ -13,7 +13,7 @@ color: var(--text-primary-color); height: 100%; display: inline-block; - font-family: "Bungee", sans-serif; + font-family: $menuFont; font-weight: normal; font-size: 1.1rem; position: relative; diff --git a/src/jsMain/resources/style/components/_overview.scss b/src/jsMain/resources/style/components/_overview.scss index 6552344..854164a 100644 --- a/src/jsMain/resources/style/components/_overview.scss +++ b/src/jsMain/resources/style/components/_overview.scss @@ -33,7 +33,7 @@ line-height: 2rem; padding: 0 1rem; font-weight: bold; - font-family: 'Montserrat', sans-serif; + font-family: $headFont; &:empty::before { display: block; diff --git a/src/jsMain/resources/style/components/_wall.scss b/src/jsMain/resources/style/components/_wall.scss index 9287fb7..b3f4af2 100644 --- a/src/jsMain/resources/style/components/_wall.scss +++ b/src/jsMain/resources/style/components/_wall.scss @@ -39,6 +39,7 @@ margin-top: -0.5rem !important; bottom: 0 !important; } + .calendar-entry::after { content: none; } @@ -65,6 +66,12 @@ line-height: 2rem; width: 100%; } + + .calendar-header { + .calendar-cell { + overflow: hidden; + } + } } .wall-calendar { diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index 108d162..a55129c 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -15,7 +15,7 @@ body, html { color: var(--text-primary-color); background: var(--background-secondary-color); - font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif; + font-family: $mainFont; font-weight: 500; width: 100%; @@ -30,6 +30,10 @@ body, html { } } +h1, h2, h3, h4, h5, h6 { + font-family: $headFont; +} + .no-select { @include no-select() } diff --git a/src/jvmMain/kotlin/de/kif/backend/Application.kt b/src/jvmMain/kotlin/de/kif/backend/Application.kt index 9a977cc..008d8b5 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Application.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Application.kt @@ -13,13 +13,21 @@ import io.ktor.http.content.files import io.ktor.http.content.static import io.ktor.jackson.jackson import io.ktor.response.respond +import io.ktor.routing.route import io.ktor.routing.routing import io.ktor.websocket.WebSockets +import org.slf4j.event.Level import java.nio.file.Paths +val prefix = Configuration.Server.prefix + fun Application.main() { install(DefaultHeaders) - install(CallLogging) + install(CallLogging) { + if (Configuration.Server.debug) { + level = Level.INFO + } + } install(ConditionalHeaders) install(Compression) install(DataConversion) @@ -42,47 +50,50 @@ fun Application.main() { security() routing { - static("/static") { - files(Configuration.Path.webPath.toFile()) - } - static("/images") { - files(Configuration.Path.uploadsPath.toFile()) - } - - val srcFile = Paths.get("src")?.toFile() - if (srcFile != null && srcFile.exists() && srcFile.isDirectory) { - static("/src") { - files("src") + route(prefix) { + static("/static") { + files(Configuration.Path.webPath.toFile()) } + + static("/images") { + files(Configuration.Path.uploadsPath.toFile()) + } + + val srcFile = Paths.get("src")?.toFile() + if (srcFile != null && srcFile.exists() && srcFile.isDirectory) { + static("/src") { + files("src") + } + } + + // UI routes + overview() + calendar() + login() + account() + + board() + wall() + + workGroup() + track() + room() + user() + + // RESTful routes + authenticateApi() + + roomApi() + scheduleApi() + trackApi() + userApi() + workGroupApi() + constraintsApi() + postApi() + + // Web socket push notifications + pushService() } - - // UI routes - overview() - calendar() - login() - account() - - board() - wall() - - workGroup() - track() - room() - user() - - // RESTful routes - authenticateApi() - - roomApi() - scheduleApi() - trackApi() - userApi() - workGroupApi() - constraintsApi() - postApi() - - // Web socket push notifications - pushService() } } diff --git a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt index 92bd9e3..6057655 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt @@ -26,11 +26,15 @@ object Configuration { private object ServerSpec : ConfigSpec("server") { val host by required() val port by required() + val debug by required() + val cPrefix by required("prefix") } object Server { val host by c(ServerSpec.host) val port by c(ServerSpec.port) + val debug by c(ServerSpec.debug) + val prefix by c(ServerSpec.cPrefix) } private object PathSpec : ConfigSpec("path") { diff --git a/src/jvmMain/kotlin/de/kif/backend/Main.kt b/src/jvmMain/kotlin/de/kif/backend/Main.kt index 3c926c3..63177f0 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Main.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Main.kt @@ -31,7 +31,7 @@ fun main(args: Array) { var password: String? = null while (password == null) { print("Password: ") - password = System.console()?.readPassword()?.toString() ?: readLine() + password = System.console()?.readPassword()?.joinToString("") ?: readLine() } println("Create root user '$username' with pw '${"*".repeat(password.length)}'") diff --git a/src/jvmMain/kotlin/de/kif/backend/Security.kt b/src/jvmMain/kotlin/de/kif/backend/Security.kt index c7382b5..5fc2739 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Security.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Security.kt @@ -14,6 +14,7 @@ import io.ktor.sessions.* import io.ktor.util.hex import io.ktor.util.pipeline.PipelineContext import org.mindrot.jbcrypt.BCrypt +import de.kif.backend.prefix interface ErrorContext { suspend infix fun onFailure(block: suspend () -> Unit) @@ -49,7 +50,7 @@ suspend inline fun PipelineContext.authenticateOrRedirect block: (user: User) -> Unit ) { authenticate(*permissions, block = block) onFailure { - call.respondRedirect("/login?redirect=${call.request.path()}}") + call.respondRedirect("$prefix/login?redirect=${call.request.path()}") } } @@ -68,7 +69,7 @@ data class PortalSession(val userId: Long) { if (user == null) { call.sessions.clear() - call.respondRedirect("/login?onFailure") + call.respondRedirect("$prefix/login?onFailure") throw IllegalAccessException() } @@ -93,7 +94,7 @@ fun Application.security() { userParamName = "username" passwordParamName = "password" challenge = FormAuthChallenge.Redirect { _ -> - "/login?onFailure" + "$prefix/login?onFailure" } validate { credential: UserPasswordCredential -> val user = UserRepository.find(credential.name) ?: return@validate null diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt index 585d116..ebb053c 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt @@ -26,6 +26,7 @@ import io.ktor.routing.post import io.ktor.util.toMap import kotlinx.html.* import mu.KotlinLogging +import de.kif.backend.prefix private val logger = KotlinLogging.logger {} @@ -40,7 +41,7 @@ fun Route.account() { br {} +"Du hast folgende Rechte: ${user.permissions.joinToString(", ") { it.germanInfo }}" br {} - a("/logout") { + a("$prefix/logout") { button(classes = "form-btn") { +"Ausloggen" } @@ -69,25 +70,25 @@ fun Route.account() { div("account-backup") { if (user.checkPermission(Permission.ROOM)) { - a("/account/backup/rooms.json", classes = "form-btn") { + a("$prefix/account/backup/rooms.json", classes = "form-btn") { attributes["download"] = "rooms-backup.json" +"Räume sichern" } } if (user.checkPermission(Permission.USER)) { - a("/account/backup/users.json", classes = "form-btn") { + a("$prefix/account/backup/users.json", classes = "form-btn") { attributes["download"] = "users-backup.json" +"Nutzer sichern" } } if (user.checkPermission(Permission.POST)) { - a("/account/backup/posts.json", classes = "form-btn") { + a("$prefix/account/backup/posts.json", classes = "form-btn") { attributes["download"] = "posts-backup.json" +"Beiträge sichern" } } if (user.checkPermission(Permission.WORK_GROUP)) { - a("/account/backup/work-groups.json", classes = "form-btn") { + a("$prefix/account/backup/work-groups.json", classes = "form-btn") { attributes["download"] = "work-groups-backup.json" +"Arbeitskreise sichern" } @@ -97,14 +98,14 @@ fun Route.account() { user.checkPermission(Permission.ROOM) && user.checkPermission(Permission.SCHEDULE) ) { - a("/account/backup/schedules.json", classes = "form-btn") { + a("$prefix/account/backup/schedules.json", classes = "form-btn") { attributes["download"] = "schedules-backup.json" +"Zeitplan sichern" } } if (user.checkPermission(Permission.ADMIN)) { - a("/account/backup.json", classes = "form-btn") { + a("$prefix/account/backup.json", classes = "form-btn") { attributes["download"] = "backup.json" +"Alles sichern" // TODO: richtiger Text? } @@ -179,7 +180,7 @@ fun Route.account() { div("account-import-wiki") { span { +"Arbeitskreise aus dem KIF-Wiki importieren" } - form(action = "/account/import", method = FormMethod.post) { + form(action = "$prefix/account/import", method = FormMethod.post) { for ((index, section) in wikiSections.withIndex()) { div("form-group") { label { @@ -330,7 +331,7 @@ fun Route.account() { Backup.import(import) } - call.respondRedirect("/account") + call.respondRedirect("$prefix/account") } onFailure { call.error(HttpStatusCode.Unauthorized) } @@ -387,7 +388,7 @@ fun Route.account() { logger.info { "Import $counter from ${importedWorkGroups.size} work groups!" } - call.respondRedirect("/account") + call.respondRedirect("$prefix/account") } onFailure { call.error(HttpStatusCode.Unauthorized) } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt index fc12978..d39fe38 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt @@ -1,16 +1,17 @@ package de.kif.backend.route import de.kif.backend.Configuration -import de.kif.backend.repository.PostRepository import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.ScheduleRepository import de.kif.backend.view.respondMain -import de.kif.common.model.Schedule import io.ktor.routing.Route import io.ktor.routing.get import kotlinx.css.CSSBuilder import kotlinx.css.Color -import kotlinx.html.* +import kotlinx.html.div +import kotlinx.html.img +import kotlinx.html.span +import kotlinx.html.unsafe import java.util.* import kotlin.math.max import kotlin.math.min @@ -120,20 +121,19 @@ fun Route.board() { } div("board-content") { div("board-calendar calendar") { - div("calendar-table") { - renderCalendar( - CalendarOrientation.ROOM_TO_TIME, - day, - min, - max, - rooms, - schedules - ) - } + renderCalendar( + CalendarOrientation.ROOM_TO_TIME, + day, + min, + max, + rooms, + schedules + ) } div("board-twitter") { unsafe { - raw(""" + raw( + """ - """.trimIndent()) + """.trimIndent() + ) } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt index 4ed5d1f..55ba0f7 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt @@ -26,6 +26,7 @@ import kotlin.collections.component2 import kotlin.collections.set import kotlin.math.max import kotlin.math.min +import de.kif.backend.prefix const val MINUTES_OF_DAY = 24 * 60 @@ -53,7 +54,9 @@ private fun DIV.calendarCell(schedule: Schedule?) { attributes["data-time"] = schedule.time.toString() attributes["data-length"] = schedule.workGroup.length.toString() - +schedule.workGroup.name + span("calendar-entry-name") { + +schedule.workGroup.name + } } } } @@ -70,66 +73,75 @@ fun DIV.renderCalendar( val minutesOfDay = to - from val now = Calendar.getInstance() - val currentTime = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE) + val d = (now.timeInMillis / (1000 * 60 * 60 * 24) - + Configuration.Schedule.referenceDate.time / (1000 * 60 * 60 * 24)).toInt() + val diff = day - d + val currentTime = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE) + (diff * 60 * 24) - div("calendar-table-box ${orientation.name.toLowerCase().replace("_", "-")}") { - div("calendar-header") { - div("calendar-cell") { - span { - +"Raum" - } - } + div("calendar-table") { + attributes["data-day"] = day.toString() + attributes["data-reference"] = Configuration.Schedule.referenceDate.time.toString() + attributes["data-now"] = now.timeInMillis.toString() - for (room in rooms) { + div("calendar-table-box ${orientation.name.toLowerCase().replace("_", "-")}") { + div("calendar-header") { div("calendar-cell") { - attributes["data-room"] = room.id.toString() - span { - +room.name + +"Raum" } } - } - } - - div("calendar-body") { - for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) { - val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let { - if (it < 0) it + MINUTES_OF_DAY else it - } - val minutes = (time % 60).toString().padStart(2, '0') - val hours = (time / 60).toString().padStart(2, '0') - val timeString = "$hours:$minutes" - - val start = i * CALENDAR_GRID_WIDTH + from - val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1 - - var rowClass = "calendar-row" - - if (currentTime in start..end) { - rowClass += " calendar-now calendar-now-${currentTime - start}" - } - - div(rowClass) { - attributes["data-time"] = start.toString() - attributes["data-day"] = day.toString() + for (room in rooms) { div("calendar-cell") { - if (time % gridLabelWidth == 0) { - span { - +timeString - } + attributes["data-room"] = room.id.toString() + + span { + +room.name } } + } + } + + div("calendar-body") { + for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) { + val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let { + if (it < 0) it + MINUTES_OF_DAY else it + } + val minutes = (time % 60).toString().padStart(2, '0') + val hours = (time / 60).toString().padStart(2, '0') + val timeString = "$hours:$minutes" + + val start = i * CALENDAR_GRID_WIDTH + from + val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1 + + var rowClass = "calendar-row" + + if (currentTime in start..end) { + rowClass += " calendar-now calendar-now-${currentTime - start}" + } + + div(rowClass) { + attributes["data-time"] = start.toString() + attributes["data-day"] = day.toString() - for (room in rooms) { div("calendar-cell") { - attributes["data-room"] = room.id.toString() + if (time % gridLabelWidth == 0) { + span { + +timeString + } + } + } - title = room.name + " - " + timeString + for (room in rooms) { + div("calendar-cell") { + attributes["data-room"] = room.id.toString() - val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() + title = room.name + " - " + timeString - calendarCell(schedule) + val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() + + calendarCell(schedule) + } } } } @@ -148,7 +160,7 @@ fun Route.calendar() { val todayDay = todayDate.time / (1000 * 60 * 60 * 24) val day = todayDay - refDay - call.respondRedirect("/calendar/$day", false) + call.respondRedirect("$prefix/calendar/$day", false) } get("/calendar/{day}/rtt") { @@ -159,7 +171,7 @@ fun Route.calendar() { path = "/" ) val day = call.parameters["day"]?.toIntOrNull() ?: 0 - call.respondRedirect("/calendar/$day") + call.respondRedirect("$prefix/calendar/$day") } get("/calendar/{day}/ttr") { @@ -170,7 +182,7 @@ fun Route.calendar() { path = "/" ) val day = call.parameters["day"]?.toIntOrNull() ?: 0 - call.respondRedirect("/calendar/$day") + call.respondRedirect("$prefix/calendar/$day") } get("/calendar/{day}") { @@ -237,20 +249,20 @@ fun Route.calendar() { div("header") { div("header-left") { if (editable || day - 1 > range.start) { - a("/calendar/${day - 1}") { i("material-icons") { +"chevron_left" } } + a("$prefix/calendar/${day - 1}") { i("material-icons") { +"chevron_left" } } } span { +dateString } if (editable || day + 1 < range.endInclusive) { - a("/calendar/${day + 1}") { i("material-icons") { +"chevron_right" } } + a("$prefix/calendar/${day + 1}") { i("material-icons") { +"chevron_right" } } } } div("header-right") { - a("/calendar/$day/rtt", classes = "form-btn") { + a("$prefix/calendar/$day/rtt", classes = "form-btn") { +"Vertikal" } - a("/calendar/$day/ttr", classes = "form-btn") { + a("$prefix/calendar/$day/ttr", classes = "form-btn") { +"Horizontal" } if (editable) { @@ -267,19 +279,16 @@ fun Route.calendar() { } div("calendar") { - attributes["data-day"] = day.toString() attributes["data-editable"] = editable.toString() - div("calendar-table") { - renderCalendar( - orientation, - day, - min, - max, - rooms, - schedules - ) - } + renderCalendar( + orientation, + day, + min, + max, + rooms, + schedules + ) if (editable) { div("calendar-edit") { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Login.kt b/src/jvmMain/kotlin/de/kif/backend/route/Login.kt index 20ed0ac..abea04d 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Login.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Login.kt @@ -16,6 +16,7 @@ import io.ktor.sessions.get import io.ktor.sessions.sessions import io.ktor.sessions.set import kotlinx.html.* +import de.kif.backend.prefix fun Route.login() { route("login") { @@ -24,11 +25,12 @@ fun Route.login() { 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"] ?: "/") + call.respondRedirect(call.parameters["redirect"] ?: "$prefix/") } } get { + val redirect = call.parameters["redirect"] ?: "$prefix/" val needLogin = call.sessions.get() == null if (needLogin) { respondMain { @@ -36,7 +38,7 @@ fun Route.login() { div { div { br { } - form("/login", method = FormMethod.post) { + form("$prefix/login?redirect=$redirect", method = FormMethod.post) { div("form-group") { label { htmlFor = "username" @@ -68,7 +70,7 @@ fun Route.login() { name = "redirect", type = InputType.hidden ) { - value = call.parameters["redirect"] ?: "/" + value = redirect } button(type = ButtonType.submit, classes = "form-btn btn-primary") { +"Einloggen" @@ -86,13 +88,13 @@ fun Route.login() { } } } else { - call.respondRedirect(call.parameters["redirect"] ?: "/") + call.respondRedirect(redirect) } } } get("logout") { call.sessions.clear() - call.respondRedirect("/") + call.respondRedirect("$prefix/") } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/News.kt b/src/jvmMain/kotlin/de/kif/backend/route/News.kt index 3ca3308..0972f8a 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/News.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/News.kt @@ -26,6 +26,7 @@ import io.ktor.routing.get import io.ktor.routing.post import kotlinx.html.* import java.io.File +import de.kif.backend.prefix fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") { var classes = "post" @@ -39,11 +40,11 @@ fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: Str attributes["data-id"] = post.id.toString() attributes["data-pinned"] = post.pinned.toString() - a("/p/${post.url}", classes = "post-name") { + a("$prefix/p/${post.url}", classes = "post-name") { +post.name } if (editable) { - a("/post/${post.id}", classes = "post-edit") { + a("$prefix/post/${post.id}", classes = "post-edit") { i("material-icons") { +"edit" } } } @@ -269,7 +270,7 @@ fun Route.overview() { } div("form-group") { - a("/") { + a("$prefix/") { button(classes = "form-btn") { +"Abbrechen" } @@ -279,7 +280,7 @@ fun Route.overview() { } } } - a("/post/${editPost.id}/delete") { + a("$prefix/post/${editPost.id}/delete") { button(classes = "form-btn btn-danger") { +"Löschen" } @@ -360,7 +361,7 @@ fun Route.overview() { PostRepository.update(post) - call.respondRedirect("/") + call.respondRedirect("$prefix/") } } @@ -474,7 +475,7 @@ fun Route.overview() { } div("form-group") { - a("/") { + a("$prefix/") { button(classes = "form-btn") { +"Abbrechen" } @@ -536,7 +537,7 @@ fun Route.overview() { PostRepository.create(post) - call.respondRedirect("/") + call.respondRedirect("$prefix/") } } @@ -546,7 +547,7 @@ fun Route.overview() { PostRepository.delete(postId) - call.respondRedirect("/") + call.respondRedirect("$prefix/") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt index 24d7740..4cf751b 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt @@ -24,6 +24,7 @@ import kotlinx.html.* import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set +import de.kif.backend.prefix fun Route.room() { @@ -38,7 +39,7 @@ fun Route.room() { searchValue = search action { - a("/room/new") { + a("$prefix/room/new") { button(classes = "form-btn btn-primary") { +"Raum hinzufügen" } @@ -92,7 +93,7 @@ fun Route.room() { } } td(classes = "action") { - a("/room/${u.id}") { + a("$prefix/room/${u.id}") { i("material-icons") { +"edit" } } } @@ -223,7 +224,7 @@ fun Route.room() { } div("form-group") { - a("/room") { + a("$prefix/room") { button(classes = "form-btn") { +"Abbrechen" } @@ -233,7 +234,7 @@ fun Route.room() { } } } - a("/room/${editRoom.id}/delete") { + a("$prefix/room/${editRoom.id}/delete") { button(classes = "form-btn btn-danger") { +"Löschen" } @@ -261,7 +262,7 @@ fun Route.room() { RoomRepository.update(room) - call.respondRedirect("/rooms") + call.respondRedirect("$prefix/rooms") } } @@ -382,7 +383,7 @@ fun Route.room() { } div("form-group") { - a("/room") { + a("$prefix/room") { button(classes = "form-btn") { +"Abbrechen" } @@ -415,7 +416,7 @@ fun Route.room() { RoomRepository.create(room) - call.respondRedirect("/rooms") + call.respondRedirect("$prefix/rooms") } } @@ -425,7 +426,7 @@ fun Route.room() { RoomRepository.delete(roomId) - call.respondRedirect("/rooms") + call.respondRedirect("$prefix/rooms") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt index 9d8bb2d..dd94322 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt @@ -1,6 +1,5 @@ package de.kif.backend.route - import de.kif.backend.authenticateOrRedirect import de.kif.backend.repository.TrackRepository import de.kif.backend.view.MainTemplate @@ -25,6 +24,7 @@ import kotlinx.css.Display import kotlinx.html.* import kotlin.collections.set import kotlin.random.Random +import de.kif.backend.prefix fun DIV.colorPicker(color: Color?) { val colorString = color?.toString() ?: Color( @@ -95,7 +95,7 @@ fun Route.track() { searchValue = search action { - a("/track/new") { + a("$prefix/track/new") { button(classes = "form-btn btn-primary") { +"Track hinzufügen" } @@ -128,7 +128,7 @@ fun Route.track() { +u.color.toString() } td(classes = "action") { - a("/track/${u.id}") { + a("$prefix/track/${u.id}") { i("material-icons") { +"edit" } } } @@ -168,7 +168,7 @@ fun Route.track() { } div("form-group") { - a("/track") { + a("$prefix/track") { button(classes = "form-btn") { +"Cancel" } @@ -178,7 +178,7 @@ fun Route.track() { } } } - a("/track/${editTrack.id}/delete") { + a("$prefix/track/${editTrack.id}/delete") { button(classes = "form-btn btn-danger") { +"Löschen" } @@ -204,7 +204,7 @@ fun Route.track() { TrackRepository.update(editTrack) - call.respondRedirect("/tracks") + call.respondRedirect("$prefix/tracks") } } @@ -232,7 +232,7 @@ fun Route.track() { colorPicker(null) } div("form-group") { - a("/track") { + a("$prefix/track") { button(classes = "form-btn") { +"Abbrechen" } @@ -262,7 +262,7 @@ fun Route.track() { TrackRepository.create(track) - call.respondRedirect("/tracks") + call.respondRedirect("$prefix/tracks") } } @@ -272,7 +272,7 @@ fun Route.track() { TrackRepository.delete(trackId) - call.respondRedirect("/tracks") + call.respondRedirect("$prefix/tracks") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/User.kt index 6f4a021..a301f1d 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/User.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/User.kt @@ -1,11 +1,10 @@ package de.kif.backend.route - import de.kif.backend.authenticateOrRedirect +import de.kif.backend.checkPassword import de.kif.backend.hashPassword +import de.kif.backend.prefix import de.kif.backend.repository.UserRepository -import de.kif.backend.view.MainTemplate -import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate import de.kif.backend.view.respondMain import de.kif.common.Search @@ -13,7 +12,6 @@ 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.request.receiveParameters import io.ktor.response.respondRedirect import io.ktor.routing.Route @@ -38,7 +36,7 @@ fun Route.user() { searchValue = search action { - a("/user/new") { + a("$prefix/user/new") { button(classes = "form-btn btn-primary") { +"Nutzer hinzufügen" } @@ -71,7 +69,7 @@ fun Route.user() { +u.permissions.joinToString(", ") { it.toString().toLowerCase() } } td(classes = "action") { - a("/user/${u.id}") { + a("$prefix/user/${u.id}") { i("material-icons") { +"edit" } } } @@ -131,17 +129,40 @@ fun Route.user() { } div("form-group") { - a("/user") { - button(classes = "form-btn") { - +"Abbrechen" - } + label { + htmlFor = "current-password" + +"Passwort ändern" } - button(type = ButtonType.submit, classes = "form-btn btn-primary") { - +"Speichern" + input( + name = "current-password", + classes = "form-control", + type = InputType.password + ) { + id = "current-password" + placeholder = "Aktuelles Passwort" + value = "" + } + input( + name = "new-password-1", + classes = "form-control", + type = InputType.password + ) { + id = "new-password-1" + placeholder = "Neues Passwort" + value = "" + } + input( + name = "new-password-2", + classes = "form-control", + type = InputType.password + ) { + id = "new-password-2" + placeholder = "Neues Passwort wiederholen" + value = "" } } } - a("/user/${editUser.id}/delete") { + a("$prefix/user/${editUser.id}/delete") { button(classes = "form-btn btn-danger") { +"Löschen" } @@ -162,6 +183,16 @@ fun Route.user() { params["username"]?.let { editUser = editUser.copy(username = it) } + val currentPassword = params["current-password"] + val newPassword1 = params["new-password-1"] + val newPassword2 = params["new-password-2"] + + if (currentPassword != null && newPassword1 != null && newPassword2 != null && currentPassword.isNotBlank() && newPassword1.isNotBlank() && newPassword2.isNotBlank()) { + if (checkPassword(currentPassword, editUser.password) && newPassword1 == newPassword2) { + editUser = editUser.copy(password = hashPassword(newPassword1)) + } + } + val permissions = Permission.values().filter { permission -> val name = permission.toString().toLowerCase() user.checkPermission(permission) && params["permission-$name"] == "on" @@ -170,7 +201,7 @@ fun Route.user() { UserRepository.update(user) - call.respondRedirect("/users") + call.respondRedirect("$prefix/users") } } @@ -235,7 +266,7 @@ fun Route.user() { } div("form-group") { - a("/user") { + a("$prefix/user") { button(classes = "form-btn") { +"Abbrechen" } @@ -270,7 +301,7 @@ fun Route.user() { UserRepository.create(newUser) - call.respondRedirect("/users") + call.respondRedirect("$prefix/users") } } @@ -285,7 +316,7 @@ fun Route.user() { UserRepository.delete(userId) } - call.respondRedirect("/users") + call.respondRedirect("$prefix/users") } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Wall.kt b/src/jvmMain/kotlin/de/kif/backend/route/Wall.kt index e7d6d82..fe58b3c 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Wall.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Wall.kt @@ -20,11 +20,12 @@ data class WallData( suspend fun genWallData(day: Int): WallData { val list = ScheduleRepository.getByDay(day) - val schedules = RoomRepository.all().associateWith { emptyMap() } + list.groupBy { it.room }.mapValues { (_, it) -> - it.associateBy { - it.time + val schedules = + RoomRepository.all().associateWith { emptyMap() } + list.groupBy { it.room }.mapValues { (_, it) -> + it.associateBy { + it.time + } } - } var max = 0 var min = 24 * 60 @@ -59,16 +60,14 @@ fun Route.wall() { for (day in days) { div("wall-box") { div("wall-calendar calendar") { - div("calendar-table") { - renderCalendar( - CalendarOrientation.TIME_TO_ROOM, - day.number, - min, - max, - day.schedules.keys.toList().sortedBy { it.id }, - day.schedules - ) - } + renderCalendar( + CalendarOrientation.TIME_TO_ROOM, + day.number, + min, + max, + day.schedules.keys.toList().sortedBy { it.id }, + day.schedules + ) } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index 44da058..0957c4b 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -19,6 +19,7 @@ import kotlinx.css.CSSBuilder import kotlinx.css.Display import kotlinx.html.* import kotlin.collections.set +import de.kif.backend.prefix private const val separator = "###" @@ -33,12 +34,12 @@ fun Route.workGroup() { searchValue = search action { - a("/tracks") { + a("$prefix/tracks") { button(classes = "form-btn") { +"Tracks bearbeiten" } } - a("/workgroup/new") { + a("$prefix/workgroup/new") { button(classes = "form-btn btn-primary") { +"Arbeitskreis hinzufügen" } @@ -132,7 +133,7 @@ fun Route.workGroup() { } } td(classes = "action") { - a("/workgroup/${u.id}") { + a("$prefix/workgroup/${u.id}") { i("material-icons") { +"edit" } } } @@ -400,6 +401,8 @@ fun Route.workGroup() { min = "-1337" max = "1337" + + placeholder = "Tag" } } ConstraintType.NotOnDay -> { @@ -415,6 +418,8 @@ fun Route.workGroup() { min = "-1337" max = "1337" + + placeholder = "Tag" } } ConstraintType.OnlyBeforeTime -> { @@ -426,7 +431,8 @@ fun Route.workGroup() { classes = "form-control" ) { value = constraint.day?.toString() ?: "" - placeholder = "day" + + placeholder = "Tag (optional)" } input( name = "constraint-only-before-time-$index", @@ -437,6 +443,8 @@ fun Route.workGroup() { min = "-1337" max = "133700" + + placeholder = "Minuten" } } ConstraintType.OnlyAfterTime -> { @@ -448,7 +456,8 @@ fun Route.workGroup() { classes = "form-control" ) { value = constraint.day?.toString() ?: "" - placeholder = "day" + + placeholder = "Tag (optional)" } input( name = "constraint-only-after-time-$index", @@ -459,6 +468,8 @@ fun Route.workGroup() { min = "-1337" max = "133700" + + placeholder = "Minuten" } } ConstraintType.NotAtSameTime -> { @@ -500,7 +511,7 @@ fun Route.workGroup() { } div("form-group") { - a("/workgroup") { + a("$prefix/workgroup") { button(classes = "form-btn") { +"Abbrechen" } @@ -510,7 +521,7 @@ fun Route.workGroup() { } } } - a("/workgroup/${editWorkGroup.id}/delete") { + a("$prefix/workgroup/${editWorkGroup.id}/delete") { button(classes = "form-btn btn-danger") { +"Löschen" } @@ -558,7 +569,7 @@ fun Route.workGroup() { WorkGroupRepository.update(editWorkGroup) - call.respondRedirect("/workgroups") + call.respondRedirect("$prefix/workgroups") } } @@ -796,7 +807,7 @@ fun Route.workGroup() { } div("form-group") { - a("/workgroup") { + a("$prefix/workgroup") { button(classes = "form-btn") { +"Abbrechen" } @@ -856,7 +867,7 @@ fun Route.workGroup() { WorkGroupRepository.create(workGroup) - call.respondRedirect("/workgroups") + call.respondRedirect("$prefix/workgroups") } } @@ -866,7 +877,7 @@ fun Route.workGroup() { WorkGroupRepository.delete(workGroupId) - call.respondRedirect("/workgroups") + call.respondRedirect("$prefix/workgroups") } } } @@ -882,14 +893,14 @@ private fun parseConstraintParam(params: Map) = params.map { (k } key.startsWith("constraint-only-after-time") -> { if ("day" in key) { - value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, day = it) } + Constraint(ConstraintType.OnlyAfterTime, day = value?.toIntOrNull()) } else { value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, time = it) } } } key.startsWith("constraint-only-before-time") -> { if ("day" in key) { - value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, day = it) } + Constraint(ConstraintType.OnlyBeforeTime, day = value?.toIntOrNull()) } else { value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, time = it) } } diff --git a/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt index e45635d..d76ac93 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt @@ -1,5 +1,6 @@ package de.kif.backend.util +import de.kif.backend.prefix import de.kif.backend.repository.* import de.kif.common.* import de.kif.common.RepositoryType @@ -27,7 +28,7 @@ object PushService { } fun Route.pushService() { - webSocket { + webSocket("/") { PushService.clients += this try { diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt index 2cde4c1..8444d8e 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt @@ -2,10 +2,14 @@ package de.kif.backend.view import de.kif.backend.PortalSession import de.kif.backend.Resources +import de.kif.backend.prefix import de.kif.common.model.User import io.ktor.application.ApplicationCall import io.ktor.application.call -import io.ktor.html.* +import io.ktor.html.Placeholder +import io.ktor.html.Template +import io.ktor.html.insert +import io.ktor.html.respondHtmlTemplate import io.ktor.request.path import io.ktor.request.uri import io.ktor.response.respondRedirect @@ -30,36 +34,41 @@ class MainTemplate( title("KIF Portal") - link(href = "/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet) - link(href = "/static/external/font/Montserrat.css", type = LinkType.textCss, rel = LinkRel.stylesheet) + link(href = "$prefix/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet) + link( + href = "$prefix/static/external/font/Montserrat.css", + type = LinkType.textCss, + rel = LinkRel.stylesheet + ) link( href = "https://fonts.googleapis.com/css?family=Bungee|Oswald|Raleway", type = LinkType.textCss, rel = LinkRel.stylesheet ) - link(href = "/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet) + link(href = "$prefix/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet) when (theme) { Theme.LIGHT -> { // Ignore } Theme.DARK -> { - link(href = "/static/style/dark.css", type = LinkType.textCss, rel = LinkRel.stylesheet) + link(href = "$prefix/static/style/dark.css", type = LinkType.textCss, rel = LinkRel.stylesheet) } Theme.PRINCESS -> { - link(href = "/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet) + link(href = "$prefix/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet) } Theme.BRETT -> { - link(href = "/static/style/board.css", type = LinkType.textCss, rel = LinkRel.stylesheet) + link(href = "$prefix/static/style/board.css", type = LinkType.textCss, rel = LinkRel.stylesheet) } } - script(src = "/static/require.min.js") {} + script(src = "$prefix/static/require.min.js") {} script { unsafe { - +"require.config({baseUrl: '/static'});\n" - +("require([${Resources.jsModules}]);\n") + +"let prefix = '$prefix';\n" + +"require.config({baseUrl: '$prefix/static'});\n" + +"require([${Resources.jsModules}]);\n" } } } @@ -96,10 +105,10 @@ class MainTemplate( } enum class Theme(val text: String, val display: Boolean, val dark: Boolean, val primaryColor: String) { - LIGHT("Hell",true, false, "#B11D33"), - DARK("Dunkel",true, true, "#ef5350"), - PRINCESS("Barbie",true, false, "#B11D33"), - BRETT("Brett",false, false, "#B11D33"); + LIGHT("Hell", true, false, "#B11D33"), + DARK("Dunkel", true, true, "#ef5350"), + PRINCESS("Barbie", true, false, "#B11D33"), + BRETT("Brett", false, false, "#B11D33"); companion object { private val lookup = values().toList().associateBy { it.name } @@ -118,7 +127,7 @@ suspend fun PipelineContext.respondMain( body: MainTemplate.(Theme) -> Unit ) { val param = call.request.queryParameters["theme"] - val url = call.request.uri.substring(1) + val url = call.request.uri.substring(1 + prefix.length) val user = call.sessions.get()?.getUser(call) if (param != null) { @@ -126,7 +135,7 @@ suspend fun PipelineContext.respondMain( name = "theme", value = Theme.lookup(param).name, maxAge = Int.MAX_VALUE, - path = "/" + path = "$prefix/" ) call.respondRedirect(call.request.path()) } else { diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt index 80a8941..6f621f3 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt @@ -1,9 +1,11 @@ package de.kif.backend.view +import de.kif.backend.Configuration import de.kif.common.model.Permission import de.kif.common.model.User import io.ktor.html.Template import kotlinx.html.* +import de.kif.backend.prefix class MenuTemplate( private val url: String, @@ -16,10 +18,10 @@ class MenuTemplate( nav("menu") { div("container") { div("menu-left") { - a("/", classes = if (tab == null) "active" else null) { + a("$prefix/", classes = if (tab == null) "active" else null) { +"Neuigkeiten" } - a("/calendar", classes = if (tab == Tab.CALENDAR) "active" else null) { + a("$prefix/calendar", classes = if (tab == Tab.CALENDAR) "active" else null) { +"Zeitplan" } } @@ -30,26 +32,26 @@ class MenuTemplate( val user = user div("menu-content") { if (user == null) { - a("/account", classes = if (tab == Tab.LOGIN) "active" else null) { + a("$prefix/account", classes = if (tab == Tab.LOGIN) "active" else null) { +"Einloggen" } } else { if (user.checkPermission(Permission.WORK_GROUP)) { - a("/workgroups", classes = if (tab == Tab.WORK_GROUP) "active" else null) { + a("$prefix/workgroups", classes = if (tab == Tab.WORK_GROUP) "active" else null) { +"Arbeitskreise" } } if (user.checkPermission(Permission.ROOM)) { - a("/rooms", classes = if (tab == Tab.ROOM) "active" else null) { + a("$prefix/rooms", classes = if (tab == Tab.ROOM) "active" else null) { +"Räume" } } if (user.checkPermission(Permission.USER)) { - a("/users", classes = if (tab == Tab.USER) "active" else null) { + a("$prefix/users", classes = if (tab == Tab.USER) "active" else null) { +"Nutzer" } } - a("/account", classes = if (tab == Tab.ACCOUNT) "active" else null) { + a("$prefix/account", classes = if (tab == Tab.ACCOUNT) "active" else null) { +user.username } } diff --git a/src/jvmMain/resources/error.html b/src/jvmMain/resources/error.html index d110e76..d084142 100644 --- a/src/jvmMain/resources/error.html +++ b/src/jvmMain/resources/error.html @@ -4,10 +4,78 @@ KIF Portal - - - - + +
diff --git a/src/jvmMain/resources/error404.html b/src/jvmMain/resources/error404.html index f0d2285..75401f9 100644 --- a/src/jvmMain/resources/error404.html +++ b/src/jvmMain/resources/error404.html @@ -4,10 +4,78 @@ KIF Portal - - - - + +
diff --git a/src/jvmMain/resources/error500.html b/src/jvmMain/resources/error500.html index 74e4e31..b218162 100644 --- a/src/jvmMain/resources/error500.html +++ b/src/jvmMain/resources/error500.html @@ -4,10 +4,78 @@ KIF Portal - - - - + +
diff --git a/src/jvmMain/resources/portal.toml b/src/jvmMain/resources/portal.toml index a970a9d..c50259a 100644 --- a/src/jvmMain/resources/portal.toml +++ b/src/jvmMain/resources/portal.toml @@ -1,6 +1,8 @@ [server] host = "localhost" port = 8080 +debug = false +prefix = "" [path] web = "web"