Add config
This commit is contained in:
parent
32596228fe
commit
997f374fe4
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@
|
||||||
build/
|
build/
|
||||||
web/
|
web/
|
||||||
.sessions/
|
.sessions/
|
||||||
|
data/
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
|
@ -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
6
portal.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[server]
|
||||||
|
host = "localhost"
|
||||||
|
port = 8080
|
||||||
|
|
||||||
|
[schedule]
|
||||||
|
reference = "2019-06-12"
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
89
src/jvmMain/kotlin/de/kif/backend/Configuration.kt
Normal file
89
src/jvmMain/kotlin/de/kif/backend/Configuration.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
16
src/jvmMain/resources/portal.toml
Normal file
16
src/jvmMain/resources/portal.toml
Normal 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"
|
Loading…
Reference in a new issue