diff --git a/.gitignore b/.gitignore index 60413a9..77c01ba 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ web/ *.swp *.swo +*.db diff --git a/build.gradle b/build.gradle index 38578cd..58e9f96 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,16 @@ def ktor_version = '1.1.2' def serialization_version = '0.10.0' kotlin { - jvm() + jvm() { + compilations.all { + kotlinOptions { + freeCompilerArgs += [ + "-Xuse-experimental=io.ktor.locations.KtorExperimentalLocationsAPI", + "-Xuse-experimental=io.ktor.util.KtorExperimentalAPI" + ] + } + } + } js() { compilations.all { kotlinOptions { @@ -62,11 +71,17 @@ kotlin { implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "io.ktor:ktor-auth:$ktor_version" - implementation "io.ktor:ktor-websockets:$ktor_version" + implementation "io.ktor:ktor-locations:$ktor_version" + //implementation "io.ktor:ktor-websockets:$ktor_version" implementation "io.ktor:ktor-html-builder:$ktor_version" - api 'io.github.microutils:kotlin-logging:1.5.4' + implementation 'org.xerial:sqlite-jdbc:3.25.2' + implementation 'org.jetbrains.exposed:exposed:0.12.2' + + implementation 'org.mindrot:jbcrypt:0.4' + + api 'io.github.microutils:kotlin-logging:1.6.23' api 'ch.qos.logback:logback-classic:1.2.3' api 'org.fusesource.jansi:jansi:1.8' } @@ -98,6 +113,8 @@ sass { main { srcDir = file("$projectDir/src/jsMain/resources/style") outDir = file("$buildDir/processedResources/js/main/style") + + exclude = "**/*.css" } } @@ -112,7 +129,10 @@ task populateWebFolder(dependsOn: [jsMainClasses, sass]) { from kotlin.sourceSets.jsMain.resources.srcDirs jsCompilations.test.runtimeDependencyFiles.each { if (it.exists() && !it.isDirectory()) { - from zipTree(it.absolutePath).matching { include '*.js' } + from zipTree(it.absolutePath).matching { + include '*.js' + exclude '*.meta.js' + } } } into webFolder @@ -142,11 +162,13 @@ clean.doFirst { task jar(type: ShadowJar, dependsOn: [jvmMainClasses, jsMainClasses, sass]) { from kotlin.targets.jvm.compilations.main.output - from (kotlin.targets.js.compilations.main.output) { + from(kotlin.targets.js.compilations.main.output) { into "web" + exclude '*.meta.js' } - from (kotlin.sourceSets.jsMain.resources.srcDirs) { + from(kotlin.sourceSets.jsMain.resources.srcDirs) { into "web" + exclude '*.meta.js' } configurations = [kotlin.targets.jvm.compilations.main.compileDependencyFiles] diff --git a/src/jsMain/kotlin/de/kif/frontend/calendar/Calendar.kt b/src/jsMain/kotlin/de/kif/frontend/calendar/Calendar.kt new file mode 100644 index 0000000..3a3f790 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/calendar/Calendar.kt @@ -0,0 +1,11 @@ +package de.kif.frontend.calendar + +import de.westermann.kwebview.View +import de.westermann.kwebview.ViewCollection +import de.westermann.kwebview.createHtmlView + +class Calendar : ViewCollection(createHtmlView()) { + init { + + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt index d881a6e..cb72995 100644 --- a/src/jsMain/kotlin/de/kif/frontend/main.kt +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -1,9 +1,19 @@ package de.kif.frontend +import de.kif.frontend.calendar.Calendar +import de.westermann.kwebview.components.boxView import de.westermann.kwebview.components.h1 import de.westermann.kwebview.components.init fun main() = init { clear() h1("Test") + boxView { + style { + width = "600px" + height = "400px" + margin = "10px" + } + +Calendar() + } } diff --git a/src/jsMain/resources/external/font/Montserrat-Black.eot b/src/jsMain/resources/external/font/Montserrat-Black.eot new file mode 100644 index 0000000..3a5362d Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Black.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-Black.woff b/src/jsMain/resources/external/font/Montserrat-Black.woff new file mode 100644 index 0000000..5c60ad0 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Black.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-Black.woff2 b/src/jsMain/resources/external/font/Montserrat-Black.woff2 new file mode 100644 index 0000000..3ac8d46 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Black.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-BlackItalic.eot b/src/jsMain/resources/external/font/Montserrat-BlackItalic.eot new file mode 100644 index 0000000..4b92a25 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-BlackItalic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-BlackItalic.woff b/src/jsMain/resources/external/font/Montserrat-BlackItalic.woff new file mode 100644 index 0000000..0caeeb5 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-BlackItalic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-BlackItalic.woff2 b/src/jsMain/resources/external/font/Montserrat-BlackItalic.woff2 new file mode 100644 index 0000000..1e5d135 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-BlackItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-Bold.eot b/src/jsMain/resources/external/font/Montserrat-Bold.eot new file mode 100644 index 0000000..5ab4fd5 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Bold.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-Bold.woff b/src/jsMain/resources/external/font/Montserrat-Bold.woff new file mode 100644 index 0000000..aad827f Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Bold.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-Bold.woff2 b/src/jsMain/resources/external/font/Montserrat-Bold.woff2 new file mode 100644 index 0000000..ad25d26 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Bold.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-BoldItalic.eot b/src/jsMain/resources/external/font/Montserrat-BoldItalic.eot new file mode 100644 index 0000000..5eeff1e Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-BoldItalic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-BoldItalic.woff b/src/jsMain/resources/external/font/Montserrat-BoldItalic.woff new file mode 100644 index 0000000..875f5b5 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-BoldItalic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-BoldItalic.woff2 b/src/jsMain/resources/external/font/Montserrat-BoldItalic.woff2 new file mode 100644 index 0000000..f9a3d40 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-BoldItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraBold.eot b/src/jsMain/resources/external/font/Montserrat-ExtraBold.eot new file mode 100644 index 0000000..203ed8e Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraBold.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraBold.woff b/src/jsMain/resources/external/font/Montserrat-ExtraBold.woff new file mode 100644 index 0000000..08c7e2e Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraBold.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraBold.woff2 b/src/jsMain/resources/external/font/Montserrat-ExtraBold.woff2 new file mode 100644 index 0000000..e961597 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraBold.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.eot b/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.eot new file mode 100644 index 0000000..329171b Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.woff b/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.woff new file mode 100644 index 0000000..935251b Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.woff2 b/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..97b0e28 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraBoldItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraLight.eot b/src/jsMain/resources/external/font/Montserrat-ExtraLight.eot new file mode 100644 index 0000000..1a9012b Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraLight.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraLight.woff b/src/jsMain/resources/external/font/Montserrat-ExtraLight.woff new file mode 100644 index 0000000..a9a848e Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraLight.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraLight.woff2 b/src/jsMain/resources/external/font/Montserrat-ExtraLight.woff2 new file mode 100644 index 0000000..5fee343 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraLight.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.eot b/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.eot new file mode 100644 index 0000000..0c5369b Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.woff b/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.woff new file mode 100644 index 0000000..112038e Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.woff2 b/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.woff2 new file mode 100644 index 0000000..959479b Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ExtraLightItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-Italic.eot b/src/jsMain/resources/external/font/Montserrat-Italic.eot new file mode 100644 index 0000000..867a104 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Italic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-Italic.woff b/src/jsMain/resources/external/font/Montserrat-Italic.woff new file mode 100644 index 0000000..1c15293 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Italic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-Italic.woff2 b/src/jsMain/resources/external/font/Montserrat-Italic.woff2 new file mode 100644 index 0000000..8ccfb98 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Italic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-Light.eot b/src/jsMain/resources/external/font/Montserrat-Light.eot new file mode 100644 index 0000000..62d678e Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Light.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-Light.woff b/src/jsMain/resources/external/font/Montserrat-Light.woff new file mode 100644 index 0000000..7aa52b2 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Light.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-Light.woff2 b/src/jsMain/resources/external/font/Montserrat-Light.woff2 new file mode 100644 index 0000000..0b253c4 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Light.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-LightItalic.eot b/src/jsMain/resources/external/font/Montserrat-LightItalic.eot new file mode 100644 index 0000000..9e70f97 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-LightItalic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-LightItalic.woff b/src/jsMain/resources/external/font/Montserrat-LightItalic.woff new file mode 100644 index 0000000..ef12fe2 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-LightItalic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-LightItalic.woff2 b/src/jsMain/resources/external/font/Montserrat-LightItalic.woff2 new file mode 100644 index 0000000..c4cc5a3 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-LightItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-Medium.eot b/src/jsMain/resources/external/font/Montserrat-Medium.eot new file mode 100644 index 0000000..abb0a84 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Medium.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-Medium.woff b/src/jsMain/resources/external/font/Montserrat-Medium.woff new file mode 100644 index 0000000..2778c8b Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Medium.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-Medium.woff2 b/src/jsMain/resources/external/font/Montserrat-Medium.woff2 new file mode 100644 index 0000000..80d6f58 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Medium.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-MediumItalic.eot b/src/jsMain/resources/external/font/Montserrat-MediumItalic.eot new file mode 100644 index 0000000..d7986e9 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-MediumItalic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-MediumItalic.woff b/src/jsMain/resources/external/font/Montserrat-MediumItalic.woff new file mode 100644 index 0000000..7a2329f Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-MediumItalic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-MediumItalic.woff2 b/src/jsMain/resources/external/font/Montserrat-MediumItalic.woff2 new file mode 100644 index 0000000..a7f9cfd Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-MediumItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-Regular.eot b/src/jsMain/resources/external/font/Montserrat-Regular.eot new file mode 100644 index 0000000..d030e7f Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Regular.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-Regular.woff b/src/jsMain/resources/external/font/Montserrat-Regular.woff new file mode 100644 index 0000000..ebb48a9 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Regular.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-Regular.woff2 b/src/jsMain/resources/external/font/Montserrat-Regular.woff2 new file mode 100644 index 0000000..3261a6a Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Regular.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-SemiBold.eot b/src/jsMain/resources/external/font/Montserrat-SemiBold.eot new file mode 100644 index 0000000..92bdd9b Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-SemiBold.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-SemiBold.woff b/src/jsMain/resources/external/font/Montserrat-SemiBold.woff new file mode 100644 index 0000000..32904f5 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-SemiBold.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-SemiBold.woff2 b/src/jsMain/resources/external/font/Montserrat-SemiBold.woff2 new file mode 100644 index 0000000..0c9bc28 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-SemiBold.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.eot b/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.eot new file mode 100644 index 0000000..9caec26 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.woff b/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.woff new file mode 100644 index 0000000..d990b52 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.woff2 b/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.woff2 new file mode 100644 index 0000000..4d597b3 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-SemiBoldItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-Thin.eot b/src/jsMain/resources/external/font/Montserrat-Thin.eot new file mode 100644 index 0000000..125b165 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Thin.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-Thin.woff b/src/jsMain/resources/external/font/Montserrat-Thin.woff new file mode 100644 index 0000000..43cffac Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Thin.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-Thin.woff2 b/src/jsMain/resources/external/font/Montserrat-Thin.woff2 new file mode 100644 index 0000000..ef8d0d5 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-Thin.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat-ThinItalic.eot b/src/jsMain/resources/external/font/Montserrat-ThinItalic.eot new file mode 100644 index 0000000..a53bd66 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ThinItalic.eot differ diff --git a/src/jsMain/resources/external/font/Montserrat-ThinItalic.woff b/src/jsMain/resources/external/font/Montserrat-ThinItalic.woff new file mode 100644 index 0000000..dd4a314 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ThinItalic.woff differ diff --git a/src/jsMain/resources/external/font/Montserrat-ThinItalic.woff2 b/src/jsMain/resources/external/font/Montserrat-ThinItalic.woff2 new file mode 100644 index 0000000..e5e9367 Binary files /dev/null and b/src/jsMain/resources/external/font/Montserrat-ThinItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/Montserrat.css b/src/jsMain/resources/external/font/Montserrat.css new file mode 100644 index 0000000..a52ef06 --- /dev/null +++ b/src/jsMain/resources/external/font/Montserrat.css @@ -0,0 +1,399 @@ +/** =================== MONTSERRAT =================== **/ + +/** Montserrat Thin **/ +@font-face { + font-family: "Montserrat"; + font-weight: 100; + font-style: normal; + src: url("Montserrat-Thin.eot"); + src: url("Montserrat-Thin.eot?#iefix") format('embedded-opentype'), + url("Montserrat-Thin.woff2") format("woff2"), + url("Montserrat-Thin.woff") format("woff"); +} + +/** Montserrat Thin-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 100; + font-style: italic; + src: url("Montserrat-ThinItalic.eot"); + src: url("Montserrat-ThinItalic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-ThinItalic.woff2") format("woff2"), + url("Montserrat-ThinItalic.woff") format("woff"); +} + +/** Montserrat ExtraLight **/ +@font-face { + font-family: "Montserrat"; + font-weight: 200; + font-style: normal; + src: url("Montserrat-ExtraLight.eot"); + src: url("Montserrat-ExtraLight.eot?#iefix") format('embedded-opentype'), + url("Montserrat-ExtraLight.woff2") format("woff2"), + url("Montserrat-ExtraLight.woff") format("woff"); +} + +/** Montserrat ExtraLight-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 200; + font-style: italic; + src: url("Montserrat-ExtraLightItalic.eot"); + src: url("Montserrat-ExtraLightItalic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-ExtraLightItalic.woff2") format("woff2"), + url("Montserrat-ExtraLightItalic.woff") format("woff"); +} + +/** Montserrat Light **/ +@font-face { + font-family: "Montserrat"; + font-weight: 300; + font-style: normal; + src: url("Montserrat-Light.eot"); + src: url("Montserrat-Light.eot?#iefix") format('embedded-opentype'), + url("Montserrat-Light.woff2") format("woff2"), + url("Montserrat-Light.woff") format("woff"); +} + +/** Montserrat Light-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 300; + font-style: italic; + src: url("Montserrat-LightItalic.eot"); + src: url("Montserrat-LightItalic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-LightItalic.woff2") format("woff2"), + url("Montserrat-LightItalic.woff") format("woff"); +} + +/** Montserrat Regular **/ +@font-face { + font-family: "Montserrat"; + font-weight: 400; + font-style: normal; + src: url("Montserrat-Regular.eot"); + src: url("Montserrat-Regular.eot?#iefix") format('embedded-opentype'), + url("Montserrat-Regular.woff2") format("woff2"), + url("Montserrat-Regular.woff") format("woff"); +} + +/** Montserrat Regular-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 400; + font-style: italic; + src: url("Montserrat-Italic.eot"); + src: url("Montserrat-Italic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-Italic.woff2") format("woff2"), + url("Montserrat-Italic.woff") format("woff"); +} + +/** Montserrat Medium **/ +@font-face { + font-family: "Montserrat"; + font-weight: 500; + font-style: normal; + src: url("Montserrat-Medium.eot"); + src: url("Montserrat-Medium.eot?#iefix") format('embedded-opentype'), + url("Montserrat-Medium.woff2") format("woff2"), + url("Montserrat-Medium.woff") format("woff"); +} + +/** Montserrat Medium-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 500; + font-style: italic; + src: url("Montserrat-MediumItalic.eot"); + src: url("Montserrat-MediumItalic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-MediumItalic.woff2") format("woff2"), + url("Montserrat-MediumItalic.woff") format("woff"); +} + +/** Montserrat SemiBold **/ +@font-face { + font-family: "Montserrat"; + font-weight: 600; + font-style: normal; + src: url("Montserrat-SemiBold.eot"); + src: url("Montserrat-SemiBold.eot?#iefix") format('embedded-opentype'), + url("Montserrat-SemiBold.woff2") format("woff2"), + url("Montserrat-SemiBold.woff") format("woff"); +} + +/** Montserrat SemiBold-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 600; + font-style: italic; + src: url("Montserrat-SemiBoldItalic.eot"); + src: url("Montserrat-SemiBoldItalic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-SemiBoldItalic.woff2") format("woff2"), + url("Montserrat-SemiBoldItalic.woff") format("woff"); +} + +/** Montserrat Bold **/ +@font-face { + font-family: "Montserrat"; + font-weight: 700; + font-style: normal; + src: url("Montserrat-Bold.eot"); + src: url("Montserrat-Bold.eot?#iefix") format('embedded-opentype'), + url("Montserrat-Bold.woff2") format("woff2"), + url("Montserrat-Bold.woff") format("woff"); +} + +/** Montserrat Bold-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 700; + font-style: italic; + src: url("Montserrat-BoldItalic.eot"); + src: url("Montserrat-BoldItalic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-BoldItalic.woff2") format("woff2"), + url("Montserrat-BoldItalic.woff") format("woff"); +} + +/** Montserrat ExtraBold **/ +@font-face { + font-family: "Montserrat"; + font-weight: 800; + font-style: normal; + src: url("Montserrat-ExtraBold.eot"); + src: url("Montserrat-ExtraBold.eot?#iefix") format('embedded-opentype'), + url("Montserrat-ExtraBold.woff2") format("woff2"), + url("Montserrat-ExtraBold.woff") format("woff"); +} + +/** Montserrat ExtraBold-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 800; + font-style: italic; + src: url("Montserrat-ExtraBoldItalic.eot"); + src: url("Montserrat-ExtraBoldItalic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-ExtraBoldItalic.woff2") format("woff2"), + url("Montserrat-ExtraBoldItalic.woff") format("woff"); +} + +/** Montserrat Black **/ +@font-face { + font-family: "Montserrat"; + font-weight: 900; + font-style: normal; + src: url("Montserrat-Black.eot"); + src: url("Montserrat-Black.eot?#iefix") format('embedded-opentype'), + url("Montserrat-Black.woff2") format("woff2"), + url("Montserrat-Black.woff") format("woff"); +} + +/** Montserrat Black-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 900; + font-style: italic; + src: url("Montserrat-BlackItalic.eot"); + src: url("Montserrat-BlackItalic.eot?#iefix") format('embedded-opentype'), + url("Montserrat-BlackItalic.woff2") format("woff2"), + url("Montserrat-BlackItalic.woff") format("woff"); +} + +/** =================== MONTSERRAT ALTERNATES =================== **/ + +/** Montserrat Alternates Thin **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 100; + font-style: normal; + src: url("MontserratAlternates-Thin.eot"); + src: url("MontserratAlternates-Thin.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-Thin.woff2") format("woff2"), + url("MontserratAlternates-Thin.woff") format("woff"); +} + +/** Montserrat Alternates Thin-Italic **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 100; + font-style: italic; + src: url("MontserratAlternates-ThinItalic.eot"); + src: url("MontserratAlternates-ThinItalic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-ThinItalic.woff2") format("woff2"), + url("MontserratAlternates-ThinItalic.woff") format("woff"); +} + +/** Montserrat Alternates ExtraLight **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 200; + font-style: normal; + src: url("MontserratAlternates-ExtraLight.eot"); + src: url("MontserratAlternates-ExtraLight.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-ExtraLight.woff2") format("woff2"), + url("MontserratAlternates-ExtraLight.woff") format("woff"); +} + +/** Montserrat Alternates ExtraLight-Italic **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 200; + font-style: italic; + src: url("MontserratAlternates-ExtraLightItalic.eot"); + src: url("MontserratAlternates-ExtraLightItalic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-ExtraLightItalic.woff2") format("woff2"), + url("MontserratAlternates-ExtraLightItalic.woff") format("woff"); +} + +/** Montserrat Alternates Light **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 300; + font-style: normal; + src: url("MontserratAlternates-Light.eot"); + src: url("MontserratAlternates-Light.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-Light.woff2") format("woff2"), + url("MontserratAlternates-Light.woff") format("woff"); +} + +/** Montserrat Alternates Light-Italic **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 300; + font-style: italic; + src: url("MontserratAlternates-LightItalic.eot"); + src: url("MontserratAlternates-LightItalic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-LightItalic.woff2") format("woff2"), + url("MontserratAlternates-LightItalic.woff") format("woff"); +} + +/** Montserrat Alternates Regular **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 400; + font-style: normal; + src: url("MontserratAlternates-Regular.eot"); + src: url("MontserratAlternates-Regular.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-Regular.woff2") format("woff2"), + url("MontserratAlternates-Regular.woff") format("woff"); +} + +/** Montserrat Alternates Regular-Italic **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 400; + font-style: italic; + src: url("MontserratAlternates-Italic.eot"); + src: url("MontserratAlternates-Italic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-Italic.woff2") format("woff2"), + url("MontserratAlternates-Italic.woff") format("woff"); +} + +/** Montserrat Alternates Medium **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 500; + font-style: normal; + src: url("MontserratAlternates-Medium.eot"); + src: url("MontserratAlternates-Medium.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-Medium.woff2") format("woff2"), + url("MontserratAlternates-Medium.woff") format("woff"); +} + +/** Montserrat Alternates Medium-Italic **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 500; + font-style: italic; + src: url("MontserratAlternates-MediumItalic.eot"); + src: url("MontserratAlternates-MediumItalic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-MediumItalic.woff2") format("woff2"), + url("MontserratAlternates-MediumItalic.woff") format("woff"); +} + +/** Montserrat Alternates SemiBold **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 600; + font-style: normal; + src: url("MontserratAlternates-SemiBold.eot"); + src: url("MontserratAlternates-SemiBold.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-SemiBold.woff2") format("woff2"), + url("MontserratAlternates-SemiBold.woff") format("woff"); +} + +/** Montserrat Alternates SemiBold-Italic **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 600; + font-style: italic; + src: url("MontserratAlternates-SemiBoldItalic.eot"); + src: url("MontserratAlternates-SemiBoldItalic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-SemiBoldItalic.woff2") format("woff2"), + url("MontserratAlternates-SemiBoldItalic.woff") format("woff"); +} + +/** Montserrat Alternates Bold **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 700; + font-style: normal; + src: url("MontserratAlternates-Bold.eot"); + src: url("MontserratAlternates-Bold.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-Bold.woff2") format("woff2"), + url("MontserratAlternates-Bold.woff") format("woff"); +} + +/** Montserrat Alternates Bold-Italic **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 700; + font-style: italic; + src: url("MontserratAlternates-BoldItalic.eot"); + src: url("MontserratAlternates-BoldItalic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-BoldItalic.woff2") format("woff2"), + url("MontserratAlternates-BoldItalic.woff") format("woff"); +} + +/** Montserrat Alternates ExtraBold **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 800; + font-style: normal; + src: url("MontserratAlternates-ExtraBold.eot"); + src: url("MontserratAlternates-ExtraBold.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-ExtraBold.woff2") format("woff2"), + url("MontserratAlternates-ExtraBold.woff") format("woff"); +} + +/** Montserrat Alternates ExtraBold-Italic **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 800; + font-style: italic; + src: url("MontserratAlternates-ExtraBoldItalic.eot"); + src: url("MontserratAlternates-ExtraBoldItalic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-ExtraBoldItalic.woff2") format("woff2"), + url("MontserratAlternates-ExtraBoldItalic.woff") format("woff"); +} + +/** Montserrat Alternates Black **/ +@font-face { + font-family: "Montserrat Alternates"; + font-weight: 900; + font-style: normal; + src: url("MontserratAlternates-Black.eot"); + src: url("MontserratAlternates-Black.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-Black.woff2") format("woff2"), + url("MontserratAlternates-Black.woff") format("woff"); +} + +/** Montserrat Alternates Black-Italic **/ +@font-face { + font-family: "Montserrat"; + font-weight: 900; + font-style: italic; + src: url("MontserratAlternates-BlackItalic.eot"); + src: url("MontserratAlternates-BlackItalic.eot?#iefix") format('embedded-opentype'), + url("MontserratAlternates-BlackItalic.woff2") format("woff2"), + url("MontserratAlternates-BlackItalic.woff") format("woff"); +} \ No newline at end of file diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Black.eot b/src/jsMain/resources/external/font/MontserratAlternates-Black.eot new file mode 100644 index 0000000..fc23ac4 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Black.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Black.woff b/src/jsMain/resources/external/font/MontserratAlternates-Black.woff new file mode 100644 index 0000000..11a3ce6 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Black.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Black.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-Black.woff2 new file mode 100644 index 0000000..235a3c8 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Black.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.eot b/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.eot new file mode 100644 index 0000000..a965308 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.woff b/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.woff new file mode 100644 index 0000000..a2ddfd3 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.woff2 new file mode 100644 index 0000000..a8004a1 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-BlackItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Bold.eot b/src/jsMain/resources/external/font/MontserratAlternates-Bold.eot new file mode 100644 index 0000000..f17c416 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Bold.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Bold.woff b/src/jsMain/resources/external/font/MontserratAlternates-Bold.woff new file mode 100644 index 0000000..9fc7e5e Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Bold.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Bold.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-Bold.woff2 new file mode 100644 index 0000000..22ff691 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Bold.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.eot b/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.eot new file mode 100644 index 0000000..90cf559 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.woff b/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.woff new file mode 100644 index 0000000..e1483bd Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.woff2 new file mode 100644 index 0000000..bed052a Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-BoldItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.eot b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.eot new file mode 100644 index 0000000..3b1d443 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.woff b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.woff new file mode 100644 index 0000000..f6a794d Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.woff2 new file mode 100644 index 0000000..e2cd944 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBold.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.eot b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.eot new file mode 100644 index 0000000..9af7c24 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.woff b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.woff new file mode 100644 index 0000000..1403e27 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..979839f Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraBoldItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.eot b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.eot new file mode 100644 index 0000000..345c60d Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.woff b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.woff new file mode 100644 index 0000000..6c47d58 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.woff2 new file mode 100644 index 0000000..9fe8bb2 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLight.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.eot b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.eot new file mode 100644 index 0000000..29b4c1f Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.woff b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.woff new file mode 100644 index 0000000..622cc9a Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.woff2 new file mode 100644 index 0000000..2632c08 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ExtraLightItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Italic.eot b/src/jsMain/resources/external/font/MontserratAlternates-Italic.eot new file mode 100644 index 0000000..6e8c22e Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Italic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Italic.woff b/src/jsMain/resources/external/font/MontserratAlternates-Italic.woff new file mode 100644 index 0000000..65094d4 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Italic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Italic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-Italic.woff2 new file mode 100644 index 0000000..40944d4 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Italic.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Light.eot b/src/jsMain/resources/external/font/MontserratAlternates-Light.eot new file mode 100644 index 0000000..e999003 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Light.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Light.woff b/src/jsMain/resources/external/font/MontserratAlternates-Light.woff new file mode 100644 index 0000000..4a9a0d4 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Light.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Light.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-Light.woff2 new file mode 100644 index 0000000..c7ef715 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Light.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.eot b/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.eot new file mode 100644 index 0000000..fd1b318 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.woff b/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.woff new file mode 100644 index 0000000..22f432f Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.woff2 new file mode 100644 index 0000000..9f3b5d0 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-LightItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Medium.eot b/src/jsMain/resources/external/font/MontserratAlternates-Medium.eot new file mode 100644 index 0000000..4dd10f2 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Medium.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Medium.woff b/src/jsMain/resources/external/font/MontserratAlternates-Medium.woff new file mode 100644 index 0000000..92bd129 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Medium.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Medium.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-Medium.woff2 new file mode 100644 index 0000000..f8f0530 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Medium.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.eot b/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.eot new file mode 100644 index 0000000..9f33af7 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.woff b/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.woff new file mode 100644 index 0000000..aefdeee Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.woff2 new file mode 100644 index 0000000..9775f42 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-MediumItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Regular.eot b/src/jsMain/resources/external/font/MontserratAlternates-Regular.eot new file mode 100644 index 0000000..ad10b6b Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Regular.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Regular.woff b/src/jsMain/resources/external/font/MontserratAlternates-Regular.woff new file mode 100644 index 0000000..3aaf1f7 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Regular.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Regular.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-Regular.woff2 new file mode 100644 index 0000000..f7d23ca Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Regular.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.eot b/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.eot new file mode 100644 index 0000000..13a121d Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.woff b/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.woff new file mode 100644 index 0000000..35ba984 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.woff2 new file mode 100644 index 0000000..80bf4c2 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-SemiBold.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.eot b/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.eot new file mode 100644 index 0000000..799cae5 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.woff b/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.woff new file mode 100644 index 0000000..39283e1 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.woff2 new file mode 100644 index 0000000..0bb26ae Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-SemiBoldItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Thin.eot b/src/jsMain/resources/external/font/MontserratAlternates-Thin.eot new file mode 100644 index 0000000..d49ba52 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Thin.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Thin.woff b/src/jsMain/resources/external/font/MontserratAlternates-Thin.woff new file mode 100644 index 0000000..9f031c2 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Thin.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-Thin.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-Thin.woff2 new file mode 100644 index 0000000..23a80f9 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-Thin.woff2 differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.eot b/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.eot new file mode 100644 index 0000000..2fcd16d Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.eot differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.woff b/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.woff new file mode 100644 index 0000000..c601966 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.woff differ diff --git a/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.woff2 b/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.woff2 new file mode 100644 index 0000000..d98a952 Binary files /dev/null and b/src/jsMain/resources/external/font/MontserratAlternates-ThinItalic.woff2 differ diff --git a/src/jsMain/resources/external/font/README.MD b/src/jsMain/resources/external/font/README.MD new file mode 100644 index 0000000..811ad40 --- /dev/null +++ b/src/jsMain/resources/external/font/README.MD @@ -0,0 +1,35 @@ +# The Montserrat Font Project +To use this font as a webfont, ```Montserrat.css``` is included. + +## How to use +### 1. @import +You can import the file into your stylesheet as follows: +```css +@import url("static/fonts/Montserrat/fonts/webfonts/Montserrat.css"); +``` + +**NOTE:** The directory where the stylesheet is placed. + +Then we can use it to style elements: +```css +body { + font-family: 'Montserrat', sans-serif; + font-weight: 400; +} +``` + +### 2. \ing a stylesheet +Similarly, you could link to the same asset as you would any other CSS filter, in the \ of the HTML document rather than in the CSS: +```html + +``` + +**NOTE:** The directory where the stylesheet is placed. + +Then we can use it to style elements: +```css +body { + font-family: 'Montserrat', sans-serif; + font-weight: 400; +} +``` \ No newline at end of file diff --git a/src/jsMain/resources/external/material-icons.css b/src/jsMain/resources/external/material-icons.css new file mode 100644 index 0000000..c134ba5 --- /dev/null +++ b/src/jsMain/resources/external/material-icons.css @@ -0,0 +1,24 @@ +/* fallback */ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), local('MaterialIcons-Regular'), url(material-icons.woff2) format('woff2'); +} + +.material-icons { + font-family: 'Material Icons' !important; + font-weight: normal; + font-style: normal; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -moz-font-feature-settings: 'liga'; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} diff --git a/src/jsMain/resources/external/material-icons.woff2 b/src/jsMain/resources/external/material-icons.woff2 new file mode 100644 index 0000000..e916217 Binary files /dev/null and b/src/jsMain/resources/external/material-icons.woff2 differ diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index 6edd8f2..62b3694 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -1,5 +1,337 @@ -$primary-background-color: yellow; +@mixin no-select() { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} -body { - background: $primary-background-color; +$background-primary-color: #fff; +$background-secondary-color: #fcfcfc; + +$text-primary-color: #333; + +$primary-color: #B11D33; +$primary-text-color: #fff; + +$error-color: #D55225; +$error-text-color: #fff; + +$border-color: #888; + +$transitionTime: 150ms; + +$bg-disabled-color: rgba($text-primary-color, .26); +$bg-enabled-color: rgba($primary-color, .5); +$lever-disabled-color: $background-primary-color; +$lever-enabled-color: $primary-color; + +body, html { + color: $text-primary-color; + background: $background-secondary-color; + + font-family: 'Montserrat', Roboto, Arial, sans-serif; + font-weight: 600; + + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +a { + text-decoration: none; + outline: none; + + &:hover { + text-decoration: none; + } +} + +.container { + width: 100%; + margin: 0 auto; +} + +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + box-sizing: border-box; +} + +@media (min-width: 576px) { + .container { + width: 540px; + } +} + +@media (min-width: 768px) { + .container { + width: 720px; + } +} + +@media (min-width: 992px) { + .container { + width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + width: 1140px; + } +} + +.menu { + background-color: $background-secondary-color; + color: $text-primary-color; + width: 100%; + clear: both; + height: 6rem; + line-height: 6rem; + + a { + padding: 0 1rem; + color: $text-primary-color; + height: 100%; + display: inline-block; + font-family: "Bungee", sans-serif; + font-weight: normal; + font-size: 1.1rem; + position: relative; + text-transform: uppercase; + + &::after { + content: ''; + display: block; + position: absolute; + left: 1rem; + right: 1rem; + bottom: 1.8em; + height: 0.25rem; + + background: transparent; + transition: background-color $transitionTime; + } + + &.active::after { + background: $text-primary-color; + } + + &:hover { + background-color: rgba($primary-text-color, 0.1); + + &::after { + background: $primary-color; + } + } + } + + & ~ * { + content: ''; + clear: both; + } +} + +.menu-left { + float: left; +} + +.menu-right { + float: right; +} + +.main { + padding: 0 1rem; +} + +.table-layout-search { + float: left; +} + +.table-layout-action { + float: right; +} + +.table-layout-table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + clear: both; + margin-top: 4rem; + + th { + text-align: start; + } + + th, td { + padding: 0 0.6rem; + } + + .action { + text-align: center; + } + + tr { + border-top: solid 1px rgba($text-primary-color, 0.1); + + &:first-child { + background-color: rgba($text-primary-color, 0.06); + height: 2.5rem; + line-height: 2.5rem; + } + + &:not(:first-child) { + height: 2rem; + line-height: 2rem; + + &:hover { + background-color: rgba($text-primary-color, 0.06); + } + } + } +} + +.form-control { + border: solid 1px $border-color; + outline: none; + padding: 0 1rem; + line-height: 2.5rem; + width: 100%; + background-color: $background-primary-color; + border-radius: 0.2rem; + margin: 1px; + transition: border-color $transitionTime; + + &:focus { + border-color: $primary-color; + border-width: 2px; + margin: 0; + } +} + +.form-group { + padding-bottom: 1rem; + display: block; + position: relative; + clear: both; + + label { + display: block; + padding-bottom: 0.3rem; + padding-left: 0.2rem; + } +} + +.form-switch { + line-height: 24px; + + input { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + z-index: 0; + } + + label { + display: block; + padding: 0 0 0 44px; + cursor: pointer; + + &:before { + content: ''; + position: absolute; + top: 5px; + left: 0; + width: 36px; + height: 14px; + background-color: $bg-disabled-color; + border-radius: 14px; + z-index: 1; + transition: background-color 0.28s cubic-bezier(.4, 0, .2, 1); + } + + &:after { + content: ''; + position: absolute; + top: 2px; + left: 0; + width: 20px; + height: 20px; + background-color: $lever-disabled-color; + border-radius: 14px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); + z-index: 2; + transition: all 0.28s cubic-bezier(.4, 0, .2, 1); + transition-property: left, background-color; + } + + @include no-select + } + + input:checked + label { + &:before { + background-color: $bg-enabled-color; + } + + &:after { + left: 16px; + background-color: $lever-enabled-color; + } + } +} + +.form-switch-group { + padding: 0.5rem 0; +} + +.form-btn { + border: solid 1px $border-color; + outline: none; + padding: 0 1rem; + line-height: 2rem; + background-color: $background-primary-color; + color: $primary-color; + + display: inline-block; + margin-right: 0.6rem; + + border-radius: 0.2rem; + font-weight: 600; + text-transform: uppercase; + font-size: 0.9rem; + + transition: border-color $transitionTime, outline-color $transitionTime; + + cursor: pointer; + + &:focus, &:hover { + border-color: $primary-color; + outline-color: $primary-color; + } +} + +.btn-primary { + background-color: $primary-color; + color: $primary-text-color; + border-color: $primary-color; +} +.btn-danger { + background-color: $error-color; + color: $error-text-color; + border-color: $error-color; +} + +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; +} + +form { + margin-bottom: 2rem; + width: 20rem; } \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/Application.kt b/src/jvmMain/kotlin/de/kif/backend/Application.kt new file mode 100644 index 0000000..75caf31 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/Application.kt @@ -0,0 +1,154 @@ +package de.kif.backend + +import de.kif.backend.database.Connection +import de.kif.backend.model.User +import de.kif.backend.route.* +import io.ktor.application.Application +import io.ktor.application.ApplicationCall +import io.ktor.application.install +import io.ktor.application.log +import io.ktor.auth.Authentication +import io.ktor.auth.FormAuthChallenge +import io.ktor.auth.UserPasswordCredential +import io.ktor.auth.form +import io.ktor.features.* +import io.ktor.http.content.files +import io.ktor.http.content.static +import io.ktor.locations.Location +import io.ktor.locations.Locations +import io.ktor.response.respondRedirect +import io.ktor.routing.routing +import io.ktor.sessions.* +import io.ktor.util.hex +import kotlinx.coroutines.launch + +data class PortalSession(val id: Int, val name: String) { + suspend fun getUser(call: ApplicationCall): User { + val user = User.find(name) + if (user == null || user.id != id) { + call.sessions.clear() + call.respondRedirect("/login?error") + throw IllegalAccessException() + } + return user + } +} + +@Location("/") +class LocationDashboard() + +@Location("/calendar") +class LocationCalendar() + +@Location("/login") +data class LocationLogin(val username: String = "", val password: String = "", val next: String = "/") + +@Location("/logout") +class LocationLogout() + +@Location("/account") +class LocationAccount() + +@Location("/user") +data class LocationUser(val search: String = "") { + @Location("/{id}") + data class Edit(val id: Int) + + @Location("/new") + class New() + + @Location("/{id}/delete") + data class Delete(val id: Int) +} + +@Location("/workgroup") +data class LocationWorkGroup(val search: String = "") { + @Location("/{id}") + data class Edit(val id: Int) + + @Location("/new") + class New() + + @Location("/{id}/delete") + data class Delete(val id: Int) +} + +@Location("/room") +data class LocationRoom(val search: String = "") { + @Location("/{id}") + data class Edit(val id: Int) + + @Location("/new") + class New() + + @Location("/{id}/delete") + data class Delete(val id: Int) +} + +@Location("/person") +data class LocationPerson(val search: String = "") { + @Location("/{id}") + data class Edit(val id: Int) + + @Location("/new") + class New() + + @Location("/{id}/delete") + data class Delete(val id: Int) +} + +fun Application.main() { + Connection.init() + + install(DefaultHeaders) + install(CallLogging) + install(ConditionalHeaders) + install(Compression) + install(DataConversion) + install(Locations) + + install(Authentication) { + form { + userParamName = LocationLogin::username.name + passwordParamName = LocationLogin::password.name + challenge = FormAuthChallenge.Redirect { _ -> + "/login?error" + } + validate { credential: UserPasswordCredential -> + val user = User.find(credential.name) ?: return@validate null + if (user.checkPassword(credential.password)) user else null + } + } + } + + val sessionKey = hex("1234567890abcdef") //TODO + install(Sessions) { + cookie("SESSION") { + transform(SessionTransportTransformerMessageAuthentication(sessionKey)) + } + } + + routing { + static("/static") { + files(Resources.directory) + } + + launch { + val firstStart = User.exists() + if (firstStart) { + log.info("Please create the first user and restart the server!") + setup() + } else { + dashboard() + calendar() + login() + account() + + workGroup() + room() + person() + user() + } + } + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/Main.kt b/src/jvmMain/kotlin/de/kif/backend/Main.kt index 6d2491f..3935099 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Main.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Main.kt @@ -1,97 +1,19 @@ package de.kif.backend -import io.ktor.application.call -import io.ktor.html.respondHtml -import io.ktor.http.content.files -import io.ktor.http.content.static -import io.ktor.routing.get -import io.ktor.routing.routing +import io.ktor.application.Application import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty -import kotlinx.html.* -import java.io.File -import java.nio.file.FileSystem -import java.nio.file.FileSystems -import java.nio.file.Files -import java.nio.file.Paths +import io.ktor.util.InternalAPI object Main { - private fun extractWebFolder(): File { - val destination = Files.createTempDirectory("web"); - - val classPath = "web" - val uri = this::class.java.classLoader.getResource(classPath).toURI() - val fileSystem: FileSystem? - val src = if (uri.scheme == "jar") { - fileSystem = FileSystems.newFileSystem(uri, mutableMapOf()) - fileSystem.getPath(classPath) - } else { - Paths.get(uri) - } - - Files.walk(src).forEach { - val name = it.toAbsolutePath().toString().let { - it.drop(classPath.length + it.indexOf(classPath, ignoreCase = true)).dropWhile { - it == '/' || it == '\\' - } - } - if (Files.isDirectory(it)) { - Files.createDirectories(destination.resolve(name)) - } else { - Files.copy(it, destination.resolve(name)) - } - } - - return destination.toFile() - } - - private fun getWebFolder(): File { - val currentDir = File(".").absoluteFile.canonicalFile - - return listOf( - "web", - "../web" - ).map { - File(currentDir, it) - }.firstOrNull { it.isDirectory }?.absoluteFile?.canonicalFile ?: extractWebFolder() - } - + @Suppress("UnusedMainParameter") @JvmStatic fun main(args: Array) { - embeddedServer(Netty, port = 8080, host = "127.0.0.1") { - val webDir = getWebFolder() - - environment.log.info("Web directory: $webDir") - - val modules = webDir.list().toList().filter { - it.endsWith(".js") && - !it.endsWith(".meta.js") && - !it.endsWith("-test.js") && - it != "require.min.js" - }.joinToString(", ") { "'${it.take(it.length - 3)}'" } - - routing { - get("/") { - call.respondHtml { - head { - title("KIF Portal") - - link(href = "/static/style/style.css", type = "text/css", rel = "stylesheet") - - script(src = "/static/require.min.js") {} - - script { - +"require.config({baseUrl: '/static'});\n" - +("require([$modules]);\n") - } - } - body {} - } - } - static("/static") { - files(webDir) - } - } - }.start(wait = true) + embeddedServer( + factory = Netty, + port = 8080, + host = "0.0.0.0", + module = Application::main + ).start(wait = true) } -} \ No newline at end of file +} diff --git a/src/jvmMain/kotlin/de/kif/backend/Resources.kt b/src/jvmMain/kotlin/de/kif/backend/Resources.kt new file mode 100644 index 0000000..ca720f8 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/Resources.kt @@ -0,0 +1,73 @@ +package de.kif.backend + +import mu.KotlinLogging +import java.io.File +import java.net.URI +import java.nio.file.* + +/** + * Manage static resource files of the ui + */ +object Resources { + + private val logger = KotlinLogging.logger {} + + /** + * Extract web directory from jar file. + * + * @return File that points the extracted web directory. + */ + private fun extractWebFolder(): File { + val destination: Path = Files.createTempDirectory("web") + + val classPath = "web" + val uri: URI = this::class.java.classLoader.getResource(classPath).toURI() + val fileSystem: FileSystem? + val src: Path = if (uri.scheme == "jar") { + fileSystem = FileSystems.newFileSystem(uri, mutableMapOf()) + fileSystem.getPath(classPath) + } else { + Paths.get(uri) + } + + Files.walk(src).forEach { path: Path -> + val name = path.toAbsolutePath().toString().let { name -> + name.drop(classPath.length + name.indexOf(classPath, ignoreCase = true)).dropWhile { + it == '/' || it == '\\' + } + } + if (Files.isDirectory(path)) { + Files.createDirectories(destination.resolve(name)) + } else { + Files.copy(path, destination.resolve(name)) + } + } + + return destination.toFile() + } + + /** + * 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. + */ + val jsModules = directory.list().toList().filter { + it.endsWith(".js") && + !it.endsWith(".meta.js") && + !it.endsWith("-test.js") && + it != "require.min.js" + }.joinToString(", ") { "'${it.take(it.length - 3)}'" } + +} diff --git a/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt b/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt new file mode 100644 index 0000000..c4ddb0c --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt @@ -0,0 +1,31 @@ +package de.kif.backend.database + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction +import java.sql.Connection.TRANSACTION_SERIALIZABLE + + +object Connection { + fun init() { + Database.connect("jdbc:sqlite:portal.db", "org.sqlite.JDBC") + TransactionManager.manager.defaultIsolationLevel = TRANSACTION_SERIALIZABLE + + transaction { + SchemaUtils.create( + DbPerson, DbPersonConstraint, + DbTrack, DbWorkGroup, DbWorkGroupConstraint, + DbLeader, DbWorkGroupOrder, + DbRoom, DbTimeSlot, DbSchedule, + DbUser, DbUserPermission + ) + } + } +} + +suspend fun dbQuery(block: () -> T): T = withContext(Dispatchers.IO) { + transaction { block() } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt b/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt new file mode 100644 index 0000000..9c07e59 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt @@ -0,0 +1,100 @@ +package de.kif.backend.database + +import de.kif.backend.model.Permission +import org.jetbrains.exposed.sql.Table + +object DbPerson : Table() { + val id = integer("id").autoIncrement().primaryKey() + val firstName = varchar("first_name", 64) + val lastName = varchar("last_name", 64) + + val arrival = long("arrival").nullable() + val departure = long("departure").nullable() +} + +object DbPersonConstraint : Table() { + val id = integer("id").autoIncrement().primaryKey() + val personId = integer("person_id") + + val type = enumeration("type", DbConstraintType::class) + val time = long("time") + val duration = integer("duration").default(0) + val day = integer("day") +} + +object DbTrack : Table() { + val id = integer("id").autoIncrement().primaryKey() + val name = varchar("name", 64) +} + +object DbWorkGroup : Table() { + val id = integer("id").autoIncrement().primaryKey() + val name = varchar("first_name", 64) + + val interested = integer("interested") + val trackId = integer("track_id").nullable() + val projector = bool("projector") + val resolution = bool("resolution") + + val length = integer("length") + + val start = long("start").nullable() + val end = long("end").nullable() +} + +object DbWorkGroupConstraint : Table() { + val id = integer("id").autoIncrement().primaryKey() + val workGroupId = integer("work_group_id") + + val type = enumeration("type", DbConstraintType::class) + val time = long("time") + val duration = integer("duration").default(0) + val day = integer("day") +} + +object DbLeader : Table() { + val workGroupId = integer("work_group_id").primaryKey(0) + val personId = integer("person_id").primaryKey(1) +} + +object DbWorkGroupOrder : Table() { + val beforeWorkGroupId = integer("before_work_group_id").primaryKey(0) + val afterWorkGroupId = integer("after_work_group_id").primaryKey(1) +} + +object DbRoom : Table() { + val id = integer("id").autoIncrement().primaryKey() + val name = varchar("name", 64) + + val places = integer("places") + val projector = bool("projector") +} + +object DbTimeSlot : Table() { + val id = integer("id").autoIncrement().primaryKey() + + val time = integer("time") + val duration = integer("duration") + val day = integer("day") +} + +object DbSchedule : Table() { + val workGroupId = integer("work_group_id").primaryKey(0) + val timeSlotId = integer("time_slot_id").primaryKey(1) + val roomId = integer("room_id").primaryKey(2) +} + +enum class DbConstraintType { + BEGIN, END, BLOCKED +} + +object DbUser : Table() { + val userId = integer("id").autoIncrement().primaryKey() + val username = varchar("username", 64).uniqueIndex() + val password = varchar("password", 64) +} + +object DbUserPermission : Table() { + val userId = integer("id").primaryKey(0) + val permission = enumeration("permission", Permission::class).primaryKey(1) +} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Day.kt b/src/jvmMain/kotlin/de/kif/backend/model/Day.kt new file mode 100644 index 0000000..9461387 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/Day.kt @@ -0,0 +1,4 @@ +package de.kif.backend.model + +class Day { +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Permission.kt b/src/jvmMain/kotlin/de/kif/backend/model/Permission.kt new file mode 100644 index 0000000..67b0ca6 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/Permission.kt @@ -0,0 +1,5 @@ +package de.kif.backend.model + +enum class Permission { + USER, SCHEDULE, WORK_GROUP, ROOM, PERSON, ADMIN +} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Person.kt b/src/jvmMain/kotlin/de/kif/backend/model/Person.kt new file mode 100644 index 0000000..efe6fd7 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/Person.kt @@ -0,0 +1,89 @@ +package de.kif.backend.model + +import de.kif.backend.database.DbPerson +import de.kif.backend.database.DbPersonConstraint +import de.kif.backend.database.dbQuery +import org.jetbrains.exposed.sql.* + +class Person( + var id: Int = -1, + var firstName: String = "", + var lastName: String = "", + var arrival: Long? = null, + var departure: Long? = null +) { + var constraints: Set = emptySet() + + suspend fun save() { + if (id < 0) { + dbQuery { + val newId = DbPerson.insert { + it[firstName] = this@Person.firstName + it[lastName] = this@Person.lastName + it[arrival] = this@Person.arrival + it[departure] = this@Person.departure + }[DbPerson.id]!! + this@Person.id = newId + } + for (constraint in constraints) { + constraint.save(this@Person.id) + } + } else { + dbQuery { + DbPerson.update({ DbPerson.id eq id }) { + it[firstName] = this@Person.firstName + it[lastName] = this@Person.lastName + it[arrival] = this@Person.arrival + it[departure] = this@Person.departure + } + + DbPersonConstraint.deleteWhere { DbPersonConstraint.personId eq id } + } + for (constraint in constraints) { + constraint.save(this@Person.id) + } + } + } + + suspend fun delete() { + val id = id + if (id >= 0) { + dbQuery { + DbPersonConstraint.deleteWhere { DbPersonConstraint.personId eq id } + DbPerson.deleteWhere { DbPerson.id eq id } + } + } + } + + suspend fun loadConstraints() { + if (id >= 0) { + constraints = PersonConstraint.get(id) + } + } + + companion object { + suspend fun get(personId: Int): Person? = dbQuery { + val result = DbPerson.select { DbPerson.id eq personId }.firstOrNull() ?: return@dbQuery null + Person( + result[DbPerson.id], + result[DbPerson.firstName], + result[DbPerson.lastName], + result[DbPerson.arrival], + result[DbPerson.departure] + ) + }?.apply { loadConstraints() } + + suspend fun list(): List = dbQuery { + val query = DbPerson.selectAll() + query.map { result -> + Person( + result[DbPerson.id], + result[DbPerson.firstName], + result[DbPerson.lastName], + result[DbPerson.arrival], + result[DbPerson.departure] + ) + } + }.onEach { it.loadConstraints() } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/PersonConstraint.kt b/src/jvmMain/kotlin/de/kif/backend/model/PersonConstraint.kt new file mode 100644 index 0000000..6754e89 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/PersonConstraint.kt @@ -0,0 +1,132 @@ +package de.kif.backend.model + +import de.kif.backend.database.DbConstraintType +import de.kif.backend.database.DbPersonConstraint +import de.kif.backend.database.dbQuery +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.update + +sealed class PersonConstraint( + var id: Int = -1 +) { + + abstract suspend fun save(personId: Int) + + suspend fun delete() { + val id = id + if (id >= 0) { + dbQuery { + DbPersonConstraint.deleteWhere { DbPersonConstraint.id eq id } + } + } + } + + class BeginOnDay( + var time: Long = 0, + var day: Int = 0 + ) : PersonConstraint() { + override suspend fun save(personId: Int) { + if (id < 0) { + dbQuery { + val newId = DbPersonConstraint.insert { + it[this@insert.personId] = personId + it[type] = DbConstraintType.BEGIN + it[time] = this@BeginOnDay.time + it[day] = this@BeginOnDay.day + }[DbPersonConstraint.id]!! + this@BeginOnDay.id = newId + } + } else { + dbQuery { + DbPersonConstraint.update({ DbPersonConstraint.id eq id }) { + it[this@update.personId] = personId + it[type] = DbConstraintType.BEGIN + it[time] = this@BeginOnDay.time + it[day] = this@BeginOnDay.day + } + } + } + } + } + + class EndOnDay( + var time: Long = 0, + var day: Int = 0 + ) : PersonConstraint() { + override suspend fun save(personId: Int) { + if (id < 0) { + dbQuery { + val newId = DbPersonConstraint.insert { + it[this@insert.personId] = personId + it[type] = DbConstraintType.END + it[time] = this@EndOnDay.time + it[day] = this@EndOnDay.day + }[DbPersonConstraint.id]!! + this@EndOnDay.id = newId + } + } else { + dbQuery { + DbPersonConstraint.update({ DbPersonConstraint.id eq id }) { + it[this@update.personId] = personId + it[type] = DbConstraintType.END + it[time] = this@EndOnDay.time + it[day] = this@EndOnDay.day + } + } + } + } + } + + class BlockedOnDay( + var time: Long = 0, + var duration: Int = 0, + var day: Int = 0 + ) : PersonConstraint() { + + override suspend fun save(personId: Int) { + if (id < 0) { + dbQuery { + val newId = DbPersonConstraint.insert { + it[this@insert.personId] = personId + it[type] = DbConstraintType.BLOCKED + it[time] = this@BlockedOnDay.time + it[duration] = this@BlockedOnDay.duration + it[day] = this@BlockedOnDay.day + }[DbPersonConstraint.id]!! + this@BlockedOnDay.id = newId + } + } else { + dbQuery { + DbPersonConstraint.update({ DbPersonConstraint.id eq id }) { + it[this@update.personId] = personId + it[type] = DbConstraintType.BLOCKED + it[time] = this@BlockedOnDay.time + it[duration] = this@BlockedOnDay.duration + it[day] = this@BlockedOnDay.day + } + } + } + } + } + + companion object { + suspend fun get(personId: Int): Set = dbQuery { + val result = DbPersonConstraint.select { DbPersonConstraint.personId eq personId } + result.map { + val id = it[DbPersonConstraint.id] + val type = it[DbPersonConstraint.type] + val time = it[DbPersonConstraint.time] + val duration = it[DbPersonConstraint.duration] + val day = it[DbPersonConstraint.day] + + when (type) { + DbConstraintType.BEGIN -> PersonConstraint.BeginOnDay(time, day) + DbConstraintType.END -> PersonConstraint.EndOnDay(time, day) + DbConstraintType.BLOCKED -> PersonConstraint.BlockedOnDay(time, duration, day) + }.also { it.id = id } + }.toSet() + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/Room.kt b/src/jvmMain/kotlin/de/kif/backend/model/Room.kt new file mode 100644 index 0000000..3d9ced9 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/Room.kt @@ -0,0 +1,66 @@ +package de.kif.backend.model + +import de.kif.backend.database.DbRoom +import de.kif.backend.database.dbQuery +import org.jetbrains.exposed.sql.* + +class Room( + var id: Int = -1, + var name: String = "", + var places: Int = 0, + var projector: Boolean = false +) { + suspend fun save() { + if (id < 0) { + dbQuery { + val newId = DbRoom.insert { + it[name] = this@Room.name + it[places] = this@Room.places + it[projector] = this@Room.projector + }[DbRoom.id]!! + this@Room.id = newId + } + } else { + dbQuery { + DbRoom.update({ DbRoom.id eq id }) { + it[name] = this@Room.name + it[places] = this@Room.places + it[projector] = this@Room.projector + } + } + } + } + + suspend fun delete() { + val id = id + if (id >= 0) { + dbQuery { + DbRoom.deleteWhere { DbRoom.id eq id } + } + } + } + + companion object { + suspend fun get(roomId: Int): Room? = dbQuery { + val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: return@dbQuery null + Room( + result[DbRoom.id], + result[DbRoom.name], + result[DbRoom.places], + result[DbRoom.projector] + ) + } + + suspend fun list(): List = dbQuery { + val query = DbRoom.selectAll() + query.map { result -> + Room( + result[DbRoom.id], + result[DbRoom.name], + result[DbRoom.places], + result[DbRoom.projector] + ) + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/TimeSlot.kt b/src/jvmMain/kotlin/de/kif/backend/model/TimeSlot.kt new file mode 100644 index 0000000..37122f2 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/TimeSlot.kt @@ -0,0 +1,4 @@ +package de.kif.backend.model + +class TimeSlot { +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/model/User.kt b/src/jvmMain/kotlin/de/kif/backend/model/User.kt new file mode 100644 index 0000000..85aee20 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/User.kt @@ -0,0 +1,110 @@ +package de.kif.backend.model + +import de.kif.backend.database.DbUser +import de.kif.backend.database.DbUserPermission +import de.kif.backend.database.dbQuery +import io.ktor.auth.Principal +import org.jetbrains.exposed.sql.* +import org.mindrot.jbcrypt.BCrypt + +class User( + var id: Int = -1, + var username: String = "", + private var password: String = "" +) : Principal { + var permissions: Set = emptySet() + + fun checkPassword(password: String): Boolean { + return BCrypt.checkpw(password, this.password) + } + + fun hashPassword(newPassword: String) { + password = BCrypt.hashpw(newPassword, BCrypt.gensalt()) + } + + fun checkPermission(permission: Permission): Boolean { + return permission in permissions || Permission.ADMIN in permissions + } + + suspend fun loadPermissions() = dbQuery { + permissions = DbUserPermission.slice(DbUserPermission.permission).select { + DbUserPermission.userId eq id + }.map { it[DbUserPermission.permission] }.toSet() + } + + suspend fun save() { + if (id < 0) { + dbQuery { + val newId = DbUser.insert { + it[username] = this@User.username + it[password] = this@User.password + }[DbUser.userId]!! + this@User.id = newId + + for (permission in permissions) { + DbUserPermission.insert { + it[userId] = newId + it[this.permission] = permission + } + } + } + } else { + dbQuery { + DbUser.update({ DbUser.userId eq id }) { + it[username] = this@User.username + it[password] = this@User.password + } + + DbUserPermission.deleteWhere { DbUserPermission.userId eq id } + + for (permission in permissions) { + DbUserPermission.insert { + it[userId] = id + it[this.permission] = permission + } + } + } + } + } + + suspend fun delete() { + val id = id + if (id >= 0) { + dbQuery { + DbUserPermission.deleteWhere { DbUserPermission.userId eq id } + DbUser.deleteWhere { DbUser.userId eq id } + } + } + } + + companion object { + suspend fun create(username: String, password: String, permissions: Set) { + val user = User(username = username) + user.hashPassword(password) + user.permissions = permissions + user.save() + } + + suspend fun find(username: String): User? = dbQuery { + val result = DbUser.select { DbUser.username eq username }.firstOrNull() ?: return@dbQuery null + User(result[DbUser.userId], result[DbUser.username], result[DbUser.password]) + + }?.apply { loadPermissions() } + + suspend fun get(userId: Int): User? = dbQuery { + val result = DbUser.select { DbUser.userId eq userId }.firstOrNull() ?: return@dbQuery null + User(result[DbUser.userId], result[DbUser.username], result[DbUser.password]) + }?.apply { loadPermissions() } + + suspend fun list(): List = dbQuery { + val query = DbUser.selectAll() + query.map { result -> + User(result[DbUser.userId], result[DbUser.username], result[DbUser.password]) + } + }.onEach { it.loadPermissions() } + + suspend fun exists(): Boolean = dbQuery { + DbUser.selectAll().count() == 0 + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/model/WorkGroup.kt new file mode 100644 index 0000000..0e30f88 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/WorkGroup.kt @@ -0,0 +1,109 @@ +package de.kif.backend.model + +import de.kif.backend.database.DbWorkGroup +import de.kif.backend.database.DbWorkGroupConstraint +import de.kif.backend.database.dbQuery +import org.jetbrains.exposed.sql.* + +class WorkGroup( + var id: Int = -1, + var name: String = "", + var interested: Int = 0, + var trackId: Int? = null, + var projector: Boolean = false, + var resolution: Boolean = false, + var length: Int = 0, + var start: Long? = null, + var end: Long? = null +) { + var constraints: Set = emptySet() + + suspend fun save() { + if (id < 0) { + dbQuery { + val newId = DbWorkGroup.insert { + it[name] = this@WorkGroup.name + it[interested] = this@WorkGroup.interested + it[trackId] = this@WorkGroup.trackId + it[projector] = this@WorkGroup.projector + it[resolution] = this@WorkGroup.resolution + it[length] = this@WorkGroup.length + it[start] = this@WorkGroup.start + it[end] = this@WorkGroup.end + }[DbWorkGroup.id]!! + this@WorkGroup.id = newId + } + for (constraint in constraints) { + constraint.save(this@WorkGroup.id) + } + } else { + dbQuery { + DbWorkGroup.update({ DbWorkGroup.id eq id }) { + it[name] = this@WorkGroup.name + it[interested] = this@WorkGroup.interested + it[trackId] = this@WorkGroup.trackId + it[projector] = this@WorkGroup.projector + it[resolution] = this@WorkGroup.resolution + it[length] = this@WorkGroup.length + it[start] = this@WorkGroup.start + it[end] = this@WorkGroup.end + } + + DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id } + } + for (constraint in constraints) { + constraint.save(this@WorkGroup.id) + } + } + } + + suspend fun delete() { + val id = id + if (id >= 0) { + dbQuery { + DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id } + DbWorkGroup.deleteWhere { DbWorkGroup.id eq id } + } + } + } + + suspend fun loadConstraints() { + if (id >= 0) { + constraints = WorkGroupConstraint.get(id) + } + } + + companion object { + suspend fun get(workGroupId: Int): WorkGroup? = dbQuery { + val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: return@dbQuery null + WorkGroup( + result[DbWorkGroup.id], + result[DbWorkGroup.name], + result[DbWorkGroup.interested], + result[DbWorkGroup.trackId], + result[DbWorkGroup.projector], + result[DbWorkGroup.resolution], + result[DbWorkGroup.length], + result[DbWorkGroup.start], + result[DbWorkGroup.end] + ) + }?.apply { loadConstraints() } + + suspend fun list(): List = dbQuery { + val query = DbWorkGroup.selectAll() + query.map { result -> + WorkGroup( + result[DbWorkGroup.id], + result[DbWorkGroup.name], + result[DbWorkGroup.interested], + result[DbWorkGroup.trackId], + result[DbWorkGroup.projector], + result[DbWorkGroup.resolution], + result[DbWorkGroup.length], + result[DbWorkGroup.start], + result[DbWorkGroup.end] + ) + } + }.onEach { it.loadConstraints() } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/model/WorkGroupConstraint.kt b/src/jvmMain/kotlin/de/kif/backend/model/WorkGroupConstraint.kt new file mode 100644 index 0000000..ab7dde5 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/model/WorkGroupConstraint.kt @@ -0,0 +1,132 @@ +package de.kif.backend.model + +import de.kif.backend.database.DbConstraintType +import de.kif.backend.database.DbWorkGroupConstraint +import de.kif.backend.database.dbQuery +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.update + +sealed class WorkGroupConstraint( + var id: Int = -1 +) { + + abstract suspend fun save(workGroupId: Int) + + suspend fun delete() { + val id = id + if (id >= 0) { + dbQuery { + DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.id eq id } + } + } + } + + class BeginOnDay( + var time: Long = 0, + var day: Int = 0 + ) : WorkGroupConstraint() { + override suspend fun save(workGroupId: Int) { + if (id < 0) { + dbQuery { + val newId = DbWorkGroupConstraint.insert { + it[this@insert.workGroupId] = workGroupId + it[type] = DbConstraintType.BEGIN + it[time] = this@BeginOnDay.time + it[day] = this@BeginOnDay.day + }[DbWorkGroupConstraint.id]!! + this@BeginOnDay.id = newId + } + } else { + dbQuery { + DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) { + it[this@update.workGroupId] = workGroupId + it[type] = DbConstraintType.BEGIN + it[time] = this@BeginOnDay.time + it[day] = this@BeginOnDay.day + } + } + } + } + } + + class EndOnDay( + var time: Long = 0, + var day: Int = 0 + ) : WorkGroupConstraint() { + override suspend fun save(workGroupId: Int) { + if (id < 0) { + dbQuery { + val newId = DbWorkGroupConstraint.insert { + it[this@insert.workGroupId] = workGroupId + it[type] = DbConstraintType.END + it[time] = this@EndOnDay.time + it[day] = this@EndOnDay.day + }[DbWorkGroupConstraint.id]!! + this@EndOnDay.id = newId + } + } else { + dbQuery { + DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) { + it[this@update.workGroupId] = workGroupId + it[type] = DbConstraintType.END + it[time] = this@EndOnDay.time + it[day] = this@EndOnDay.day + } + } + } + } + } + + class BlockedOnDay( + var time: Long = 0, + var duration: Int = 0, + var day: Int = 0 + ) : WorkGroupConstraint() { + + override suspend fun save(workGroupId: Int) { + if (id < 0) { + dbQuery { + val newId = DbWorkGroupConstraint.insert { + it[this@insert.workGroupId] = workGroupId + it[type] = DbConstraintType.BLOCKED + it[time] = this@BlockedOnDay.time + it[duration] = this@BlockedOnDay.duration + it[day] = this@BlockedOnDay.day + }[DbWorkGroupConstraint.id]!! + this@BlockedOnDay.id = newId + } + } else { + dbQuery { + DbWorkGroupConstraint.update({ DbWorkGroupConstraint.id eq id }) { + it[this@update.workGroupId] = workGroupId + it[type] = DbConstraintType.BLOCKED + it[time] = this@BlockedOnDay.time + it[duration] = this@BlockedOnDay.duration + it[day] = this@BlockedOnDay.day + } + } + } + } + } + + companion object { + suspend fun get(workGroupId: Int): Set = dbQuery { + val result = DbWorkGroupConstraint.select { DbWorkGroupConstraint.workGroupId eq workGroupId } + result.map { + val id = it[DbWorkGroupConstraint.id] + val type = it[DbWorkGroupConstraint.type] + val time = it[DbWorkGroupConstraint.time] + val duration = it[DbWorkGroupConstraint.duration] + val day = it[DbWorkGroupConstraint.day] + + when (type) { + DbConstraintType.BEGIN -> WorkGroupConstraint.BeginOnDay(time, day) + DbConstraintType.END -> WorkGroupConstraint.EndOnDay(time, day) + DbConstraintType.BLOCKED -> WorkGroupConstraint.BlockedOnDay(time, duration, day) + }.also { it.id = id } + }.toSet() + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt new file mode 100644 index 0000000..e5b9df6 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt @@ -0,0 +1,46 @@ +package de.kif.backend.route + +import de.kif.backend.LocationAccount +import de.kif.backend.LocationLogin +import de.kif.backend.PortalSession +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate +import io.ktor.application.call +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.get +import io.ktor.request.path +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.sessions.get +import io.ktor.sessions.sessions +import kotlinx.html.* + +fun Route.account() { + get { + val user = call.sessions.get()?.getUser(call) + if (user == null) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.ACCOUNT + } + content { + h1 { +"Account" } + div { + +"Welcome ${user.username}!" + br {} + +"You have the following rights: ${user.permissions}" + br {} + a("/logout") { + button { + +"Logout" + } + } + } + } + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt new file mode 100644 index 0000000..00ec006 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt @@ -0,0 +1,28 @@ +package de.kif.backend.route + +import de.kif.backend.LocationCalendar +import de.kif.backend.PortalSession +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate +import io.ktor.application.call +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.get +import io.ktor.routing.Route +import io.ktor.sessions.get +import io.ktor.sessions.sessions +import kotlinx.html.h1 + +fun Route.calendar() { + get { + val user = call.sessions.get()?.getUser(call) + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.CALENDAR + } + content { + h1 { +"Calendar" } + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt b/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt new file mode 100644 index 0000000..a71934a --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt @@ -0,0 +1,28 @@ +package de.kif.backend.route + +import de.kif.backend.LocationDashboard +import de.kif.backend.PortalSession +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate +import io.ktor.application.call +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.get +import io.ktor.routing.Route +import io.ktor.sessions.get +import io.ktor.sessions.sessions +import kotlinx.html.h1 + +fun Route.dashboard() { + get { + val user = call.sessions.get()?.getUser(call) + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.DASHBOARD + } + content { + h1 { +"Dashboard" } + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Login.kt b/src/jvmMain/kotlin/de/kif/backend/route/Login.kt new file mode 100644 index 0000000..7cfa6d0 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Login.kt @@ -0,0 +1,102 @@ +package de.kif.backend.route + +import de.kif.backend.LocationLogin +import de.kif.backend.LocationLogout +import de.kif.backend.PortalSession +import de.kif.backend.model.User +import de.kif.backend.view.MainTemplate +import io.ktor.application.call +import io.ktor.auth.authenticate +import io.ktor.auth.principal +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.location +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.sessions.clear +import io.ktor.sessions.get +import io.ktor.sessions.sessions +import io.ktor.sessions.set +import kotlinx.html.* + +fun Route.login() { + location { + authenticate { + post { + val principal = call.principal() ?: return@post + call.sessions.set(PortalSession(principal.id, principal.username)) + call.respondRedirect(call.parameters[LocationLogin::next.name] ?: "/") + } + } + + get { + val needLogin = call.sessions.get() == null + if (needLogin) { + call.respondHtmlTemplate(MainTemplate()) { + content { + div { + div { + br { } + form("/login", method = FormMethod.post) { + div("form-group") { + label { + htmlFor = LocationLogin::username.name + +"Username" + } + input( + name = LocationLogin::username.name, + classes = "form-control" + ) { + id = LocationLogin::username.name + placeholder = "Username" + } + } + div("form-group") { + label { + htmlFor = LocationLogin::password.name + +"Password" + } + input( + name = LocationLogin::password.name, + classes = "form-control", + type = InputType.password + ) { + id = LocationLogin::password.name + placeholder = "Password" + } + } + input( + name = LocationLogin::next.name, + type = InputType.hidden + ) { + value = call.parameters[LocationLogin::next.name] ?: "/" + } + button(type = ButtonType.submit, classes = "btn btn-primary") { + +"LocationLogin" + } + } + + if ("error" in call.parameters) { + br { } + div("alert alert-danger") { + +"Username or password incorrect!" + } + } + } + } + } + } + } else { + call.respondRedirect(call.parameters[LocationLogin::next.name] ?: "/") + } + } + } + + location { + get { + call.sessions.clear() + call.respondRedirect("/") + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Person.kt b/src/jvmMain/kotlin/de/kif/backend/route/Person.kt new file mode 100644 index 0000000..73c4659 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Person.kt @@ -0,0 +1,256 @@ +package de.kif.backend.route + +import de.kif.backend.LocationLogin +import de.kif.backend.LocationPerson +import de.kif.backend.PortalSession +import de.kif.backend.model.Permission +import de.kif.backend.model.Person +import de.kif.backend.util.Search +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate +import de.kif.backend.view.TableTemplate +import io.ktor.application.call +import io.ktor.html.insert +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.get +import io.ktor.locations.post +import io.ktor.request.path +import io.ktor.request.receiveParameters +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.sessions.get +import io.ktor.sessions.sessions +import io.ktor.util.toMap +import kotlinx.html.* + +fun Route.person() { + get { param -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.PERSON)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val list = Person.list() + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.PERSON + } + content { + h1 { +"Persons" } + insert(TableTemplate()) { + searchValue = param.search + + action { + a("/person/new") { + +"Add person" + } + } + + header { + th { + +"First name" + } + th { + +"Last name" + } + th(classes = "action") { + +"Action" + } + } + + for (u in list) { + if (Search.match(param.search, u.firstName, u.lastName)) { + entry { + attributes["data-search"] = Search.pack(u.firstName, u.lastName) + td { + +u.firstName + } + td { + +u.lastName + } + td(classes = "action") { + a("/person/${u.id}") { + i("material-icons") { +"edit" } + } + } + } + } + } + } + } + } + } + } + + get { personId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.PERSON)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val editPerson = Person.get(personId.id) ?: return@get + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.PERSON + } + content { + h1 { +"Edit person" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "first-name" + +"First name" + } + input( + name = "first-name", + classes = "form-control" + ) { + id = "first-name" + placeholder = "First name" + value = editPerson.firstName + } + } + div("form-group") { + label { + htmlFor = "last-name" + +"Last name" + } + input( + name = "last-name", + classes = "form-control" + ) { + id = "last-name" + placeholder = "Last name" + value = editPerson.lastName + } + } + + div("form-group") { + a("/person") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Save" + } + } + } + a("/person/${editPerson.id}/delete") { + button(classes = "form-btn btn-danger") { + +"Delete" + } + } + } + } + } + } + + post { personId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.PERSON)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + val editPerson = Person.get(personId.id) ?: return@post + + params["first-name"]?.let { editPerson.firstName = it } + params["last-name"]?.let { editPerson.lastName = it } + + editPerson.save() + + call.respondRedirect("/person") + } + } + + get { + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.PERSON)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.PERSON + } + content { + h1 { +"Create person" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "first-name" + +"First name" + } + input( + name = "first-name", + classes = "form-control" + ) { + id = "first-name" + placeholder = "First name" + value = "" + } + } + div("form-group") { + label { + htmlFor = "last-name" + +"Last name" + } + input( + name = "last-name", + classes = "form-control" + ) { + id = "last-name" + placeholder = "Last name" + value = "" + } + } + + div("form-group") { + a("/person") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Create" + } + } + } + } + } + } + } + + post { + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.PERSON)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + + val firstName = params["first-name"] ?: return@post + val lastName = params["last-name"] ?: return@post + + Person(firstName = firstName, lastName = lastName).save() + + call.respondRedirect("/person") + } + } + + get { personId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.PERSON)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val deletePerson = Person.get(personId.id) ?: return@get + + deletePerson.delete() + + call.respondRedirect("/person") + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt new file mode 100644 index 0000000..3a10aaa --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt @@ -0,0 +1,306 @@ +package de.kif.backend.route + +import de.kif.backend.LocationLogin +import de.kif.backend.LocationRoom +import de.kif.backend.PortalSession +import de.kif.backend.model.Permission +import de.kif.backend.model.Room +import de.kif.backend.util.Search +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate +import de.kif.backend.view.TableTemplate +import io.ktor.application.call +import io.ktor.html.insert +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.get +import io.ktor.locations.post +import io.ktor.request.path +import io.ktor.request.receiveParameters +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.sessions.get +import io.ktor.sessions.sessions +import io.ktor.util.toMap +import kotlinx.html.* + +fun Route.room() { + get { param -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.ROOM)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val list = Room.list() + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.ROOM + } + content { + h1 { +"Rooms" } + insert(TableTemplate()) { + searchValue = param.search + + action { + a("/room/new") { + +"Add room" + } + } + + header { + th { + +"Name" + } + th { + +"Places" + } + th { + +"Projector" + } + th(classes = "action") { + +"Action" + } + } + + for (u in list) { + if (Search.match(param.search, u.name)) { + entry { + attributes["data-search"] = Search.pack(u.name) + td { + +u.name + } + td { + +u.places.toString() + } + td { + +u.projector.toString() + } + td(classes = "action") { + a("/room/${u.id}") { + i("material-icons") { +"edit" } + } + } + } + } + } + } + } + } + } + } + + get { roomId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.ROOM)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val editRoom = Room.get(roomId.id) ?: return@get + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.ROOM + } + content { + h1 { +"Edit room" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "name" + +"Name" + } + input( + name = "name", + classes = "form-control" + ) { + id = "name" + placeholder = "Name" + value = editRoom.name + } + } + div("form-group") { + label { + htmlFor = "places" + +"Places" + } + input( + name = "places", + classes = "form-control", + type = InputType.number + ) { + id = "places" + placeholder = "Places" + value = editRoom.places.toString() + + min = "0" + max = "1337" + } + } + + div("form-switch-group") { + div("form-group form-switch") { + label { + htmlFor = "projector" + +"Projector" + } + input( + name = "projector", + classes = "form-control", + type = InputType.checkBox + ) { + id = "projector" + checked = editRoom.projector + } + } + } + + div("form-group") { + a("/room") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Save" + } + } + } + a("/room/${editRoom.id}/delete") { + button(classes = "form-btn btn-danger") { + +"Delete" + } + } + } + } + } + } + + post { roomId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.ROOM)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + val editRoom = Room.get(roomId.id) ?: return@post + + params["name"]?.let { editRoom.name = it } + params["places"]?.let { editRoom.places = it.toIntOrNull() ?: 0 } + params["projector"]?.let { editRoom.projector = it == "on" } + + editRoom.save() + + call.respondRedirect("/room") + } + } + + get { + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.ROOM)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.ROOM + } + content { + h1 { +"Create room" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "name" + +"Name" + } + input( + name = "name", + classes = "form-control" + ) { + id = "name" + placeholder = "Name" + value = "" + } + } + div("form-group") { + label { + htmlFor = "places" + +"Places" + } + input( + name = "places", + classes = "form-control", + type = InputType.number + ) { + id = "places" + placeholder = "Places" + value = "0" + + min = "0" + max = "1337" + } + } + + div("form-switch-group") { + div("form-group form-switch") { + label { + htmlFor = "projector" + +"Projector" + } + input( + name = "projector", + classes = "form-control", + type = InputType.checkBox + ) { + id = "projector" + checked = false + } + } + } + + div("form-group") { + a("/room") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Create" + } + } + } + } + } + } + } + + post { + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.ROOM)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + + val name = params["name"] ?: return@post + val places = (params["places"] ?: return@post).toIntOrNull() ?: 0 + val projector = params["projector"] == "on" + + Room(name = name, places = places, projector = projector).save() + + call.respondRedirect("/room") + } + } + + get { roomId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.ROOM)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val deleteRoom = Room.get(roomId.id) ?: return@get + + deleteRoom.delete() + + call.respondRedirect("/room") + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Setup.kt b/src/jvmMain/kotlin/de/kif/backend/route/Setup.kt new file mode 100644 index 0000000..0e35a25 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Setup.kt @@ -0,0 +1,102 @@ +package de.kif.backend.route + +import de.kif.backend.LocationLogin +import de.kif.backend.model.Permission +import de.kif.backend.model.User +import de.kif.backend.database.DbUser +import de.kif.backend.view.MainTemplate +import io.ktor.application.call +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.get +import io.ktor.request.receiveParameters +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.routing.route +import kotlinx.html.* +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction + +fun Route.setup() { + route("/") { + get { + call.respondHtmlTemplate(MainTemplate()) { + transaction { + val firstStart = DbUser.selectAll().count() == 0 + + menuTemplate { + setup = true + } + + if (firstStart) { + content { + div { + h1 { +"Create account" } + form("/", method = FormMethod.post) { + div("form-group") { + label { + htmlFor = LocationLogin::username.name + +"Username" + } + input( + name = LocationLogin::username.name, + classes = "form-control" + ) { + id = LocationLogin::username.name + placeholder = "Username" + } + } + div("form-group") { + label { + htmlFor = LocationLogin::password.name + +"Password" + } + input( + name = LocationLogin::password.name, + classes = "form-control", + type = InputType.password + ) { + id = LocationLogin::password.name + placeholder = "Password" + } + } + button(type = ButtonType.submit, classes = "btn btn-primary") { + +"Create" + } + } + } + } + } else { + content { + div { + h1 { +"Setup complete" } + p { + +"Please restart the server!" + } + } + } + } + } + } + } + + post { + val parameters = call.receiveParameters() + val username = parameters[LocationLogin::username.name] + val password = parameters[LocationLogin::password.name] + + if (username == null || password == null) { + call.respondRedirect("/") + return@post + } + + User.create(username, password, Permission.values().toSet()) + call.respondRedirect("/") + } + } + + get("*") { + call.respondRedirect("/") + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/User.kt new file mode 100644 index 0000000..6b23d82 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/User.kt @@ -0,0 +1,311 @@ +package de.kif.backend.route + +import de.kif.backend.LocationLogin +import de.kif.backend.LocationUser +import de.kif.backend.PortalSession +import de.kif.backend.model.Permission +import de.kif.backend.model.User +import de.kif.backend.util.Search +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate +import de.kif.backend.view.TableTemplate +import io.ktor.application.call +import io.ktor.html.insert +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.get +import io.ktor.locations.post +import io.ktor.request.path +import io.ktor.request.receiveParameters +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.sessions.get +import io.ktor.sessions.sessions +import io.ktor.util.toMap +import kotlinx.html.* + +fun Route.user() { + get { param -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.USER)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val list = User.list() + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.USER + } + content { + h1 { +"Users" } + insert(TableTemplate()) { + searchValue = param.search + + action { + a("/user/new") { + +"Add user" + } + } + + header { + th { + +"Username" + } + th { + +"Permissions" + } + th(classes = "action") { + +"Action" + } + } + + for (u in list) { + if (Search.match(param.search, u.username)) { + entry { + attributes["data-search"] = Search.pack(u.username) + td { + +u.username + } + td { + +u.permissions.joinToString(", ") { it.toString().toLowerCase() } + } + td(classes = "action") { + a("/user/${u.id}") { + i("material-icons") { +"edit" } + } + } + } + } + } + } + } + } + } + } + + get { userId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.USER)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val editUser = User.get(userId.id) ?: return@get + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.USER + } + content { + h1 { +"Edit user" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "username" + +"Username" + } + input( + name = "username", + classes = "form-control" + ) { + id = "username" + placeholder = "Username" + value = editUser.username + } + } + + div("form-switch-group") { + for (permission in Permission.values()) { + val name = permission.toString().toLowerCase() + div("form-group form-switch") { + label { + htmlFor = "permission-$name" + +name.capitalize() + } + input( + name = "permission-$name", + classes = "form-control", + type = InputType.checkBox + ) { + id = "permission-$name" + checked = permission in editUser.permissions + + if (!user.checkPermission(permission)) { + readonly = true + } + } + } + } + } + + div("form-group") { + a("/user") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Save" + } + } + } + a("/user/${editUser.id}/delete") { + button(classes = "form-btn btn-danger") { + +"Delete" + } + } + } + } + } + } + + post { userId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.USER)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + val editUser = User.get(userId.id) ?: return@post + + params["username"]?.let { editUser.username = it } + + for (permission in Permission.values()) { + val name = permission.toString().toLowerCase() + if (user.checkPermission(permission)) { + if (params["permission-$name"] == "on") { + editUser.permissions += permission + } else { + editUser.permissions -= permission + } + } + } + editUser.save() + + call.respondRedirect("/user") + } + } + + get { + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.USER)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.USER + } + content { + h1 { +"Create user" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "username" + +"Username" + } + input( + name = "username", + classes = "form-control" + ) { + id = "username" + placeholder = "Username" + value = "" + } + } + div("form-group") { + label { + htmlFor = "password" + +"Password" + } + input( + name = "password", + classes = "form-control", + type = InputType.password + ) { + id = "password" + placeholder = "Password" + value = "" + } + } + + div("form-switch-group") { + for (permission in Permission.values()) { + val name = permission.toString().toLowerCase() + div("form-group form-switch") { + label { + htmlFor = "permission-$name" + +name.capitalize() + } + input( + name = "permission-$name", + classes = "form-control", + type = InputType.checkBox + ) { + id = "permission-$name" + checked = false + + if (!user.checkPermission(permission)) { + readonly = true + } + } + } + } + } + + div("form-group") { + a("/user") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Create" + } + } + } + } + } + } + } + + post { + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.USER)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + + val username = params["username"] ?: return@post + val password = params["password"] ?: return@post + + val permissions = Permission.values().mapNotNull { permission -> + if (user.checkPermission(permission) && params["permission-${permission.toString().toLowerCase()}"] == "on") + permission + else + null + }.toSet() + + User.create(username, password, permissions) + + call.respondRedirect("/user") + } + } + + get { userId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.USER)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val deleteUser = User.get(userId.id) ?: return@get + + if (user.checkPermission(Permission.USER) && + (Permission.ADMIN !in deleteUser.permissions || Permission.ADMIN in user.permissions) + ) { + deleteUser.delete() + } + + call.respondRedirect("/user") + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt new file mode 100644 index 0000000..48e19b7 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -0,0 +1,431 @@ +package de.kif.backend.route + +import de.kif.backend.LocationLogin +import de.kif.backend.LocationWorkGroup +import de.kif.backend.PortalSession +import de.kif.backend.model.Permission +import de.kif.backend.model.WorkGroup +import de.kif.backend.util.Search +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate +import de.kif.backend.view.TableTemplate +import io.ktor.application.call +import io.ktor.html.insert +import io.ktor.html.respondHtmlTemplate +import io.ktor.locations.get +import io.ktor.locations.post +import io.ktor.request.path +import io.ktor.request.receiveParameters +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.sessions.get +import io.ktor.sessions.sessions +import io.ktor.util.toMap +import kotlinx.html.* + +fun Route.workGroup() { + get { param -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val list = WorkGroup.list() + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.WORK_GROUP + } + content { + h1 { +"Work groups" } + insert(TableTemplate()) { + searchValue = param.search + + action { + a("/workgroup/new") { + +"Add work group" + } + } + + header { + th { + +"Name" + } + th { + +"Interested" + } + th { + +"Track" + } + th { + +"Projector" + } + th { + +"Resolution" + } + th { + +"Length" + } + th(classes = "action") { + +"Action" + } + } + + for (u in list) { + if (Search.match(param.search, u.name)) { + entry { + attributes["data-search"] = Search.pack(u.name) + td { + +u.name + } + td { + +u.interested.toString() + } + td { + +u.trackId.toString() + } + td { + +u.projector.toString() + } + td { + +u.resolution.toString() + } + td { + +u.length.toString() + } + td(classes = "action") { + a("/workgroup/${u.id}") { + i("material-icons") { +"edit" } + } + } + } + } + } + } + } + } + } + } + + get { workGroupId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val editWorkGroup = WorkGroup.get(workGroupId.id) ?: return@get + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.WORK_GROUP + } + content { + h1 { +"Edit work group" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "name" + +"Name" + } + input( + name = "name", + classes = "form-control" + ) { + id = "name" + placeholder = "Name" + value = editWorkGroup.name + } + } + div("form-group") { + label { + htmlFor = "interested" + +"Interested" + } + input( + name = "interested", + classes = "form-control", + type = InputType.number + ) { + id = "interested" + value = editWorkGroup.interested.toString() + + min = "0" + max = "1337" + } + } + div("form-group") { + label { + htmlFor = "track" + +"Track" + } + input( + name = "track", + classes = "form-control", + type = InputType.number + ) { + id = "track" + value = editWorkGroup.trackId.toString() + + min = "0" + max = "1337" + } + } + div("form-group") { + label { + htmlFor = "length" + +"Length" + } + input( + name = "length", + classes = "form-control", + type = InputType.number + ) { + id = "length" + value = editWorkGroup.length.toString() + + min = "0" + max = "1440" + } + } + + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "projector", + classes = "form-control", + type = InputType.checkBox + ) { + id = "projector" + checked = editWorkGroup.projector + } + label { + htmlFor = "projector" + +"Projector" + } + } + div("form-group form-switch") { + input( + name = "resolution", + classes = "form-control", + type = InputType.checkBox + ) { + id = "resolution" + checked = editWorkGroup.resolution + } + label { + htmlFor = "resolution" + +"Resolution" + } + } + } + + div("form-group") { + a("/workgroup") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Save" + } + } + } + a("/workgroup/${editWorkGroup.id}/delete") { + button(classes = "form-btn btn-danger") { + +"Delete" + } + } + } + } + } + } + + post { workGroupId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + val editWorkGroup = WorkGroup.get(workGroupId.id) ?: return@post + + params["name"]?.let { editWorkGroup.name = it} + params["interested"]?.toIntOrNull()?.let { editWorkGroup.interested = it} + params["track"]?.toIntOrNull()?.let { editWorkGroup.trackId = it} + params["projector"]?.toBoolean()?.let { editWorkGroup.projector = it} + params["resolution"]?.toBoolean()?.let { editWorkGroup.resolution = it} + params["length"]?.toIntOrNull()?.let { editWorkGroup.length = it} + + editWorkGroup.save() + + call.respondRedirect("/workgroup") + } + } + + get { + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.WORK_GROUP + } + content { + h1 { +"Create work group" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "name" + +"Name" + } + input( + name = "name", + classes = "form-control" + ) { + id = "name" + placeholder = "Name" + value = "" + } + } + div("form-group") { + label { + htmlFor = "interested" + +"Interested" + } + input( + name = "interested", + classes = "form-control", + type = InputType.number + ) { + id = "interested" + value = "0" + + min = "0" + max = "1337" + } + } + div("form-group") { + label { + htmlFor = "track" + +"Track" + } + input( + name = "track", + classes = "form-control", + type = InputType.number + ) { + id = "track" + value = "" + + min = "0" + max = "1337" + } + } + div("form-group") { + label { + htmlFor = "length" + +"Length" + } + input( + name = "length", + classes = "form-control", + type = InputType.number + ) { + id = "length" + value = "0" + + min = "0" + max = "1440" + } + } + + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "projector", + classes = "form-control", + type = InputType.checkBox + ) { + id = "projector" + checked = false + } + label { + htmlFor = "projector" + +"Projector" + } + } + div("form-group form-switch") { + input( + name = "resolution", + classes = "form-control", + type = InputType.checkBox + ) { + id = "resolution" + checked = false + } + label { + htmlFor = "resolution" + +"Resolution" + } + } + } + + div("form-group") { + a("/workgroup") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Create" + } + } + } + } + } + } + } + + post { + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + + val name = params["name"] ?: return@post + val interested = (params["interested"] ?: return@post).toIntOrNull() ?: 0 + val trackId = (params["track"] ?: return@post).toIntOrNull() + val projector = params["projector"] == "on" + val resolution = params["resolution"] == "on" + val length = (params["length"] ?: return@post).toIntOrNull() ?: 0 + + WorkGroup( + name = name, + interested = interested, + trackId = trackId, + projector = projector, + resolution = resolution, + length = length + ).save() + + call.respondRedirect("/workgroup") + } + } + + get { workGroupId -> + val user = call.sessions.get()?.getUser(call) + if (user == null || !user.checkPermission(Permission.WORK_GROUP)) { + call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}") + } else { + val deleteWorkGroup = WorkGroup.get(workGroupId.id) ?: return@get + + deleteWorkGroup.delete() + + call.respondRedirect("/workgroup") + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/util/Search.kt b/src/jvmMain/kotlin/de/kif/backend/util/Search.kt new file mode 100644 index 0000000..5467a32 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/util/Search.kt @@ -0,0 +1,29 @@ +package de.kif.backend.util + +object Search { + private fun permute(list: List): List> { + if (list.size <= 1) return listOf(list) + + val perm = mutableListOf>() + + for (elem in list) { + val p = permute(list - elem) + + for (x in p) { + perm += x + elem + } + } + + return perm + } + + fun match(search: String, vararg params: String): Boolean { + val s = search.toLowerCase().replace(" ", "") + + return permute(params.map { it.toLowerCase().replace(" ", "") }).any { + it.joinToString("").contains(s) + } + } + + fun pack(vararg params: String): String = params.joinToString("|") { it.toLowerCase() } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt new file mode 100644 index 0000000..2848c86 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt @@ -0,0 +1,40 @@ +package de.kif.backend.view + +import io.ktor.html.Placeholder +import io.ktor.html.Template +import io.ktor.html.TemplatePlaceholder +import io.ktor.html.insert +import kotlinx.html.* + +class MainTemplate : Template { + val content = Placeholder() + val menuTemplate =TemplatePlaceholder() + + override fun HTML.apply() { + head { + 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 = "https://fonts.googleapis.com/css?family=Bungee|Oswald", type = LinkType.textCss, rel = LinkRel.stylesheet) + link(href = "/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet) + + script(src = "/static/require.min.js") {} + + /*script { + unsafe { + +"require.config({baseUrl: '/static'});\n" + +("require([${Resources.jsModules}]);\n") + } + }*/ + } + body { + insert(MenuTemplate(), menuTemplate) + div("container") { + div("main") { + insert(content) + } + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt new file mode 100644 index 0000000..bb24a9e --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt @@ -0,0 +1,75 @@ +package de.kif.backend.view + +import de.kif.backend.model.Permission +import de.kif.backend.model.User +import io.ktor.html.Template +import kotlinx.html.FlowContent +import kotlinx.html.a +import kotlinx.html.div +import kotlinx.html.nav + +class MenuTemplate() : Template { + + var setup = false + var active: Tab = Tab.DASHBOARD + var user: User? = null + + override fun FlowContent.apply() { + nav("menu") { + div("container") { + div("menu-left") { + if (setup) { + a(classes = "active") { + +"Setup" + } + } else { + a("/", classes = if (active == Tab.DASHBOARD) "active" else null) { + +"Dashboard" + } + a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) { + +"Calendar" + } + } + } + if (!setup) { + div("menu-right") { + val user = user + if (user == null) { + a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { + +"Login" + } + } else { + if (user.checkPermission(Permission.WORK_GROUP)) { + a("/workgroup", classes = if (active == Tab.WORK_GROUP) "active" else null) { + +"Work groups" + } + } + if (user.checkPermission(Permission.ROOM)) { + a("/room", classes = if (active == Tab.ROOM) "active" else null) { + +"Rooms" + } + } + if (user.checkPermission(Permission.PERSON)) { + a("/person", classes = if (active == Tab.PERSON) "active" else null) { + +"Persons" + } + } + if (user.checkPermission(Permission.PERSON)) { + a("/user", classes = if (active == Tab.USER) "active" else null) { + +"Users" + } + } + a("/account", classes = if (active == Tab.ACCOUNT) "active" else null) { + +user.username + } + } + } + } + } + } + } + + enum class Tab { + DASHBOARD, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, PERSON, USER + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt new file mode 100644 index 0000000..8120abf --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/view/TableTemplate.kt @@ -0,0 +1,42 @@ +package de.kif.backend.view + +import io.ktor.html.* +import kotlinx.html.* + + +class TableTemplate() : Template { + + var searchValue = "" + + val action = Placeholder() + val header = Placeholder() + val entry = PlaceholderList() + + override fun FlowContent.apply() { + div("table-layout") { + form(classes = "table-layout-search") { + input(InputType.search, name = "search") { + placeholder = "Search" + value = searchValue + } + button(type = ButtonType.submit) { + i("material-icons") { +"search" } + } + } + div("table-layout-action") { + insert(action) + } + + } + table("table-layout-table") { + tr { + insert(header) + } + each(entry) { + tr { + insert(it) + } + } + } + } +}