Compare commits

...

3 commits

Author SHA1 Message Date
Lars Westermann
594ac544dd
Add announcement 2019-06-11 13:50:11 +02:00
Lars Westermann
7ac2e1c208
Remove warnings 2019-06-11 10:55:44 +02:00
Lars Westermann
7b7a9b0fc2
Remove websockets 2019-06-11 10:36:12 +02:00
32 changed files with 505 additions and 156 deletions

View file

@ -83,7 +83,6 @@ kotlin {
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-auth:$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'

View file

@ -48,8 +48,8 @@ fun checkConstraints(
if (
schedule != s &&
leader in s.workGroup.leader &&
start <= s.getAbsoluteEndTime() &&
s.getAbsoluteStartTime() <= end
start < s.getAbsoluteEndTime() &&
s.getAbsoluteStartTime() < end
) {
errors += ConstraintError("Work group with leader $leader cannot be at same time with ${s.workGroup.name}!")
}

View file

@ -1,35 +1,38 @@
package de.kif.common
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
@Serializable
data class MessageBox(
val timestamp: Long,
val valid: Boolean,
val messages: List<Message>
)
@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, POST
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST, ANNOUNCEMENT
}
object Serialization {
private val jsonContext = SerializersModule {}
val json = Json(context = jsonContext)
fun <T> stringify(serializer: SerializationStrategy<T>, obj: T) = json.stringify(serializer, obj)
fun <T> parse(serializer: DeserializationStrategy<T>, str: String) = json.parse(serializer, str)
}

View file

@ -10,12 +10,12 @@ data class SearchElement(
) {
fun stringify(): String {
return Message.json.stringify(serializer(), this)
return Serialization.stringify(serializer(), this)
}
companion object {
fun parse(data: String): SearchElement {
return Message.json.parse(serializer(), data)
return Serialization.parse(serializer(), data)
}
}
}

View file

@ -1,68 +1,92 @@
package de.kif.frontend
import de.kif.common.Message
import de.kif.common.MessageBox
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 de.westermann.kwebview.clearInterval
import de.westermann.kwebview.createHtmlView
import de.westermann.kwebview.interval
import kotlinx.serialization.DynamicObjectParser
import org.w3c.dom.get
import org.w3c.xhr.XMLHttpRequest
import kotlin.browser.document
import kotlin.browser.window
class WebSocketClient() {
class WebSocketClient {
private val prefix = js("prefix")
private val url = "$prefix/api/updates"
private val body = document.body ?: createHtmlView()
private val parser = DynamicObjectParser()
private val useSsl = "https" in window.location.protocol
private val wsProtocol = if (useSsl) "wss" else "ws"
private val url = "$wsProtocol://${window.location.host}$prefix/websocket"
private var timestamp = body.dataset["timestamp"]?.toLongOrNull() ?: 0L
private var intervalId: Int? = null
private lateinit var ws: WebSocket
private var reconnect = false
private fun reload() {
val id = intervalId ?: return
clearInterval(id)
intervalId = null
@Suppress("UNUSED_PARAMETER")
private fun onOpen(event: Event) {
console.log("Connected!")
if (reconnect) {
window.location.reload()
}
window.location.reload()
}
private fun onMessage(messageEvent: MessageEvent) {
val message = Message.parse(messageEvent.data?.toString() ?: "")
private fun onMessage(messageBox: MessageBox) {
body.classList.remove("offline")
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)
if (messageBox.valid) {
timestamp = messageBox.timestamp
for (message in messageBox.messages) {
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)
}
}
}
}
} else {
reload()
}
}
@Suppress("UNUSED_PARAMETER")
private fun onClose(event: Event) {
console.log("Disconnected!")
reconnect = true
async(1000) {
connect()
private fun onError(code: Int) {
if (!body.classList.contains("offline")) {
console.log("Offline reason: $code")
}
body.classList.add("offline")
}
@Suppress("UNUSED_PARAMETER")
private fun onError(event: Event) {
console.log("An error occurred!")
}
private fun request() {
val xmlHttpRequest = XMLHttpRequest()
private fun connect() {
ws = WebSocket(url)
xmlHttpRequest.onreadystatechange = {
try {
if (xmlHttpRequest.readyState == 4.toShort()) {
if (xmlHttpRequest.status == 200.toShort() || xmlHttpRequest.status == 304.toShort()) {
val json = JSON.parse<JsonResponse>(xmlHttpRequest.responseText)
ws.onopen = this::onOpen
ws.onmessage = this::onMessage
ws.onclose = this::onClose
ws.onerror = this::onError
if (json.OK) {
val message = parser.parse(json.data, MessageBox.serializer())
onMessage(message)
} else {
onError(-1)
}
} else {
onError(xmlHttpRequest.status.toInt())
}
}
} catch (e: Exception) {
console.error(e)
onError(-2)
}
}
xmlHttpRequest.open("GET", "$url?timestamp=$timestamp", true)
xmlHttpRequest.overrideMimeType("application/json")
xmlHttpRequest.send()
}
private val messageHandlers: List<MessageHandler> = listOf(
@ -71,11 +95,14 @@ class WebSocketClient() {
TrackRepository.handler,
UserRepository.handler,
WorkGroupRepository.handler,
PostRepository.handler
PostRepository.handler,
AnnouncementRepository.handler
)
init {
connect()
intervalId = interval(500) {
request()
}
}
}

View file

@ -2,6 +2,7 @@ package de.kif.frontend
import de.kif.frontend.views.board.initBoard
import de.kif.frontend.views.calendar.initCalendar
import de.kif.frontend.views.initAnnouncement
import de.kif.frontend.views.table.initTableLayout
import de.kif.frontend.views.initWorkGroupConstraints
import de.kif.frontend.views.overview.initOverviewMain
@ -34,4 +35,7 @@ fun main() = init {
if (document.getElementsByClassName("board").length > 0) {
initBoard()
}
if (document.getElementsByClassName("announcement").length > 0) {
initAnnouncement()
}
}

View file

@ -8,10 +8,15 @@ import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.js.Promise
interface JsonResponse {
val OK: Boolean
val data: dynamic
}
suspend fun repositoryGet(
url: String
): dynamic {
val promise = Promise<dynamic> { resolve, reject ->
val promise = Promise<JsonResponse> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
@ -47,7 +52,7 @@ suspend fun repositoryPost(
url: String,
data: String? = null
): dynamic {
val promise = Promise<dynamic> { resolve, reject ->
val promise = Promise<JsonResponse> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {

View file

@ -0,0 +1,40 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
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
import kotlinx.serialization.serializer
object AnnouncementRepository {
private val prefix = js("prefix")
val onUpdate = EventHandler<Unit>()
private val parser = DynamicObjectParser()
suspend fun getAnnouncement(): String {
val json = repositoryGet("$prefix/api/announcement") ?: return ""
return parser.parse(json, String.serializer())
}
suspend fun setAnnouncement(value: String){
return repositoryPost("$prefix/api/announcement", Serialization.stringify(String.serializer(), value))
?: throw IllegalStateException("Cannot set announcement!")
}
val handler = object : MessageHandler(RepositoryType.ROOM) {
override fun onCreate(id: Long) {}
override fun onUpdate(id: Long) = onUpdate.emit(Unit)
override fun onDelete(id: Long) {}
}
}

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.Post
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object PostRepository : Repository<Post> {
}
override suspend fun create(model: Post): Long {
return repositoryPost("$prefix/api/posts", Message.json.stringify(Post.serializer(), model))
return repositoryPost("$prefix/api/posts", Serialization.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("$prefix/api/post/${model.id}", Message.json.stringify(Post.serializer(), model))
repositoryPost("$prefix/api/post/${model.id}", Serialization.stringify(Post.serializer(), model))
}
override suspend fun delete(id: Long) {

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.Room
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object RoomRepository : Repository<Room> {
}
override suspend fun create(model: Room): Long {
return repositoryPost("$prefix/api/rooms", Message.json.stringify(Room.serializer(), model))
return repositoryPost("$prefix/api/rooms", Serialization.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("$prefix/api/room/${model.id}", Message.json.stringify(Room.serializer(), model))
repositoryPost("$prefix/api/room/${model.id}", Serialization.stringify(Room.serializer(), model))
}
override suspend fun delete(id: Long) {

View file

@ -1,9 +1,6 @@
package de.kif.frontend.repository
import de.kif.common.ConstraintMap
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.*
import de.kif.common.model.Schedule
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
@ -31,13 +28,13 @@ object ScheduleRepository : Repository<Schedule> {
}
override suspend fun create(model: Schedule): Long {
return repositoryPost("$prefix/api/schedules", Message.json.stringify(Schedule.serializer(), model))
return repositoryPost("$prefix/api/schedules", Serialization.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("$prefix/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model))
repositoryPost("$prefix/api/schedule/${model.id}", Serialization.stringify(Schedule.serializer(), model))
}
override suspend fun delete(id: Long) {

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.Track
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object TrackRepository : Repository<Track> {
}
override suspend fun create(model: Track): Long {
return repositoryPost("$prefix/api/tracks", Message.json.stringify(Track.serializer(), model))
return repositoryPost("$prefix/api/tracks", Serialization.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("$prefix/api/track/${model.id}", Message.json.stringify(Track.serializer(), model))
repositoryPost("$prefix/api/track/${model.id}", Serialization.stringify(Track.serializer(), model))
}
override suspend fun delete(id: Long) {

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.User
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object UserRepository : Repository<User> {
}
override suspend fun create(model: User): Long {
return repositoryPost("$prefix/api/users", Message.json.stringify(User.serializer(), model))
return repositoryPost("$prefix/api/users", Serialization.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("$prefix/api/user/${model.id}", Message.json.stringify(User.serializer(), model))
repositoryPost("$prefix/api/user/${model.id}", Serialization.stringify(User.serializer(), model))
}
override suspend fun delete(id: Long) {

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.WorkGroup
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object WorkGroupRepository : Repository<WorkGroup> {
}
override suspend fun create(model: WorkGroup): Long {
return repositoryPost("$prefix/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))
return repositoryPost("$prefix/api/workgroups", Serialization.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("$prefix/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model))
repositoryPost("$prefix/api/workgroup/${model.id}", Serialization.stringify(WorkGroup.serializer(), model))
}
override suspend fun delete(id: Long) {

View file

@ -0,0 +1,21 @@
package de.kif.frontend.views
import de.kif.frontend.launch
import de.kif.frontend.repository.AnnouncementRepository
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.browser.document
fun initAnnouncement() {
val announcement = document.getElementsByClassName("announcement")[0] as? HTMLElement ?: return
val span = announcement.children[0] as? HTMLElement ?: return
AnnouncementRepository.onUpdate {
launch {
val text = AnnouncementRepository.getAnnouncement()
announcement.classList.toggle("announcement-blank", text.isBlank())
span.textContent = text
}
}
}

View file

@ -58,7 +58,7 @@
width: 100%;
height: 100%;
border-bottom: solid 1px var(--table-border-color);
box-shadow: 0 0 4px black;
box-shadow: 0 0 4px var(--shadow-color);
top: -1rem;
padding-top: 1.2rem;
}
@ -205,3 +205,10 @@
margin-top: -1px !important;
}
}
.board-announcement {
position: fixed;
bottom: 3rem;
z-index: 10;
margin-bottom: 0 !important;
}

View file

@ -160,3 +160,42 @@ a {
}
}
}
.offline-banner {
position: fixed;
bottom: 3rem;
left: 0;
right: 0;
height: 3rem;
line-height: 3rem;
text-align: center;
font-size: 1.2rem;
background-color: var(--primary-color);
color: var(--primary-text-color);
z-index: 12;
box-shadow: 0 1px 4px var(--shadow-color);
display: none;
}
body.offline {
.offline-banner {
display: block;
}
}
.announcement {
height: 3rem;
line-height: 3rem;
text-align: center;
width: 100%;
font-size: 1.2rem;
background-color: var(--primary-color);
color: var(--primary-text-color);
box-shadow: 0 1px 4px var(--shadow-color);
margin-bottom: 1rem;
display: block;
&.announcement-blank {
display: none;
}
}

View file

@ -15,7 +15,6 @@ 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
@ -31,7 +30,7 @@ fun Application.main() {
install(ConditionalHeaders)
install(Compression)
install(DataConversion)
install(WebSockets)
install(AutoHeadResponse)
install(StatusPages) {
exception<Throwable> {
@ -91,6 +90,7 @@ fun Application.main() {
workGroupApi()
constraintsApi()
postApi()
announcementApi()
// Web socket push notifications
pushService()

View file

@ -42,6 +42,7 @@ object Configuration {
val sessions by required<String>()
val uploads by required<String>()
val database by required<String>()
val announcement by required<String>()
}
object Path {
@ -56,6 +57,9 @@ object Configuration {
val database by c(PathSpec.database)
val databasePath: java.nio.file.Path by lazy { Paths.get(database).toAbsolutePath() }
val announcement by c(PathSpec.announcement)
val announcementPath: java.nio.file.Path by lazy { Paths.get(announcement).toAbsolutePath() }
}
private object ScheduleSpec : ConfigSpec("schedule") {

View file

@ -0,0 +1,53 @@
package de.kif.backend.repository
import de.kif.backend.Configuration
import de.kif.backend.util.PushService
import de.kif.common.MessageType
import de.kif.common.RepositoryType
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
object AnnouncementRepository {
val onUpdate = EventHandler<Unit>()
private val file = Configuration.Path.announcementPath.toFile()
private var announcement = ""
fun setAnnouncement(value: String) {
val str = value.replace("\n", " ").trim()
if (announcement == str) return
announcement = str
try {
file.writeText(str)
} catch (e: Exception) {
}
onUpdate.emit(Unit)
}
fun getAnnouncement(): String {
return announcement
}
fun registerPushService() {
onUpdate {
runBlocking {
PushService.notify(MessageType.UPDATE, RepositoryType.ANNOUNCEMENT, 0L)
}
}
}
init {
try {
if (!file.exists()) {
file.createNewFile()
}
announcement = file.readText().trim()
} catch (e: Exception) {
}
}
}

View file

@ -3,10 +3,7 @@ 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.Message
import de.kif.common.MessageType
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.*
import de.kif.common.model.Constraint
import de.kif.common.model.WorkGroup
import de.westermann.kobserve.event.EventHandler
@ -36,8 +33,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
val accessible = row[DbWorkGroup.accessible]
val length = row[DbWorkGroup.length]
val language = row[DbWorkGroup.language]
val leader = Message.json.parse(String.serializer().list, row[DbWorkGroup.leader])
val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
val leader = Serialization.parse(String.serializer().list, row[DbWorkGroup.leader])
val constraints = Serialization.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
val createdAt = row[DbWorkGroup.createdAt]
val updatedAt = row[DbWorkGroup.updatedAt]
@ -92,8 +89,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[accessible] = model.accessible
it[length] = model.length
it[language] = model.language
it[leader] = Message.json.stringify(String.serializer().list, model.leader)
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
it[leader] = Serialization.stringify(String.serializer().list, model.leader)
it[constraints] = Serialization.stringify(Constraint.serializer().list, model.constraints)
it[createdAt] = now
it[updatedAt] = now
@ -124,8 +121,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[accessible] = model.accessible
it[length] = model.length
it[language] = model.language
it[leader] = Message.json.stringify(String.serializer().list, model.leader)
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
it[leader] = Serialization.stringify(String.serializer().list, model.leader)
it[constraints] = Serialization.stringify(Constraint.serializer().list, model.constraints)
it[updatedAt] = now
}

View file

@ -1,6 +1,8 @@
package de.kif.backend.route
import de.kif.backend.Configuration
import de.kif.backend.prefix
import de.kif.backend.repository.AnnouncementRepository
import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.view.respondMain
@ -87,6 +89,18 @@ fun Route.board() {
val nowLocale = now.time + timeOffset
respondMain(true, true) { theme ->
content {
val announcement = AnnouncementRepository.getAnnouncement()
var classes = "board-announcement announcement"
if (announcement.isBlank()) {
classes += " announcement-blank"
}
div(classes) {
span {
+announcement
}
}
div("board") {
div("board-header") {
div("board-running") {
@ -142,7 +156,7 @@ fun Route.board() {
}
}
div("board-logo") {
img("KIF 47.0", "/static/images/logo.svg")
img("KIF 47.0", "$prefix/static/images/logo.svg")
div("board-header-date") {
attributes["data-now"] = (now.time - timeOffset).toString()
}

View file

@ -1,6 +1,7 @@
package de.kif.backend.route
import de.kif.backend.*
import de.kif.backend.repository.AnnouncementRepository
import de.kif.backend.repository.PostRepository
import de.kif.backend.util.markdownToHtml
import de.kif.backend.view.respondMain
@ -13,11 +14,13 @@ import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider
import io.ktor.request.receiveMultipart
import io.ktor.request.receiveParameters
import io.ktor.response.respond
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.html.*
import java.io.File
@ -75,9 +78,12 @@ fun Route.overview() {
content {
if (editable) {
div("overview-new") {
a("post/new", classes = "form-btn") {
a("$prefix/post/new", classes = "form-btn") {
+"Neuer Eintrag"
}
a("$prefix/announcement", classes = "form-btn") {
+"Ankündigungsbanner bearbeiten"
}
}
}
div("overview") {
@ -133,7 +139,7 @@ fun Route.overview() {
}
get("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user ->
authenticateOrRedirect(Permission.POST) {
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editPost = PostRepository.get(postId) ?: return@get
respondMain {
@ -291,7 +297,7 @@ fun Route.overview() {
}
post("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user ->
authenticateOrRedirect(Permission.POST) {
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
var imageUploadName: String? = null
@ -361,7 +367,7 @@ fun Route.overview() {
}
get("/post/new") {
authenticateOrRedirect(Permission.POST) { user ->
authenticateOrRedirect(Permission.POST) {
respondMain {
content {
h1 { +"Beitrag erstellen" }
@ -491,7 +497,7 @@ fun Route.overview() {
}
post("/post/new") {
authenticateOrRedirect(Permission.POST) { user ->
authenticateOrRedirect(Permission.POST) {
var imageUploadName: String? = null
val params = mutableMapOf<String, String>()
@ -537,7 +543,7 @@ fun Route.overview() {
}
get("/post/{id}/delete") {
authenticateOrRedirect(Permission.POST) { user ->
authenticateOrRedirect(Permission.POST) {
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
PostRepository.delete(postId)
@ -545,4 +551,57 @@ fun Route.overview() {
call.respondRedirect("$prefix/")
}
}
get("/announcement") {
authenticateOrRedirect(Permission.POST) {
respondMain {
content {
h1 { +"Ankündigungsbanner bearbeiten" }
form(method = FormMethod.post) {
div("form-group") {
label {
htmlFor = "announcement"
+"Ankündigung"
}
input(
name = "announcement",
classes = "form-control"
) {
id = "announcement"
placeholder = "Ankündigung"
value = AnnouncementRepository.getAnnouncement()
}
}
div("form-group") {
a("$prefix/") {
button(classes = "form-btn") {
+"Abbrechen"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Speichern"
}
}
}
}
}
}
}
post("/announcement") {
authenticateOrRedirect(Permission.POST) {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val announcement = params["announcement"] ?: return@post
AnnouncementRepository.setAnnouncement(announcement)
call.respondRedirect("$prefix/")
}
}
}

View file

@ -29,7 +29,7 @@ import de.kif.backend.prefix
fun Route.room() {
get("/rooms") {
authenticateOrRedirect(Permission.ROOM) { user ->
authenticateOrRedirect(Permission.ROOM) {
val search = call.parameters["search"] ?: ""
val list = RoomRepository.all()
@ -106,7 +106,7 @@ fun Route.room() {
}
get("/room/{id}") {
authenticateOrRedirect(Permission.ROOM) { user ->
authenticateOrRedirect(Permission.ROOM) {
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editRoom = RoomRepository.get(roomId) ?: return@get
respondMain {
@ -260,7 +260,7 @@ fun Route.room() {
}
post("/room/{id}") {
authenticateOrRedirect(Permission.ROOM) { user ->
authenticateOrRedirect(Permission.ROOM) {
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
@ -283,7 +283,7 @@ fun Route.room() {
}
get("/room/new") {
authenticateOrRedirect(Permission.ROOM) { user ->
authenticateOrRedirect(Permission.ROOM) {
respondMain {
content {
h1 { +"Raum erstellen" }
@ -430,7 +430,7 @@ fun Route.room() {
}
post("/room/new") {
authenticateOrRedirect(Permission.ROOM) { user ->
authenticateOrRedirect(Permission.ROOM) {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
@ -453,7 +453,7 @@ fun Route.room() {
}
get("/room/{id}/delete") {
authenticateOrRedirect(Permission.ROOM) { user ->
authenticateOrRedirect(Permission.ROOM) {
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
RoomRepository.delete(roomId)

View file

@ -83,7 +83,7 @@ fun DIV.colorPicker(color: Color?) {
fun Route.track() {
get("/tracks") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val search = call.parameters["search"] ?: ""
val list = TrackRepository.all()
respondMain {
@ -145,7 +145,7 @@ fun Route.track() {
}
get("/track/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editTrack = TrackRepository.get(trackId) ?: return@get
respondMain {
@ -192,7 +192,7 @@ fun Route.track() {
}
post("/track/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
@ -212,7 +212,7 @@ fun Route.track() {
}
get("/track/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
respondMain {
content {
h1 { +"Track erstellen" }
@ -251,7 +251,7 @@ fun Route.track() {
}
post("/track/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
@ -270,7 +270,7 @@ fun Route.track() {
}
get("track/{id}/delete") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
TrackRepository.delete(trackId)

View file

@ -26,8 +26,8 @@ import kotlin.collections.component2
import kotlin.collections.set
fun Route.user() {
get("/users") { param ->
authenticateOrRedirect(Permission.USER) { user ->
get("/users") {
authenticateOrRedirect(Permission.USER) {
val search = call.parameters["search"] ?: ""
val list = UserRepository.all()
respondMain {

View file

@ -25,7 +25,7 @@ private const val separator = "###"
fun Route.workGroup() {
get("workgroups") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val search = call.parameters["search"] ?: ""
val list = WorkGroupRepository.all()
respondMain {
@ -146,7 +146,7 @@ fun Route.workGroup() {
}
get("/workgroup/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@get
val tracks = TrackRepository.all()
@ -532,7 +532,7 @@ fun Route.workGroup() {
}
post("/workgroup/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
@ -574,7 +574,7 @@ fun Route.workGroup() {
}
get("/workgroup/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val tracks = TrackRepository.all()
respondMain {
content {
@ -823,7 +823,7 @@ fun Route.workGroup() {
}
post("/workgroup/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
@ -872,7 +872,7 @@ fun Route.workGroup() {
}
get("/workgroup/{id}/delete") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
authenticateOrRedirect(Permission.WORK_GROUP) {
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
WorkGroupRepository.delete(workGroupId)

View file

@ -0,0 +1,39 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.AnnouncementRepository
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.announcementApi() {
get("/api/announcement") {
try {
call.success(AnnouncementRepository.getAnnouncement())
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/announcement") {
try {
authenticate(Permission.POST) {
val announcement = call.receive<String>()
AnnouncementRepository.setAnnouncement(announcement)
call.success()
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
}

View file

@ -4,6 +4,7 @@ import de.kif.backend.database.Connection
import de.kif.backend.repository.*
import de.kif.common.Message
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.*
import kotlinx.serialization.Serializable
@ -39,15 +40,16 @@ data class Backup(
RepositoryType.USER -> backup.copy(users = UserRepository.all())
RepositoryType.WORK_GROUP -> backup.copy(workGroups = WorkGroupRepository.all())
RepositoryType.POST -> backup.copy(posts = PostRepository.all())
else -> backup
}
}
return Message.json.stringify(serializer(), backup)
return Serialization.stringify(serializer(), backup)
}
@Suppress("UNUSED_VARIABLE")
suspend fun import(data: String) {
val backup = Message.json.parse(serializer(), data)
val backup = Serialization.parse(serializer(), data)
backup.users.forEach { UserRepository.create(it); println("Import user ${it.username}") }
backup.posts.forEach { PostRepository.create(it); println("Import post") }

View file

@ -1,48 +1,60 @@
package de.kif.backend.util
import de.kif.backend.prefix
import de.kif.backend.repository.*
import de.kif.common.*
import de.kif.backend.route.api.error
import de.kif.backend.route.api.success
import de.kif.common.Message
import de.kif.common.MessageBox
import de.kif.common.MessageType
import de.kif.common.RepositoryType
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.routing.Route
import io.ktor.websocket.WebSocketServerSession
import io.ktor.websocket.webSocket
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import io.ktor.routing.get
object PushService {
var clients: List<WebSocketServerSession> = emptyList()
var leastValidTimestamp = System.currentTimeMillis()
suspend fun notify(type: MessageType, repository: RepositoryType, id: Long) {
try {
val data = Message(type, repository, id).stringify()
for (client in clients) {
client.outgoing.send(Frame.Text(data))
}
} catch (e: Exception) {
e.printStackTrace()
}
/**
* Save the message with the current timestamp
*/
fun notify(type: MessageType, repository: RepositoryType, id: Long) {
val timestamp = System.currentTimeMillis()
val message = Message(type, repository, id)
}
/**
* Get all messages created after the given timestamp.
* The return Box has the current timestamp. If the leastValidTimestamp is less then the current timestamp set the
* valid flag to false and return an empty message list.
*/
fun getMessages(timestamp: Long?): MessageBox {
return MessageBox(
System.currentTimeMillis(),
timestamp != null && timestamp > leastValidTimestamp,
emptyList()
)
}
/**
* Delete all messages before the given timestamp
*/
fun gc(timestamp: Long) {
leastValidTimestamp = timestamp
}
}
fun Route.pushService() {
webSocket("/websocket") {
PushService.clients += this
get("/api/updates") {
try {
while (true) {
val text = (incoming.receive() as Frame.Text).readText()
println("onMessage($text)")
outgoing.send(Frame.Text(text))
}
} catch (_: ClosedReceiveChannelException) {
PushService.clients -= this
} catch (e: Throwable) {
println("onFailure ${closeReason.await()}")
e.printStackTrace()
PushService.clients -= this
val timestamp = call.request.queryParameters["timestamp"]?.toLongOrNull()
val messageBox = PushService.getMessages(timestamp)
call.success(messageBox)
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
@ -52,4 +64,5 @@ fun Route.pushService() {
UserRepository.registerPushService()
WorkGroupRepository.registerPushService()
PostRepository.registerPushService()
AnnouncementRepository.registerPushService()
}

View file

@ -3,6 +3,7 @@ package de.kif.backend.view
import de.kif.backend.PortalSession
import de.kif.backend.Resources
import de.kif.backend.prefix
import de.kif.backend.repository.AnnouncementRepository
import de.kif.common.model.User
import io.ktor.application.ApplicationCall
import io.ktor.application.call
@ -73,10 +74,25 @@ class MainTemplate(
}
}
body {
attributes["data-timestamp"] = currentTimeMillis().toString()
if (!noMenu) {
insert(MenuTemplate(url, user)) {}
}
if (!noMenu) {
val announcement = AnnouncementRepository.getAnnouncement()
var classes = "announcement"
if (announcement.isBlank()) {
classes += " announcement-blank"
}
div(classes) {
span {
+announcement
}
}
}
val containerClasses = if (stretch) "container-full" else "container"
div(containerClasses) {
div("main") {
@ -84,6 +100,10 @@ class MainTemplate(
}
}
div("offline-banner") {
span { +"Die Verbindung zum Server ist unterbrochen" }
}
if (!noMenu) {
div("footer") {
div("container") {

View file

@ -9,6 +9,7 @@ web = "web"
sessions = "data/sessions"
uploads = "data/uploads"
database = "data/portal.db"
announcement = "data/announcement.txt"
[schedule]
reference = "1970-01-01"