Add config

This commit is contained in:
Lars Westermann 2019-05-29 12:44:33 +02:00
parent 32596228fe
commit 997f374fe4
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
18 changed files with 287 additions and 120 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
build/ build/
web/ web/
.sessions/ .sessions/
data/
*.swp *.swp
*.swo *.swo

View file

@ -22,6 +22,7 @@ version "0.1.0"
repositories { repositories {
jcenter() jcenter()
maven { url 'https://jitpack.io' }
maven { url "https://dl.bintray.com/kotlin/ktor" } maven { url "https://dl.bintray.com/kotlin/ktor" }
maven { url "https://dl.bintray.com/jetbrains/markdown" } maven { url "https://dl.bintray.com/jetbrains/markdown" }
maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" }
@ -36,6 +37,7 @@ kotlin {
jvm() { jvm() {
compilations.all { compilations.all {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += [ freeCompilerArgs += [
"-Xuse-experimental=io.ktor.util.KtorExperimentalAPI" "-Xuse-experimental=io.ktor.util.KtorExperimentalAPI"
] ]
@ -88,6 +90,7 @@ kotlin {
implementation "de.westermann:KObserve-jvm:$observable_version" implementation "de.westermann:KObserve-jvm:$observable_version"
implementation 'com.github.uchuhimo:konf:master-SNAPSHOT'
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10' implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
api 'io.github.microutils:kotlin-logging:1.6.23' api 'io.github.microutils:kotlin-logging:1.6.23'
api 'ch.qos.logback:logback-classic:1.2.3' api 'ch.qos.logback:logback-classic:1.2.3'
@ -167,6 +170,7 @@ task run(type: JavaExec, dependsOn: [jvmMainClasses, jsJar]) {
clean.doFirst { clean.doFirst {
delete webFolder delete webFolder
delete ".sessions" delete ".sessions"
delete "data"
} }
task jar(type: ShadowJar, dependsOn: [jvmMainClasses, jsMainClasses, sass]) { task jar(type: ShadowJar, dependsOn: [jvmMainClasses, jsMainClasses, sass]) {

6
portal.toml Normal file
View file

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

View file

@ -9,7 +9,8 @@ data class Post(
override val id: Long? = null, override val id: Long? = null,
val name: String, val name: String,
val content: String, val content: String,
val url: String val url: String,
val pinned: Boolean = false
) : Model { ) : Model {
override fun createSearch() = SearchElement( override fun createSearch() = SearchElement(

View file

@ -3,11 +3,28 @@ package de.kif.frontend.views.overview
import de.kif.frontend.iterator import de.kif.frontend.iterator
import de.kif.frontend.launch import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository import de.kif.frontend.repository.PostRepository
import de.westermann.kobserve.event.subscribe
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.events.EventListener import org.w3c.dom.events.EventListener
import org.w3c.dom.get import org.w3c.dom.get
import kotlin.browser.document import kotlin.browser.document
import kotlin.dom.clear
private fun sortOverviewPosts(container: HTMLElement) {
val list = container.children.iterator().asSequence().toList()
val sorted = list.sortedWith(compareBy(
{ if (it.dataset["pinned"]?.toBoolean() == true) 0 else 1 },
{ -(it.dataset["id"]?.toLong() ?: -1) }
))
container.clear()
for (element in sorted) {
container.appendChild(element)
}
}
fun initOverviewMain() { fun initOverviewMain() {
val main = document.getElementsByClassName("overview-main")[0] as HTMLElement val main = document.getElementsByClassName("overview-main")[0] as HTMLElement
@ -15,24 +32,13 @@ fun initOverviewMain() {
PostRepository.onCreate { PostRepository.onCreate {
val post = PostView.create(it) val post = PostView.create(it)
post.classList += "overview-post" post.classList += "overview-post"
main.appendChild(post.html)
val first = main.firstElementChild as? HTMLElement sortOverviewPosts(main)
}
if (first == null) { subscribe<PostChangeEvent> {
main.appendChild(post.html) sortOverviewPosts(main)
return@onCreate
}
if (first.classList.contains("post")) {
main.insertBefore(post.html, first)
return@onCreate
}
val next = first.nextElementSibling as? HTMLElement
if (next == null) {
main.appendChild(post.html)
} else {
main.insertBefore(post.html, next)
}
} }
} }

View file

@ -1,8 +1,8 @@
package de.kif.frontend.views.overview package de.kif.frontend.views.overview
import de.kif.common.model.Post
import de.kif.frontend.launch import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository import de.kif.frontend.repository.PostRepository
import de.westermann.kobserve.event.emit
import de.westermann.kwebview.View import de.westermann.kwebview.View
import de.westermann.kwebview.components.Link import de.westermann.kwebview.components.Link
import de.westermann.kwebview.createHtmlView import de.westermann.kwebview.createHtmlView
@ -16,7 +16,13 @@ class PostView(
view: HTMLElement view: HTMLElement
) : View(view) { ) : View(view) {
private var postId = dataset["id"]?.toLongOrNull() ?: -1 val postId = dataset["id"]?.toLongOrNull() ?: -1
var pinned: Boolean
get() = dataset["pinned"] == "true"
set(value) {
dataset["pinned"] = value.toString()
}
private val nameView: Link private val nameView: Link
private val contentView: View private val contentView: View
@ -27,8 +33,11 @@ class PostView(
nameView.text = p.name nameView.text = p.name
nameView.target = "/p/${p.url}" nameView.target = "/p/${p.url}"
pinned = p.pinned
contentView.html.innerHTML = PostRepository.htmlByUrl(p.url) contentView.html.innerHTML = PostRepository.htmlByUrl(p.url)
emit(PostChangeEvent(postId))
} }
} }
@ -67,3 +76,5 @@ class PostView(
} }
} }
} }
data class PostChangeEvent(val id: Long)

View file

@ -967,12 +967,6 @@ form {
min-width: 20%; min-width: 20%;
} }
.overview-shortcuts {
a {
display: block;
}
}
.overview-twitter { .overview-twitter {
height: 20rem; height: 20rem;
background-color: #b3e6f9; background-color: #b3e6f9;

View file

@ -23,7 +23,7 @@ fun Application.main() {
install(ContentNegotiation) { install(ContentNegotiation) {
jackson { jackson {
enable(SerializationFeature.INDENT_OUTPUT) // Pretty Prints the JSON enable(SerializationFeature.INDENT_OUTPUT)
} }
} }
@ -31,7 +31,7 @@ fun Application.main() {
routing { routing {
static("/static") { static("/static") {
files(Resources.directory) files(Configuration.Path.webPath.toFile())
} }
// UI routes // UI routes

View file

@ -0,0 +1,89 @@
package de.kif.backend
import com.uchuhimo.konf.Config
import com.uchuhimo.konf.ConfigSpec
import com.uchuhimo.konf.Item
import java.io.FileNotFoundException
import java.nio.file.Paths
import java.text.SimpleDateFormat
import java.util.*
import kotlin.reflect.KProperty
object Configuration {
private val config: Config
class ConfigDelegate<T>(val item: Item<T>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return config[item]
}
}
fun <T> c(item: Item<T>) = ConfigDelegate(item)
private object ServerSpec : ConfigSpec("server") {
val host by required<String>()
val port by required<Int>()
}
object Server {
val host by c(ServerSpec.host)
val port by c(ServerSpec.port)
}
private object PathSpec : ConfigSpec("path") {
val web by required<String>()
val sessions by required<String>()
val uploads by required<String>()
val database by required<String>()
}
object Path {
val web by c(PathSpec.web)
val webPath: java.nio.file.Path by lazy { Paths.get(web).toAbsolutePath() }
val sessions by c(PathSpec.sessions)
val sessionsPath: java.nio.file.Path by lazy { Paths.get(sessions).toAbsolutePath() }
val uploads by c(PathSpec.uploads)
val uploadsPath: java.nio.file.Path by lazy { Paths.get(uploads).toAbsolutePath() }
val database by c(PathSpec.database)
val databasePath: java.nio.file.Path by lazy { Paths.get(database).toAbsolutePath() }
}
private object ScheduleSpec : ConfigSpec("schedule") {
val reference by required<String>()
}
object Schedule {
val reference by c(ScheduleSpec.reference)
val referenceDate: Date by lazy { SimpleDateFormat("yyyy-MM-dd").parse(reference) }
}
private object SecuritySpec : ConfigSpec("security") {
val sessionName by required<String>("session_name")
val signKey by required<String>("sign_key")
}
object Security {
val sessionName by c(SecuritySpec.sessionName)
val signKey by c(SecuritySpec.signKey)
}
init {
var config = Config {
addSpec(ServerSpec)
addSpec(PathSpec)
addSpec(ScheduleSpec)
addSpec(SecuritySpec)
}.from.toml.resource("portal.toml")
try {
config = config.from.toml.file("portal.toml")
} catch (_: FileNotFoundException) { }
this.config = config.from.env()
.from.systemProperties()
}
}

View file

@ -8,11 +8,14 @@ import io.ktor.application.Application
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.nio.file.Files
object Main { object Main {
@Suppress("UnusedMainParameter") @Suppress("UnusedMainParameter")
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
Resources.init()
Connection.init() Connection.init()
runBlocking { runBlocking {
@ -47,8 +50,8 @@ object Main {
embeddedServer( embeddedServer(
factory = Netty, factory = Netty,
port = 8080, port = Configuration.Server.port,
host = "0.0.0.0", host = Configuration.Server.host,
module = Application::main module = Application::main
).start(wait = true) ).start(wait = true)
} }

View file

@ -17,11 +17,17 @@ object Resources {
* *
* @return File that points the extracted web directory. * @return File that points the extracted web directory.
*/ */
private fun extractWebFolder(): File { private fun extractWeb() {
val destination: Path = Files.createTempDirectory("web") val destination: Path = Configuration.Path.webPath
val classPath = "web" val classPath = "web"
val uri: URI = this::class.java.classLoader.getResource(classPath).toURI() val uri = this::class.java.classLoader.getResource(classPath)?.toURI()
if (uri == null) {
logger.warn { "Cannot extract web content" }
return
}
val fileSystem: FileSystem? val fileSystem: FileSystem?
val src: Path = if (uri.scheme == "jar") { val src: Path = if (uri.scheme == "jar") {
fileSystem = FileSystems.newFileSystem(uri, mutableMapOf<String, Any?>()) fileSystem = FileSystems.newFileSystem(uri, mutableMapOf<String, Any?>())
@ -43,31 +49,33 @@ object Resources {
} }
} }
return destination.toFile() logger.info { "Successfully extract web content" }
}
/**
* File that points the web directory.
*/
val directory = File(".").absoluteFile.canonicalFile.let { currentDir ->
listOf(
"web",
"../web"
).map {
File(currentDir, it)
}.firstOrNull { it.isDirectory }?.absoluteFile?.canonicalFile ?: extractWebFolder()
}.also {
logger.info { "Web directory: $it" }
} }
/** /**
* List of js modules to be included. * List of js modules to be included.
*/ */
val jsModules = directory.list().toList().filter { val jsModules by lazy {
it.endsWith(".js") && Configuration.Path.webPath.toFile().list().toList().filter {
!it.endsWith(".meta.js") && it.endsWith(".js") &&
!it.endsWith("-test.js") && !it.endsWith(".meta.js") &&
it != "require.min.js" !it.endsWith("-test.js") &&
}.joinToString(", ") { "'${it.take(it.length - 3)}'" } it != "require.min.js"
}.joinToString(", ") { "'${it.take(it.length - 3)}'" }
}
fun init() {
Files.createDirectories(Configuration.Path.databasePath.parent)
Files.createDirectories(Configuration.Path.sessionsPath)
Files.createDirectories(Configuration.Path.uploadsPath)
Files.createDirectories(Configuration.Path.webPath)
logger.info { "Database path: ${Configuration.Path.databasePath}" }
logger.info { "Sessions path: ${Configuration.Path.sessionsPath}" }
logger.info { "Uploads path: ${Configuration.Path.uploadsPath}" }
logger.info { "Web path: ${Configuration.Path.webPath}" }
logger.info { "Extract web content..." }
extractWeb()
}
} }

View file

@ -14,7 +14,6 @@ import io.ktor.sessions.*
import io.ktor.util.hex import io.ktor.util.hex
import io.ktor.util.pipeline.PipelineContext import io.ktor.util.pipeline.PipelineContext
import org.mindrot.jbcrypt.BCrypt import org.mindrot.jbcrypt.BCrypt
import java.io.File
interface ErrorContext { interface ErrorContext {
suspend infix fun onFailure(block: suspend () -> Unit) suspend infix fun onFailure(block: suspend () -> Unit)
@ -103,20 +102,12 @@ fun Application.security() {
} }
} }
val encryptionKey = val signKey = hex(Configuration.Security.signKey.replace(" ", ""))
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) { install(Sessions) {
cookie<PortalSession>( cookie<PortalSession>(
"SESSION", Configuration.Security.sessionName,
directorySessionStorage(File(".sessions"), cached = false) directorySessionStorage(Configuration.Path.sessionsPath.toFile(), cached = false)
) { ) {
cookie.path = "/" cookie.path = "/"
transform(SessionTransportTransformerMessageAuthentication(signKey)) transform(SessionTransportTransformerMessageAuthentication(signKey))

View file

@ -1,5 +1,6 @@
package de.kif.backend.database package de.kif.backend.database
import de.kif.backend.Configuration
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@ -11,7 +12,8 @@ import java.sql.Connection.TRANSACTION_SERIALIZABLE
object Connection { object Connection {
fun init() { fun init() {
Database.connect("jdbc:sqlite:portal.db", "org.sqlite.JDBC") val dbPath = Configuration.Path.databasePath.toString()
Database.connect("jdbc:sqlite:$dbPath", "org.sqlite.JDBC")
TransactionManager.manager.defaultIsolationLevel = TRANSACTION_SERIALIZABLE TransactionManager.manager.defaultIsolationLevel = TRANSACTION_SERIALIZABLE
transaction { transaction {

View file

@ -68,4 +68,5 @@ object DbPost : Table() {
val content = text("content") val content = text("content")
val url = varchar("url", 64).uniqueIndex() val url = varchar("url", 64).uniqueIndex()
val pinned = bool("pinned")
} }

View file

@ -22,8 +22,9 @@ object PostRepository : Repository<Post> {
val name = row[DbPost.name] val name = row[DbPost.name]
val content = row[DbPost.content] val content = row[DbPost.content]
val url = row[DbPost.url] val url = row[DbPost.url]
val pinned = row[DbPost.pinned]
return Post(id, name, content, url) return Post(id, name, content, url, pinned)
} }
override suspend fun get(id: Long): Post? { override suspend fun get(id: Long): Post? {
@ -33,11 +34,24 @@ object PostRepository : Repository<Post> {
} }
override suspend fun create(model: Post): Long { override suspend fun create(model: Post): Long {
if (model.pinned) {
for (it in getPinned()) {
update(it.copy(pinned = false))
}
}
return dbQuery { return dbQuery {
if (model.pinned) {
DbPost.update({ DbPost.pinned eq true }) {
it[pinned] = false
}
}
val id = DbPost.insert { val id = DbPost.insert {
it[name] = model.name it[name] = model.name
it[content] = model.content it[content] = model.content
it[url] = model.url it[url] = model.url
it[pinned] = model.pinned
}[DbPost.id] ?: throw IllegalStateException("Cannot create model!") }[DbPost.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id) onCreate.emit(id)
@ -48,11 +62,21 @@ object PostRepository : Repository<Post> {
override suspend fun update(model: Post) { override suspend fun update(model: Post) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
if (model.pinned) {
for (it in getPinned()) {
if (it.id != model.id) {
update(it.copy(pinned = false))
}
}
}
dbQuery { dbQuery {
DbPost.update({ DbPost.id eq model.id }) { DbPost.update({ DbPost.id eq model.id }) {
it[name] = model.name it[name] = model.name
it[content] = model.content it[content] = model.content
it[url] = model.url it[url] = model.url
it[pinned] = model.pinned
} }
onUpdate.emit(model.id) onUpdate.emit(model.id)
@ -69,7 +93,10 @@ object PostRepository : Repository<Post> {
override suspend fun all(): List<Post> { override suspend fun all(): List<Post> {
return dbQuery { return dbQuery {
val result = DbPost.selectAll() val result = DbPost.selectAll().orderBy(
DbPost.pinned to SortOrder.ASC,
DbPost.id to SortOrder.ASC
)
result.map(this::rowToModel) result.map(this::rowToModel)
} }
@ -79,14 +106,20 @@ object PostRepository : Repository<Post> {
return dbQuery { return dbQuery {
val result = DbPost.select { DbPost.url eq url } val result = DbPost.select { DbPost.url eq url }
runBlocking { result.firstOrNull()?.let {
result.firstOrNull()?.let { rowToModel(it)
rowToModel(it)
}
} }
} }
} }
suspend fun getPinned(): List<Post> {
return dbQuery {
val result = DbPost.select { DbPost.pinned eq true }
result.map(this::rowToModel)
}
}
fun registerPushService() { fun registerPushService() {
onCreate { onCreate {
runBlocking { runBlocking {

View file

@ -27,6 +27,8 @@ fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: Str
} }
div(classes) { div(classes) {
attributes["data-id"] = post.id.toString() attributes["data-id"] = post.id.toString()
attributes["data-pinned"] = post.pinned.toString()
a("/p/${post.url}", classes = "post-name") { a("/p/${post.url}", classes = "post-name") {
+post.name +post.name
} }
@ -58,37 +60,16 @@ fun Route.overview() {
content { content {
div("overview") { div("overview") {
div("overview-main") { div("overview-main") {
if (editable) {
div("overview-new") {
a("post/new", classes = "form-btn") {
+"New"
}
}
}
for (post in postList) { for (post in postList) {
createPost(post, editable, "overview-post") createPost(post, editable, "overview-post")
} }
} }
div("overview-side") { div("overview-side") {
div("overview-shortcuts") { if (editable) {
a { div("overview-new") {
+"Wiki" a("post/new", classes = "form-btn") {
} +"New"
a { }
+"Wiki"
}
a {
+"Wiki"
}
a {
+"Wiki"
}
a {
+"Wiki"
}
a {
+"Wiki"
} }
} }
div("overview-twitter") { div("overview-twitter") {
@ -124,7 +105,6 @@ fun Route.overview() {
} }
} }
get("/post/{id}") { get("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) { user ->
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
@ -153,6 +133,7 @@ fun Route.overview() {
value = editPost.name value = editPost.name
} }
} }
div("form-group") { div("form-group") {
label { label {
htmlFor = "url" htmlFor = "url"
@ -162,12 +143,29 @@ fun Route.overview() {
name = "url", name = "url",
classes = "form-control" classes = "form-control"
) { ) {
id = "places" id = "url"
placeholder = "Places" placeholder = "Url"
value = editPost.url value = editPost.url
} }
} }
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "pinned",
classes = "form-control",
type = InputType.checkBox
) {
id = "pinned"
checked = editPost.pinned
}
label {
htmlFor = "pinned"
+"Pinned"
}
}
}
div("form-group") { div("form-group") {
label { label {
htmlFor = "content" htmlFor = "content"
@ -218,6 +216,7 @@ fun Route.overview() {
params["name"]?.let { post = post.copy(name = it) } params["name"]?.let { post = post.copy(name = it) }
params["url"]?.let { post = post.copy(url = it) } params["url"]?.let { post = post.copy(url = it) }
params["content"]?.let { post = post.copy(content = it) } params["content"]?.let { post = post.copy(content = it) }
params["pinned"]?.let { post = post.copy(pinned = it == "on") }
PostRepository.update(post) PostRepository.update(post)
@ -260,12 +259,29 @@ fun Route.overview() {
name = "url", name = "url",
classes = "form-control" classes = "form-control"
) { ) {
id = "places" id = "url"
placeholder = "Places" placeholder = "Url"
value = Post.generateUrl() value = Post.generateUrl()
} }
} }
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "pinned",
classes = "form-control",
type = InputType.checkBox
) {
id = "pinned"
checked = false
}
label {
htmlFor = "pinned"
+"Pinned"
}
}
}
div("form-group") { div("form-group") {
label { label {
htmlFor = "content" htmlFor = "content"
@ -309,8 +325,9 @@ fun Route.overview() {
val name = params["name"] ?: return@post val name = params["name"] ?: return@post
val content = params["content"] ?: return@post val content = params["content"] ?: return@post
val url = params["url"] ?: return@post val url = params["url"] ?: return@post
val pinned = params["pinned"] == "on"
val post = Post(null, name, content, url) val post = Post(null, name, content, url, pinned)
PostRepository.create(post) PostRepository.create(post)

View file

@ -166,9 +166,7 @@ fun Route.room() {
+"Projector" +"Projector"
} }
} }
}
div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
name = "internet", name = "internet",
@ -183,9 +181,7 @@ fun Route.room() {
+"Internet" +"Internet"
} }
} }
}
div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
name = "whiteboard", name = "whiteboard",
@ -200,9 +196,7 @@ fun Route.room() {
+"Whiteboard" +"Whiteboard"
} }
} }
}
div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
name = "blackboard", name = "blackboard",
@ -217,9 +211,7 @@ fun Route.room() {
+"Blackboard" +"Blackboard"
} }
} }
}
div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
name = "accessible", name = "accessible",
@ -337,9 +329,7 @@ fun Route.room() {
+"Projector" +"Projector"
} }
} }
}
div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
name = "internet", name = "internet",
@ -354,9 +344,7 @@ fun Route.room() {
+"Internet" +"Internet"
} }
} }
}
div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
name = "whiteboard", name = "whiteboard",
@ -371,9 +359,7 @@ fun Route.room() {
+"Whiteboard" +"Whiteboard"
} }
} }
}
div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
name = "blackboard", name = "blackboard",
@ -388,9 +374,7 @@ fun Route.room() {
+"Blackboard" +"Blackboard"
} }
} }
}
div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
name = "accessible", name = "accessible",

View file

@ -0,0 +1,16 @@
[server]
host = "localhost"
port = 8080
[path]
web = "web"
sessions = "data/sessions"
uploads = "data/uploads"
database = "data/portal.db"
[schedule]
reference = "2019-03-27"
[security]
session_name = "SESSION"
sign_key = "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"