Add path prefix

This commit is contained in:
Lars Westermann 2019-06-10 10:09:36 +02:00
parent 19956ebafb
commit afbced61e3
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
39 changed files with 656 additions and 305 deletions

View file

@ -1,6 +1,8 @@
[server]
host = "localhost"
port = 8080
prefix = ""
debug = false
[schedule]
reference = "2019-06-12"

View file

@ -11,7 +11,8 @@ import org.w3c.dom.events.Event
import kotlin.browser.window
class WebSocketClient() {
private val url = "ws://${window.location.host}/"
val prefix = js("prefix")
private val url = "ws://${window.location.host}$prefix/"
private lateinit var ws: WebSocket
private var reconnect = false

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object PostRepository : Repository<Post> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
@ -18,35 +20,35 @@ object PostRepository : Repository<Post> {
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Post? {
val json = repositoryGet("/api/post/$id") ?: return null
val json = repositoryGet("$prefix/api/post/$id") ?: return null
return parser.parse(json, Post.serializer())
}
override suspend fun create(model: Post): Long {
return repositoryPost("/api/posts", Message.json.stringify(Post.serializer(), model))
return repositoryPost("$prefix/api/posts", Message.json.stringify(Post.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Post) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/post/${model.id}", Message.json.stringify(Post.serializer(), model))
repositoryPost("$prefix/api/post/${model.id}", Message.json.stringify(Post.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/post/$id/delete")
repositoryPost("$prefix/api/post/$id/delete")
}
override suspend fun all(): List<Post> {
val json = repositoryGet("/api/posts") ?: return emptyList()
val json = repositoryGet("$prefix/api/posts") ?: return emptyList()
return parser.parse(json, Post.serializer().list)
}
suspend fun htmlByUrl(url: String): String {
return repositoryRawGet("/api/p/$url")
return repositoryRawGet("$prefix/api/p/$url")
}
suspend fun render(data: String): String {
return repositoryPost("/api/render", data)
return repositoryPost("$prefix/api/render", data)
}
val handler = object : MessageHandler(RepositoryType.POST) {

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object RoomRepository : Repository<Room> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
@ -18,26 +20,26 @@ object RoomRepository : Repository<Room> {
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Room? {
val json = repositoryGet("/api/room/$id") ?: return null
val json = repositoryGet("$prefix/api/room/$id") ?: return null
return parser.parse(json, Room.serializer())
}
override suspend fun create(model: Room): Long {
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))
return repositoryPost("$prefix/api/rooms", Message.json.stringify(Room.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Room) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/room/${model.id}", Message.json.stringify(Room.serializer(), model))
repositoryPost("$prefix/api/room/${model.id}", Message.json.stringify(Room.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/room/$id/delete")
repositoryPost("$prefix/api/room/$id/delete")
}
override suspend fun all(): List<Room> {
val json = repositoryGet("/api/rooms") ?: return emptyList()
val json = repositoryGet("$prefix/api/rooms") ?: return emptyList()
return parser.parse(json, Room.serializer().list)
}

View file

@ -12,6 +12,8 @@ import kotlinx.serialization.list
object ScheduleRepository : Repository<Schedule> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
@ -19,26 +21,26 @@ object ScheduleRepository : Repository<Schedule> {
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Schedule? {
val json = repositoryGet("/api/schedule/$id") ?: return null
val json = repositoryGet("$prefix/api/schedule/$id") ?: return null
return parser.parse(json, Schedule.serializer())
}
override suspend fun create(model: Schedule): Long {
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))
return repositoryPost("$prefix/api/schedules", Message.json.stringify(Schedule.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Schedule) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model))
repositoryPost("$prefix/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/schedule/$id/delete")
repositoryPost("$prefix/api/schedule/$id/delete")
}
override suspend fun all(): List<Schedule> {
val json = repositoryGet("/api/schedules") ?: return emptyList()
val json = repositoryGet("$prefix/api/schedules") ?: return emptyList()
return parser.parse(json, Schedule.serializer().list)
}
@ -52,12 +54,12 @@ object ScheduleRepository : Repository<Schedule> {
}
suspend fun checkConstraints(): ConstraintMap {
val json = repositoryGet("/api/constraints")
val json = repositoryGet("$prefix/api/constraints")
return parser.parse(json, ConstraintMap.serializer())
}
suspend fun checkConstraintsFor(schedule: Schedule): ConstraintMap {
val json = repositoryGet("/api/constraint/${schedule.id}")
val json = repositoryGet("$prefix/api/constraint/${schedule.id}")
return parser.parse(json, ConstraintMap.serializer())
}
}

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object TrackRepository : Repository<Track> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
@ -18,26 +20,26 @@ object TrackRepository : Repository<Track> {
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): Track? {
val json = repositoryGet("/api/track/$id") ?: return null
val json = repositoryGet("$prefix/api/track/$id") ?: return null
return parser.parse(json, Track.serializer())
}
override suspend fun create(model: Track): Long {
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))
return repositoryPost("$prefix/api/tracks", Message.json.stringify(Track.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: Track) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/track/${model.id}", Message.json.stringify(Track.serializer(), model))
repositoryPost("$prefix/api/track/${model.id}", Message.json.stringify(Track.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/track/$id/delete")
repositoryPost("$prefix/api/track/$id/delete")
}
override suspend fun all(): List<Track> {
val json = repositoryGet("/api/tracks") ?: return emptyList()
val json = repositoryGet("$prefix/api/tracks") ?: return emptyList()
return parser.parse(json, Track.serializer().list)
}

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object UserRepository : Repository<User> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
@ -18,26 +20,26 @@ object UserRepository : Repository<User> {
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): User? {
val json = repositoryGet("/api/user/$id") ?: return null
val json = repositoryGet("$prefix/api/user/$id") ?: return null
return parser.parse(json, User.serializer())
}
override suspend fun create(model: User): Long {
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))
return repositoryPost("$prefix/api/users", Message.json.stringify(User.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: User) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/user/${model.id}", Message.json.stringify(User.serializer(), model))
repositoryPost("$prefix/api/user/${model.id}", Message.json.stringify(User.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/user/$id/delete")
repositoryPost("$prefix/api/user/$id/delete")
}
override suspend fun all(): List<User> {
val json = repositoryGet("/api/users") ?: return emptyList()
val json = repositoryGet("$prefix/api/users") ?: return emptyList()
return parser.parse(json, User.serializer().list)
}

View file

@ -11,6 +11,8 @@ import kotlinx.serialization.list
object WorkGroupRepository : Repository<WorkGroup> {
val prefix = js("prefix")
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
@ -18,26 +20,26 @@ object WorkGroupRepository : Repository<WorkGroup> {
private val parser = DynamicObjectParser()
override suspend fun get(id: Long): WorkGroup? {
val json = repositoryGet("/api/workgroup/$id") ?: return null
val json = repositoryGet("$prefix/api/workgroup/$id") ?: return null
return parser.parse(json, WorkGroup.serializer())
}
override suspend fun create(model: WorkGroup): Long {
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))
return repositoryPost("$prefix/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))
?: throw IllegalStateException("Cannot create model!")
}
override suspend fun update(model: WorkGroup) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model))
repositoryPost("$prefix/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model))
}
override suspend fun delete(id: Long) {
repositoryPost("/api/workgroup/$id/delete")
repositoryPost("$prefix/api/workgroup/$id/delete")
}
override suspend fun all(): List<WorkGroup> {
val json = repositoryGet("/api/workgroups") ?: return emptyList()
val json = repositoryGet("$prefix/api/workgroups") ?: return emptyList()
return parser.parse(json, WorkGroup.serializer().list)
}

View file

@ -47,6 +47,7 @@ fun initWorkGroupConstraints() {
html.name = "constraint-only-on-day-${index++}"
min = -1337.0
max = 1337.0
placeholder = "Tag"
}.html)
}.html)
}
@ -64,6 +65,7 @@ fun initWorkGroupConstraints() {
html.name = "constraint-not-on-day-${index++}"
min = -1337.0
max = 1337.0
placeholder = "Tag"
}.html)
}.html)
}
@ -78,13 +80,15 @@ fun initWorkGroupConstraints() {
}.html)
html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control"
html.name = "constraint-only-before-time-day-${index++}"
html.name = "constraint-only-before-time-day-${index}"
placeholder = "Tag (optional)"
}.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-only-before-time-${index++}"
min = -1337.0
max = 133700.0
placeholder = "Minuten"
}.html)
}.html)
}
@ -99,13 +103,15 @@ fun initWorkGroupConstraints() {
}.html)
html.appendChild(InputView(InputType.TEXT).apply {
classList += "form-control"
html.name = "constraint-only-after-time-day-${index++}"
html.name = "constraint-only-after-time-day-${index}"
placeholder = "Tag (optional)"
}.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-only-after-time-${index++}"
min = -1337.0
max = 133700.0
placeholder = "Minuten"
}.html)
}.html)
}

View file

@ -9,17 +9,20 @@ import de.westermann.kwebview.iterator
import org.w3c.dom.*
import kotlin.browser.document
import kotlin.browser.window
import kotlin.js.Date
class Calendar(calendar: HTMLElement) : View(calendar) {
var autoScroll = true
val day: Int = calendar.dataset["day"]?.toIntOrNull() ?: -1
val calendarTable = calendar.getElementsByClassName("calendar-table")[0] as HTMLElement
private val calendarTableHeader = calendar.getElementsByClassName("calendar-header")[0] as HTMLElement
val day = calendarTable.dataset["day"]?.toIntOrNull() ?: -1
val referenceDate = calendarTable.dataset["reference"]?.toLongOrNull() ?: -1L
val nowDate = calendarTable.dataset["now"]?.toLongOrNull() ?: -1L
val timeDifference = Date.now().toLong() - nowDate
fun scrollVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
scrollAllVerticalBy(pixel, scrollBehavior)
}
@ -46,7 +49,6 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
Orientation.ROOM_TO_TIME
}
init {
scroll += calendarTable
@ -131,7 +133,6 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
private var scroll = listOf<HTMLElement>()
private fun scrollAllVerticalBy(pixel: Double, scrollBehavior: ScrollBehavior = ScrollBehavior.SMOOTH) {
println("scroll ${scroll.size} elemenets")
for (calendarTable in scroll) {
calendarTable.scrollBy(ScrollToOptions(0.0, pixel, scrollBehavior))
}

View file

@ -86,9 +86,14 @@ class CalendarBody(val calendar: Calendar, view: HTMLElement) : ViewCollection<C
}
fun update(scroll: ScrollBehavior) {
val currentTime = Date().let {
it.getHours() * 60 + it.getMinutes()
}
val now = Date.now().toLong() - calendar.timeDifference
val refDay = calendar.referenceDate / (1000 * 60 * 60 * 24)
val nowDay = now / (1000 * 60 * 60 * 24)
val d = nowDay - refDay
val diff = (day - d).toInt()
val date = Date(now)
val currentTime = date.getHours() * 60 + date.getMinutes() + (diff * 60 * 24)
val rowTime = (currentTime / 15) * 15
var activeRow: CalendarRow? = null
@ -116,7 +121,7 @@ class CalendarBody(val calendar: Calendar, view: HTMLElement) : ViewCollection<C
if (calendar.autoScroll && activeRow != null) {
if (calendar.orientation == Calendar.Orientation.ROOM_TO_TIME) {
calendar.scrollVerticalTo((activeRow.offsetTop).toDouble(), scroll)
calendar.scrollVerticalTo((activeRow.offsetTop - 150).toDouble(), scroll)
} else {
calendar.scrollHorizontalTo((activeRow.offsetLeft - 100).toDouble(), scroll)
}

View file

@ -7,10 +7,13 @@ import de.westermann.kwebview.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.RepositoryDelegate
import de.kif.frontend.repository.ScheduleRepository
import de.kif.frontend.views.overview.getByClassOrCreate
import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.events.MouseEvent
import kotlin.browser.document
import kotlin.browser.window
import kotlin.dom.appendText
import kotlin.dom.isText
@ -19,6 +22,7 @@ import kotlin.js.Date
class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var ignoreEditHover = false
private var newCell: CalendarCell? = null
private var language by dataset.property("language")
@ -43,9 +47,20 @@ class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : Vie
private var moveLockRoom: Long? = null
private var moveLockTime: Int? = null
private val nameView = view.getByClassOrCreate<HTMLSpanElement>("calendar-entry-name")
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
if (ignoreEditHover) {
for (element in document.elementsFromPoint(position.x, position.y)) {
if (element.classList.contains("calendar-edit")) {
return
}
}
ignoreEditHover = false
}
val cell = calendar.calendarCells.find {
position in it.dimension
}
@ -245,13 +260,7 @@ class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : Vie
}
}
for (element in html.childNodes) {
if (element.isText) {
html.removeChild(element)
}
}
html.appendText(workGroup.name)
nameView.textContent = workGroup.name
}
companion object {
@ -268,6 +277,7 @@ class CalendarEntry(private val calendar: CalendarBody, view: HTMLElement) : Vie
entry.load(workGroup)
entry.mouseDelta = Point.ZERO
entry.ignoreEditHover = true
entry.startDrag()
return entry

View file

@ -1,6 +1,10 @@
$border-radius: 0.2rem;
$transitionTime: 150ms;
$mainFont: 'Raleway', Roboto, Arial, sans-serif;
$menuFont: 'Bungee', 'Raleway', Roboto, Arial, sans-serif;
$headFont: 'Montserrat', 'Raleway', Roboto, Arial, sans-serif;
@mixin no-select() {
-webkit-touch-callout: none;
-webkit-user-select: none;

View file

@ -213,6 +213,8 @@
span:first-child {
color: var(--text-primary-color);
width: 6rem;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: transparent;
@ -297,6 +299,13 @@
@include no-select()
}
.calendar-entry-name {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.calendar-table-box {
display: flex;
flex-wrap: nowrap;
@ -404,7 +413,7 @@
left: 6rem;
right: 0;
height: 1px;
border-bottom: solid 1px $primary-color;
border-bottom: solid 1px var(--primary-color);
}
&.calendar-now::after {
@ -414,8 +423,8 @@
left: 6rem;
transform: scale(1, 0.5) rotate(-45deg);
transform-origin: bottom;
border-bottom: solid 0.4rem $primary-color;
border-right: solid 0.4rem $primary-color;
border-bottom: solid 0.4rem var(--primary-color);
border-right: solid 0.4rem var(--primary-color);
border-top: solid 0.4rem transparent;
border-left: solid 0.4rem transparent;
margin-top: -0.55rem;
@ -498,7 +507,7 @@
top: 3rem;
bottom: 0;
width: 1px;
border-right: solid 1px $primary-color;
border-right: solid 1px var(--primary-color);
}
&.calendar-now::after {
@ -508,8 +517,8 @@
top: 3rem;
transform: scale(0.5, 1) rotate(45deg);
transform-origin: right;
border-bottom: solid 0.4rem $primary-color;
border-right: solid 0.4rem $primary-color;
border-bottom: solid 0.4rem var(--primary-color);
border-right: solid 0.4rem var(--primary-color);
border-top: solid 0.4rem transparent;
border-left: solid 0.4rem transparent;
margin-top: -0.3rem;

View file

@ -13,7 +13,7 @@
color: var(--text-primary-color);
height: 100%;
display: inline-block;
font-family: "Bungee", sans-serif;
font-family: $menuFont;
font-weight: normal;
font-size: 1.1rem;
position: relative;

View file

@ -33,7 +33,7 @@
line-height: 2rem;
padding: 0 1rem;
font-weight: bold;
font-family: 'Montserrat', sans-serif;
font-family: $headFont;
&:empty::before {
display: block;

View file

@ -39,6 +39,7 @@
margin-top: -0.5rem !important;
bottom: 0 !important;
}
.calendar-entry::after {
content: none;
}
@ -65,6 +66,12 @@
line-height: 2rem;
width: 100%;
}
.calendar-header {
.calendar-cell {
overflow: hidden;
}
}
}
.wall-calendar {

View file

@ -15,7 +15,7 @@ body, html {
color: var(--text-primary-color);
background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif;
font-family: $mainFont;
font-weight: 500;
width: 100%;
@ -30,6 +30,10 @@ body, html {
}
}
h1, h2, h3, h4, h5, h6 {
font-family: $headFont;
}
.no-select {
@include no-select()
}

View file

@ -13,13 +13,21 @@ import io.ktor.http.content.files
import io.ktor.http.content.static
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.websocket.WebSockets
import org.slf4j.event.Level
import java.nio.file.Paths
val prefix = Configuration.Server.prefix
fun Application.main() {
install(DefaultHeaders)
install(CallLogging)
install(CallLogging) {
if (Configuration.Server.debug) {
level = Level.INFO
}
}
install(ConditionalHeaders)
install(Compression)
install(DataConversion)
@ -42,47 +50,50 @@ fun Application.main() {
security()
routing {
static("/static") {
files(Configuration.Path.webPath.toFile())
}
static("/images") {
files(Configuration.Path.uploadsPath.toFile())
}
val srcFile = Paths.get("src")?.toFile()
if (srcFile != null && srcFile.exists() && srcFile.isDirectory) {
static("/src") {
files("src")
route(prefix) {
static("/static") {
files(Configuration.Path.webPath.toFile())
}
static("/images") {
files(Configuration.Path.uploadsPath.toFile())
}
val srcFile = Paths.get("src")?.toFile()
if (srcFile != null && srcFile.exists() && srcFile.isDirectory) {
static("/src") {
files("src")
}
}
// UI routes
overview()
calendar()
login()
account()
board()
wall()
workGroup()
track()
room()
user()
// RESTful routes
authenticateApi()
roomApi()
scheduleApi()
trackApi()
userApi()
workGroupApi()
constraintsApi()
postApi()
// Web socket push notifications
pushService()
}
// UI routes
overview()
calendar()
login()
account()
board()
wall()
workGroup()
track()
room()
user()
// RESTful routes
authenticateApi()
roomApi()
scheduleApi()
trackApi()
userApi()
workGroupApi()
constraintsApi()
postApi()
// Web socket push notifications
pushService()
}
}

View file

@ -26,11 +26,15 @@ object Configuration {
private object ServerSpec : ConfigSpec("server") {
val host by required<String>()
val port by required<Int>()
val debug by required<Boolean>()
val cPrefix by required<String>("prefix")
}
object Server {
val host by c(ServerSpec.host)
val port by c(ServerSpec.port)
val debug by c(ServerSpec.debug)
val prefix by c(ServerSpec.cPrefix)
}
private object PathSpec : ConfigSpec("path") {

View file

@ -31,7 +31,7 @@ fun main(args: Array<String>) {
var password: String? = null
while (password == null) {
print("Password: ")
password = System.console()?.readPassword()?.toString() ?: readLine()
password = System.console()?.readPassword()?.joinToString("") ?: readLine()
}
println("Create root user '$username' with pw '${"*".repeat(password.length)}'")

View file

@ -14,6 +14,7 @@ import io.ktor.sessions.*
import io.ktor.util.hex
import io.ktor.util.pipeline.PipelineContext
import org.mindrot.jbcrypt.BCrypt
import de.kif.backend.prefix
interface ErrorContext {
suspend infix fun onFailure(block: suspend () -> Unit)
@ -49,7 +50,7 @@ suspend inline fun PipelineContext<Unit, ApplicationCall>.authenticateOrRedirect
block: (user: User) -> Unit
) {
authenticate(*permissions, block = block) onFailure {
call.respondRedirect("/login?redirect=${call.request.path()}}")
call.respondRedirect("$prefix/login?redirect=${call.request.path()}")
}
}
@ -68,7 +69,7 @@ data class PortalSession(val userId: Long) {
if (user == null) {
call.sessions.clear<PortalSession>()
call.respondRedirect("/login?onFailure")
call.respondRedirect("$prefix/login?onFailure")
throw IllegalAccessException()
}
@ -93,7 +94,7 @@ fun Application.security() {
userParamName = "username"
passwordParamName = "password"
challenge = FormAuthChallenge.Redirect { _ ->
"/login?onFailure"
"$prefix/login?onFailure"
}
validate { credential: UserPasswordCredential ->
val user = UserRepository.find(credential.name) ?: return@validate null

View file

@ -26,6 +26,7 @@ import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.html.*
import mu.KotlinLogging
import de.kif.backend.prefix
private val logger = KotlinLogging.logger {}
@ -40,7 +41,7 @@ fun Route.account() {
br {}
+"Du hast folgende Rechte: ${user.permissions.joinToString(", ") { it.germanInfo }}"
br {}
a("/logout") {
a("$prefix/logout") {
button(classes = "form-btn") {
+"Ausloggen"
}
@ -69,25 +70,25 @@ fun Route.account() {
div("account-backup") {
if (user.checkPermission(Permission.ROOM)) {
a("/account/backup/rooms.json", classes = "form-btn") {
a("$prefix/account/backup/rooms.json", classes = "form-btn") {
attributes["download"] = "rooms-backup.json"
+"Räume sichern"
}
}
if (user.checkPermission(Permission.USER)) {
a("/account/backup/users.json", classes = "form-btn") {
a("$prefix/account/backup/users.json", classes = "form-btn") {
attributes["download"] = "users-backup.json"
+"Nutzer sichern"
}
}
if (user.checkPermission(Permission.POST)) {
a("/account/backup/posts.json", classes = "form-btn") {
a("$prefix/account/backup/posts.json", classes = "form-btn") {
attributes["download"] = "posts-backup.json"
+"Beiträge sichern"
}
}
if (user.checkPermission(Permission.WORK_GROUP)) {
a("/account/backup/work-groups.json", classes = "form-btn") {
a("$prefix/account/backup/work-groups.json", classes = "form-btn") {
attributes["download"] = "work-groups-backup.json"
+"Arbeitskreise sichern"
}
@ -97,14 +98,14 @@ fun Route.account() {
user.checkPermission(Permission.ROOM) &&
user.checkPermission(Permission.SCHEDULE)
) {
a("/account/backup/schedules.json", classes = "form-btn") {
a("$prefix/account/backup/schedules.json", classes = "form-btn") {
attributes["download"] = "schedules-backup.json"
+"Zeitplan sichern"
}
}
if (user.checkPermission(Permission.ADMIN)) {
a("/account/backup.json", classes = "form-btn") {
a("$prefix/account/backup.json", classes = "form-btn") {
attributes["download"] = "backup.json"
+"Alles sichern" // TODO: richtiger Text?
}
@ -179,7 +180,7 @@ fun Route.account() {
div("account-import-wiki") {
span { +"Arbeitskreise aus dem KIF-Wiki importieren" }
form(action = "/account/import", method = FormMethod.post) {
form(action = "$prefix/account/import", method = FormMethod.post) {
for ((index, section) in wikiSections.withIndex()) {
div("form-group") {
label {
@ -330,7 +331,7 @@ fun Route.account() {
Backup.import(import)
}
call.respondRedirect("/account")
call.respondRedirect("$prefix/account")
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
@ -387,7 +388,7 @@ fun Route.account() {
logger.info { "Import $counter from ${importedWorkGroups.size} work groups!" }
call.respondRedirect("/account")
call.respondRedirect("$prefix/account")
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}

View file

@ -1,16 +1,17 @@
package de.kif.backend.route
import de.kif.backend.Configuration
import de.kif.backend.repository.PostRepository
import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.view.respondMain
import de.kif.common.model.Schedule
import io.ktor.routing.Route
import io.ktor.routing.get
import kotlinx.css.CSSBuilder
import kotlinx.css.Color
import kotlinx.html.*
import kotlinx.html.div
import kotlinx.html.img
import kotlinx.html.span
import kotlinx.html.unsafe
import java.util.*
import kotlin.math.max
import kotlin.math.min
@ -120,20 +121,19 @@ fun Route.board() {
}
div("board-content") {
div("board-calendar calendar") {
div("calendar-table") {
renderCalendar(
CalendarOrientation.ROOM_TO_TIME,
day,
min,
max,
rooms,
schedules
)
}
renderCalendar(
CalendarOrientation.ROOM_TO_TIME,
day,
min,
max,
rooms,
schedules
)
}
div("board-twitter") {
unsafe {
raw("""
raw(
"""
<a
class="twitter-timeline"
href="${Configuration.Twitter.timeline}"
@ -145,7 +145,8 @@ fun Route.board() {
data-dnt="true"
>Twitter wall</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
""".trimIndent())
""".trimIndent()
)
}
}
}

View file

@ -26,6 +26,7 @@ import kotlin.collections.component2
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
import de.kif.backend.prefix
const val MINUTES_OF_DAY = 24 * 60
@ -53,7 +54,9 @@ private fun DIV.calendarCell(schedule: Schedule?) {
attributes["data-time"] = schedule.time.toString()
attributes["data-length"] = schedule.workGroup.length.toString()
+schedule.workGroup.name
span("calendar-entry-name") {
+schedule.workGroup.name
}
}
}
}
@ -70,66 +73,75 @@ fun DIV.renderCalendar(
val minutesOfDay = to - from
val now = Calendar.getInstance()
val currentTime = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE)
val d = (now.timeInMillis / (1000 * 60 * 60 * 24) -
Configuration.Schedule.referenceDate.time / (1000 * 60 * 60 * 24)).toInt()
val diff = day - d
val currentTime = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE) + (diff * 60 * 24)
div("calendar-table-box ${orientation.name.toLowerCase().replace("_", "-")}") {
div("calendar-header") {
div("calendar-cell") {
span {
+"Raum"
}
}
div("calendar-table") {
attributes["data-day"] = day.toString()
attributes["data-reference"] = Configuration.Schedule.referenceDate.time.toString()
attributes["data-now"] = now.timeInMillis.toString()
for (room in rooms) {
div("calendar-table-box ${orientation.name.toLowerCase().replace("_", "-")}") {
div("calendar-header") {
div("calendar-cell") {
attributes["data-room"] = room.id.toString()
span {
+room.name
+"Raum"
}
}
}
}
div("calendar-body") {
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it
}
val minutes = (time % 60).toString().padStart(2, '0')
val hours = (time / 60).toString().padStart(2, '0')
val timeString = "$hours:$minutes"
val start = i * CALENDAR_GRID_WIDTH + from
val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1
var rowClass = "calendar-row"
if (currentTime in start..end) {
rowClass += " calendar-now calendar-now-${currentTime - start}"
}
div(rowClass) {
attributes["data-time"] = start.toString()
attributes["data-day"] = day.toString()
for (room in rooms) {
div("calendar-cell") {
if (time % gridLabelWidth == 0) {
span {
+timeString
}
attributes["data-room"] = room.id.toString()
span {
+room.name
}
}
}
}
div("calendar-body") {
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it
}
val minutes = (time % 60).toString().padStart(2, '0')
val hours = (time / 60).toString().padStart(2, '0')
val timeString = "$hours:$minutes"
val start = i * CALENDAR_GRID_WIDTH + from
val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1
var rowClass = "calendar-row"
if (currentTime in start..end) {
rowClass += " calendar-now calendar-now-${currentTime - start}"
}
div(rowClass) {
attributes["data-time"] = start.toString()
attributes["data-day"] = day.toString()
for (room in rooms) {
div("calendar-cell") {
attributes["data-room"] = room.id.toString()
if (time % gridLabelWidth == 0) {
span {
+timeString
}
}
}
title = room.name + " - " + timeString
for (room in rooms) {
div("calendar-cell") {
attributes["data-room"] = room.id.toString()
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
title = room.name + " - " + timeString
calendarCell(schedule)
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
calendarCell(schedule)
}
}
}
}
@ -148,7 +160,7 @@ fun Route.calendar() {
val todayDay = todayDate.time / (1000 * 60 * 60 * 24)
val day = todayDay - refDay
call.respondRedirect("/calendar/$day", false)
call.respondRedirect("$prefix/calendar/$day", false)
}
get("/calendar/{day}/rtt") {
@ -159,7 +171,7 @@ fun Route.calendar() {
path = "/"
)
val day = call.parameters["day"]?.toIntOrNull() ?: 0
call.respondRedirect("/calendar/$day")
call.respondRedirect("$prefix/calendar/$day")
}
get("/calendar/{day}/ttr") {
@ -170,7 +182,7 @@ fun Route.calendar() {
path = "/"
)
val day = call.parameters["day"]?.toIntOrNull() ?: 0
call.respondRedirect("/calendar/$day")
call.respondRedirect("$prefix/calendar/$day")
}
get("/calendar/{day}") {
@ -237,20 +249,20 @@ fun Route.calendar() {
div("header") {
div("header-left") {
if (editable || day - 1 > range.start) {
a("/calendar/${day - 1}") { i("material-icons") { +"chevron_left" } }
a("$prefix/calendar/${day - 1}") { i("material-icons") { +"chevron_left" } }
}
span {
+dateString
}
if (editable || day + 1 < range.endInclusive) {
a("/calendar/${day + 1}") { i("material-icons") { +"chevron_right" } }
a("$prefix/calendar/${day + 1}") { i("material-icons") { +"chevron_right" } }
}
}
div("header-right") {
a("/calendar/$day/rtt", classes = "form-btn") {
a("$prefix/calendar/$day/rtt", classes = "form-btn") {
+"Vertikal"
}
a("/calendar/$day/ttr", classes = "form-btn") {
a("$prefix/calendar/$day/ttr", classes = "form-btn") {
+"Horizontal"
}
if (editable) {
@ -267,19 +279,16 @@ fun Route.calendar() {
}
div("calendar") {
attributes["data-day"] = day.toString()
attributes["data-editable"] = editable.toString()
div("calendar-table") {
renderCalendar(
orientation,
day,
min,
max,
rooms,
schedules
)
}
renderCalendar(
orientation,
day,
min,
max,
rooms,
schedules
)
if (editable) {
div("calendar-edit") {

View file

@ -16,6 +16,7 @@ import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.sessions.set
import kotlinx.html.*
import de.kif.backend.prefix
fun Route.login() {
route("login") {
@ -24,11 +25,12 @@ fun Route.login() {
val principal = call.principal<UserPrinciple>() ?: return@post
if (principal.user.id == null) return@post
call.sessions.set(PortalSession(principal.user.id))
call.respondRedirect(call.parameters["redirect"] ?: "/")
call.respondRedirect(call.parameters["redirect"] ?: "$prefix/")
}
}
get {
val redirect = call.parameters["redirect"] ?: "$prefix/"
val needLogin = call.sessions.get<PortalSession>() == null
if (needLogin) {
respondMain {
@ -36,7 +38,7 @@ fun Route.login() {
div {
div {
br { }
form("/login", method = FormMethod.post) {
form("$prefix/login?redirect=$redirect", method = FormMethod.post) {
div("form-group") {
label {
htmlFor = "username"
@ -68,7 +70,7 @@ fun Route.login() {
name = "redirect",
type = InputType.hidden
) {
value = call.parameters["redirect"] ?: "/"
value = redirect
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Einloggen"
@ -86,13 +88,13 @@ fun Route.login() {
}
}
} else {
call.respondRedirect(call.parameters["redirect"] ?: "/")
call.respondRedirect(redirect)
}
}
}
get("logout") {
call.sessions.clear<PortalSession>()
call.respondRedirect("/")
call.respondRedirect("$prefix/")
}
}

View file

@ -26,6 +26,7 @@ import io.ktor.routing.get
import io.ktor.routing.post
import kotlinx.html.*
import java.io.File
import de.kif.backend.prefix
fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") {
var classes = "post"
@ -39,11 +40,11 @@ fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: Str
attributes["data-id"] = post.id.toString()
attributes["data-pinned"] = post.pinned.toString()
a("/p/${post.url}", classes = "post-name") {
a("$prefix/p/${post.url}", classes = "post-name") {
+post.name
}
if (editable) {
a("/post/${post.id}", classes = "post-edit") {
a("$prefix/post/${post.id}", classes = "post-edit") {
i("material-icons") { +"edit" }
}
}
@ -269,7 +270,7 @@ fun Route.overview() {
}
div("form-group") {
a("/") {
a("$prefix/") {
button(classes = "form-btn") {
+"Abbrechen"
}
@ -279,7 +280,7 @@ fun Route.overview() {
}
}
}
a("/post/${editPost.id}/delete") {
a("$prefix/post/${editPost.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Löschen"
}
@ -360,7 +361,7 @@ fun Route.overview() {
PostRepository.update(post)
call.respondRedirect("/")
call.respondRedirect("$prefix/")
}
}
@ -474,7 +475,7 @@ fun Route.overview() {
}
div("form-group") {
a("/") {
a("$prefix/") {
button(classes = "form-btn") {
+"Abbrechen"
}
@ -536,7 +537,7 @@ fun Route.overview() {
PostRepository.create(post)
call.respondRedirect("/")
call.respondRedirect("$prefix/")
}
}
@ -546,7 +547,7 @@ fun Route.overview() {
PostRepository.delete(postId)
call.respondRedirect("/")
call.respondRedirect("$prefix/")
}
}
}

View file

@ -24,6 +24,7 @@ import kotlinx.html.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
import de.kif.backend.prefix
fun Route.room() {
@ -38,7 +39,7 @@ fun Route.room() {
searchValue = search
action {
a("/room/new") {
a("$prefix/room/new") {
button(classes = "form-btn btn-primary") {
+"Raum hinzufügen"
}
@ -92,7 +93,7 @@ fun Route.room() {
}
}
td(classes = "action") {
a("/room/${u.id}") {
a("$prefix/room/${u.id}") {
i("material-icons") { +"edit" }
}
}
@ -223,7 +224,7 @@ fun Route.room() {
}
div("form-group") {
a("/room") {
a("$prefix/room") {
button(classes = "form-btn") {
+"Abbrechen"
}
@ -233,7 +234,7 @@ fun Route.room() {
}
}
}
a("/room/${editRoom.id}/delete") {
a("$prefix/room/${editRoom.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Löschen"
}
@ -261,7 +262,7 @@ fun Route.room() {
RoomRepository.update(room)
call.respondRedirect("/rooms")
call.respondRedirect("$prefix/rooms")
}
}
@ -382,7 +383,7 @@ fun Route.room() {
}
div("form-group") {
a("/room") {
a("$prefix/room") {
button(classes = "form-btn") {
+"Abbrechen"
}
@ -415,7 +416,7 @@ fun Route.room() {
RoomRepository.create(room)
call.respondRedirect("/rooms")
call.respondRedirect("$prefix/rooms")
}
}
@ -425,7 +426,7 @@ fun Route.room() {
RoomRepository.delete(roomId)
call.respondRedirect("/rooms")
call.respondRedirect("$prefix/rooms")
}
}
}

View file

@ -1,6 +1,5 @@
package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.TrackRepository
import de.kif.backend.view.MainTemplate
@ -25,6 +24,7 @@ import kotlinx.css.Display
import kotlinx.html.*
import kotlin.collections.set
import kotlin.random.Random
import de.kif.backend.prefix
fun DIV.colorPicker(color: Color?) {
val colorString = color?.toString() ?: Color(
@ -95,7 +95,7 @@ fun Route.track() {
searchValue = search
action {
a("/track/new") {
a("$prefix/track/new") {
button(classes = "form-btn btn-primary") {
+"Track hinzufügen"
}
@ -128,7 +128,7 @@ fun Route.track() {
+u.color.toString()
}
td(classes = "action") {
a("/track/${u.id}") {
a("$prefix/track/${u.id}") {
i("material-icons") { +"edit" }
}
}
@ -168,7 +168,7 @@ fun Route.track() {
}
div("form-group") {
a("/track") {
a("$prefix/track") {
button(classes = "form-btn") {
+"Cancel"
}
@ -178,7 +178,7 @@ fun Route.track() {
}
}
}
a("/track/${editTrack.id}/delete") {
a("$prefix/track/${editTrack.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Löschen"
}
@ -204,7 +204,7 @@ fun Route.track() {
TrackRepository.update(editTrack)
call.respondRedirect("/tracks")
call.respondRedirect("$prefix/tracks")
}
}
@ -232,7 +232,7 @@ fun Route.track() {
colorPicker(null)
}
div("form-group") {
a("/track") {
a("$prefix/track") {
button(classes = "form-btn") {
+"Abbrechen"
}
@ -262,7 +262,7 @@ fun Route.track() {
TrackRepository.create(track)
call.respondRedirect("/tracks")
call.respondRedirect("$prefix/tracks")
}
}
@ -272,7 +272,7 @@ fun Route.track() {
TrackRepository.delete(trackId)
call.respondRedirect("/tracks")
call.respondRedirect("$prefix/tracks")
}
}
}

View file

@ -1,11 +1,10 @@
package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.checkPassword
import de.kif.backend.hashPassword
import de.kif.backend.prefix
import de.kif.backend.repository.UserRepository
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.backend.view.respondMain
import de.kif.common.Search
@ -13,7 +12,6 @@ import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
@ -38,7 +36,7 @@ fun Route.user() {
searchValue = search
action {
a("/user/new") {
a("$prefix/user/new") {
button(classes = "form-btn btn-primary") {
+"Nutzer hinzufügen"
}
@ -71,7 +69,7 @@ fun Route.user() {
+u.permissions.joinToString(", ") { it.toString().toLowerCase() }
}
td(classes = "action") {
a("/user/${u.id}") {
a("$prefix/user/${u.id}") {
i("material-icons") { +"edit" }
}
}
@ -131,17 +129,40 @@ fun Route.user() {
}
div("form-group") {
a("/user") {
button(classes = "form-btn") {
+"Abbrechen"
}
label {
htmlFor = "current-password"
+"Passwort ändern"
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Speichern"
input(
name = "current-password",
classes = "form-control",
type = InputType.password
) {
id = "current-password"
placeholder = "Aktuelles Passwort"
value = ""
}
input(
name = "new-password-1",
classes = "form-control",
type = InputType.password
) {
id = "new-password-1"
placeholder = "Neues Passwort"
value = ""
}
input(
name = "new-password-2",
classes = "form-control",
type = InputType.password
) {
id = "new-password-2"
placeholder = "Neues Passwort wiederholen"
value = ""
}
}
}
a("/user/${editUser.id}/delete") {
a("$prefix/user/${editUser.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Löschen"
}
@ -162,6 +183,16 @@ fun Route.user() {
params["username"]?.let { editUser = editUser.copy(username = it) }
val currentPassword = params["current-password"]
val newPassword1 = params["new-password-1"]
val newPassword2 = params["new-password-2"]
if (currentPassword != null && newPassword1 != null && newPassword2 != null && currentPassword.isNotBlank() && newPassword1.isNotBlank() && newPassword2.isNotBlank()) {
if (checkPassword(currentPassword, editUser.password) && newPassword1 == newPassword2) {
editUser = editUser.copy(password = hashPassword(newPassword1))
}
}
val permissions = Permission.values().filter { permission ->
val name = permission.toString().toLowerCase()
user.checkPermission(permission) && params["permission-$name"] == "on"
@ -170,7 +201,7 @@ fun Route.user() {
UserRepository.update(user)
call.respondRedirect("/users")
call.respondRedirect("$prefix/users")
}
}
@ -235,7 +266,7 @@ fun Route.user() {
}
div("form-group") {
a("/user") {
a("$prefix/user") {
button(classes = "form-btn") {
+"Abbrechen"
}
@ -270,7 +301,7 @@ fun Route.user() {
UserRepository.create(newUser)
call.respondRedirect("/users")
call.respondRedirect("$prefix/users")
}
}
@ -285,7 +316,7 @@ fun Route.user() {
UserRepository.delete(userId)
}
call.respondRedirect("/users")
call.respondRedirect("$prefix/users")
}
}
}

View file

@ -20,11 +20,12 @@ data class WallData(
suspend fun genWallData(day: Int): WallData {
val list = ScheduleRepository.getByDay(day)
val schedules = RoomRepository.all().associateWith { emptyMap<Int, Schedule>() } + list.groupBy { it.room }.mapValues { (_, it) ->
it.associateBy {
it.time
val schedules =
RoomRepository.all().associateWith { emptyMap<Int, Schedule>() } + list.groupBy { it.room }.mapValues { (_, it) ->
it.associateBy {
it.time
}
}
}
var max = 0
var min = 24 * 60
@ -59,16 +60,14 @@ fun Route.wall() {
for (day in days) {
div("wall-box") {
div("wall-calendar calendar") {
div("calendar-table") {
renderCalendar(
CalendarOrientation.TIME_TO_ROOM,
day.number,
min,
max,
day.schedules.keys.toList().sortedBy { it.id },
day.schedules
)
}
renderCalendar(
CalendarOrientation.TIME_TO_ROOM,
day.number,
min,
max,
day.schedules.keys.toList().sortedBy { it.id },
day.schedules
)
}
}
}

View file

@ -19,6 +19,7 @@ import kotlinx.css.CSSBuilder
import kotlinx.css.Display
import kotlinx.html.*
import kotlin.collections.set
import de.kif.backend.prefix
private const val separator = "###"
@ -33,12 +34,12 @@ fun Route.workGroup() {
searchValue = search
action {
a("/tracks") {
a("$prefix/tracks") {
button(classes = "form-btn") {
+"Tracks bearbeiten"
}
}
a("/workgroup/new") {
a("$prefix/workgroup/new") {
button(classes = "form-btn btn-primary") {
+"Arbeitskreis hinzufügen"
}
@ -132,7 +133,7 @@ fun Route.workGroup() {
}
}
td(classes = "action") {
a("/workgroup/${u.id}") {
a("$prefix/workgroup/${u.id}") {
i("material-icons") { +"edit" }
}
}
@ -400,6 +401,8 @@ fun Route.workGroup() {
min = "-1337"
max = "1337"
placeholder = "Tag"
}
}
ConstraintType.NotOnDay -> {
@ -415,6 +418,8 @@ fun Route.workGroup() {
min = "-1337"
max = "1337"
placeholder = "Tag"
}
}
ConstraintType.OnlyBeforeTime -> {
@ -426,7 +431,8 @@ fun Route.workGroup() {
classes = "form-control"
) {
value = constraint.day?.toString() ?: ""
placeholder = "day"
placeholder = "Tag (optional)"
}
input(
name = "constraint-only-before-time-$index",
@ -437,6 +443,8 @@ fun Route.workGroup() {
min = "-1337"
max = "133700"
placeholder = "Minuten"
}
}
ConstraintType.OnlyAfterTime -> {
@ -448,7 +456,8 @@ fun Route.workGroup() {
classes = "form-control"
) {
value = constraint.day?.toString() ?: ""
placeholder = "day"
placeholder = "Tag (optional)"
}
input(
name = "constraint-only-after-time-$index",
@ -459,6 +468,8 @@ fun Route.workGroup() {
min = "-1337"
max = "133700"
placeholder = "Minuten"
}
}
ConstraintType.NotAtSameTime -> {
@ -500,7 +511,7 @@ fun Route.workGroup() {
}
div("form-group") {
a("/workgroup") {
a("$prefix/workgroup") {
button(classes = "form-btn") {
+"Abbrechen"
}
@ -510,7 +521,7 @@ fun Route.workGroup() {
}
}
}
a("/workgroup/${editWorkGroup.id}/delete") {
a("$prefix/workgroup/${editWorkGroup.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Löschen"
}
@ -558,7 +569,7 @@ fun Route.workGroup() {
WorkGroupRepository.update(editWorkGroup)
call.respondRedirect("/workgroups")
call.respondRedirect("$prefix/workgroups")
}
}
@ -796,7 +807,7 @@ fun Route.workGroup() {
}
div("form-group") {
a("/workgroup") {
a("$prefix/workgroup") {
button(classes = "form-btn") {
+"Abbrechen"
}
@ -856,7 +867,7 @@ fun Route.workGroup() {
WorkGroupRepository.create(workGroup)
call.respondRedirect("/workgroups")
call.respondRedirect("$prefix/workgroups")
}
}
@ -866,7 +877,7 @@ fun Route.workGroup() {
WorkGroupRepository.delete(workGroupId)
call.respondRedirect("/workgroups")
call.respondRedirect("$prefix/workgroups")
}
}
}
@ -882,14 +893,14 @@ private fun parseConstraintParam(params: Map<String, String?>) = params.map { (k
}
key.startsWith("constraint-only-after-time") -> {
if ("day" in key) {
value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, day = it) }
Constraint(ConstraintType.OnlyAfterTime, day = value?.toIntOrNull())
} else {
value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, time = it) }
}
}
key.startsWith("constraint-only-before-time") -> {
if ("day" in key) {
value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, day = it) }
Constraint(ConstraintType.OnlyBeforeTime, day = value?.toIntOrNull())
} else {
value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, time = it) }
}

View file

@ -1,5 +1,6 @@
package de.kif.backend.util
import de.kif.backend.prefix
import de.kif.backend.repository.*
import de.kif.common.*
import de.kif.common.RepositoryType
@ -27,7 +28,7 @@ object PushService {
}
fun Route.pushService() {
webSocket {
webSocket("/") {
PushService.clients += this
try {

View file

@ -2,10 +2,14 @@ package de.kif.backend.view
import de.kif.backend.PortalSession
import de.kif.backend.Resources
import de.kif.backend.prefix
import de.kif.common.model.User
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.html.*
import io.ktor.html.Placeholder
import io.ktor.html.Template
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.request.path
import io.ktor.request.uri
import io.ktor.response.respondRedirect
@ -30,36 +34,41 @@ class MainTemplate(
title("KIF Portal")
link(href = "/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(href = "/static/external/font/Montserrat.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(href = "$prefix/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(
href = "$prefix/static/external/font/Montserrat.css",
type = LinkType.textCss,
rel = LinkRel.stylesheet
)
link(
href = "https://fonts.googleapis.com/css?family=Bungee|Oswald|Raleway",
type = LinkType.textCss,
rel = LinkRel.stylesheet
)
link(href = "/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(href = "$prefix/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
when (theme) {
Theme.LIGHT -> {
// Ignore
}
Theme.DARK -> {
link(href = "/static/style/dark.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(href = "$prefix/static/style/dark.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
}
Theme.PRINCESS -> {
link(href = "/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(href = "$prefix/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
}
Theme.BRETT -> {
link(href = "/static/style/board.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
link(href = "$prefix/static/style/board.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
}
}
script(src = "/static/require.min.js") {}
script(src = "$prefix/static/require.min.js") {}
script {
unsafe {
+"require.config({baseUrl: '/static'});\n"
+("require([${Resources.jsModules}]);\n")
+"let prefix = '$prefix';\n"
+"require.config({baseUrl: '$prefix/static'});\n"
+"require([${Resources.jsModules}]);\n"
}
}
}
@ -96,10 +105,10 @@ class MainTemplate(
}
enum class Theme(val text: String, val display: Boolean, val dark: Boolean, val primaryColor: String) {
LIGHT("Hell",true, false, "#B11D33"),
DARK("Dunkel",true, true, "#ef5350"),
PRINCESS("Barbie",true, false, "#B11D33"),
BRETT("Brett",false, false, "#B11D33");
LIGHT("Hell", true, false, "#B11D33"),
DARK("Dunkel", true, true, "#ef5350"),
PRINCESS("Barbie", true, false, "#B11D33"),
BRETT("Brett", false, false, "#B11D33");
companion object {
private val lookup = values().toList().associateBy { it.name }
@ -118,7 +127,7 @@ suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
body: MainTemplate.(Theme) -> Unit
) {
val param = call.request.queryParameters["theme"]
val url = call.request.uri.substring(1)
val url = call.request.uri.substring(1 + prefix.length)
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (param != null) {
@ -126,7 +135,7 @@ suspend fun PipelineContext<Unit, ApplicationCall>.respondMain(
name = "theme",
value = Theme.lookup(param).name,
maxAge = Int.MAX_VALUE,
path = "/"
path = "$prefix/"
)
call.respondRedirect(call.request.path())
} else {

View file

@ -1,9 +1,11 @@
package de.kif.backend.view
import de.kif.backend.Configuration
import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.html.Template
import kotlinx.html.*
import de.kif.backend.prefix
class MenuTemplate(
private val url: String,
@ -16,10 +18,10 @@ class MenuTemplate(
nav("menu") {
div("container") {
div("menu-left") {
a("/", classes = if (tab == null) "active" else null) {
a("$prefix/", classes = if (tab == null) "active" else null) {
+"Neuigkeiten"
}
a("/calendar", classes = if (tab == Tab.CALENDAR) "active" else null) {
a("$prefix/calendar", classes = if (tab == Tab.CALENDAR) "active" else null) {
+"Zeitplan"
}
}
@ -30,26 +32,26 @@ class MenuTemplate(
val user = user
div("menu-content") {
if (user == null) {
a("/account", classes = if (tab == Tab.LOGIN) "active" else null) {
a("$prefix/account", classes = if (tab == Tab.LOGIN) "active" else null) {
+"Einloggen"
}
} else {
if (user.checkPermission(Permission.WORK_GROUP)) {
a("/workgroups", classes = if (tab == Tab.WORK_GROUP) "active" else null) {
a("$prefix/workgroups", classes = if (tab == Tab.WORK_GROUP) "active" else null) {
+"Arbeitskreise"
}
}
if (user.checkPermission(Permission.ROOM)) {
a("/rooms", classes = if (tab == Tab.ROOM) "active" else null) {
a("$prefix/rooms", classes = if (tab == Tab.ROOM) "active" else null) {
+"Räume"
}
}
if (user.checkPermission(Permission.USER)) {
a("/users", classes = if (tab == Tab.USER) "active" else null) {
a("$prefix/users", classes = if (tab == Tab.USER) "active" else null) {
+"Nutzer"
}
}
a("/account", classes = if (tab == Tab.ACCOUNT) "active" else null) {
a("$prefix/account", classes = if (tab == Tab.ACCOUNT) "active" else null) {
+user.username
}
}

View file

@ -4,10 +4,78 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KIF Portal</title>
<link href="/static/external/material-icons.css" rel="Stylesheet" type="text/css">
<link href="/static/external/font/Montserrat.css" rel="Stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Bungee|Oswald" rel="Stylesheet" type="text/css">
<link href="/static/style/style.css" rel="Stylesheet" type="text/css">
<style>
:root {
--background-secondary-color: #f5f5f5;
--text-primary-color: #333;
}
.main-error {
width: 100%;
height: 4rem;
text-align: center;
margin-top: -2rem;
position: absolute;
top: 48%;
left: 0;
}
.main-error span {
display: block;
font-size: 1.2rem;
}
.main-error span:first-child {
font-size: 1.5rem;
padding-bottom: 0.4rem;
}
body, html {
color: var(--text-primary-color);
background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif;
font-weight: 500;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
.container {
width: 100%;
margin: 0 auto;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
@media (min-width: 768px) {
.container {
width: 720px;
}
}
@media (min-width: 992px) {
.container {
width: 960px;
}
}
@media (min-width: 1200px) {
.container {
width: 1140px;
}
}
</style>
</head>
<body>
<div class="container">

View file

@ -4,10 +4,78 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KIF Portal</title>
<link href="/static/external/material-icons.css" rel="Stylesheet" type="text/css">
<link href="/static/external/font/Montserrat.css" rel="Stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Bungee|Oswald" rel="Stylesheet" type="text/css">
<link href="/static/style/style.css" rel="Stylesheet" type="text/css">
<style>
:root {
--background-secondary-color: #f5f5f5;
--text-primary-color: #333;
}
.main-error {
width: 100%;
height: 4rem;
text-align: center;
margin-top: -2rem;
position: absolute;
top: 48%;
left: 0;
}
.main-error span {
display: block;
font-size: 1.2rem;
}
.main-error span:first-child {
font-size: 1.5rem;
padding-bottom: 0.4rem;
}
body, html {
color: var(--text-primary-color);
background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif;
font-weight: 500;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
.container {
width: 100%;
margin: 0 auto;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
@media (min-width: 768px) {
.container {
width: 720px;
}
}
@media (min-width: 992px) {
.container {
width: 960px;
}
}
@media (min-width: 1200px) {
.container {
width: 1140px;
}
}
</style>
</head>
<body>
<div class="container">

View file

@ -4,10 +4,78 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KIF Portal</title>
<link href="/static/external/material-icons.css" rel="Stylesheet" type="text/css">
<link href="/static/external/font/Montserrat.css" rel="Stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Bungee|Oswald" rel="Stylesheet" type="text/css">
<link href="/static/style/style.css" rel="Stylesheet" type="text/css">
<style>
:root {
--background-secondary-color: #f5f5f5;
--text-primary-color: #333;
}
.main-error {
width: 100%;
height: 4rem;
text-align: center;
margin-top: -2rem;
position: absolute;
top: 48%;
left: 0;
}
.main-error span {
display: block;
font-size: 1.2rem;
}
.main-error span:first-child {
font-size: 1.5rem;
padding-bottom: 0.4rem;
}
body, html {
color: var(--text-primary-color);
background: var(--background-secondary-color);
font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif;
font-weight: 500;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
.container {
width: 100%;
margin: 0 auto;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
@media (min-width: 768px) {
.container {
width: 720px;
}
}
@media (min-width: 992px) {
.container {
width: 960px;
}
}
@media (min-width: 1200px) {
.container {
width: 1140px;
}
}
</style>
</head>
<body>
<div class="container">

View file

@ -1,6 +1,8 @@
[server]
host = "localhost"
port = 8080
debug = false
prefix = ""
[path]
web = "web"