Add repositories

- Make model classes imutable
- Move model classes to common module
- Add repositories for backend and frontend
  - Backend repositories communicates with sqlite
  - Frontend repositories commuincates with rest api
- Add rest api
  - Authentication via cookies
- Add coroutines to forntend
- Add change listener to repositories
- Transmit change events to frontend repositories via websocket
- Switch to server side sessions
- Move setup from ui to cli
This commit is contained in:
Lars Westermann 2019-05-12 16:40:21 +02:00
parent 4e5dc610a3
commit b7d6476a70
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
66 changed files with 2854 additions and 2336 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
.idea/
build/
web/
.sessions/
*.swp
*.swo

View file

@ -35,7 +35,6 @@ kotlin {
compilations.all {
kotlinOptions {
freeCompilerArgs += [
"-Xuse-experimental=io.ktor.locations.KtorExperimentalLocationsAPI",
"-Xuse-experimental=io.ktor.util.KtorExperimentalAPI"
]
}
@ -54,6 +53,7 @@ kotlin {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
implementation "de.westermann:KObserve-metadata:0.9.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
}
@ -72,8 +72,9 @@ kotlin {
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-auth:$ktor_version"
implementation "io.ktor:ktor-locations:$ktor_version"
implementation "io.ktor:ktor-server-sessions:$ktor_version"
implementation "io.ktor:ktor-websockets:$ktor_version"
implementation "io.ktor:ktor-jackson:$ktor_version"
implementation 'org.jetbrains:kotlin-css-jvm:1.0.0-pre.70-kotlin-1.3.21'
implementation "io.ktor:ktor-html-builder:$ktor_version"
@ -83,6 +84,8 @@ kotlin {
implementation 'org.mindrot:jbcrypt:0.4'
implementation "de.westermann:KObserve-jvm:0.9.1"
api 'io.github.microutils:kotlin-logging:1.6.23'
api 'ch.qos.logback:logback-classic:1.2.3'
api 'org.fusesource.jansi:jansi:1.8'
@ -155,10 +158,12 @@ task run(type: JavaExec, dependsOn: [jvmMainClasses, jsJar]) {
]
}
args = []
standardInput = System.in
}
clean.doFirst {
delete webFolder
delete ".sessions"
}
task jar(type: ShadowJar, dependsOn: [jvmMainClasses, jsMainClasses, sass]) {

View file

@ -1,3 +1,3 @@
package kif.common.model
package de.kif.common
const val CALENDAR_GRID_WIDTH = 15

View file

@ -0,0 +1,35 @@
package de.kif.common
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
@Serializable
data class Message(
val type: MessageType,
val repository: RepositoryType,
val id: Long
) {
fun stringify(): String {
return json.stringify(serializer(), this)
}
companion object {
private val jsonContext = SerializersModule {}
val json = Json(context = jsonContext)
fun parse(data: String): Message {
return json.parse(serializer(), data)
}
}
}
enum class MessageType {
CREATE, UPDATE, DELETE
}
enum class RepositoryType {
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP
}

View file

@ -0,0 +1,22 @@
package de.kif.common
import de.kif.common.model.Model
import de.westermann.kobserve.event.EventHandler
interface Repository<T : Model> {
suspend fun get(id: Long): T?
suspend fun create(model: T): Long
suspend fun update(model: T)
suspend fun delete(id: Long)
suspend fun all(): List<T>
val onCreate: EventHandler<Long>
val onUpdate: EventHandler<Long>
val onDelete: EventHandler<Long>
}

View file

@ -1,7 +1,6 @@
package kif.common.model
package de.kif.common.model
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
data class Color(
@ -21,25 +20,15 @@ data class Color(
}
}
@Transient
val redDouble
get() = red / 255.0
fun calcRedDouble() = red / 255.0
@Transient
val greenDouble
get() = green / 255.0
fun calcGreenDouble() = green / 255.0
@Transient
val blueDouble
get() = blue / 255.0
fun calcBlueDouble() = blue / 255.0
@Transient
val luminance: Double
get() = 0.2126 * redDouble + 0.7152 * greenDouble + 0.0722 * blueDouble
fun calcLuminance(): Double = 0.2126 * calcRedDouble() + 0.7152 * calcGreenDouble() + 0.0722 * calcBlueDouble()
@Transient
val textColor: Color
get() = if (luminance < 0.7) WHITE else BLACK
fun calcTextColor(): Color = if (calcLuminance() < 0.7) WHITE else BLACK
companion object {
fun parse(color: String): Color {
@ -94,3 +83,5 @@ data class Color(
)
}
}
fun String.parseColor() = Color.parse(this)

View file

@ -0,0 +1,6 @@
package de.kif.common.model
enum class Language(val code: String, val localeName: String) {
GERMAN("DE", "Deutsch"),
ENGLISH("EN", "English")
}

View file

@ -0,0 +1,3 @@
package de.kif.common.model
interface Model

View file

@ -1,4 +1,4 @@
package de.kif.backend.model
package de.kif.common.model
enum class Permission {
USER, SCHEDULE, WORK_GROUP, ROOM, PERSON, ADMIN

View file

@ -0,0 +1,11 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class Room(
val id: Long? = null,
val name: String,
val places: Int,
val projector: Boolean
) : Model

View file

@ -0,0 +1,12 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class Schedule(
val id: Long?,
val workGroup: WorkGroup,
val room: Room,
val day: Int,
val time: Int
) : Model

View file

@ -0,0 +1,10 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class Track(
val id: Long?,
var name: String,
var color: Color
) : Model

View file

@ -0,0 +1,16 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: Long?,
val username: String,
val password: String,
val permissions: Set<Permission>
) : Model {
fun checkPermission(permission: Permission): Boolean {
return permission in permissions || Permission.ADMIN in permissions
}
}

View file

@ -0,0 +1,15 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class WorkGroup(
val id: Long?,
val name: String,
val interested: Int,
val track: Track?,
val projector: Boolean,
val resolution: Boolean,
val length: Int,
val language: Language
) : Model

View file

@ -1,58 +0,0 @@
package kif.common.model
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
@Serializable
data class Message(
@Polymorphic
val message: MessageType
) {
fun stringify(): String = Companion.stringify(this)
companion object {
private val module = SerializersModule {
polymorphic(MessageType::class) {
MessageCreateCalendarEntry::class with MessageCreateCalendarEntry.serializer()
MessageDeleteCalendarEntry::class with MessageDeleteCalendarEntry.serializer()
}
}
private val json = Json(context = module)
fun stringify(message: Message): String {
return json.stringify(serializer(), message)
}
fun parse(message: String): Message {
return json.parse(serializer(), message)
}
}
}
abstract class MessageType()
@Serializable
data class MessageCreateCalendarEntry(
val day: Int,
val time: Int,
val cellTime: Int,
val room: Int,
val workGroupId: Int,
val workGroupName: String,
val workGroupLength: Int,
val workGroupLanguage: String,
val workGroupColor: Color? = null
) : MessageType()
@Serializable
data class MessageDeleteCalendarEntry(
val day: Int,
val time: Int,
val roomId: Int,
val workGroupId: Int
) : MessageType()

View file

@ -0,0 +1,76 @@
package de.kif.frontend
import de.kif.common.Message
import de.kif.common.MessageType
import de.kif.common.RepositoryType
import de.kif.frontend.repository.*
import de.westermann.kwebview.async
import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
import kotlin.browser.window
class WebSocketClient() {
private val url = "ws://${window.location.host}/"
private lateinit var ws: WebSocket
@Suppress("UNUSED_PARAMETER")
private fun onOpen(event: Event) {
console.log("Connected!")
}
private fun onMessage(messageEvent: MessageEvent) {
val message = Message.parse(messageEvent.data?.toString() ?: "")
for (handler in messageHandlers) {
if (handler.repository == message.repository) {
when (message.type) {
MessageType.CREATE -> handler.onCreate(message.id)
MessageType.UPDATE -> handler.onUpdate(message.id)
MessageType.DELETE -> handler.onDelete(message.id)
}
}
}
}
@Suppress("UNUSED_PARAMETER")
private fun onClose(event: Event) {
console.log("Disconnected!")
async(1000) {
connect()
}
}
@Suppress("UNUSED_PARAMETER")
private fun onError(event: Event) {
console.log("An error occurred!")
}
private fun connect() {
ws = WebSocket(url)
ws.onopen = this::onOpen
ws.onmessage = this::onMessage
ws.onclose = this::onClose
ws.onerror = this::onError
}
private val messageHandlers: List<MessageHandler> = listOf(
RoomRepository.handler,
ScheduleRepository.handler,
TrackRepository.handler,
UserRepository.handler,
WorkGroupRepository.handler
)
init {
connect()
}
}
abstract class MessageHandler(val repository: RepositoryType) {
abstract fun onCreate(id: Long)
abstract fun onUpdate(id: Long)
abstract fun onDelete(id: Long)
}

View file

@ -1,11 +0,0 @@
package de.kif.frontend.calendar
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection
import de.westermann.kwebview.createHtmlView
class Calendar : ViewCollection<View>(createHtmlView()) {
init {
}
}

View file

@ -0,0 +1,39 @@
package de.kif.frontend
import org.w3c.dom.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.startCoroutine
operator fun HTMLCollection.iterator() = object : Iterator<HTMLElement> {
private var index = 0
override fun hasNext(): Boolean {
return index < this@iterator.length
}
override fun next(): HTMLElement {
return this@iterator.get(index++) as HTMLElement
}
}
operator fun NodeList.iterator() = object : Iterator<Node> {
private var index = 0
override fun hasNext(): Boolean {
return index < this@iterator.length
}
override fun next(): Node {
return this@iterator.get(index++)!!
}
}
fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) =
block.startCoroutine(Continuation(context) { result ->
result.onFailure { exception ->
console.error(exception)
}
})

View file

@ -1,381 +1,13 @@
package de.kif.frontend
import de.westermann.kobserve.property.mapBinding
import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body
import de.kif.frontend.views.initCalendar
import de.westermann.kwebview.components.init
import kif.common.model.*
import org.w3c.dom.*
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import kotlin.browser.document
import kotlin.browser.window
import kotlin.collections.Iterator
import kotlin.collections.List
import kotlin.collections.emptyList
import kotlin.collections.filter
import kotlin.collections.find
import kotlin.collections.forEach
import kotlin.collections.listOf
import kotlin.collections.minus
import kotlin.collections.plus
import kotlin.dom.appendText
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
init {
var linkM10: HTMLAnchorElement? = null
var linkM5: HTMLAnchorElement? = null
var linkReset: HTMLAnchorElement? = null
var linkP5: HTMLAnchorElement? = null
var linkP10: HTMLAnchorElement? = null
var linkDel: HTMLAnchorElement? = null
for (element in html.children) {
when {
element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement
}
}
linkM10 = linkM10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m10")
link.textContent = "-10"
html.appendChild(link)
link
}
linkM10.removeAttribute("href")
linkM10.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 10}/${entry.workGroup}") {
println("success")
}
}
})
linkM5 = linkM5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m5")
link.textContent = "-5"
html.appendChild(link)
link
}
linkM5.removeAttribute("href")
linkM5.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 5}/${entry.workGroup}") {
println("success")
}
}
})
linkReset = linkReset ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-reset")
link.textContent = "reset"
html.appendChild(link)
link
}
linkReset.removeAttribute("href")
linkReset.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.cellTime}/${entry.workGroup}") {
println("success")
}
}
})
linkP5 = linkP5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p5")
link.textContent = "+5"
html.appendChild(link)
link
}
linkP5.removeAttribute("href")
linkP5.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 5}/${entry.workGroup}") {
println("success")
}
}
})
linkP10 = linkP10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p10")
link.textContent = "+10"
html.appendChild(link)
link
}
linkP10.removeAttribute("href")
linkP10.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 10}/${entry.workGroup}") {
println("success")
}
}
})
linkDel = linkDel ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-del")
link.textContent = "del"
html.appendChild(link)
link
}
linkDel.removeAttribute("href")
linkDel.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
println("success")
}
})
}
}
class CalendarEntry(view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null
var day by dataset.property("day")
val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 }
var time by dataset.property("time")
val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 }
var room by dataset.property("room")
val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 }
var cellTime by dataset.property("cellTime")
var language by dataset.property("language")
var workGroup by dataset.property("workgroup")
val workGroupId by dataset.property("workgroup").mapBinding { it?.toIntOrNull() ?: 0 }
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
newCell?.classList?.remove("drag")
val cell = calendarCells.find {
position in it.dimension
}
if (cell != null) {
cell.classList.add("drag")
cell += this
newCell = cell
}
event.preventDefault()
event.stopPropagation()
}
private fun onFinishMove(event: MouseEvent) {
classList -= "drag"
newCell?.let { cell ->
cell.classList -= "drag"
val newTime = cell.time
val newRoom = cell.room
val day =
(document.getElementsByClassName("calendar")[0] as? HTMLElement)?.dataset?.get("day")?.toIntOrNull()
?: 0
classList += "pending"
get("/calendar/$day/$room/$time/-1") {
get("/calendar/$day/$newRoom/$newTime/$workGroup") {
println("success")
}
}
}
newCell = null
for (it in listeners) {
it.detach()
}
listeners = emptyList()
event.preventDefault()
event.stopPropagation()
}
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
init {
onMouseDown { event ->
if (event.target != html || "pending" in classList) {
event.stopPropagation()
return@onMouseDown
}
classList += "drag"
mouseDelta = event.toPoint() - point
listeners = listOf(
Body.onMouseMove.reference(this::onMove),
Body.onMouseUp.reference(this::onFinishMove),
Body.onMouseLeave.reference(this::onFinishMove)
)
event.preventDefault()
event.stopPropagation()
}
var calendarTools: CalendarTools? = null
for (item in html.children) {
if (item.classList.contains("calendar-tools")) {
calendarTools = CalendarTools(this, item)
break
}
}
if (calendarTools == null) {
calendarTools = CalendarTools(this, createHtmlView())
html.appendChild(calendarTools.html)
}
}
companion object {
fun create(
day: Int,
time: Int,
cellTime: Int,
room: Int,
workGroupId: Int,
workGroupName: String,
workGroupLength: Int,
workGroupLanguage: String,
workGroupColor: Color?
): CalendarEntry {
val entry = CalendarEntry(createHtmlView())
entry.day = day.toString()
entry.time = time.toString()
entry.cellTime = cellTime.toString()
entry.room = room.toString()
entry.workGroup = workGroupId.toString()
entry.language = workGroupLanguage
if (workGroupColor != null) {
entry.style {
val size = workGroupLength / CALENDAR_GRID_WIDTH.toDouble()
val pos = (time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
val pct = "${pos * 100}%"
val sz = "${size * 100}%"
left = pct
top = "calc($pct + 0.1rem)"
width = sz
height = "calc($sz - 0.2rem)"
backgroundColor = workGroupColor.toString()
color = workGroupColor.textColor.toString()
}
}
entry.html.appendText(workGroupName)
return entry
}
}
}
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
var day by dataset.property("day")
val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 }
var time by dataset.property("time")
val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 }
var room by dataset.property("room")
val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 }
}
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = emptyList()
fun main() = init {
WebSocketClient()
val ws = WebSocket("ws://${window.location.host}/".also { println(it) })
ws.onmessage = {
val messageWrapper = Message.parse(it.data?.toString() ?: "")
val message = messageWrapper.message
println(message)
when (message) {
is MessageCreateCalendarEntry -> {
val entry = CalendarEntry.create(
message.day,
message.time,
message.cellTime,
message.room,
message.workGroupId,
message.workGroupName,
message.workGroupLength,
message.workGroupLanguage,
message.workGroupColor
)
for (cell in calendarCells) {
if (cell.dayId == message.day && cell.timeId == message.cellTime && cell.roomId == message.room) {
cell.html.appendChild(entry.html)
calendarEntries += entry
break
}
}
}
is MessageDeleteCalendarEntry -> {
val remove = calendarEntries.filter { entry ->
entry.dayId == message.day &&
entry.timeId == message.time &&
entry.roomId == message.roomId &&
entry.workGroupId == message.workGroupId
}
calendarEntries -= remove
remove.forEach {
it.html.remove()
}
}
else -> {
}
}
if (document.getElementsByClassName("calendar").length > 0) {
initCalendar()
}
ws.onopen = {
console.log("yes!")
}
calendarEntries = document.getElementsByClassName("calendar-entry")
.iterator().asSequence().map(::CalendarEntry).toList()
calendarCells = document.getElementsByClassName("calendar-cell")
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
}
operator fun HTMLCollection.iterator() = object : Iterator<HTMLElement> {
private var index = 0
override fun hasNext(): Boolean {
return index < this@iterator.length
}
override fun next(): HTMLElement {
return this@iterator.get(index++) as HTMLElement
}
}

View file

@ -0,0 +1,106 @@
package de.kif.frontend.repository
import de.kif.common.Repository
import de.kif.common.model.Model
import org.w3c.xhr.XMLHttpRequest
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.js.Promise
suspend fun repositoryGet(
url: String
): dynamic {
console.log("GET: $url")
val promise = Promise<dynamic> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
resolve(JSON.parse(xhttp.responseText))
} else {
reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}"))
}
}
}
xhttp.open("GET", url, true)
xhttp.send()
}
try {
val d = promise.await()
return if (d.OK) {
d.data
} else {
null
}
} catch (e: NoSuchElementException) {
console.error(e)
return null
}
}
suspend fun repositoryPost(
url: String,
data: String? = null
): dynamic {
console.log("POST: $url", data)
val promise = Promise<dynamic> { resolve, reject ->
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
resolve(JSON.parse(xhttp.responseText))
} else {
reject(NoSuchElementException("${xhttp.status}: ${xhttp.statusText}"))
}
}
}
xhttp.open("POST", url, true)
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(data)
}
try {
val d = promise.await()
return if (d.OK) {
d.data
} else {
null
}
} catch (e: NoSuchElementException) {
console.error(e)
return null
}
}
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
then({ cont.resume(it) }, { cont.resumeWithException(it) })
}
class RepositoryDelegate<T : Model>(
private val repository: Repository<T>,
private val id: Long
) {
private var backing: T? = null
suspend fun get(): T {
if (backing == null) {
backing = repository.get(id) ?: throw NoSuchElementException()
}
return backing!!
}
fun set(value: T) {
backing = value
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Room
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object RoomRepository : Repository<Room> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Room? {
val json = repositoryGet("/api/room/$id") ?: return null
return parser.parse(json, Room.serializer())
}
override suspend fun create(model: Room): Long {
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Room) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/room/${model.id}", Message.json.stringify(Room.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/room/$id/delete")
}
override suspend fun all(): List<Room> {
val json = repositoryGet("/api/rooms") ?: return emptyList()
return parser.parse(json, Room.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.ROOM) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Schedule
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object ScheduleRepository : Repository<Schedule> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Schedule? {
val json = repositoryGet("/api/schedule/$id") ?: return null
return parser.parse(json, Schedule.serializer())
}
override suspend fun create(model: Schedule): Long {
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Schedule) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/schedule/$id/delete")
}
override suspend fun all(): List<Schedule> {
val json = repositoryGet("/api/schedules") ?: return emptyList()
return parser.parse(json, Schedule.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.SCHEDULE) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Track
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object TrackRepository : Repository<Track> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Track? {
val json = repositoryGet("/api/track/$id") ?: return null
return parser.parse(json, Track.serializer())
}
override suspend fun create(model: Track): Long {
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Track) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/track/${model.id}", Message.json.stringify(Track.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/track/$id/delete")
}
override suspend fun all(): List<Track> {
val json = repositoryGet("/api/tracks") ?: return emptyList()
return parser.parse(json, Track.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.TRACK) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.User
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object UserRepository : Repository<User> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): User? {
val json = repositoryGet("/api/user/$id") ?: return null
return parser.parse(json, User.serializer())
}
override suspend fun create(model: User): Long {
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: User) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/user/${model.id}", Message.json.stringify(User.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/user/$id/delete")
}
override suspend fun all(): List<User> {
val json = repositoryGet("/api/users") ?: return emptyList()
return parser.parse(json, User.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.USER) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,52 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.WorkGroup
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
object WorkGroupRepository : Repository<WorkGroup> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): WorkGroup? {
val json = repositoryGet("/api/workgroup/$id") ?: return null
return parser.parse(json, WorkGroup.serializer())
}
override suspend fun create(model: WorkGroup): Long {
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))?.toLong()
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: WorkGroup) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/workgroup/$id/delete")
}
override suspend fun all(): List<WorkGroup> {
val json = repositoryGet("/api/workgroups") ?: return emptyList()
return parser.parse(json, WorkGroup.serializer().list)
}
val handler = object : MessageHandler(RepositoryType.WORK_GROUP) {
override fun onCreate(id: Long) = onCreate.emit(id)
override fun onUpdate(id: Long) = onUpdate.emit(id)
override fun onDelete(id: Long) = onDelete.emit(id)
}
}

View file

@ -0,0 +1,359 @@
package de.kif.frontend.views
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Room
import de.kif.common.model.Schedule
import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.RoomRepository
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.get
import kotlin.browser.document
import kotlin.dom.appendText
import kotlin.dom.isText
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
init {
var linkM10: HTMLAnchorElement? = null
var linkM5: HTMLAnchorElement? = null
var linkReset: HTMLAnchorElement? = null
var linkP5: HTMLAnchorElement? = null
var linkP10: HTMLAnchorElement? = null
var linkDel: HTMLAnchorElement? = null
for (element in html.children) {
when {
element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement
}
}
linkM10 = linkM10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m10")
link.textContent = "-10"
html.appendChild(link)
link
}
linkM10.removeAttribute("href")
linkM10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 10))
}
})
linkM5 = linkM5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m5")
link.textContent = "-5"
html.appendChild(link)
link
}
linkM5.removeAttribute("href")
linkM5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time - 5))
}
})
linkReset = linkReset ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-reset")
link.textContent = "reset"
html.appendChild(link)
link
}
linkReset.removeAttribute("href")
linkReset.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH))
}
})
linkP5 = linkP5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p5")
link.textContent = "+5"
html.appendChild(link)
link
}
linkP5.removeAttribute("href")
linkP5.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 5))
}
})
linkP10 = linkP10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p10")
link.textContent = "+10"
html.appendChild(link)
link
}
linkP10.removeAttribute("href")
linkP10.addEventListener("click", EventListener {
entry.pending = true
launch {
val s = entry.schedule.get()
ScheduleRepository.update(s.copy(time = s.time + 10))
}
})
linkDel = linkDel ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-del")
link.textContent = "del"
html.appendChild(link)
link
}
linkDel.removeAttribute("href")
linkDel.addEventListener("click", EventListener {
entry.pending = true
launch {
ScheduleRepository.delete(entry.scheduleId)
}
})
}
}
class CalendarEntry(view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null
private var language by dataset.property("language")
val scheduleId = dataset["id"]?.toLongOrNull() ?: 0
val schedule = RepositoryDelegate(ScheduleRepository, scheduleId)
var pending by classList.property("pending")
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
val cell = calendarCells.find {
position in it.dimension
}
if (cell != null) {
cell += this
if (newCell == null) {
style {
left = "0"
top = "0.1rem"
}
}
newCell = cell
}
event.preventDefault()
event.stopPropagation()
}
private fun onFinishMove(event: MouseEvent) {
classList -= "drag"
newCell?.let { cell ->
launch {
val newTime = cell.time
val newRoom = cell.getRoom()
pending = true
val s = schedule.get().copy(room = newRoom, time = newTime)
ScheduleRepository.update(s)
}
}
newCell = null
for (it in listeners) {
it.detach()
}
listeners = emptyList()
event.preventDefault()
event.stopPropagation()
}
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
init {
onMouseDown { event ->
if (event.target != html || "pending" in classList) {
event.stopPropagation()
return@onMouseDown
}
launch {
classList += "drag"
val s = schedule.get()
val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val p = calendarCells.find {
it.day == s.day && it.time == time && it.roomId == s.room.id
}?.dimension?.center ?: dimension.center
mouseDelta = event.toPoint() - p
listeners = listOf(
Body.onMouseMove.reference(this::onMove),
Body.onMouseUp.reference(this::onFinishMove),
Body.onMouseLeave.reference(this::onFinishMove)
)
}
event.preventDefault()
event.stopPropagation()
}
var calendarTools: CalendarTools? = null
for (item in html.children) {
if (item.classList.contains("calendar-tools")) {
calendarTools = CalendarTools(this, item)
break
}
}
if (calendarTools == null) {
calendarTools = CalendarTools(this, createHtmlView())
html.appendChild(calendarTools.html)
}
}
fun load(schedule: Schedule) {
pending = false
language = schedule.workGroup.language.code
this.schedule.set(schedule)
style {
val size = schedule.workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
val ps = "${pos * 100}%"
val sz = "${size * 100}%"
left = ps
top = "calc($ps + 0.1rem)"
width = sz
height = "calc($sz - 0.2rem)"
if (schedule.workGroup.track?.color != null) {
backgroundColor = schedule.workGroup.track.color.toString()
color = schedule.workGroup.track.color.calcTextColor().toString()
}
}
for (element in html.childNodes) {
if (element.isText) {
html.removeChild(element)
}
}
html.appendText(schedule.workGroup.name)
val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
val cell = calendarCells.find {
it.day == schedule.day && it.time == time && it.roomId == schedule.room.id
}
if (cell != null && cell.html != html.parentElement) {
cell += this
}
}
companion object {
fun create(schedule: Schedule): CalendarEntry {
val entry = CalendarEntry(createHtmlView())
entry.load(schedule)
return entry
}
}
}
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
val day = dataset["day"]?.toIntOrNull() ?: 0
val time = dataset["time"]?.toIntOrNull() ?: 0
val roomId = dataset["room"]?.toLongOrNull() ?: 0
private lateinit var room: Room
suspend fun getRoom(): Room {
if (this::room.isInitialized) {
return room
}
room = RoomRepository.get(roomId) ?: throw NoSuchElementException()
return room
}
}
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = emptyList()
fun initCalendar() {
calendarEntries = document.getElementsByClassName("calendar-entry")
.iterator().asSequence().map(::CalendarEntry).toList()
calendarCells = document.getElementsByClassName("calendar-cell")
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
ScheduleRepository.onCreate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
CalendarEntry.create(schedule)
}
}
ScheduleRepository.onUpdate {
launch {
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
var found = false
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.load(schedule)
found = true
}
}
if (!found) {
CalendarEntry.create(schedule)
}
}
}
ScheduleRepository.onDelete {
for (entry in calendarEntries) {
if (entry.scheduleId == it) {
entry.html.remove()
}
}
}
}

View file

@ -3,6 +3,7 @@ package de.westermann.kwebview
import de.westermann.kobserve.event.EventListener
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.property.property
import org.w3c.dom.DOMTokenList
/**
@ -97,6 +98,23 @@ class ClassList(
)
}
fun property(clazz: String): Property<Boolean> {
if (clazz in bound) {
throw IllegalArgumentException("Class is already bound!")
}
val property = property(get(clazz))
bound[clazz] = Bound(property,
property.onChange.reference {
list.toggle(clazz, property.value)
}
)
return property
}
fun unbind(clazz: String) {
if (clazz !in bound) {
throw IllegalArgumentException("Class is not bound!")

View file

@ -7,10 +7,10 @@ import kotlin.math.min
* @author lars
*/
data class Dimension(
val left: Double,
val top: Double,
val width: Double = 0.0,
val height: Double = 0.0
val left: Double,
val top: Double,
val width: Double = 0.0,
val height: Double = 0.0
) {
constructor(position: Point, size: Point = Point.ZERO) : this(position.x, position.y, size.x, size.y)
@ -27,12 +27,15 @@ data class Dimension(
val bottom: Double
get() = top + height
val center: Point
get() = Point(left + width / 2.0, top + height / 2.0)
val edges: Set<Point>
get() = setOf(
Point(left, top),
Point(right, top),
Point(left, bottom),
Point(right, bottom)
Point(left, top),
Point(right, top),
Point(left, bottom),
Point(right, bottom)
)
val normalized: Dimension

View file

@ -110,7 +110,7 @@ fun get(
}
}
fun post(
fun postForm(
url: String,
data: Map<String, String> = emptyMap(),
onError: (Int) -> Unit = {},
@ -141,3 +141,43 @@ fun post(
xhttp.send()
}
}
fun postJson(
url: String,
data: dynamic,
onError: (Int) -> Unit = {},
onSuccess: (String) -> Unit = {}
) {
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
console.log(xhttp.status)
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
onSuccess(xhttp.responseText)
} else {
onError(xhttp.status.toInt())
}
}
}
xhttp.open("POST", url, true)
if (data.isNotEmpty()) {
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(data)
} else {
xhttp.send()
}
}
fun jsonObject(block: (dynamic) -> Unit): dynamic {
val json = js("{}")
block(json)
return json
}
fun jsonArray(block: (dynamic) -> Unit): dynamic {
val json = js("[]")
block(json)
return json
}

View file

@ -1,203 +1,60 @@
package de.kif.backend
import de.kif.backend.database.Connection
import de.kif.backend.model.User
import com.fasterxml.jackson.databind.SerializationFeature
import de.kif.backend.route.*
import de.kif.backend.route.api.*
import de.kif.backend.util.pushService
import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.install
import io.ktor.application.log
import io.ktor.auth.Authentication
import io.ktor.auth.FormAuthChallenge
import io.ktor.auth.UserPasswordCredential
import io.ktor.auth.form
import io.ktor.features.*
import io.ktor.http.content.files
import io.ktor.http.content.static
import io.ktor.locations.Location
import io.ktor.locations.Locations
import io.ktor.response.respondRedirect
import io.ktor.jackson.jackson
import io.ktor.routing.routing
import io.ktor.sessions.*
import io.ktor.util.hex
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
data class PortalSession(val id: Int, val name: String) {
suspend fun getUser(call: ApplicationCall): User {
val user = User.find(name)
if (user == null || user.id != id) {
call.sessions.clear<PortalSession>()
call.respondRedirect("/login?error")
throw IllegalAccessException()
}
return user
}
}
@Location("/")
class LocationDashboard()
@Location("/calendar/{day}")
data class LocationCalendar(val day: Int) {
@Location("/{room}/{time}")
data class LocationCalendarEdit(
val calendar: LocationCalendar,
val room: Int,
val time: Int,
val search: String = ""
) {
val day: Int get() = calendar.day
}
@Location("/{room}/{time}/{workGroup}")
data class LocationCalendarSet(
val calendar: LocationCalendar,
val room: Int,
val time: Int,
val workGroup: Int,
val next: String? = null
) {
val day: Int get() = calendar.day
}
@Location("/time-to-room")
data class LocationCalendarTimeToRoom(val calendar: LocationCalendar){
val day: Int get() = calendar.day
}
@Location("/room-to-time")
data class LocationCalendarRoomToTime(val calendar: LocationCalendar){
val day: Int get() = calendar.day
}
}
@Location("/login")
data class LocationLogin(val username: String = "", val password: String = "", val next: String = "/")
@Location("/logout")
class LocationLogout()
@Location("/account")
class LocationAccount()
@Location("/user")
data class LocationUser(val search: String = "") {
@Location("/{id}")
data class Edit(val id: Int)
@Location("/new")
class New()
@Location("/{id}/delete")
data class Delete(val id: Int)
}
@Location("/workgroup")
data class LocationWorkGroup(val search: String = "") {
@Location("/{id}")
data class Edit(val id: Int)
@Location("/new")
class New()
@Location("/{id}/delete")
data class Delete(val id: Int)
}
@Location("/track")
data class LocationTrack(val search: String = "") {
@Location("/{id}")
data class Edit(val id: Int)
@Location("/new")
class New()
@Location("/{id}/delete")
data class Delete(val id: Int)
}
@Location("/room")
data class LocationRoom(val search: String = "") {
@Location("/{id}")
data class Edit(val id: Int)
@Location("/new")
class New()
@Location("/{id}/delete")
data class Delete(val id: Int)
}
@Location("/person")
data class LocationPerson(val search: String = "") {
@Location("/{id}")
data class Edit(val id: Int)
@Location("/new")
class New()
@Location("/{id}/delete")
data class Delete(val id: Int)
}
fun Application.main() {
Connection.init()
install(DefaultHeaders)
install(CallLogging)
install(ConditionalHeaders)
install(Compression)
install(DataConversion)
install(Locations)
install(WebSockets)
install(Authentication) {
form {
userParamName = LocationLogin::username.name
passwordParamName = LocationLogin::password.name
challenge = FormAuthChallenge.Redirect { _ ->
"/login?error"
}
validate { credential: UserPasswordCredential ->
val user = User.find(credential.name) ?: return@validate null
if (user.checkPassword(credential.password)) user else null
}
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT) // Pretty Prints the JSON
}
}
val sessionKey = hex("1234567890abcdef") //TODO
install(Sessions) {
cookie<PortalSession>("SESSION") {
transform(SessionTransportTransformerMessageAuthentication(sessionKey))
}
}
security()
routing {
static("/static") {
files(Resources.directory)
}
launch {
val firstStart = User.exists()
if (firstStart) {
log.info("Please create the first user and restart the server!")
setup()
} else {
dashboard()
calendar()
login()
account()
// UI routes
dashboard()
calendar()
login()
account()
workGroup()
track()
room()
person()
user()
workGroup()
track()
room()
user()
pushService()
}
}
// RESTful routes
authenticateApi()
roomApi()
scheduleApi()
trackApi()
userApi()
workGroupApi()
// Web socket push notifications
pushService()
}
}
}

View file

@ -1,13 +1,50 @@
package de.kif.backend
import de.kif.backend.database.Connection
import de.kif.backend.repository.UserRepository
import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.application.Application
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.suspendCoroutine
object Main {
@Suppress("UnusedMainParameter")
@JvmStatic
fun main(args: Array<String>) {
Connection.init()
runBlocking {
if (UserRepository.all().isEmpty()) {
println("Please create a root user")
var username: String? = null
while (username == null) {
print("Username: ")
username = readLine()
}
var password: String? = null
while (password == null) {
print("Password: ")
password = System.console()?.readPassword()?.toString() ?: readLine()
}
UserRepository.create(
User(
null,
username,
hashPassword(password),
setOf(Permission.ADMIN)
)
)
}
}
embeddedServer(
factory = Netty,
port = 8080,

View file

@ -0,0 +1,125 @@
package de.kif.backend
import de.kif.backend.repository.UserRepository
import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.*
import io.ktor.request.path
import io.ktor.response.respondRedirect
import io.ktor.sessions.*
import io.ktor.util.hex
import io.ktor.util.pipeline.PipelineContext
import org.mindrot.jbcrypt.BCrypt
import java.io.File
interface ErrorContext {
suspend infix fun onFailure(block: suspend () -> Unit)
}
object ErrorContextIgnore : ErrorContext {
override suspend fun onFailure(block: suspend () -> Unit) {
// do nothing
}
}
class ErrorContextOccur() : ErrorContext {
override suspend fun onFailure(block: suspend () -> Unit) {
block()
}
}
suspend inline fun PipelineContext<Unit, ApplicationCall>.authenticate(
vararg permissions: Permission,
block: (user: User) -> Unit
): ErrorContext {
val user = call.sessions.get<PortalSession>()?.getUser(call)
return if (user == null || permissions.any { !user.checkPermission(it) }) {
ErrorContextOccur()
} else {
block(user)
ErrorContextIgnore
}
}
suspend inline fun PipelineContext<Unit, ApplicationCall>.authenticateOrRedirect(
vararg permissions: Permission,
block: (user: User) -> Unit
) {
authenticate(*permissions, block = block) onFailure {
call.respondRedirect("/login?redirect=${call.request.path()}}")
}
}
suspend fun PipelineContext<Unit, ApplicationCall>.isAuthenticated(vararg permissions: Permission): User? {
val user = call.sessions.get<PortalSession>()?.getUser(call)
return if (user == null || permissions.any { !user.checkPermission(it) }) {
null
} else {
user
}
}
data class PortalSession(val userId: Long) {
suspend fun getUser(call: ApplicationCall): User {
val user = UserRepository.get(userId)
if (user == null) {
call.sessions.clear<PortalSession>()
call.respondRedirect("/login?onFailure")
throw IllegalAccessException()
}
return user
}
}
data class UserPrinciple(val user: User) : Principal
fun checkPassword(password: String, hash: String): Boolean {
return BCrypt.checkpw(password, hash)
}
fun hashPassword(password: String): String {
return BCrypt.hashpw(password, BCrypt.gensalt())
}
fun Application.security() {
install(Authentication) {
form {
userParamName = "username"
passwordParamName = "password"
challenge = FormAuthChallenge.Redirect { _ ->
"/login?onFailure"
}
validate { credential: UserPasswordCredential ->
val user = UserRepository.find(credential.name) ?: return@validate null
if (checkPassword(credential.password, user.password)) UserPrinciple(user) else null
}
}
}
val encryptionKey =
hex("80 51 b8 13 b4 73 a9 69 c7 b0 10 ad 08 06 11 e3".replace(" ", ""))
val signKey =
hex(
"d1 20 23 8c 01 f8 f0 0d 9d 7c ff 68 21 97 75 31 38 3f fb 91 20 3a 8d 86 d4 e9 d8 50 f8 71 f1 dc".replace(
" ",
""
)
)
install(Sessions) {
cookie<PortalSession>(
"SESSION",
directorySessionStorage(File(".sessions"), cached = false)
) {
cookie.path = "/"
transform(SessionTransportTransformerMessageAuthentication(signKey))
}
}
}

View file

@ -16,9 +16,7 @@ object Connection {
transaction {
SchemaUtils.create(
DbPerson, DbPersonConstraint,
DbTrack, DbWorkGroup, DbWorkGroupConstraint,
DbLeader, DbWorkGroupOrder,
DbTrack, DbWorkGroup,
DbRoom, DbSchedule,
DbUser, DbUserPermission
)

View file

@ -1,71 +1,30 @@
package de.kif.backend.database
import de.kif.backend.model.Permission
import de.kif.common.model.Language
import de.kif.common.model.Permission
import org.jetbrains.exposed.sql.Table
object DbPerson : Table() {
val id = integer("id").autoIncrement().primaryKey()
val firstName = varchar("first_name", 64)
val lastName = varchar("last_name", 64)
val arrival = long("arrival").nullable()
val departure = long("departure").nullable()
}
object DbPersonConstraint : Table() {
val id = integer("id").autoIncrement().primaryKey()
val personId = integer("person_id")
val type = enumeration("type", DbConstraintType::class)
val time = long("time")
val duration = integer("duration").default(0)
val day = integer("day")
}
object DbTrack : Table() {
val id = integer("id").autoIncrement().primaryKey()
val id = long("id").autoIncrement().primaryKey()
val name = varchar("name", 64)
val color = varchar("color", 32)
}
object DbWorkGroup : Table() {
val id = integer("id").autoIncrement().primaryKey()
val id = long("id").autoIncrement().primaryKey()
val name = varchar("first_name", 64)
val interested = integer("interested")
val trackId = integer("track_id").nullable()
val trackId = long("track_id").nullable()
val projector = bool("projector")
val resolution = bool("resolution")
val language = enumeration("language", Language::class)
val length = integer("length")
val start = long("start").nullable()
val end = long("end").nullable()
}
object DbWorkGroupConstraint : Table() {
val id = integer("id").autoIncrement().primaryKey()
val workGroupId = integer("work_group_id")
val type = enumeration("type", DbConstraintType::class)
val time = long("time")
val duration = integer("duration").default(0)
val day = integer("day")
}
object DbLeader : Table() {
val workGroupId = integer("work_group_id").primaryKey(0)
val personId = integer("person_id").primaryKey(1)
}
object DbWorkGroupOrder : Table() {
val beforeWorkGroupId = integer("before_work_group_id").primaryKey(0)
val afterWorkGroupId = integer("after_work_group_id").primaryKey(1)
}
object DbRoom : Table() {
val id = integer("id").autoIncrement().primaryKey()
val id = long("id").autoIncrement().primaryKey()
val name = varchar("name", 64)
val places = integer("places")
@ -73,30 +32,20 @@ object DbRoom : Table() {
}
object DbSchedule : Table() {
val workGroupId = integer("work_group_id").primaryKey(0)
val day = integer("day").primaryKey(1)
val time = integer("time_slot").primaryKey(2)
val roomId = integer("room_id").primaryKey(3)
}
enum class DbConstraintType {
BEGIN, END, BLOCKED
val id = long("id").autoIncrement().primaryKey()
val workGroupId = long("work_group_id").index()
val roomId = long("room_id").index()
val day = integer("day").index()
val time = integer("time_slot")
}
object DbUser : Table() {
val userId = integer("id").autoIncrement().primaryKey()
val id = long("id").autoIncrement().primaryKey()
val username = varchar("username", 64).uniqueIndex()
val password = varchar("password", 64)
}
object DbUserPermission : Table() {
val userId = integer("id").primaryKey(0)
val userId = long("id").primaryKey(0)
val permission = enumeration("permission", Permission::class).primaryKey(1)
}
enum class Language(val value: String) {
GERMAN("Deutsch"), ENGLISH("English");
override fun toString() = value
val code = value.take(2).toLowerCase()
}

View file

@ -1,89 +0,0 @@
package de.kif.backend.model
import de.kif.backend.database.DbPerson
import de.kif.backend.database.DbPersonConstraint
import de.kif.backend.database.dbQuery
import org.jetbrains.exposed.sql.*
class Person(
var id: Int = -1,
var firstName: String = "",
var lastName: String = "",
var arrival: Long? = null,
var departure: Long? = null
) {
var constraints: Set<PersonConstraint> = emptySet()
suspend fun save() {
if (id < 0) {
dbQuery {
val newId = DbPerson.insert {
it[firstName] = this@Person.firstName
it[lastName] = this@Person.lastName
it[arrival] = this@Person.arrival
it[departure] = this@Person.departure
}[DbPerson.id]!!
this@Person.id = newId
}
for (constraint in constraints) {
constraint.save(this@Person.id)
}
} else {
dbQuery {
DbPerson.update({ DbPerson.id eq id }) {
it[firstName] = this@Person.firstName
it[lastName] = this@Person.lastName
it[arrival] = this@Person.arrival
it[departure] = this@Person.departure
}
DbPersonConstraint.deleteWhere { DbPersonConstraint.personId eq id }
}
for (constraint in constraints) {
constraint.save(this@Person.id)
}
}
}
suspend fun delete() {
val id = id
if (id >= 0) {
dbQuery {
DbPersonConstraint.deleteWhere { DbPersonConstraint.personId eq id }
DbPerson.deleteWhere { DbPerson.id eq id }
}
}
}
suspend fun loadConstraints() {
if (id >= 0) {
constraints = PersonConstraint.get(id)
}
}
companion object {
suspend fun get(personId: Int): Person? = dbQuery {
val result = DbPerson.select { DbPerson.id eq personId }.firstOrNull() ?: return@dbQuery null
Person(
result[DbPerson.id],
result[DbPerson.firstName],
result[DbPerson.lastName],
result[DbPerson.arrival],
result[DbPerson.departure]
)
}?.apply { loadConstraints() }
suspend fun list(): List<Person> = dbQuery {
val query = DbPerson.selectAll()
query.map { result ->
Person(
result[DbPerson.id],
result[DbPerson.firstName],
result[DbPerson.lastName],
result[DbPerson.arrival],
result[DbPerson.departure]
)
}
}.onEach { it.loadConstraints() }
}
}

View file

@ -1,132 +0,0 @@
package de.kif.backend.model
import de.kif.backend.database.DbConstraintType
import de.kif.backend.database.DbPersonConstraint
import de.kif.backend.database.dbQuery
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.update
sealed class PersonConstraint(
var id: Int = -1
) {
abstract suspend fun save(personId: Int)
suspend fun delete() {
val id = id
if (id >= 0) {
dbQuery {
DbPersonConstraint.deleteWhere { DbPersonConstraint.id eq id }
}
}
}
class BeginOnDay(
var time: Long = 0,
var day: Int = 0
) : PersonConstraint() {
override suspend fun save(personId: Int) {
if (id < 0) {
dbQuery {
val newId = DbPersonConstraint.insert {
it[this@insert.personId] = personId
it[type] = DbConstraintType.BEGIN
it[time] = this@BeginOnDay.time
it[day] = this@BeginOnDay.day
}[DbPersonConstraint.id]!!
this@BeginOnDay.id = newId
}
} else {
dbQuery {
DbPersonConstraint.update({ DbPersonConstraint.id eq id }) {
it[this@update.personId] = personId
it[type] = DbConstraintType.BEGIN
it[time] = this@BeginOnDay.time
it[day] = this@BeginOnDay.day
}
}
}
}
}
class EndOnDay(
var time: Long = 0,
var day: Int = 0
) : PersonConstraint() {
override suspend fun save(personId: Int) {
if (id < 0) {
dbQuery {
val newId = DbPersonConstraint.insert {
it[this@insert.personId] = personId
it[type] = DbConstraintType.END
it[time] = this@EndOnDay.time
it[day] = this@EndOnDay.day
}[DbPersonConstraint.id]!!
this@EndOnDay.id = newId
}
} else {
dbQuery {
DbPersonConstraint.update({ DbPersonConstraint.id eq id }) {
it[this@update.personId] = personId
it[type] = DbConstraintType.END
it[time] = this@EndOnDay.time
it[day] = this@EndOnDay.day
}
}
}
}
}
class BlockedOnDay(
var time: Long = 0,
var duration: Int = 0,
var day: Int = 0
) : PersonConstraint() {
override suspend fun save(personId: Int) {
if (id < 0) {
dbQuery {
val newId = DbPersonConstraint.insert {
it[this@insert.personId] = personId
it[type] = DbConstraintType.BLOCKED
it[time] = this@BlockedOnDay.time
it[duration] = this@BlockedOnDay.duration
it[day] = this@BlockedOnDay.day
}[DbPersonConstraint.id]!!
this@BlockedOnDay.id = newId
}
} else {
dbQuery {
DbPersonConstraint.update({ DbPersonConstraint.id eq id }) {
it[this@update.personId] = personId
it[type] = DbConstraintType.BLOCKED
it[time] = this@BlockedOnDay.time
it[duration] = this@BlockedOnDay.duration
it[day] = this@BlockedOnDay.day
}
}
}
}
}
companion object {
suspend fun get(personId: Int): Set<PersonConstraint> = dbQuery {
val result = DbPersonConstraint.select { DbPersonConstraint.personId eq personId }
result.map {
val id = it[DbPersonConstraint.id]
val type = it[DbPersonConstraint.type]
val time = it[DbPersonConstraint.time]
val duration = it[DbPersonConstraint.duration]
val day = it[DbPersonConstraint.day]
when (type) {
DbConstraintType.BEGIN -> PersonConstraint.BeginOnDay(time, day)
DbConstraintType.END -> PersonConstraint.EndOnDay(time, day)
DbConstraintType.BLOCKED -> PersonConstraint.BlockedOnDay(time, duration, day)
}.also { it.id = id }
}.toSet()
}
}
}

View file

@ -1,85 +0,0 @@
package de.kif.backend.model
import de.kif.backend.database.DbRoom
import de.kif.backend.database.dbQuery
import org.jetbrains.exposed.sql.*
class Room(
var id: Int = -1,
var name: String = "",
var places: Int = 0,
var projector: Boolean = false
) {
suspend fun save() {
if (id < 0) {
dbQuery {
val newId = DbRoom.insert {
it[name] = this@Room.name
it[places] = this@Room.places
it[projector] = this@Room.projector
}[DbRoom.id]!!
this@Room.id = newId
}
} else {
dbQuery {
DbRoom.update({ DbRoom.id eq id }) {
it[name] = this@Room.name
it[places] = this@Room.places
it[projector] = this@Room.projector
}
}
}
}
suspend fun delete() {
val id = id
if (id >= 0) {
for (it in Schedule.getByRoom(id)) {
it.delete()
}
dbQuery {
DbRoom.deleteWhere { DbRoom.id eq id }
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Room
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id
}
companion object {
suspend fun get(roomId: Int): Room = dbQuery {
val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: throw IllegalArgumentException()
Room(
result[DbRoom.id],
result[DbRoom.name],
result[DbRoom.places],
result[DbRoom.projector]
)
}
suspend fun list(): List<Room> = dbQuery {
val query = DbRoom.selectAll()
query.map { result ->
Room(
result[DbRoom.id],
result[DbRoom.name],
result[DbRoom.places],
result[DbRoom.projector]
)
}
}
}
}

View file

@ -1,99 +0,0 @@
package de.kif.backend.model
import de.kif.backend.database.DbSchedule
import de.kif.backend.database.dbQuery
import org.jetbrains.exposed.sql.*
data class Schedule(
val workGroupId: Int,
val day: Int,
val time: Int,
val roomId: Int
) {
lateinit var workGroup: WorkGroup
lateinit var room: Room
suspend fun save() {
delete()
dbQuery {
DbSchedule.insert {
it[workGroupId] = this@Schedule.workGroupId
it[day] = this@Schedule.day
it[time] = this@Schedule.time
it[roomId] = this@Schedule.roomId
}
}
}
suspend fun delete() {
dbQuery {
DbSchedule.deleteWhere {
(DbSchedule.workGroupId eq workGroupId) and
(DbSchedule.day eq day) and
(DbSchedule.time eq time) and
(DbSchedule.roomId eq roomId)
}
}
}
suspend fun loadConstraints() {
try {
workGroup = WorkGroup.get(workGroupId)
room = Room.get(roomId)
} catch (e: IllegalArgumentException) {
delete()
throw e
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Schedule
if (workGroupId != other.workGroupId) return false
return true
}
override fun hashCode(): Int {
return workGroupId
}
companion object {
private suspend fun parseQuery(block: () -> Query): List<Schedule> =
dbQuery {
val query = block()
query.map { result ->
Schedule(
result[DbSchedule.workGroupId],
result[DbSchedule.day],
result[DbSchedule.time],
result[DbSchedule.roomId]
)
}
}.onEach { it.loadConstraints() }
suspend fun getByRoom(roomId: Int): List<Schedule> = parseQuery {
DbSchedule.select { DbSchedule.roomId eq roomId }
}
suspend fun getByRoom(roomId: Int, day: Int, time: Int): List<Schedule> = parseQuery {
DbSchedule.select { (DbSchedule.roomId eq roomId) and (DbSchedule.day eq day) and (DbSchedule.time eq time) }
}
suspend fun getByWorkGroup(workGroupId: Int): List<Schedule> = parseQuery {
DbSchedule.select { DbSchedule.workGroupId eq workGroupId }
}
suspend fun getByDay(day: Int): List<Schedule> = parseQuery {
DbSchedule.select { DbSchedule.day eq day }
}
suspend fun list(): List<Schedule> = parseQuery {
DbSchedule.selectAll()
}
}
}

View file

@ -1,63 +0,0 @@
package de.kif.backend.model
import de.kif.backend.database.DbTrack
import de.kif.backend.database.dbQuery
import kif.common.model.Color
import org.jetbrains.exposed.sql.*
import java.lang.IllegalArgumentException
class Track(
var id: Int = -1,
var name: String = "",
var color: Color
) {
suspend fun save() {
if (id < 0) {
dbQuery {
val newId = DbTrack.insert {
it[name] = this@Track.name
it[color] = this@Track.color.toString()
}[DbTrack.id]!!
this@Track.id = newId
}
} else {
dbQuery {
DbTrack.update({ DbTrack.id eq id }) {
it[name] = this@Track.name
it[color] = this@Track.color.toString()
}
}
}
}
suspend fun delete() {
val id = id
if (id >= 0) {
dbQuery {
DbTrack.deleteWhere { DbTrack.id eq id }
}
}
}
companion object {
suspend fun get(TrackId: Int): Track = dbQuery {
val result = DbTrack.select { DbTrack.id eq TrackId }.firstOrNull() ?: throw IllegalArgumentException()
Track(
result[DbTrack.id],
result[DbTrack.name],
Color.parse(result[DbTrack.color])
)
}
suspend fun list(): List<Track> = dbQuery {
val query = DbTrack.selectAll()
query.map { result ->
Track(
result[DbTrack.id],
result[DbTrack.name],
Color.parse(result[DbTrack.color])
)
}
}
}
}

View file

@ -1,110 +0,0 @@
package de.kif.backend.model
import de.kif.backend.database.DbUser
import de.kif.backend.database.DbUserPermission
import de.kif.backend.database.dbQuery
import io.ktor.auth.Principal
import org.jetbrains.exposed.sql.*
import org.mindrot.jbcrypt.BCrypt
class User(
var id: Int = -1,
var username: String = "",
private var password: String = ""
) : Principal {
var permissions: Set<Permission> = emptySet()
fun checkPassword(password: String): Boolean {
return BCrypt.checkpw(password, this.password)
}
fun hashPassword(newPassword: String) {
password = BCrypt.hashpw(newPassword, BCrypt.gensalt())
}
fun checkPermission(permission: Permission): Boolean {
return permission in permissions || Permission.ADMIN in permissions
}
suspend fun loadPermissions() = dbQuery {
permissions = DbUserPermission.slice(DbUserPermission.permission).select {
DbUserPermission.userId eq id
}.map { it[DbUserPermission.permission] }.toSet()
}
suspend fun save() {
if (id < 0) {
dbQuery {
val newId = DbUser.insert {
it[username] = this@User.username
it[password] = this@User.password
}[DbUser.userId]!!
this@User.id = newId
for (permission in permissions) {
DbUserPermission.insert {
it[userId] = newId
it[this.permission] = permission
}
}
}
} else {
dbQuery {
DbUser.update({ DbUser.userId eq id }) {
it[username] = this@User.username
it[password] = this@User.password
}
DbUserPermission.deleteWhere { DbUserPermission.userId eq id }
for (permission in permissions) {
DbUserPermission.insert {
it[userId] = id
it[this.permission] = permission
}
}
}
}
}
suspend fun delete() {
val id = id
if (id >= 0) {
dbQuery {
DbUserPermission.deleteWhere { DbUserPermission.userId eq id }
DbUser.deleteWhere { DbUser.userId eq id }
}
}
}
companion object {
suspend fun create(username: String, password: String, permissions: Set<Permission>) {
val user = User(username = username)
user.hashPassword(password)
user.permissions = permissions
user.save()
}
suspend fun find(username: String): User? = dbQuery {
val result = DbUser.select { DbUser.username eq username }.firstOrNull() ?: return@dbQuery null
User(result[DbUser.userId], result[DbUser.username], result[DbUser.password])
}?.apply { loadPermissions() }
suspend fun get(userId: Int): User? = dbQuery {
val result = DbUser.select { DbUser.userId eq userId }.firstOrNull() ?: return@dbQuery null
User(result[DbUser.userId], result[DbUser.username], result[DbUser.password])
}?.apply { loadPermissions() }
suspend fun list(): List<User> = dbQuery {
val query = DbUser.selectAll()
query.map { result ->
User(result[DbUser.userId], result[DbUser.username], result[DbUser.password])
}
}.onEach { it.loadPermissions() }
suspend fun exists(): Boolean = dbQuery {
DbUser.selectAll().count() == 0
}
}
}

View file

@ -1,121 +0,0 @@
package de.kif.backend.model
import de.kif.backend.database.DbWorkGroup
import de.kif.backend.database.DbWorkGroupConstraint
import de.kif.backend.database.Language
import de.kif.backend.database.dbQuery
import io.ktor.features.NotFoundException
import org.jetbrains.exposed.sql.*
class WorkGroup(
var id: Int = -1,
var name: String = "",
var interested: Int = 0,
var trackId: Int? = null,
var projector: Boolean = false,
var resolution: Boolean = false,
var length: Int = 0,
var language: Language = Language.GERMAN,
var start: Long? = null,
var end: Long? = null
) {
var constraints: Set<WorkGroupConstraint> = emptySet()
var track: Track? = null
suspend fun save() {
if (id < 0) {
dbQuery {
val newId = DbWorkGroup.insert {
it[name] = this@WorkGroup.name
it[interested] = this@WorkGroup.interested
it[trackId] = this@WorkGroup.trackId
it[projector] = this@WorkGroup.projector
it[resolution] = this@WorkGroup.resolution
it[length] = this@WorkGroup.length
it[language] = this@WorkGroup.language
it[start] = this@WorkGroup.start
it[end] = this@WorkGroup.end
}[DbWorkGroup.id]!!
this@WorkGroup.id = newId
}
for (constraint in constraints) {
constraint.save(this@WorkGroup.id)
}
} else {
dbQuery {
DbWorkGroup.update({ DbWorkGroup.id eq id }) {
it[name] = this@WorkGroup.name
it[interested] = this@WorkGroup.interested
it[trackId] = this@WorkGroup.trackId
it[projector] = this@WorkGroup.projector
it[resolution] = this@WorkGroup.resolution
it[length] = this@WorkGroup.length
it[language] = this@WorkGroup.language
it[start] = this@WorkGroup.start
it[end] = this@WorkGroup.end
}
DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id }
}
for (constraint in constraints) {
constraint.save(this@WorkGroup.id)
}
}
}
suspend fun delete() {
val id = id
if (id >= 0) {
for (it in Schedule.getByWorkGroup(id)) {
it.delete()
}
dbQuery {
DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id }
DbWorkGroup.deleteWhere { DbWorkGroup.id eq id }
}
}
}
suspend fun loadConstraints() {
if (id >= 0) {
constraints = WorkGroupConstraint.get(id)
track = trackId?.let { if (it < 0) null else Track.get(it) }
}
}
companion object {
suspend fun get(workGroupId: Int): WorkGroup = dbQuery {
val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: throw IllegalArgumentException()
WorkGroup(
result[DbWorkGroup.id],
result[DbWorkGroup.name],
result[DbWorkGroup.interested],
result[DbWorkGroup.trackId],
result[DbWorkGroup.projector],
result[DbWorkGroup.resolution],
result[DbWorkGroup.length],
result[DbWorkGroup.language],
result[DbWorkGroup.start],
result[DbWorkGroup.end]
)
}.apply { loadConstraints() }
suspend fun list(): List<WorkGroup> = dbQuery {
val query = DbWorkGroup.selectAll()
query.map { result ->
WorkGroup(
result[DbWorkGroup.id],
result[DbWorkGroup.name],
result[DbWorkGroup.interested],
result[DbWorkGroup.trackId],
result[DbWorkGroup.projector],
result[DbWorkGroup.resolution],
result[DbWorkGroup.length],
result[DbWorkGroup.language],
result[DbWorkGroup.start],
result[DbWorkGroup.end]
)
}
}.onEach { it.loadConstraints() }
}
}

View file

@ -1,132 +0,0 @@
package de.kif.backend.model
import de.kif.backend.database.DbConstraintType
import de.kif.backend.database.DbWorkGroupConstraint
import de.kif.backend.database.dbQuery
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.update
sealed class WorkGroupConstraint(
var id: Int = -1
) {
abstract suspend fun save(workGroupId: Int)
suspend fun delete() {
val id = id
if (id >= 0) {
dbQuery {
DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.id eq id }
}
}
}
class BeginOnDay(
var time: Long = 0,
var day: Int = 0
) : WorkGroupConstraint() {
override suspend fun save(workGroupId: Int) {
if (id < 0) {
dbQuery {
val newId = DbWorkGroupConstraint.insert {
it[this@insert.workGroupId] = workGroupId
it[type] = DbConstraintType.BEGIN
it[time] = this@BeginOnDay.time
it[day] = this@BeginOnDay.day
}[DbWorkGroupConstraint.id]!!
this@BeginOnDay.id = newId
}
} else {
dbQuery {
DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) {
it[this@update.workGroupId] = workGroupId
it[type] = DbConstraintType.BEGIN
it[time] = this@BeginOnDay.time
it[day] = this@BeginOnDay.day
}
}
}
}
}
class EndOnDay(
var time: Long = 0,
var day: Int = 0
) : WorkGroupConstraint() {
override suspend fun save(workGroupId: Int) {
if (id < 0) {
dbQuery {
val newId = DbWorkGroupConstraint.insert {
it[this@insert.workGroupId] = workGroupId
it[type] = DbConstraintType.END
it[time] = this@EndOnDay.time
it[day] = this@EndOnDay.day
}[DbWorkGroupConstraint.id]!!
this@EndOnDay.id = newId
}
} else {
dbQuery {
DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) {
it[this@update.workGroupId] = workGroupId
it[type] = DbConstraintType.END
it[time] = this@EndOnDay.time
it[day] = this@EndOnDay.day
}
}
}
}
}
class BlockedOnDay(
var time: Long = 0,
var duration: Int = 0,
var day: Int = 0
) : WorkGroupConstraint() {
override suspend fun save(workGroupId: Int) {
if (id < 0) {
dbQuery {
val newId = DbWorkGroupConstraint.insert {
it[this@insert.workGroupId] = workGroupId
it[type] = DbConstraintType.BLOCKED
it[time] = this@BlockedOnDay.time
it[duration] = this@BlockedOnDay.duration
it[day] = this@BlockedOnDay.day
}[DbWorkGroupConstraint.id]!!
this@BlockedOnDay.id = newId
}
} else {
dbQuery {
DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) {
it[this@update.workGroupId] = workGroupId
it[type] = DbConstraintType.BLOCKED
it[time] = this@BlockedOnDay.time
it[duration] = this@BlockedOnDay.duration
it[day] = this@BlockedOnDay.day
}
}
}
}
}
companion object {
suspend fun get(workGroupId: Int): Set<WorkGroupConstraint> = dbQuery {
val result = DbWorkGroupConstraint.select { DbWorkGroupConstraint.workGroupId eq workGroupId }
result.map {
val id = it[DbWorkGroupConstraint.id]
val type = it[DbWorkGroupConstraint.type]
val time = it[DbWorkGroupConstraint.time]
val duration = it[DbWorkGroupConstraint.duration]
val day = it[DbWorkGroupConstraint.day]
when (type) {
DbConstraintType.BEGIN -> WorkGroupConstraint.BeginOnDay(time, day)
DbConstraintType.END -> WorkGroupConstraint.EndOnDay(time, day)
DbConstraintType.BLOCKED -> WorkGroupConstraint.BlockedOnDay(time, duration, day)
}.also { it.id = id }
}.toSet()
}
}
}

View file

@ -0,0 +1,93 @@
package de.kif.backend.repository
import de.kif.backend.database.DbRoom
import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService
import de.kif.common.*
import de.kif.common.model.Room
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
object RoomRepository : Repository<Room> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private fun rowToModel(row: ResultRow): Room {
val id = row[DbRoom.id]
val name = row[DbRoom.name]
val places = row[DbRoom.places]
val projector = row[DbRoom.projector]
return Room(id, name, places, projector)
}
override suspend fun get(id: Long): Room? {
return dbQuery {
rowToModel(DbRoom.select { DbRoom.id eq id }.firstOrNull() ?: return@dbQuery null)
}
}
override suspend fun create(model: Room): Long {
return dbQuery {
val id = DbRoom.insert {
it[name] = model.name
it[places] = model.places
it[projector] = model.projector
}[DbRoom.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id)
id
}
}
override suspend fun update(model: Room) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
dbQuery {
DbRoom.update({ DbRoom.id eq model.id }) {
it[name] = model.name
it[places] = model.places
it[projector] = model.projector
}
onUpdate.emit(model.id)
}
}
override suspend fun delete(id: Long) {
onDelete.emit(id)
dbQuery {
DbRoom.deleteWhere { DbRoom.id eq id }
}
}
override suspend fun all(): List<Room> {
return dbQuery {
val result = DbRoom.selectAll()
result.map(this::rowToModel)
}
}
fun registerPushService() {
onCreate {
runBlocking {
PushService.notify(MessageType.CREATE, RepositoryType.ROOM, it)
}
}
onUpdate {
runBlocking {
PushService.notify(MessageType.UPDATE, RepositoryType.ROOM, it)
}
}
onDelete {
runBlocking {
PushService.notify(MessageType.DELETE, RepositoryType.ROOM, it)
}
}
}
}

View file

@ -0,0 +1,177 @@
package de.kif.backend.repository
import de.kif.backend.database.DbSchedule
import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService
import de.kif.common.*
import de.kif.common.model.Schedule
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
object ScheduleRepository : Repository<Schedule> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private suspend fun rowToModel(row: ResultRow): Schedule {
val id = row[DbSchedule.id]
val workGroupId = row[DbSchedule.workGroupId]
val roomId = row[DbSchedule.roomId]
val day = row[DbSchedule.day]
val time = row[DbSchedule.time]
val workGroup = WorkGroupRepository.get(workGroupId)
?: throw IllegalStateException("Work group for schedule does not exist!")
val room = RoomRepository.get(roomId)
?: throw IllegalStateException("Room for schedule does not exist!")
return Schedule(id, workGroup, room, day, time)
}
override suspend fun get(id: Long): Schedule? {
return dbQuery {
val row = DbSchedule.select { DbSchedule.id eq id }.firstOrNull() ?: return@dbQuery null
runBlocking {
rowToModel(row)
}
}
}
override suspend fun create(model: Schedule): Long {
return dbQuery {
val id = DbSchedule.insert {
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
it[day] = model.day
it[time] = model.time
}[DbSchedule.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id)
id
}
}
override suspend fun update(model: Schedule) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
dbQuery {
DbSchedule.update({ DbSchedule.id eq model.id }) {
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
it[day] = model.day
it[time] = model.time
}
onUpdate.emit(model.id)
}
}
override suspend fun delete(id: Long) {
onDelete.emit(id)
dbQuery {
DbSchedule.deleteWhere { DbSchedule.id eq id }
}
}
override suspend fun all(): List<Schedule> {
return dbQuery {
val result = DbSchedule.selectAll()
runBlocking {
result.map {
rowToModel(it)
}
}
}
}
suspend fun getByDay(day: Int): List<Schedule> {
return dbQuery {
val result = DbSchedule.select { DbSchedule.day eq day }
runBlocking {
result.map {
rowToModel(it)
}
}
}
}
suspend fun getByWorkGroup(workGroupId: Long): List<Schedule> {
return dbQuery {
val result = DbSchedule.select { DbSchedule.workGroupId eq workGroupId }
runBlocking {
result.map {
rowToModel(it)
}
}
}
}
suspend fun getByRoom(roomId: Long): List<Schedule> {
return dbQuery {
val result = DbSchedule.select { DbSchedule.roomId eq roomId }
runBlocking {
result.map {
rowToModel(it)
}
}
}
}
init {
RoomRepository.onUpdate { roomId ->
runBlocking {
getByRoom(roomId).forEach { schedule ->
if (schedule.id != null) onUpdate.emit(schedule.id)
}
}
}
RoomRepository.onDelete { roomId ->
runBlocking {
getByRoom(roomId).forEach { schedule ->
if (schedule.id != null) delete(schedule.id)
}
}
}
WorkGroupRepository.onUpdate { workGroupId ->
runBlocking {
getByWorkGroup(workGroupId).forEach { schedule ->
if (schedule.id != null) onUpdate.emit(schedule.id)
}
}
}
WorkGroupRepository.onDelete { workGroupId ->
runBlocking {
getByWorkGroup(workGroupId).forEach { schedule ->
if (schedule.id != null) delete(schedule.id)
}
}
}
}
fun registerPushService() {
onCreate {
runBlocking {
PushService.notify(MessageType.CREATE, RepositoryType.SCHEDULE, it)
}
}
onUpdate {
runBlocking {
PushService.notify(MessageType.UPDATE, RepositoryType.SCHEDULE, it)
}
}
onDelete {
runBlocking {
PushService.notify(MessageType.DELETE, RepositoryType.SCHEDULE, it)
}
}
}
}

View file

@ -0,0 +1,93 @@
package de.kif.backend.repository
import de.kif.backend.database.DbTrack
import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService
import de.kif.common.MessageType
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Track
import de.kif.common.model.parseColor
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
object TrackRepository : Repository<Track> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private fun rowToModel(row: ResultRow): Track {
val id = row[DbTrack.id]
val name = row[DbTrack.name]
val color = row[DbTrack.color].parseColor()
return Track(id, name, color)
}
override suspend fun get(id: Long): Track? {
return dbQuery {
rowToModel(DbTrack.select { DbTrack.id eq id }.firstOrNull() ?: return@dbQuery null)
}
}
override suspend fun create(model: Track): Long {
return dbQuery {
val id = DbTrack.insert {
it[name] = model.name
it[color] = model.color.toString()
}[DbTrack.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id)
id
}
}
override suspend fun update(model: Track) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
dbQuery {
DbTrack.update({ DbTrack.id eq model.id }) {
it[name] = model.name
it[color] = model.color.toString()
}
onUpdate.emit(model.id)
}
}
override suspend fun delete(id: Long) {
onDelete.emit(id)
dbQuery {
DbTrack.deleteWhere { DbTrack.id eq id }
}
}
override suspend fun all(): List<Track> {
return dbQuery {
val result = DbTrack.selectAll()
result.map(this::rowToModel)
}
}
fun registerPushService() {
onCreate {
runBlocking {
PushService.notify(MessageType.CREATE, RepositoryType.TRACK, it)
}
}
onUpdate {
runBlocking {
PushService.notify(MessageType.UPDATE, RepositoryType.TRACK, it)
}
}
onDelete {
runBlocking {
PushService.notify(MessageType.DELETE, RepositoryType.TRACK, it)
}
}
}
}

View file

@ -0,0 +1,120 @@
package de.kif.backend.repository
import de.kif.backend.database.DbUser
import de.kif.backend.database.DbUserPermission
import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService
import de.kif.common.MessageType
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.User
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
object UserRepository : Repository<User> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private fun rowToModel(row: ResultRow): User {
val id = row[DbUser.id]
val username = row[DbUser.username]
val password = row[DbUser.password]
val permissions = DbUserPermission.slice(DbUserPermission.permission).select {
DbUserPermission.userId eq id
}.map { it[DbUserPermission.permission] }.toSet()
return User(id, username, password, permissions)
}
override suspend fun get(id: Long): User? {
return dbQuery {
rowToModel(DbUser.select { DbUser.id eq id }.firstOrNull() ?: return@dbQuery null)
}
}
override suspend fun create(model: User): Long {
return dbQuery {
val id = DbUser.insert {
it[username] = model.username
it[password] = model.password
}[DbUser.id] ?: throw IllegalStateException("Cannot create model!")
for (permission in model.permissions) {
DbUserPermission.insert {
it[userId] = id
it[this.permission] = permission
}
}
onCreate.emit(id)
id
}
}
override suspend fun update(model: User) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
dbQuery {
DbUser.update({ DbUser.id eq model.id }) {
it[username] = model.username
it[password] = model.password
}
DbUserPermission.deleteWhere { DbUserPermission.userId eq model.id }
for (permission in model.permissions) {
DbUserPermission.insert {
it[userId] = model.id
it[this.permission] = permission
}
}
onUpdate.emit(model.id)
}
}
override suspend fun delete(id: Long) {
onDelete.emit(id)
dbQuery {
DbUserPermission.deleteWhere { DbUserPermission.userId eq id }
DbUser.deleteWhere { DbUser.id eq id }
}
}
override suspend fun all(): List<User> {
return dbQuery {
val result = DbUser.selectAll()
result.map(this::rowToModel)
}
}
suspend fun find(username: String): User? {
return dbQuery {
rowToModel(DbUser.select { DbUser.username eq username }.firstOrNull() ?: return@dbQuery null)
}
}
fun registerPushService() {
onCreate {
runBlocking {
PushService.notify(MessageType.CREATE, RepositoryType.USER, it)
}
}
onUpdate {
runBlocking {
PushService.notify(MessageType.UPDATE, RepositoryType.USER, it)
}
}
onDelete {
runBlocking {
PushService.notify(MessageType.DELETE, RepositoryType.USER, it)
}
}
}
}

View file

@ -0,0 +1,148 @@
package de.kif.backend.repository
import de.kif.backend.database.DbWorkGroup
import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService
import de.kif.common.MessageType
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.WorkGroup
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
object WorkGroupRepository : Repository<WorkGroup> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private suspend fun rowToModel(row: ResultRow): WorkGroup {
val id = row[DbWorkGroup.id]
val name = row[DbWorkGroup.name]
val interested = row[DbWorkGroup.interested]
val trackId = row[DbWorkGroup.trackId]
val projector = row[DbWorkGroup.projector]
val resolution = row[DbWorkGroup.resolution]
val length = row[DbWorkGroup.length]
val language = row[DbWorkGroup.language]
val track = trackId?.let { TrackRepository.get(it) }
return WorkGroup(id, name, interested, track, projector, resolution, length, language)
}
override suspend fun get(id: Long): WorkGroup? {
return dbQuery {
val row = DbWorkGroup.select { DbWorkGroup.id eq id }.firstOrNull() ?: return@dbQuery null
runBlocking {
rowToModel(row)
}
}
}
override suspend fun create(model: WorkGroup): Long {
return dbQuery {
val id = DbWorkGroup.insert {
it[name] = model.name
it[interested] = model.interested
it[trackId] = model.track?.id
it[projector] = model.projector
it[resolution] = model.resolution
it[length] = model.length
it[language] = model.language
}[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id)
id
}
}
override suspend fun update(model: WorkGroup) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
dbQuery {
DbWorkGroup.update({ DbWorkGroup.id eq model.id }) {
it[name] = model.name
it[interested] = model.interested
it[trackId] = model.track?.id
it[projector] = model.projector
it[resolution] = model.resolution
it[length] = model.length
it[language] = model.language
}
onUpdate.emit(model.id)
}
}
override suspend fun delete(id: Long) {
onDelete.emit(id)
dbQuery {
DbWorkGroup.deleteWhere { DbWorkGroup.id eq id }
}
}
override suspend fun all(): List<WorkGroup> {
return dbQuery {
val result = DbWorkGroup.selectAll()
result.map {
runBlocking {
rowToModel(it)
}
}
}
}
suspend fun getByTrack(trackId: Long?): List<WorkGroup> {
return dbQuery {
val result = DbWorkGroup.select { DbWorkGroup.trackId eq trackId }
result.map {
runBlocking {
rowToModel(it)
}
}
}
}
init {
TrackRepository.onUpdate { roomId ->
runBlocking {
getByTrack(roomId).forEach { workGroup ->
if (workGroup.id != null) onUpdate.emit(workGroup.id)
}
}
}
TrackRepository.onDelete { roomId ->
runBlocking {
getByTrack(roomId).forEach { workGroup ->
if (workGroup.id != null) {
update(workGroup.copy(track = null))
}
}
}
}
}
fun registerPushService() {
onCreate {
runBlocking {
PushService.notify(MessageType.CREATE, RepositoryType.WORK_GROUP, it)
}
}
onUpdate {
runBlocking {
PushService.notify(MessageType.UPDATE, RepositoryType.WORK_GROUP, it)
}
}
onDelete {
runBlocking {
PushService.notify(MessageType.DELETE, RepositoryType.WORK_GROUP, it)
}
}
}
}

View file

@ -1,26 +1,17 @@
package de.kif.backend.route
import de.kif.backend.LocationAccount
import de.kif.backend.LocationLogin
import de.kif.backend.PortalSession
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.request.path
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.routing.get
import kotlinx.html.*
fun Route.account() {
get<LocationAccount> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
get("/account") {
authenticateOrRedirect { user ->
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -34,7 +25,7 @@ fun Route.account() {
+"You have the following rights: ${user.permissions}"
br {}
a("/logout") {
button(classes="form-btn") {
button(classes = "form-btn") {
+"Logout"
}
}

View file

@ -1,40 +1,38 @@
package de.kif.backend.route
import de.kif.backend.LocationCalendar
import de.kif.backend.LocationLogin
import de.kif.backend.PortalSession
import de.kif.backend.model.Permission
import de.kif.backend.model.Room
import de.kif.backend.model.Schedule
import de.kif.backend.model.WorkGroup
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.isAuthenticated
import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.repository.WorkGroupRepository
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Permission
import de.kif.common.model.Room
import de.kif.common.model.Schedule
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.request.path
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import kif.common.model.CALENDAR_GRID_WIDTH
import kif.common.model.MessageCreateCalendarEntry
import kif.common.model.MessageDeleteCalendarEntry
import kotlinx.css.CSSBuilder
import kotlinx.css.Color
import kotlinx.css.pct
import kotlinx.css.rem
import kotlinx.html.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
const val MINUTES_OF_DAY = 24 * 60
private fun DIV.calendarCell(schedule: Schedule?, day: Int, time: Int) {
private fun DIV.calendarCell(schedule: Schedule?) {
if (schedule != null) {
span("calendar-entry") {
attributes["style"] = CSSBuilder().apply {
@ -50,42 +48,38 @@ private fun DIV.calendarCell(schedule: Schedule?, day: Int, time: Int) {
val c = schedule.workGroup.track?.color
if (c != null) {
backgroundColor = Color(c.toString())
color = Color(c.textColor.toString())
color = Color(c.calcTextColor().toString())
}
}.toString()
attributes["data-language"] = schedule.workGroup.language.code
attributes["data-day"] = schedule.day.toString()
attributes["data-room"] = schedule.room.id.toString()
attributes["data-time"] = schedule.time.toString()
attributes["data-cell-time"] = time.toString()
attributes["data-workgroup"] = schedule.workGroup.id.toString()
attributes["data-id"] = schedule.id.toString()
+schedule.workGroup.name
div("calendar-tools") {
a(
classes = "calendar-tools-m10",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time - 10}/${schedule.workGroupId}"
href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time - 10}/${schedule.workGroup.id}"
) { +"-10" }
a(
classes = "calendar-tools-m5",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time - 5}/${schedule.workGroupId}"
href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time - 5}/${schedule.workGroup.id}"
) { +"-05" }
a(
classes = "calendar-tools-reset",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/$time/${schedule.workGroupId}"
href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH}/${schedule.workGroup.id}"
) { +"reset" }
a(
classes = "calendar-tools-p5",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time + 5}/${schedule.workGroupId}"
href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time + 5}/${schedule.workGroup.id}"
) { +"+05" }
a(
classes = "calendar-tools-p10",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time + 10}/${schedule.workGroupId}"
href = "/calendar/${schedule.day}/${schedule.id}/delete?redirect=/calendar/${schedule.day}/${schedule.room.id}/${schedule.time + 10}/${schedule.workGroup.id}"
) { +"+10" }
a(
classes = "calendar-tools-del",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1"
href = "/calendar/${schedule.day}/${schedule.id}/delete"
) { +"del" }
}
}
@ -148,7 +142,7 @@ private fun DIV.renderTimeToRoom(
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
calendarCell(schedule, day, time)
calendarCell(schedule)
val href = if (allowEdit) "/calendar/$day/${room.id}/$start" else null
a(href, classes = "calendar-link")
@ -216,7 +210,7 @@ private fun DIV.renderRoomToTime(
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
calendarCell(schedule, day, time)
calendarCell(schedule)
val href = if (allowEdit) "/calendar/$day/${room.id}/$start" else null
a(href, classes = "calendar-link")
@ -233,37 +227,40 @@ fun Route.calendar() {
call.respondRedirect("/calendar/0", true)
}
get<LocationCalendar.LocationCalendarRoomToTime> { param ->
get("/calendar/{day}/rtt") {
call.response.cookies.append(
"orientation",
CalendarOrientation.ROOM_TO_TIME.name,
maxAge = Int.MAX_VALUE,
path = "/"
)
call.respondRedirect("/calendar/${param.day}")
val day = call.parameters["day"]?.toIntOrNull() ?: 0
call.respondRedirect("/calendar/$day")
}
get<LocationCalendar.LocationCalendarTimeToRoom> { param ->
get("/calendar/{day}/ttr") {
call.response.cookies.append(
"orientation",
CalendarOrientation.TIME_TO_ROOM.name,
maxAge = Int.MAX_VALUE,
path = "/"
)
call.respondRedirect("/calendar/${param.day}")
val day = call.parameters["day"]?.toIntOrNull() ?: 0
call.respondRedirect("/calendar/$day")
}
get<LocationCalendar> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
val allowEdit = user?.checkPermission(Permission.SCHEDULE) ?: false
val rooms = Room.list()
get("/calendar/{day}") {
val user = isAuthenticated(Permission.SCHEDULE)
val day = call.parameters["day"]?.toIntOrNull() ?: return@get
val rooms = RoomRepository.all()
val orientation = call.request.cookies["orientation"]?.let { name ->
CalendarOrientation.values().find { it.name == name }
} ?: CalendarOrientation.ROOM_TO_TIME
val day = param.day
val h = Schedule.getByDay(day)
val h = ScheduleRepository.getByDay(day)
val schedules = h.groupBy { it.room }.mapValues { (_, it) ->
it.associateBy {
it.time
@ -327,7 +324,7 @@ fun Route.calendar() {
max,
rooms,
schedules,
allowEdit
user != null
)
CalendarOrientation.TIME_TO_ROOM -> renderTimeToRoom(
day,
@ -335,7 +332,7 @@ fun Route.calendar() {
max,
rooms,
schedules,
allowEdit
user != null
)
}
}
@ -345,15 +342,15 @@ fun Route.calendar() {
}
}
get<LocationCalendar.LocationCalendarEdit> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.SCHEDULE)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val list = WorkGroup.list()
val room = Room.get(param.room)
val day = param.day
val time = param.time
get("/calendar/{day}/{room}/{time}") {
authenticateOrRedirect(Permission.SCHEDULE) { user ->
val day = call.parameters["day"]?.toIntOrNull() ?: return@get
val time = call.parameters["time"]?.toIntOrNull() ?: return@get
val roomId = call.parameters["room"]?.toLongOrNull() ?: return@get
val search = call.parameters["search"] ?: ""
val list = WorkGroupRepository.all()
val room = RoomRepository.get(roomId) ?: return@get
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
@ -363,12 +360,12 @@ fun Route.calendar() {
content {
h1 { +"Select work groups" }
insert(TableTemplate()) {
searchValue = param.search
searchValue = search
action {
a("/calendar/$day/${room.id}/$time/-1") {
a("/calendar/$day") {
button(classes = "form-btn btn-primary") {
+"Delete"
+"Cancel"
}
}
}
@ -395,7 +392,7 @@ fun Route.calendar() {
}
for (u in list) {
if (Search.match(param.search, u.name)) {
if (Search.match(search, u.name)) {
val href = "/calendar/$day/${room.id}/$time/${u.id}"
entry {
attributes["data-search"] = Search.pack(u.name)
@ -427,46 +424,33 @@ fun Route.calendar() {
}
}
}
get("/calendar/{day}/{room}/{time}/{workgroup}") {
authenticateOrRedirect(Permission.SCHEDULE) { user ->
val day = call.parameters["day"]?.toIntOrNull() ?: return@get
val time = call.parameters["time"]?.toIntOrNull() ?: return@get
val roomId = call.parameters["room"]?.toLongOrNull() ?: return@get
val workGroupId = call.parameters["workgroup"]?.toLongOrNull() ?: return@get
get<LocationCalendar.LocationCalendarSet> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.SCHEDULE)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
for (it in Schedule.getByRoom(param.room, param.day, param.time)) {
PushService.notify(
MessageDeleteCalendarEntry(
it.day,
it.time,
it.room.id,
it.workGroup.id
)
)
it.delete()
}
if (param.workGroup >= 0) {
val schedule = Schedule(param.workGroup, param.day, param.time, param.room)
schedule.save()
schedule.loadConstraints()
val room = RoomRepository.get(roomId) ?: return@get
val workGroup = WorkGroupRepository.get(workGroupId) ?: return@get
val cellTime = (schedule.time / 15) * 15
val schedule = Schedule(null, workGroup, room, day, time)
ScheduleRepository.create(schedule)
PushService.notify(
MessageCreateCalendarEntry(
schedule.day,
schedule.time,
cellTime,
schedule.room.id,
schedule.workGroup.id,
schedule.workGroup.name,
schedule.workGroup.length,
schedule.workGroup.language.code,
schedule.workGroup.track?.color
)
)
}
val redirect = call.parameters["redirect"]
call.respondRedirect(redirect ?: "/calendar/$day")
}
}
call.respondRedirect(param.next ?: "/calendar/${param.day}")
get("/calendar/{day}/{schedule}/delete") {
authenticateOrRedirect(Permission.SCHEDULE) { user ->
val day = call.parameters["day"]?.toIntOrNull() ?: return@get
val scheduleId = call.parameters["schedule"]?.toLongOrNull() ?: return@get
ScheduleRepository.delete(scheduleId)
val redirect = call.parameters["redirect"]
call.respondRedirect(redirect ?: "/calendar/$day")
}
}
}

View file

@ -1,19 +1,18 @@
package de.kif.backend.route
import de.kif.backend.LocationDashboard
import de.kif.backend.PortalSession
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import kotlinx.html.h1
fun Route.dashboard() {
get<LocationDashboard> {
get("") {
val user = call.sessions.get<PortalSession>()?.getUser(call)
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {

View file

@ -1,19 +1,17 @@
package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.LocationLogout
import de.kif.backend.PortalSession
import de.kif.backend.model.User
import de.kif.backend.UserPrinciple
import de.kif.backend.view.MainTemplate
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.auth.principal
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.location
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.routing.route
import io.ktor.sessions.clear
import io.ktor.sessions.get
import io.ktor.sessions.sessions
@ -21,12 +19,13 @@ import io.ktor.sessions.set
import kotlinx.html.*
fun Route.login() {
location<LocationLogin> {
route("login") {
authenticate {
post {
val principal = call.principal<User>() ?: return@post
call.sessions.set(PortalSession(principal.id, principal.username))
call.respondRedirect(call.parameters[LocationLogin::next.name] ?: "/")
val principal = call.principal<UserPrinciple>() ?: return@post
if (principal.user.id == null) return@post
call.sessions.set(PortalSession(principal.user.id))
call.respondRedirect(call.parameters["redirect"] ?: "/")
}
}
@ -41,43 +40,43 @@ fun Route.login() {
form("/login", method = FormMethod.post) {
div("form-group") {
label {
htmlFor = LocationLogin::username.name
htmlFor = "username"
+"Username"
}
input(
name = LocationLogin::username.name,
name = "username",
classes = "form-control"
) {
id = LocationLogin::username.name
id = "username"
placeholder = "Username"
}
}
div("form-group") {
label {
htmlFor = LocationLogin::password.name
htmlFor = "password"
+"Password"
}
input(
name = LocationLogin::password.name,
name = "password",
classes = "form-control",
type = InputType.password
) {
id = LocationLogin::password.name
id = "password"
placeholder = "Password"
}
}
input(
name = LocationLogin::next.name,
name = "redirect",
type = InputType.hidden
) {
value = call.parameters[LocationLogin::next.name] ?: "/"
value = call.parameters["redirect"] ?: "/"
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Login"
}
}
if ("error" in call.parameters) {
if ("onFailure" in call.parameters) {
br { }
div("alert alert-danger") {
+"Username or password incorrect!"
@ -88,15 +87,13 @@ fun Route.login() {
}
}
} else {
call.respondRedirect(call.parameters[LocationLogin::next.name] ?: "/")
call.respondRedirect(call.parameters["redirect"] ?: "/")
}
}
}
location<LocationLogout> {
get {
call.sessions.clear<PortalSession>()
call.respondRedirect("/")
}
get("logout") {
call.sessions.clear<PortalSession>()
call.respondRedirect("/")
}
}

View file

@ -1,258 +0,0 @@
package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.LocationPerson
import de.kif.backend.PortalSession
import de.kif.backend.model.Permission
import de.kif.backend.model.Person
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.path
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.util.toMap
import kotlinx.html.*
fun Route.person() {
get<LocationPerson> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.PERSON)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val list = Person.list()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.PERSON
}
content {
h1 { +"Persons" }
insert(TableTemplate()) {
searchValue = param.search
action {
a("/person/new") {
button(classes="form-btn btn-primary") {
+"Add person"
}
}
}
header {
th {
+"First name"
}
th {
+"Last name"
}
th(classes = "action") {
+"Action"
}
}
for (u in list) {
if (Search.match(param.search, u.firstName, u.lastName)) {
entry {
attributes["data-search"] = Search.pack(u.firstName, u.lastName)
td {
+u.firstName
}
td {
+u.lastName
}
td(classes = "action") {
a("/person/${u.id}") {
i("material-icons") { +"edit" }
}
}
}
}
}
}
}
}
}
}
get<LocationPerson.Edit> { personId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.PERSON)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val editPerson = Person.get(personId.id) ?: return@get
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.PERSON
}
content {
h1 { +"Edit person" }
form(method = FormMethod.post) {
div("form-group") {
label {
htmlFor = "first-name"
+"First name"
}
input(
name = "first-name",
classes = "form-control"
) {
id = "first-name"
placeholder = "First name"
value = editPerson.firstName
}
}
div("form-group") {
label {
htmlFor = "last-name"
+"Last name"
}
input(
name = "last-name",
classes = "form-control"
) {
id = "last-name"
placeholder = "Last name"
value = editPerson.lastName
}
}
div("form-group") {
a("/person") {
button(classes = "form-btn") {
+"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Save"
}
}
}
a("/person/${editPerson.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Delete"
}
}
}
}
}
}
post<LocationPerson.Edit> { personId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.PERSON)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val editPerson = Person.get(personId.id) ?: return@post
params["first-name"]?.let { editPerson.firstName = it }
params["last-name"]?.let { editPerson.lastName = it }
editPerson.save()
call.respondRedirect("/person")
}
}
get<LocationPerson.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.PERSON)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.PERSON
}
content {
h1 { +"Create person" }
form(method = FormMethod.post) {
div("form-group") {
label {
htmlFor = "first-name"
+"First name"
}
input(
name = "first-name",
classes = "form-control"
) {
id = "first-name"
placeholder = "First name"
value = ""
}
}
div("form-group") {
label {
htmlFor = "last-name"
+"Last name"
}
input(
name = "last-name",
classes = "form-control"
) {
id = "last-name"
placeholder = "Last name"
value = ""
}
}
div("form-group") {
a("/person") {
button(classes = "form-btn") {
+"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Create"
}
}
}
}
}
}
}
post<LocationPerson.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.PERSON)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val firstName = params["first-name"] ?: return@post
val lastName = params["last-name"] ?: return@post
Person(firstName = firstName, lastName = lastName).save()
call.respondRedirect("/person")
}
}
get<LocationPerson.Delete> { personId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.PERSON)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val deletePerson = Person.get(personId.id) ?: return@get
deletePerson.delete()
call.respondRedirect("/person")
}
}
}

View file

@ -1,35 +1,35 @@
package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.LocationRoom
import de.kif.backend.PortalSession
import de.kif.backend.model.Permission
import de.kif.backend.model.Room
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.RoomRepository
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.model.Permission
import de.kif.common.model.Room
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.path
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.html.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.firstOrNull
import kotlin.collections.mapValues
import kotlin.collections.set
fun Route.room() {
get<LocationRoom> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.ROOM)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val list = Room.list()
get("/rooms") {
authenticateOrRedirect(Permission.ROOM) { user ->
val search = call.parameters["search"] ?: ""
val list = RoomRepository.all()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -38,11 +38,11 @@ fun Route.room() {
content {
h1 { +"Rooms" }
insert(TableTemplate()) {
searchValue = param.search
searchValue = search
action {
a("/room/new") {
button(classes="form-btn btn-primary") {
button(classes = "form-btn btn-primary") {
+"Add room"
}
}
@ -64,7 +64,7 @@ fun Route.room() {
}
for (u in list) {
if (Search.match(param.search, u.name)) {
if (Search.match(search, u.name)) {
entry {
attributes["data-search"] = Search.pack(u.name)
td {
@ -90,12 +90,10 @@ fun Route.room() {
}
}
get<LocationRoom.Edit> { roomId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.ROOM)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val editRoom = Room.get(roomId.id)
get("/room/{id}") {
authenticateOrRedirect(Permission.ROOM) { user ->
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editRoom = RoomRepository.get(roomId) ?: return@get
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -175,31 +173,26 @@ fun Route.room() {
}
}
post<LocationRoom.Edit> { roomId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.ROOM)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
post("/room/{id}") {
authenticateOrRedirect(Permission.ROOM) { user ->
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val editRoom = Room.get(roomId.id)
var room = RoomRepository.get(roomId) ?: return@post
params["name"]?.let { editRoom.name = it }
params["places"]?.let { editRoom.places = it.toIntOrNull() ?: 0 }
params["projector"]?.let { editRoom.projector = it == "on" }
params["name"]?.let { room = room.copy(name = it) }
params["places"]?.let { room = room.copy(places = it.toIntOrNull() ?: 0) }
params["projector"]?.let { room = room.copy(projector = it == "on") }
editRoom.save()
RoomRepository.update(room)
call.respondRedirect("/room")
call.respondRedirect("/rooms")
}
}
get<LocationRoom.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.ROOM)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
get("/room/new") {
authenticateOrRedirect(Permission.ROOM) { user ->
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -274,11 +267,8 @@ fun Route.room() {
}
}
post<LocationRoom.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.ROOM)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
post("/room/new") {
authenticateOrRedirect(Permission.ROOM) { user ->
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
@ -287,22 +277,21 @@ fun Route.room() {
val places = (params["places"] ?: return@post).toIntOrNull() ?: 0
val projector = params["projector"] == "on"
Room(name = name, places = places, projector = projector).save()
val room = Room(null, name, places, projector)
call.respondRedirect("/room")
RoomRepository.create(room)
call.respondRedirect("/rooms")
}
}
get<LocationRoom.Delete> { roomId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.ROOM)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val deleteRoom = Room.get(roomId.id)
get("/room/{id}/delete") {
authenticateOrRedirect(Permission.ROOM) { user ->
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
deleteRoom.delete()
RoomRepository.delete(roomId)
call.respondRedirect("/room")
call.respondRedirect("/rooms")
}
}
}

View file

@ -1,101 +0,0 @@
package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.database.DbUser
import de.kif.backend.model.Permission
import de.kif.backend.model.User
import de.kif.backend.view.MainTemplate
import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.routing.route
import kotlinx.html.*
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
fun Route.setup() {
route("/") {
get {
call.respondHtmlTemplate(MainTemplate()) {
transaction {
val firstStart = DbUser.selectAll().count() == 0
menuTemplate {
setup = true
}
if (firstStart) {
content {
div {
h1 { +"Create account" }
form("/", method = FormMethod.post) {
div("form-group") {
label {
htmlFor = LocationLogin::username.name
+"Username"
}
input(
name = LocationLogin::username.name,
classes = "form-control"
) {
id = LocationLogin::username.name
placeholder = "Username"
}
}
div("form-group") {
label {
htmlFor = LocationLogin::password.name
+"Password"
}
input(
name = LocationLogin::password.name,
classes = "form-control",
type = InputType.password
) {
id = LocationLogin::password.name
placeholder = "Password"
}
}
button(type = ButtonType.submit, classes = "btn btn-primary") {
+"Create"
}
}
}
}
} else {
content {
div {
h1 { +"Setup complete" }
p {
+"Please restart the server!"
}
}
}
}
}
}
}
post {
val parameters = call.receiveParameters()
val username = parameters[LocationLogin::username.name]
val password = parameters[LocationLogin::password.name]
if (username == null || password == null) {
call.respondRedirect("/")
return@post
}
User.create(username, password, Permission.values().toSet())
call.respondRedirect("/")
}
}
get("*") {
call.respondRedirect("/")
}
}

View file

@ -1,29 +1,27 @@
package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.LocationTrack
import de.kif.backend.PortalSession
import de.kif.backend.model.Permission
import de.kif.backend.model.Track
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.TrackRepository
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.model.Color
import de.kif.common.model.Permission
import de.kif.common.model.Track
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.path
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kif.common.model.Color
import kotlinx.css.CSSBuilder
import kotlinx.html.*
import kotlin.collections.set
import kotlin.random.Random
fun DIV.colorPicker(color: Color?) {
@ -85,12 +83,10 @@ fun DIV.colorPicker(color: Color?) {
}
fun Route.track() {
get<LocationTrack> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val list = Track.list()
get("/tracks") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val search = call.parameters["search"] ?: ""
val list = TrackRepository.all()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -99,7 +95,7 @@ fun Route.track() {
content {
h1 { +"Tracks" }
insert(TableTemplate()) {
searchValue = param.search
searchValue = search
action {
a("/track/new") {
@ -122,7 +118,7 @@ fun Route.track() {
}
for (u in list) {
if (Search.match(param.search, u.name)) {
if (Search.match(search, u.name)) {
entry {
attributes["data-search"] = Search.pack(u.name)
td {
@ -145,12 +141,10 @@ fun Route.track() {
}
}
get<LocationTrack.Edit> { trackId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val editTrack = Track.get(trackId.id)
get("/track/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editTrack = TrackRepository.get(trackId) ?: return@get
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -198,33 +192,28 @@ fun Route.track() {
}
}
post<LocationTrack.Edit> { trackId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
post("/track/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val editTrack = Track.get(trackId.id)
var editTrack = TrackRepository.get(trackId) ?: return@post
params["name"]?.let { editTrack.name = it }
params["name"]?.let { editTrack = editTrack.copy(name = it) }
editTrack.color = (params["color"] ?: return@post).let { c ->
editTrack = editTrack.copy(color = (params["color"] ?: return@post).let { c ->
Color.default.find { it.first == c }
}?.second ?: (params["color-input"] ?: return@post).let(Color.Companion::parse)
}?.second ?: (params["color-input"] ?: return@post).let(Color.Companion::parse))
editTrack.save()
TrackRepository.update(editTrack)
call.respondRedirect("/track")
call.respondRedirect("/tracks")
}
}
get<LocationTrack.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
get("/track/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -266,11 +255,8 @@ fun Route.track() {
}
}
post<LocationTrack.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
post("/track/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
@ -280,25 +266,21 @@ fun Route.track() {
Color.default.find { it.first == c }
}?.second ?: (params["color-input"] ?: return@post).let(Color.Companion::parse)
Track(
name = name,
color = color
).save()
val track = Track(null, name, color)
call.respondRedirect("/track")
TrackRepository.create(track)
call.respondRedirect("/tracks")
}
}
get<LocationTrack.Delete> { trackId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val deleteTrack = Track.get(trackId.id)
get("track/{id}/delete") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
deleteTrack.delete()
TrackRepository.delete(trackId)
call.respondRedirect("/track")
call.respondRedirect("/tracks")
}
}
}

View file

@ -1,35 +1,40 @@
package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.LocationUser
import de.kif.backend.PortalSession
import de.kif.backend.model.Permission
import de.kif.backend.model.User
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.hashPassword
import de.kif.backend.repository.UserRepository
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.path
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.html.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.filter
import kotlin.collections.firstOrNull
import kotlin.collections.joinToString
import kotlin.collections.mapNotNull
import kotlin.collections.mapValues
import kotlin.collections.set
import kotlin.collections.toSet
fun Route.user() {
get<LocationUser> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.USER)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val list = User.list()
get("/users") { param ->
authenticateOrRedirect(Permission.USER) { user ->
val search = call.parameters["search"] ?: ""
val list = UserRepository.all()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -38,11 +43,11 @@ fun Route.user() {
content {
h1 { +"Users" }
insert(TableTemplate()) {
searchValue = param.search
searchValue = search
action {
a("/user/new") {
button(classes="form-btn btn-primary") {
button(classes = "form-btn btn-primary") {
+"Add user"
}
}
@ -61,7 +66,7 @@ fun Route.user() {
}
for (u in list) {
if (Search.match(param.search, u.username)) {
if (Search.match(search, u.username)) {
entry {
attributes["data-search"] = Search.pack(u.username)
td {
@ -84,12 +89,10 @@ fun Route.user() {
}
}
get<LocationUser.Edit> { userId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.USER)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val editUser = User.get(userId.id) ?: return@get
get("/user/{id}") {
authenticateOrRedirect(Permission.USER) { user ->
val userId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editUser = UserRepository.get(userId) ?: return@get
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -158,39 +161,31 @@ fun Route.user() {
}
}
post<LocationUser.Edit> { userId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.USER)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
post("/user/{id}") {
authenticateOrRedirect(Permission.USER) { user ->
val userId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val editUser = User.get(userId.id) ?: return@post
params["username"]?.let { editUser.username = it }
var editUser = UserRepository.get(userId) ?: return@post
for (permission in Permission.values()) {
params["username"]?.let { editUser = editUser.copy(username = it) }
val permissions = Permission.values().filter { permission ->
val name = permission.toString().toLowerCase()
if (user.checkPermission(permission)) {
if (params["permission-$name"] == "on") {
editUser.permissions += permission
} else {
editUser.permissions -= permission
}
}
}
editUser.save()
user.checkPermission(permission) && params["permission-$name"] == "on"
}.toSet()
editUser = editUser.copy(permissions = permissions)
call.respondRedirect("/user")
UserRepository.update(user)
call.respondRedirect("/users")
}
}
get<LocationUser.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.USER)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
get("/user/new") {
authenticateOrRedirect(Permission.USER) { user ->
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -269,11 +264,8 @@ fun Route.user() {
}
}
post<LocationUser.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.USER)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
post("/user/new") {
authenticateOrRedirect(Permission.USER) { user ->
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
@ -288,26 +280,26 @@ fun Route.user() {
null
}.toSet()
User.create(username, password, permissions)
val newUser = User(null, username, hashPassword(password), permissions)
call.respondRedirect("/user")
UserRepository.create(newUser)
call.respondRedirect("/users")
}
}
get<LocationUser.Delete> { userId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.USER)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val deleteUser = User.get(userId.id) ?: return@get
get("/user/{id}/delete") {
authenticateOrRedirect(Permission.USER) { user ->
val userId = call.parameters["id"]?.toLongOrNull() ?: return@get
val deleteUser = UserRepository.get(userId) ?: return@get
if (user.checkPermission(Permission.USER) &&
(Permission.ADMIN !in deleteUser.permissions || Permission.ADMIN in user.permissions)
) {
deleteUser.delete()
UserRepository.delete(userId)
}
call.respondRedirect("/user")
call.respondRedirect("/users")
}
}
}

View file

@ -1,37 +1,38 @@
package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.LocationWorkGroup
import de.kif.backend.PortalSession
import de.kif.backend.database.Language
import de.kif.backend.model.Permission
import de.kif.backend.model.Track
import de.kif.backend.model.WorkGroup
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.TrackRepository
import de.kif.backend.repository.WorkGroupRepository
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.model.Language
import de.kif.common.model.Permission
import de.kif.common.model.WorkGroup
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.path
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.html.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.find
import kotlin.collections.firstOrNull
import kotlin.collections.mapValues
import kotlin.collections.set
fun Route.workGroup() {
get<LocationWorkGroup> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val list = WorkGroup.list()
get("workgroups") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val search = call.parameters["search"] ?: ""
val list = WorkGroupRepository.all()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -40,10 +41,10 @@ fun Route.workGroup() {
content {
h1 { +"Work groups" }
insert(TableTemplate()) {
searchValue = param.search
searchValue = search
action {
a("/track") {
a("/tracks") {
button(classes = "form-btn") {
+"Edit tracks"
}
@ -83,7 +84,7 @@ fun Route.workGroup() {
}
for (u in list) {
if (Search.match(param.search, u.name)) {
if (Search.match(search, u.name)) {
entry {
attributes["data-search"] = Search.pack(u.name)
td {
@ -121,13 +122,11 @@ fun Route.workGroup() {
}
}
get<LocationWorkGroup.Edit> { workGroupId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val editWorkGroup = WorkGroup.get(workGroupId.id)
val tracks = Track.list()
get("/workgroup/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@get
val tracks = TrackRepository.all()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -179,20 +178,20 @@ fun Route.workGroup() {
name = "track"
option {
selected = (editWorkGroup.trackId ?: -1) < 0
selected = (editWorkGroup.track?.id ?: -1) < 0
value = "-1"
+"None"
}
for (track in tracks) {
option {
selected = editWorkGroup.trackId == track.id
selected = editWorkGroup.track?.id == track.id
value = track.id.toString()
+track.name
}
}
}
a("/track", classes = "form-btn") {
a("/tracks", classes = "form-btn") {
i("material-icons") { +"edit" }
}
}
@ -228,8 +227,8 @@ fun Route.workGroup() {
for (language in Language.values()) {
option {
selected = editWorkGroup.language == language
value = language.name
+language.toString()
value = language.code
+language.localeName
}
}
}
@ -287,36 +286,37 @@ fun Route.workGroup() {
}
}
post<LocationWorkGroup.Edit> { workGroupId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
post("/workgroup/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val editWorkGroup = WorkGroup.get(workGroupId.id)
var editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@post
params["name"]?.let { editWorkGroup.name = it }
params["interested"]?.toIntOrNull()?.let { editWorkGroup.interested = it }
params["track"]?.toIntOrNull()?.let { editWorkGroup.trackId = it }
params["projector"]?.let { editWorkGroup.projector = it == "on" }
params["resolution"]?.let { editWorkGroup.resolution = it == "on" }
params["length"]?.toIntOrNull()?.let { editWorkGroup.length = it }
params["language"]?.let { editWorkGroup.language = Language.values().find { l -> l.name == it } ?: Language.GERMAN }
params["name"]?.let { editWorkGroup = editWorkGroup.copy(name = it) }
params["interested"]?.toIntOrNull()?.let { editWorkGroup = editWorkGroup.copy(interested = it) }
params["track"]?.toLongOrNull()?.let {
val track = TrackRepository.get(it)
editWorkGroup = editWorkGroup.copy(track = track)
}
params["projector"]?.let { editWorkGroup = editWorkGroup.copy(projector = it == "on") }
params["resolution"]?.let { editWorkGroup = editWorkGroup.copy(resolution = it == "on") }
params["length"]?.toIntOrNull()?.let { editWorkGroup = editWorkGroup.copy(length = it) }
params["language"]?.let {
editWorkGroup =
editWorkGroup.copy(language = Language.values().find { l -> l.code == it } ?: Language.GERMAN)
}
editWorkGroup.save()
WorkGroupRepository.update(editWorkGroup)
call.respondRedirect("/workgroup")
call.respondRedirect("/workgroups")
}
}
get<LocationWorkGroup.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val tracks = Track.list()
get("/workgroup/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val tracks = TrackRepository.all()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -410,8 +410,8 @@ fun Route.workGroup() {
for (language in Language.values()) {
option {
selected = language == Language.GERMAN
value = language.name
+language.toString()
value = language.code
+language.localeName
}
}
}
@ -464,47 +464,46 @@ fun Route.workGroup() {
}
}
post<LocationWorkGroup.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
post("/workgroup/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val name = params["name"] ?: return@post
val interested = (params["interested"] ?: return@post).toIntOrNull() ?: 0
val trackId = (params["track"] ?: return@post).toIntOrNull()
val track = (params["track"] ?: return@post).toLongOrNull()?.let { TrackRepository.get(it) }
val projector = params["projector"] == "on"
val resolution = params["resolution"] == "on"
val length = (params["length"] ?: return@post).toIntOrNull() ?: 0
val language = (params["language"] ?: return@post).let { Language.values().find { l -> l.name == it } ?: Language.GERMAN }
val language = (params["language"] ?: return@post).let {
Language.values().find { l -> l.code == it } ?: Language.GERMAN
}
WorkGroup(
val workGroup = WorkGroup(
null,
name = name,
interested = interested,
trackId = trackId,
track = track,
projector = projector,
resolution = resolution,
length = length,
language = language
).save()
)
call.respondRedirect("/workgroup")
WorkGroupRepository.create(workGroup)
call.respondRedirect("/workgroups")
}
}
get<LocationWorkGroup.Delete> { workGroupId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val deleteWorkGroup = WorkGroup.get(workGroupId.id)
get("/workgroup/{id}/delete") {
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
deleteWorkGroup.delete()
WorkGroupRepository.delete(workGroupId)
call.respondRedirect("/workgroup")
call.respondRedirect("/workgroups")
}
}
}

View file

@ -0,0 +1,61 @@
package de.kif.backend.route.api
import de.kif.backend.PortalSession
import de.kif.backend.checkPassword
import de.kif.backend.repository.UserRepository
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.routing.post
import io.ktor.sessions.sessions
import io.ktor.sessions.set
data class Credentials(
val username: String,
val password: String
)
fun Route.authenticateApi() {
post("/api/authenticate") {
val credentials = call.receive<Credentials>()
val user = UserRepository.find(credentials.username)
if (user?.id == null || !checkPassword(credentials.password, user.password)) {
call.error(HttpStatusCode.Unauthorized)
return@post
}
call.sessions.set(PortalSession(user.id))
call.respond(HttpStatusCode.OK, mapOf("OK" to true))
}
}
suspend fun ApplicationCall.success(data: Any? = null) {
val map: Map<String, Any>
if (data == null) {
map = mapOf("OK" to true)
} else {
map = mapOf(
"OK" to true,
"data" to data
)
}
respond(HttpStatusCode.OK, map)
}
suspend fun ApplicationCall.error(code: HttpStatusCode) {
respond(
code,
mapOf(
"OK" to false,
"errorCode" to code.value,
"errorDescription" to code.description
)
)
}

View file

@ -0,0 +1,92 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.RoomRepository
import de.kif.common.model.Permission
import de.kif.common.model.Room
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
fun Route.roomApi() {
get("/api/rooms") {
try {
val rooms = RoomRepository.all()
call.success(rooms)
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/rooms") {
try {
authenticate(Permission.ROOM) {
val room = call.receive<Room>()
val id = RoomRepository.create(room)
call.success(mapOf("id" to id))
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
get("/api/room/{id}") {
try {
val id = call.parameters["id"]?.toLongOrNull()
val room = id?.let { RoomRepository.get(it) }
if (room != null) {
call.success(room)
} else {
call.error(HttpStatusCode.NotFound)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/room/{id}") {
try {
authenticate {
val id = call.parameters["id"]?.toLongOrNull()
val room = call.receive<Room>().copy(id = id)
if (room.id != null) {
RoomRepository.update(room)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/room/{id}/delete") {
try {
authenticate {
val id = call.parameters["id"]?.toLongOrNull()
if (id != null) {
RoomRepository.delete(id)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
}

View file

@ -0,0 +1,121 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository
import de.kif.common.model.Permission
import de.kif.common.model.Schedule
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
data class ScheduleMove(val day: Int, val time: Int, val roomId: Long)
fun Route.scheduleApi() {
get("/api/schedules") {
try {
val schedules = ScheduleRepository.all()
call.success(schedules)
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/schedules") {
try {
authenticate(Permission.SCHEDULE) {
val schedule = call.receive<Schedule>()
val id = ScheduleRepository.create(schedule)
call.success(mapOf("id" to id))
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
get("/api/schedule/{id}") {
try {
val id = call.parameters["id"]?.toLongOrNull()
val schedule = id?.let { ScheduleRepository.get(it) }
if (schedule != null) {
call.success(schedule)
} else {
call.error(HttpStatusCode.NotFound)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/schedule/{id}") {
try {
authenticate(Permission.SCHEDULE) {
val id = call.parameters["id"]?.toLongOrNull()
val schedule = call.receive<Schedule>().copy(id = id)
if (schedule.id != null) {
ScheduleRepository.update(schedule)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/schedule/{id}/delete") {
try {
authenticate(Permission.SCHEDULE) {
val id = call.parameters["id"]?.toLongOrNull()
if (id != null) {
ScheduleRepository.delete(id)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/schedule/{id}/move") {
try {
authenticate(Permission.SCHEDULE) {
val id = call.parameters["id"]?.toLongOrNull()
val scheduleMove = call.receive<ScheduleMove>()
var schedule = id?.let { ScheduleRepository.get(it) }
val room = RoomRepository.get(scheduleMove.roomId)
if (schedule != null && room != null) {
schedule = schedule.copy(day = scheduleMove.day, time = scheduleMove.time, room = room)
ScheduleRepository.update(schedule)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
}

View file

@ -0,0 +1,92 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.TrackRepository
import de.kif.common.model.Permission
import de.kif.common.model.Track
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
fun Route.trackApi() {
get("/api/tracks") {
try {
val tracks = TrackRepository.all()
call.success(tracks)
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/tracks") {
try {
authenticate(Permission.WORK_GROUP) {
val track = call.receive<Track>()
val id = TrackRepository.create(track)
call.success(mapOf("id" to id))
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
get("/api/track/{id}") {
try {
val id = call.parameters["id"]?.toLongOrNull()
val track = id?.let { TrackRepository.get(it) }
if (track != null) {
TrackRepository.update(track)
call.success(track)
} else {
call.error(HttpStatusCode.NotFound)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/track/{id}") {
try {
authenticate {
val id = call.parameters["id"]?.toLongOrNull()
val track = call.receive<Track>().copy(id = id)
if (track.id != null) {
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/track/{id}/delete") {
try {
authenticate {
val id = call.parameters["id"]?.toLongOrNull()
if (id != null) {
TrackRepository.delete(id)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
}

View file

@ -0,0 +1,99 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.UserRepository
import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
fun Route.userApi() {
get("/api/users") {
try {
authenticate(Permission.USER) {
val users = UserRepository.all()
call.success(users)
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/users") {
try {
authenticate(Permission.USER) {
val user = call.receive<User>()
val id = UserRepository.create(user)
call.success(mapOf("id" to id))
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
get("/api/user/{id}") {
try {
authenticate(Permission.USER) {
val id = call.parameters["id"]?.toLongOrNull()
val user = id?.let { UserRepository.get(it) }
if (user != null) {
call.success(user)
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/user/{id}") {
try {
authenticate {
val id = call.parameters["id"]?.toLongOrNull()
val user = call.receive<User>().copy(id = id)
if (user.id != null) {
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/user/{id}/delete") {
try {
authenticate {
val id = call.parameters["id"]?.toLongOrNull()
if (id != null) {
UserRepository.delete(id)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
}

View file

@ -0,0 +1,92 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.WorkGroupRepository
import de.kif.common.model.Permission
import de.kif.common.model.WorkGroup
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
fun Route.workGroupApi() {
get("/api/workgroups") {
try {
val workGroups = WorkGroupRepository.all()
call.success(workGroups)
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/workgroups") {
try {
authenticate(Permission.WORK_GROUP) {
val workGroup = call.receive<WorkGroup>()
val id = WorkGroupRepository.create(workGroup)
call.success(mapOf("id" to id))
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
get("/api/workgroup/{id}") {
try {
val id = call.parameters["id"]?.toLongOrNull()
val workGroup = id?.let { WorkGroupRepository.get(it) }
if (workGroup != null) {
call.success(workGroup)
} else {
call.error(HttpStatusCode.NotFound)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/workgroup/{id}") {
try {
authenticate {
val id = call.parameters["id"]?.toLongOrNull()
val workGroup = call.receive<WorkGroup>().copy(id = id)
if (workGroup.id != null) {
WorkGroupRepository.update(workGroup)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/workgroup/{id}/delete") {
try {
authenticate {
val id = call.parameters["id"]?.toLongOrNull()
if (id != null) {
WorkGroupRepository.delete(id)
call.success()
} else {
call.error(HttpStatusCode.NotFound)
}
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
}

View file

@ -1,22 +1,22 @@
package de.kif.backend.route
package de.kif.backend.util
import de.kif.backend.repository.*
import de.kif.common.*
import de.kif.common.RepositoryType
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import io.ktor.routing.Route
import io.ktor.websocket.WebSocketServerSession
import io.ktor.websocket.webSocket
import kif.common.model.Message
import kif.common.model.MessageType
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import java.lang.Exception
object PushService {
var clients: List<WebSocketServerSession> = emptyList()
suspend fun notify(messageType: MessageType) {
suspend fun notify(type: MessageType, repository: RepositoryType, id: Long) {
try {
val data = Message(messageType).stringify()
val data = Message(type, repository, id).stringify()
for (client in clients) {
client.outgoing.send(Frame.Text(data))
}
@ -39,9 +39,15 @@ fun Route.pushService() {
} catch (_: ClosedReceiveChannelException) {
PushService.clients -= this
} catch (e: Throwable) {
println("onError ${closeReason.await()}")
println("onFailure ${closeReason.await()}")
e.printStackTrace()
PushService.clients -= this
}
}
RoomRepository.registerPushService()
ScheduleRepository.registerPushService()
TrackRepository.registerPushService()
UserRepository.registerPushService()
WorkGroupRepository.registerPushService()
}

View file

@ -1,13 +1,12 @@
package de.kif.backend.view
import de.kif.backend.model.Permission
import de.kif.backend.model.User
import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.html.Template
import kotlinx.html.*
class MenuTemplate() : Template<FlowContent> {
var setup = false
var active: Tab = Tab.DASHBOARD
var user: User? = null
@ -15,55 +14,42 @@ class MenuTemplate() : Template<FlowContent> {
nav("menu") {
div("container") {
div("menu-left") {
if (setup) {
a(classes = "active") {
+"Setup"
}
} else {
a("/", classes = if (active == Tab.DASHBOARD) "active" else null) {
+"Dashboard"
}
a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) {
+"Calendar"
}
a("/", classes = if (active == Tab.DASHBOARD) "active" else null) {
+"Dashboard"
}
a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) {
+"Calendar"
}
}
if (!setup) {
div("menu-right") {
span("menu-icon") {
i("material-icons") { +"menu" }
}
val user = user
div("menu-content") {
if (user == null) {
a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) {
+"Login"
div("menu-right") {
span("menu-icon") {
i("material-icons") { +"menu" }
}
val user = user
div("menu-content") {
if (user == null) {
a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) {
+"Login"
}
} else {
if (user.checkPermission(Permission.WORK_GROUP)) {
a("/workgroups", classes = if (active == Tab.WORK_GROUP) "active" else null) {
+"Work groups"
}
} else {
if (user.checkPermission(Permission.WORK_GROUP)) {
a("/workgroup", classes = if (active == Tab.WORK_GROUP) "active" else null) {
+"Work groups"
}
}
if (user.checkPermission(Permission.ROOM)) {
a("/rooms", classes = if (active == Tab.ROOM) "active" else null) {
+"Rooms"
}
if (user.checkPermission(Permission.ROOM)) {
a("/room", classes = if (active == Tab.ROOM) "active" else null) {
+"Rooms"
}
}
if (user.checkPermission(Permission.PERSON)) {
a("/person", classes = if (active == Tab.PERSON) "active" else null) {
+"Persons"
}
}
if (user.checkPermission(Permission.PERSON)) {
a("/user", classes = if (active == Tab.USER) "active" else null) {
+"Users"
}
}
a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) {
+user.username
}
if (user.checkPermission(Permission.PERSON)) {
a("/users", classes = if (active == Tab.USER) "active" else null) {
+"Users"
}
}
a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) {
+user.username
}
}
}
}

View file

@ -0,0 +1,38 @@
GET http://localhost:8080/api/rooms
###
POST http://localhost:8080/api/authenticate
Content-Type: application/json
{
"username": "admin",
"password": "admin"
}
> {%
client.assert(response.body.OK === true, "Login failed!");
client.global.set("auth_token", response.headers.valueOf("Set-Cookie"));
%}
###
POST http://localhost:8080/api/rooms
Content-Type: application/json
Cookie: {{auth_token}}
{
"name": "E007",
"places": 20,
"projector": true
}
###
POST http://localhost:8080/api/room/2/delete
Content-Type: application/json
Cookie: {{auth_token}}
###