diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/deployment/staging/docker-compose.yml b/deployment/staging/docker-compose.yml
new file mode 100644
index 0000000..78a67e7
--- /dev/null
+++ b/deployment/staging/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3"
+services:
+ front:
+ image: docker.e-bs.cz/mdr-pdb-frontend-server:0.1.0-SNAPSHOT
+ environment:
+ APP_BASE: https://tc163.cmi.cz
+ ports:
+ - "19003:8080"
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/deployment/staging/docker-compose.yml b/deployment/staging/docker-compose.yml
new file mode 100644
index 0000000..78a67e7
--- /dev/null
+++ b/deployment/staging/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3"
+services:
+ front:
+ image: docker.e-bs.cz/mdr-pdb-frontend-server:0.1.0-SNAPSHOT
+ environment:
+ APP_BASE: https://tc163.cmi.cz
+ ports:
+ - "19003:8080"
diff --git a/project/MockDataExport.scala b/project/MockDataExport.scala
index 73ee24a..bee338e 100644
--- a/project/MockDataExport.scala
+++ b/project/MockDataExport.scala
@@ -1,10 +1,13 @@
import sbt._
import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
import scala.xml.XML
import scala.xml.Elem
object MockDataExport extends AutoPlugin {
- override def trigger = noTrigger
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
object autoImport {
lazy val generateOrgDbData =
@@ -23,15 +26,26 @@
orgDbHeliosExportFile := orgDbExportDir.value / "HeliosData.xml",
generateOrgDbData := {
val file = orgDbOutputFile.value
- val heliosData =
- XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
- IO.write(
- file,
- userData(heliosData)
- )
- Seq(file)
- }
- // TODO: cached run & auto run on fastLinkJS
+ val heliosFile = orgDbHeliosExportFile.value
+ def doExport() = {
+ val heliosData =
+ XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
+ IO.write(
+ file,
+ userData(heliosData)
+ )
+ }
+ val cachedFun =
+ FileFunction.cached(streams.value.cacheDirectory / "orgdb_export") {
+ _ =>
+ doExport()
+ Set(file)
+ }
+ cachedFun(Set(heliosFile)).toSeq
+ },
+ (Compile / fastLinkJS) := (Compile / fastLinkJS)
+ .dependsOn(generateOrgDbData)
+ .value
)
def escaped(v: String): String = v.replaceAll("\"", "\\\"")
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/deployment/staging/docker-compose.yml b/deployment/staging/docker-compose.yml
new file mode 100644
index 0000000..78a67e7
--- /dev/null
+++ b/deployment/staging/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3"
+services:
+ front:
+ image: docker.e-bs.cz/mdr-pdb-frontend-server:0.1.0-SNAPSHOT
+ environment:
+ APP_BASE: https://tc163.cmi.cz
+ ports:
+ - "19003:8080"
diff --git a/project/MockDataExport.scala b/project/MockDataExport.scala
index 73ee24a..bee338e 100644
--- a/project/MockDataExport.scala
+++ b/project/MockDataExport.scala
@@ -1,10 +1,13 @@
import sbt._
import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
import scala.xml.XML
import scala.xml.Elem
object MockDataExport extends AutoPlugin {
- override def trigger = noTrigger
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
object autoImport {
lazy val generateOrgDbData =
@@ -23,15 +26,26 @@
orgDbHeliosExportFile := orgDbExportDir.value / "HeliosData.xml",
generateOrgDbData := {
val file = orgDbOutputFile.value
- val heliosData =
- XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
- IO.write(
- file,
- userData(heliosData)
- )
- Seq(file)
- }
- // TODO: cached run & auto run on fastLinkJS
+ val heliosFile = orgDbHeliosExportFile.value
+ def doExport() = {
+ val heliosData =
+ XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
+ IO.write(
+ file,
+ userData(heliosData)
+ )
+ }
+ val cachedFun =
+ FileFunction.cached(streams.value.cacheDirectory / "orgdb_export") {
+ _ =>
+ doExport()
+ Set(file)
+ }
+ cachedFun(Set(heliosFile)).toSeq
+ },
+ (Compile / fastLinkJS) := (Compile / fastLinkJS)
+ .dependsOn(generateOrgDbData)
+ .value
)
def escaped(v: String): String = v.replaceAll("\"", "\\\"")
diff --git a/project/VitePlugin.scala b/project/VitePlugin.scala
new file mode 100644
index 0000000..1a20989
--- /dev/null
+++ b/project/VitePlugin.scala
@@ -0,0 +1,118 @@
+import sbt._
+import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
+import scala.sys.process._
+import sbt.nio.file.FileTreeView
+
+class ViteDevServer() {
+ private var worker: Option[Worker] = None
+
+ def start(workDir: File, logger: Logger, globalLogger: Logger): Unit =
+ this.synchronized {
+ stop()
+ worker = Some(new Worker(workDir, logger, globalLogger))
+ }
+
+ def stop(): Unit = this.synchronized {
+ worker.foreach { w =>
+ w.stop()
+ worker = None
+ }
+ }
+
+ private class Worker(
+ workDir: File,
+ logger: Logger,
+ globalLogger: Logger
+ ) {
+ logger.info("Starting vite dev server")
+ val command = Seq("yarn", "dev")
+ val process = Process(command, workDir).run(
+ ProcessLogger(globalLogger.info(_), globalLogger.error(_))
+ )
+
+ def stop(): Unit = {
+ logger.info("Stopping vite dev server")
+ process.destroy()
+ }
+ }
+
+ override def finalize() = stop()
+}
+
+object VitePlugin extends AutoPlugin {
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
+
+ object autoImport {
+ lazy val viteBuild = taskKey[File]("Vite build")
+ lazy val viteMonitoredFiles =
+ taskKey[Seq[File]]("Files monitored for vite build")
+ lazy val startViteDev = taskKey[Unit]("Start vite dev mode")
+ lazy val stopViteDev = taskKey[Unit]("Stop vite dev mode")
+ }
+
+ import autoImport._
+
+ private val viteDist =
+ SettingKey[File]("viteDist", "Vite dist directory", KeyRanks.Invisible)
+
+ private val viteDevServer = SettingKey[ViteDevServer](
+ "viteDevServer",
+ "Global vite dev server",
+ KeyRanks.Invisible
+ )
+
+ override def projectSettings = Seq(
+ viteDist := target.value / "vite",
+ viteDevServer := new ViteDevServer(),
+ startViteDev := {
+ val workDir = baseDirectory.value
+ val log = streams.value.log
+ val globalLog = state.value.globalLogging.full
+ val server = viteDevServer.value
+ server.start(workDir, log, globalLog)
+ },
+ stopViteDev := {
+ viteDevServer.value.stop()
+ },
+ viteMonitoredFiles := {
+ val baseGlob = baseDirectory.value.toGlob
+ def baseFiles(pattern: String): Glob = baseGlob / pattern
+ val viteConfigs =
+ FileTreeView.default.list(
+ List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
+ )
+ val linkerDirectory =
+ (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
+ val viteInputs = FileTreeView.default.list(
+ linkerDirectory.toGlob / "*.js"
+ )
+ (viteConfigs ++ viteInputs).map(_._1.toFile)
+ },
+ viteBuild := {
+ val s = streams.value
+ val dist = viteDist.value
+ val files = viteMonitoredFiles.value
+ // We depend on fullLinkJS
+ val _ = (Compile / fullLinkJS).value
+ def doBuild() = Process(
+ "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
+ baseDirectory.value
+ ) ! s.log
+ val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
+ doBuild()
+ Set(dist)
+ }
+ cachedFun(files.toSet).head
+ },
+ (onLoad in Global) := {
+ (onLoad in Global).value.compose(
+ _.addExitHook {
+ viteDevServer.value.stop()
+ }
+ )
+ }
+ )
+}
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/deployment/staging/docker-compose.yml b/deployment/staging/docker-compose.yml
new file mode 100644
index 0000000..78a67e7
--- /dev/null
+++ b/deployment/staging/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3"
+services:
+ front:
+ image: docker.e-bs.cz/mdr-pdb-frontend-server:0.1.0-SNAPSHOT
+ environment:
+ APP_BASE: https://tc163.cmi.cz
+ ports:
+ - "19003:8080"
diff --git a/project/MockDataExport.scala b/project/MockDataExport.scala
index 73ee24a..bee338e 100644
--- a/project/MockDataExport.scala
+++ b/project/MockDataExport.scala
@@ -1,10 +1,13 @@
import sbt._
import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
import scala.xml.XML
import scala.xml.Elem
object MockDataExport extends AutoPlugin {
- override def trigger = noTrigger
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
object autoImport {
lazy val generateOrgDbData =
@@ -23,15 +26,26 @@
orgDbHeliosExportFile := orgDbExportDir.value / "HeliosData.xml",
generateOrgDbData := {
val file = orgDbOutputFile.value
- val heliosData =
- XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
- IO.write(
- file,
- userData(heliosData)
- )
- Seq(file)
- }
- // TODO: cached run & auto run on fastLinkJS
+ val heliosFile = orgDbHeliosExportFile.value
+ def doExport() = {
+ val heliosData =
+ XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
+ IO.write(
+ file,
+ userData(heliosData)
+ )
+ }
+ val cachedFun =
+ FileFunction.cached(streams.value.cacheDirectory / "orgdb_export") {
+ _ =>
+ doExport()
+ Set(file)
+ }
+ cachedFun(Set(heliosFile)).toSeq
+ },
+ (Compile / fastLinkJS) := (Compile / fastLinkJS)
+ .dependsOn(generateOrgDbData)
+ .value
)
def escaped(v: String): String = v.replaceAll("\"", "\\\"")
diff --git a/project/VitePlugin.scala b/project/VitePlugin.scala
new file mode 100644
index 0000000..1a20989
--- /dev/null
+++ b/project/VitePlugin.scala
@@ -0,0 +1,118 @@
+import sbt._
+import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
+import scala.sys.process._
+import sbt.nio.file.FileTreeView
+
+class ViteDevServer() {
+ private var worker: Option[Worker] = None
+
+ def start(workDir: File, logger: Logger, globalLogger: Logger): Unit =
+ this.synchronized {
+ stop()
+ worker = Some(new Worker(workDir, logger, globalLogger))
+ }
+
+ def stop(): Unit = this.synchronized {
+ worker.foreach { w =>
+ w.stop()
+ worker = None
+ }
+ }
+
+ private class Worker(
+ workDir: File,
+ logger: Logger,
+ globalLogger: Logger
+ ) {
+ logger.info("Starting vite dev server")
+ val command = Seq("yarn", "dev")
+ val process = Process(command, workDir).run(
+ ProcessLogger(globalLogger.info(_), globalLogger.error(_))
+ )
+
+ def stop(): Unit = {
+ logger.info("Stopping vite dev server")
+ process.destroy()
+ }
+ }
+
+ override def finalize() = stop()
+}
+
+object VitePlugin extends AutoPlugin {
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
+
+ object autoImport {
+ lazy val viteBuild = taskKey[File]("Vite build")
+ lazy val viteMonitoredFiles =
+ taskKey[Seq[File]]("Files monitored for vite build")
+ lazy val startViteDev = taskKey[Unit]("Start vite dev mode")
+ lazy val stopViteDev = taskKey[Unit]("Stop vite dev mode")
+ }
+
+ import autoImport._
+
+ private val viteDist =
+ SettingKey[File]("viteDist", "Vite dist directory", KeyRanks.Invisible)
+
+ private val viteDevServer = SettingKey[ViteDevServer](
+ "viteDevServer",
+ "Global vite dev server",
+ KeyRanks.Invisible
+ )
+
+ override def projectSettings = Seq(
+ viteDist := target.value / "vite",
+ viteDevServer := new ViteDevServer(),
+ startViteDev := {
+ val workDir = baseDirectory.value
+ val log = streams.value.log
+ val globalLog = state.value.globalLogging.full
+ val server = viteDevServer.value
+ server.start(workDir, log, globalLog)
+ },
+ stopViteDev := {
+ viteDevServer.value.stop()
+ },
+ viteMonitoredFiles := {
+ val baseGlob = baseDirectory.value.toGlob
+ def baseFiles(pattern: String): Glob = baseGlob / pattern
+ val viteConfigs =
+ FileTreeView.default.list(
+ List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
+ )
+ val linkerDirectory =
+ (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
+ val viteInputs = FileTreeView.default.list(
+ linkerDirectory.toGlob / "*.js"
+ )
+ (viteConfigs ++ viteInputs).map(_._1.toFile)
+ },
+ viteBuild := {
+ val s = streams.value
+ val dist = viteDist.value
+ val files = viteMonitoredFiles.value
+ // We depend on fullLinkJS
+ val _ = (Compile / fullLinkJS).value
+ def doBuild() = Process(
+ "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
+ baseDirectory.value
+ ) ! s.log
+ val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
+ doBuild()
+ Set(dist)
+ }
+ cachedFun(files.toSet).head
+ },
+ (onLoad in Global) := {
+ (onLoad in Global).value.compose(
+ _.addExitHook {
+ viteDevServer.value.stop()
+ }
+ )
+ }
+ )
+}
diff --git a/project/plugins.sbt b/project/plugins.sbt
index cf3faa4..8169a90 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,3 +1,5 @@
addIWProjects
addScalaJSSupport
+
+addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/deployment/staging/docker-compose.yml b/deployment/staging/docker-compose.yml
new file mode 100644
index 0000000..78a67e7
--- /dev/null
+++ b/deployment/staging/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3"
+services:
+ front:
+ image: docker.e-bs.cz/mdr-pdb-frontend-server:0.1.0-SNAPSHOT
+ environment:
+ APP_BASE: https://tc163.cmi.cz
+ ports:
+ - "19003:8080"
diff --git a/project/MockDataExport.scala b/project/MockDataExport.scala
index 73ee24a..bee338e 100644
--- a/project/MockDataExport.scala
+++ b/project/MockDataExport.scala
@@ -1,10 +1,13 @@
import sbt._
import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
import scala.xml.XML
import scala.xml.Elem
object MockDataExport extends AutoPlugin {
- override def trigger = noTrigger
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
object autoImport {
lazy val generateOrgDbData =
@@ -23,15 +26,26 @@
orgDbHeliosExportFile := orgDbExportDir.value / "HeliosData.xml",
generateOrgDbData := {
val file = orgDbOutputFile.value
- val heliosData =
- XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
- IO.write(
- file,
- userData(heliosData)
- )
- Seq(file)
- }
- // TODO: cached run & auto run on fastLinkJS
+ val heliosFile = orgDbHeliosExportFile.value
+ def doExport() = {
+ val heliosData =
+ XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
+ IO.write(
+ file,
+ userData(heliosData)
+ )
+ }
+ val cachedFun =
+ FileFunction.cached(streams.value.cacheDirectory / "orgdb_export") {
+ _ =>
+ doExport()
+ Set(file)
+ }
+ cachedFun(Set(heliosFile)).toSeq
+ },
+ (Compile / fastLinkJS) := (Compile / fastLinkJS)
+ .dependsOn(generateOrgDbData)
+ .value
)
def escaped(v: String): String = v.replaceAll("\"", "\\\"")
diff --git a/project/VitePlugin.scala b/project/VitePlugin.scala
new file mode 100644
index 0000000..1a20989
--- /dev/null
+++ b/project/VitePlugin.scala
@@ -0,0 +1,118 @@
+import sbt._
+import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
+import scala.sys.process._
+import sbt.nio.file.FileTreeView
+
+class ViteDevServer() {
+ private var worker: Option[Worker] = None
+
+ def start(workDir: File, logger: Logger, globalLogger: Logger): Unit =
+ this.synchronized {
+ stop()
+ worker = Some(new Worker(workDir, logger, globalLogger))
+ }
+
+ def stop(): Unit = this.synchronized {
+ worker.foreach { w =>
+ w.stop()
+ worker = None
+ }
+ }
+
+ private class Worker(
+ workDir: File,
+ logger: Logger,
+ globalLogger: Logger
+ ) {
+ logger.info("Starting vite dev server")
+ val command = Seq("yarn", "dev")
+ val process = Process(command, workDir).run(
+ ProcessLogger(globalLogger.info(_), globalLogger.error(_))
+ )
+
+ def stop(): Unit = {
+ logger.info("Stopping vite dev server")
+ process.destroy()
+ }
+ }
+
+ override def finalize() = stop()
+}
+
+object VitePlugin extends AutoPlugin {
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
+
+ object autoImport {
+ lazy val viteBuild = taskKey[File]("Vite build")
+ lazy val viteMonitoredFiles =
+ taskKey[Seq[File]]("Files monitored for vite build")
+ lazy val startViteDev = taskKey[Unit]("Start vite dev mode")
+ lazy val stopViteDev = taskKey[Unit]("Stop vite dev mode")
+ }
+
+ import autoImport._
+
+ private val viteDist =
+ SettingKey[File]("viteDist", "Vite dist directory", KeyRanks.Invisible)
+
+ private val viteDevServer = SettingKey[ViteDevServer](
+ "viteDevServer",
+ "Global vite dev server",
+ KeyRanks.Invisible
+ )
+
+ override def projectSettings = Seq(
+ viteDist := target.value / "vite",
+ viteDevServer := new ViteDevServer(),
+ startViteDev := {
+ val workDir = baseDirectory.value
+ val log = streams.value.log
+ val globalLog = state.value.globalLogging.full
+ val server = viteDevServer.value
+ server.start(workDir, log, globalLog)
+ },
+ stopViteDev := {
+ viteDevServer.value.stop()
+ },
+ viteMonitoredFiles := {
+ val baseGlob = baseDirectory.value.toGlob
+ def baseFiles(pattern: String): Glob = baseGlob / pattern
+ val viteConfigs =
+ FileTreeView.default.list(
+ List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
+ )
+ val linkerDirectory =
+ (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
+ val viteInputs = FileTreeView.default.list(
+ linkerDirectory.toGlob / "*.js"
+ )
+ (viteConfigs ++ viteInputs).map(_._1.toFile)
+ },
+ viteBuild := {
+ val s = streams.value
+ val dist = viteDist.value
+ val files = viteMonitoredFiles.value
+ // We depend on fullLinkJS
+ val _ = (Compile / fullLinkJS).value
+ def doBuild() = Process(
+ "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
+ baseDirectory.value
+ ) ! s.log
+ val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
+ doBuild()
+ Set(dist)
+ }
+ cachedFun(files.toSet).head
+ },
+ (onLoad in Global) := {
+ (onLoad in Global).value.compose(
+ _.addExitHook {
+ viteDevServer.value.stop()
+ }
+ )
+ }
+ )
+}
diff --git a/project/plugins.sbt b/project/plugins.sbt
index cf3faa4..8169a90 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,3 +1,5 @@
addIWProjects
addScalaJSSupport
+
+addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml
new file mode 100644
index 0000000..08867d4
--- /dev/null
+++ b/server/src/main/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/deployment/staging/docker-compose.yml b/deployment/staging/docker-compose.yml
new file mode 100644
index 0000000..78a67e7
--- /dev/null
+++ b/deployment/staging/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3"
+services:
+ front:
+ image: docker.e-bs.cz/mdr-pdb-frontend-server:0.1.0-SNAPSHOT
+ environment:
+ APP_BASE: https://tc163.cmi.cz
+ ports:
+ - "19003:8080"
diff --git a/project/MockDataExport.scala b/project/MockDataExport.scala
index 73ee24a..bee338e 100644
--- a/project/MockDataExport.scala
+++ b/project/MockDataExport.scala
@@ -1,10 +1,13 @@
import sbt._
import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
import scala.xml.XML
import scala.xml.Elem
object MockDataExport extends AutoPlugin {
- override def trigger = noTrigger
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
object autoImport {
lazy val generateOrgDbData =
@@ -23,15 +26,26 @@
orgDbHeliosExportFile := orgDbExportDir.value / "HeliosData.xml",
generateOrgDbData := {
val file = orgDbOutputFile.value
- val heliosData =
- XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
- IO.write(
- file,
- userData(heliosData)
- )
- Seq(file)
- }
- // TODO: cached run & auto run on fastLinkJS
+ val heliosFile = orgDbHeliosExportFile.value
+ def doExport() = {
+ val heliosData =
+ XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
+ IO.write(
+ file,
+ userData(heliosData)
+ )
+ }
+ val cachedFun =
+ FileFunction.cached(streams.value.cacheDirectory / "orgdb_export") {
+ _ =>
+ doExport()
+ Set(file)
+ }
+ cachedFun(Set(heliosFile)).toSeq
+ },
+ (Compile / fastLinkJS) := (Compile / fastLinkJS)
+ .dependsOn(generateOrgDbData)
+ .value
)
def escaped(v: String): String = v.replaceAll("\"", "\\\"")
diff --git a/project/VitePlugin.scala b/project/VitePlugin.scala
new file mode 100644
index 0000000..1a20989
--- /dev/null
+++ b/project/VitePlugin.scala
@@ -0,0 +1,118 @@
+import sbt._
+import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
+import scala.sys.process._
+import sbt.nio.file.FileTreeView
+
+class ViteDevServer() {
+ private var worker: Option[Worker] = None
+
+ def start(workDir: File, logger: Logger, globalLogger: Logger): Unit =
+ this.synchronized {
+ stop()
+ worker = Some(new Worker(workDir, logger, globalLogger))
+ }
+
+ def stop(): Unit = this.synchronized {
+ worker.foreach { w =>
+ w.stop()
+ worker = None
+ }
+ }
+
+ private class Worker(
+ workDir: File,
+ logger: Logger,
+ globalLogger: Logger
+ ) {
+ logger.info("Starting vite dev server")
+ val command = Seq("yarn", "dev")
+ val process = Process(command, workDir).run(
+ ProcessLogger(globalLogger.info(_), globalLogger.error(_))
+ )
+
+ def stop(): Unit = {
+ logger.info("Stopping vite dev server")
+ process.destroy()
+ }
+ }
+
+ override def finalize() = stop()
+}
+
+object VitePlugin extends AutoPlugin {
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
+
+ object autoImport {
+ lazy val viteBuild = taskKey[File]("Vite build")
+ lazy val viteMonitoredFiles =
+ taskKey[Seq[File]]("Files monitored for vite build")
+ lazy val startViteDev = taskKey[Unit]("Start vite dev mode")
+ lazy val stopViteDev = taskKey[Unit]("Stop vite dev mode")
+ }
+
+ import autoImport._
+
+ private val viteDist =
+ SettingKey[File]("viteDist", "Vite dist directory", KeyRanks.Invisible)
+
+ private val viteDevServer = SettingKey[ViteDevServer](
+ "viteDevServer",
+ "Global vite dev server",
+ KeyRanks.Invisible
+ )
+
+ override def projectSettings = Seq(
+ viteDist := target.value / "vite",
+ viteDevServer := new ViteDevServer(),
+ startViteDev := {
+ val workDir = baseDirectory.value
+ val log = streams.value.log
+ val globalLog = state.value.globalLogging.full
+ val server = viteDevServer.value
+ server.start(workDir, log, globalLog)
+ },
+ stopViteDev := {
+ viteDevServer.value.stop()
+ },
+ viteMonitoredFiles := {
+ val baseGlob = baseDirectory.value.toGlob
+ def baseFiles(pattern: String): Glob = baseGlob / pattern
+ val viteConfigs =
+ FileTreeView.default.list(
+ List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
+ )
+ val linkerDirectory =
+ (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
+ val viteInputs = FileTreeView.default.list(
+ linkerDirectory.toGlob / "*.js"
+ )
+ (viteConfigs ++ viteInputs).map(_._1.toFile)
+ },
+ viteBuild := {
+ val s = streams.value
+ val dist = viteDist.value
+ val files = viteMonitoredFiles.value
+ // We depend on fullLinkJS
+ val _ = (Compile / fullLinkJS).value
+ def doBuild() = Process(
+ "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
+ baseDirectory.value
+ ) ! s.log
+ val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
+ doBuild()
+ Set(dist)
+ }
+ cachedFun(files.toSet).head
+ },
+ (onLoad in Global) := {
+ (onLoad in Global).value.compose(
+ _.addExitHook {
+ viteDevServer.value.stop()
+ }
+ )
+ }
+ )
+}
diff --git a/project/plugins.sbt b/project/plugins.sbt
index cf3faa4..8169a90 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,3 +1,5 @@
addIWProjects
addScalaJSSupport
+
+addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml
new file mode 100644
index 0000000..08867d4
--- /dev/null
+++ b/server/src/main/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala
new file mode 100644
index 0000000..6b00719
--- /dev/null
+++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala
@@ -0,0 +1,171 @@
+package mdr.pdb.server
+
+import zio.*
+
+import zio.interop.catz.*
+import zio.interop.catz.implicits.{*, given}
+
+import org.http4s.*
+import org.http4s.dsl.Http4sDsl
+import org.http4s.dsl.io.*
+import org.http4s.implicits.{*, given}
+import org.http4s.server.Router
+import org.http4s.syntax.all.{*, given}
+
+import sttp.tapir.*
+import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
+
+import org.pac4j.http4s.*
+
+import org.pac4j.core.authorization.generator.AuthorizationGenerator
+import org.pac4j.core.client.Clients
+import org.pac4j.core.config.Config
+import org.pac4j.core.context.WebContext
+import org.pac4j.core.context.session.SessionStore
+import org.pac4j.core.profile.CommonProfile
+import org.pac4j.core.profile.UserProfile
+import org.pac4j.oidc.client.OidcClient
+import org.pac4j.oidc.config.OidcConfiguration
+
+import scala.concurrent.duration.{*, given}
+import java.util.Optional
+
+trait HttpApplication {
+ def routes(): UIO[HttpRoutes[AppTask]]
+}
+
+object HttpApplicationLive {
+ import zio.config.*
+
+ case class AppConfig(appPath: String, urlBase: String)
+
+ val appConfigDesc: ConfigDescriptor[AppConfig] =
+ import ConfigDescriptor.*
+ nested("APP")(
+ string("PATH") zip string("BASE").default("http://localhost:8080")
+ ).to[AppConfig]
+
+ def layer(
+ contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask]
+ ): RLayer[System, HttpApplication] =
+ val configLayer = ZConfig.fromSystemEnv(
+ appConfigDesc,
+ keyDelimiter = Some('_'),
+ valueDelimiter = Some(',')
+ )
+ val appLayer =
+ (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication]
+ configLayer >>> appLayer
+}
+
+import HttpApplicationLive.AppConfig
+
+case class HttpApplicationLive(
+ config: AppConfig,
+ contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask]
+) extends HttpApplication:
+ val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {}
+ import dsl.*
+
+ // TODO: zio-config
+ def oidcClient(): OidcClient = {
+ val oidcConfiguration = new OidcConfiguration()
+ oidcConfiguration.setClientId("mdrpdbtest")
+ oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02")
+ oidcConfiguration.setDiscoveryURI(
+ "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration"
+ )
+ oidcConfiguration.setUseNonce(true)
+ // oidcConfiguration.addCustomParam("prompt", "consent")
+ val oidcClient = new OidcClient(oidcConfiguration)
+
+ val authorizationGenerator = new AuthorizationGenerator {
+ override def generate(
+ context: WebContext,
+ sessionStore: SessionStore,
+ profile: UserProfile
+ ): Optional[UserProfile] = {
+ profile.addRole("ROLE_ADMIN")
+ Optional.of(profile)
+ }
+ }
+ oidcClient.setAuthorizationGenerator(authorizationGenerator)
+ oidcClient
+ }
+
+ val pac4jConfig =
+ val clients =
+ Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient())
+ val conf = org.pac4j.core.config.Config(clients)
+ conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]())
+ conf.setSessionStore(Http4sCacheSessionStore[AppTask]())
+ conf
+
+ private val sessionConfig = SessionConfig(
+ cookieName = "session",
+ mkCookie = ResponseCookie(_, _, path = Some("/")),
+ secret = "This is a secret",
+ maxAge = 5.minutes
+ )
+
+ val callbackService =
+ CallbackService[AppTask](pac4jConfig, contextBuilder)
+
+ val localLogoutService = LogoutService[AppTask](
+ pac4jConfig,
+ contextBuilder,
+ Some(config.urlBase),
+ destroySession = true
+ )
+ val centralLogoutService = LogoutService[AppTask](
+ pac4jConfig,
+ contextBuilder,
+ defaultUrl = Some(config.urlBase),
+ logoutUrlPattern = Some(s"${config.urlBase}.*"),
+ localLogout = false,
+ destroySession = true,
+ centralLogout = true
+ )
+
+ def filesService(appPath: String): HttpRoutes[AppTask] =
+ ZHttp4sServerInterpreter()
+ .from(
+ List(
+ fileGetServerEndpoint("pdb" / "app")(
+ s"${appPath}/index.html"
+ ),
+ filesGetServerEndpoint("pdb")(appPath)
+ )
+ )
+ .toRoutes
+
+ val smMW = Session.sessionManagement[AppTask](sessionConfig)
+ val sfMW = SecurityFilterMiddleware
+ .securityFilter[AppTask](pac4jConfig, contextBuilder)
+
+ def authedProtectedPages(appPath: String): HttpRoutes[AppTask] =
+ smMW.compose(sfMW)(
+ filesService(appPath).local(
+ (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req
+ )
+ )
+
+ val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of {
+ case req @ GET -> Root / "callback" =>
+ callbackService.callback(req)
+ case req @ POST -> Root / "callback" =>
+ callbackService.callback(req)
+ case req @ GET -> Root / "logout" =>
+ localLogoutService.logout(req)
+ case req @ GET -> Root / "centralLogout" =>
+ centralLogoutService.logout(req)
+ }
+
+ def httpApp(appPath: String): HttpRoutes[AppTask] =
+ Router(
+ "/mdr/pdb/auth" -> smMW(rootRoutes),
+ "/mdr" -> authedProtectedPages(appPath)
+ )
+
+ override def routes(): UIO[HttpRoutes[AppTask]] =
+ ZIO.succeed(httpApp(config.appPath))
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/deployment/staging/docker-compose.yml b/deployment/staging/docker-compose.yml
new file mode 100644
index 0000000..78a67e7
--- /dev/null
+++ b/deployment/staging/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3"
+services:
+ front:
+ image: docker.e-bs.cz/mdr-pdb-frontend-server:0.1.0-SNAPSHOT
+ environment:
+ APP_BASE: https://tc163.cmi.cz
+ ports:
+ - "19003:8080"
diff --git a/project/MockDataExport.scala b/project/MockDataExport.scala
index 73ee24a..bee338e 100644
--- a/project/MockDataExport.scala
+++ b/project/MockDataExport.scala
@@ -1,10 +1,13 @@
import sbt._
import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
import scala.xml.XML
import scala.xml.Elem
object MockDataExport extends AutoPlugin {
- override def trigger = noTrigger
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
object autoImport {
lazy val generateOrgDbData =
@@ -23,15 +26,26 @@
orgDbHeliosExportFile := orgDbExportDir.value / "HeliosData.xml",
generateOrgDbData := {
val file = orgDbOutputFile.value
- val heliosData =
- XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
- IO.write(
- file,
- userData(heliosData)
- )
- Seq(file)
- }
- // TODO: cached run & auto run on fastLinkJS
+ val heliosFile = orgDbHeliosExportFile.value
+ def doExport() = {
+ val heliosData =
+ XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
+ IO.write(
+ file,
+ userData(heliosData)
+ )
+ }
+ val cachedFun =
+ FileFunction.cached(streams.value.cacheDirectory / "orgdb_export") {
+ _ =>
+ doExport()
+ Set(file)
+ }
+ cachedFun(Set(heliosFile)).toSeq
+ },
+ (Compile / fastLinkJS) := (Compile / fastLinkJS)
+ .dependsOn(generateOrgDbData)
+ .value
)
def escaped(v: String): String = v.replaceAll("\"", "\\\"")
diff --git a/project/VitePlugin.scala b/project/VitePlugin.scala
new file mode 100644
index 0000000..1a20989
--- /dev/null
+++ b/project/VitePlugin.scala
@@ -0,0 +1,118 @@
+import sbt._
+import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
+import scala.sys.process._
+import sbt.nio.file.FileTreeView
+
+class ViteDevServer() {
+ private var worker: Option[Worker] = None
+
+ def start(workDir: File, logger: Logger, globalLogger: Logger): Unit =
+ this.synchronized {
+ stop()
+ worker = Some(new Worker(workDir, logger, globalLogger))
+ }
+
+ def stop(): Unit = this.synchronized {
+ worker.foreach { w =>
+ w.stop()
+ worker = None
+ }
+ }
+
+ private class Worker(
+ workDir: File,
+ logger: Logger,
+ globalLogger: Logger
+ ) {
+ logger.info("Starting vite dev server")
+ val command = Seq("yarn", "dev")
+ val process = Process(command, workDir).run(
+ ProcessLogger(globalLogger.info(_), globalLogger.error(_))
+ )
+
+ def stop(): Unit = {
+ logger.info("Stopping vite dev server")
+ process.destroy()
+ }
+ }
+
+ override def finalize() = stop()
+}
+
+object VitePlugin extends AutoPlugin {
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
+
+ object autoImport {
+ lazy val viteBuild = taskKey[File]("Vite build")
+ lazy val viteMonitoredFiles =
+ taskKey[Seq[File]]("Files monitored for vite build")
+ lazy val startViteDev = taskKey[Unit]("Start vite dev mode")
+ lazy val stopViteDev = taskKey[Unit]("Stop vite dev mode")
+ }
+
+ import autoImport._
+
+ private val viteDist =
+ SettingKey[File]("viteDist", "Vite dist directory", KeyRanks.Invisible)
+
+ private val viteDevServer = SettingKey[ViteDevServer](
+ "viteDevServer",
+ "Global vite dev server",
+ KeyRanks.Invisible
+ )
+
+ override def projectSettings = Seq(
+ viteDist := target.value / "vite",
+ viteDevServer := new ViteDevServer(),
+ startViteDev := {
+ val workDir = baseDirectory.value
+ val log = streams.value.log
+ val globalLog = state.value.globalLogging.full
+ val server = viteDevServer.value
+ server.start(workDir, log, globalLog)
+ },
+ stopViteDev := {
+ viteDevServer.value.stop()
+ },
+ viteMonitoredFiles := {
+ val baseGlob = baseDirectory.value.toGlob
+ def baseFiles(pattern: String): Glob = baseGlob / pattern
+ val viteConfigs =
+ FileTreeView.default.list(
+ List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
+ )
+ val linkerDirectory =
+ (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
+ val viteInputs = FileTreeView.default.list(
+ linkerDirectory.toGlob / "*.js"
+ )
+ (viteConfigs ++ viteInputs).map(_._1.toFile)
+ },
+ viteBuild := {
+ val s = streams.value
+ val dist = viteDist.value
+ val files = viteMonitoredFiles.value
+ // We depend on fullLinkJS
+ val _ = (Compile / fullLinkJS).value
+ def doBuild() = Process(
+ "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
+ baseDirectory.value
+ ) ! s.log
+ val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
+ doBuild()
+ Set(dist)
+ }
+ cachedFun(files.toSet).head
+ },
+ (onLoad in Global) := {
+ (onLoad in Global).value.compose(
+ _.addExitHook {
+ viteDevServer.value.stop()
+ }
+ )
+ }
+ )
+}
diff --git a/project/plugins.sbt b/project/plugins.sbt
index cf3faa4..8169a90 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,3 +1,5 @@
addIWProjects
addScalaJSSupport
+
+addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml
new file mode 100644
index 0000000..08867d4
--- /dev/null
+++ b/server/src/main/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala
new file mode 100644
index 0000000..6b00719
--- /dev/null
+++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala
@@ -0,0 +1,171 @@
+package mdr.pdb.server
+
+import zio.*
+
+import zio.interop.catz.*
+import zio.interop.catz.implicits.{*, given}
+
+import org.http4s.*
+import org.http4s.dsl.Http4sDsl
+import org.http4s.dsl.io.*
+import org.http4s.implicits.{*, given}
+import org.http4s.server.Router
+import org.http4s.syntax.all.{*, given}
+
+import sttp.tapir.*
+import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
+
+import org.pac4j.http4s.*
+
+import org.pac4j.core.authorization.generator.AuthorizationGenerator
+import org.pac4j.core.client.Clients
+import org.pac4j.core.config.Config
+import org.pac4j.core.context.WebContext
+import org.pac4j.core.context.session.SessionStore
+import org.pac4j.core.profile.CommonProfile
+import org.pac4j.core.profile.UserProfile
+import org.pac4j.oidc.client.OidcClient
+import org.pac4j.oidc.config.OidcConfiguration
+
+import scala.concurrent.duration.{*, given}
+import java.util.Optional
+
+trait HttpApplication {
+ def routes(): UIO[HttpRoutes[AppTask]]
+}
+
+object HttpApplicationLive {
+ import zio.config.*
+
+ case class AppConfig(appPath: String, urlBase: String)
+
+ val appConfigDesc: ConfigDescriptor[AppConfig] =
+ import ConfigDescriptor.*
+ nested("APP")(
+ string("PATH") zip string("BASE").default("http://localhost:8080")
+ ).to[AppConfig]
+
+ def layer(
+ contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask]
+ ): RLayer[System, HttpApplication] =
+ val configLayer = ZConfig.fromSystemEnv(
+ appConfigDesc,
+ keyDelimiter = Some('_'),
+ valueDelimiter = Some(',')
+ )
+ val appLayer =
+ (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication]
+ configLayer >>> appLayer
+}
+
+import HttpApplicationLive.AppConfig
+
+case class HttpApplicationLive(
+ config: AppConfig,
+ contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask]
+) extends HttpApplication:
+ val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {}
+ import dsl.*
+
+ // TODO: zio-config
+ def oidcClient(): OidcClient = {
+ val oidcConfiguration = new OidcConfiguration()
+ oidcConfiguration.setClientId("mdrpdbtest")
+ oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02")
+ oidcConfiguration.setDiscoveryURI(
+ "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration"
+ )
+ oidcConfiguration.setUseNonce(true)
+ // oidcConfiguration.addCustomParam("prompt", "consent")
+ val oidcClient = new OidcClient(oidcConfiguration)
+
+ val authorizationGenerator = new AuthorizationGenerator {
+ override def generate(
+ context: WebContext,
+ sessionStore: SessionStore,
+ profile: UserProfile
+ ): Optional[UserProfile] = {
+ profile.addRole("ROLE_ADMIN")
+ Optional.of(profile)
+ }
+ }
+ oidcClient.setAuthorizationGenerator(authorizationGenerator)
+ oidcClient
+ }
+
+ val pac4jConfig =
+ val clients =
+ Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient())
+ val conf = org.pac4j.core.config.Config(clients)
+ conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]())
+ conf.setSessionStore(Http4sCacheSessionStore[AppTask]())
+ conf
+
+ private val sessionConfig = SessionConfig(
+ cookieName = "session",
+ mkCookie = ResponseCookie(_, _, path = Some("/")),
+ secret = "This is a secret",
+ maxAge = 5.minutes
+ )
+
+ val callbackService =
+ CallbackService[AppTask](pac4jConfig, contextBuilder)
+
+ val localLogoutService = LogoutService[AppTask](
+ pac4jConfig,
+ contextBuilder,
+ Some(config.urlBase),
+ destroySession = true
+ )
+ val centralLogoutService = LogoutService[AppTask](
+ pac4jConfig,
+ contextBuilder,
+ defaultUrl = Some(config.urlBase),
+ logoutUrlPattern = Some(s"${config.urlBase}.*"),
+ localLogout = false,
+ destroySession = true,
+ centralLogout = true
+ )
+
+ def filesService(appPath: String): HttpRoutes[AppTask] =
+ ZHttp4sServerInterpreter()
+ .from(
+ List(
+ fileGetServerEndpoint("pdb" / "app")(
+ s"${appPath}/index.html"
+ ),
+ filesGetServerEndpoint("pdb")(appPath)
+ )
+ )
+ .toRoutes
+
+ val smMW = Session.sessionManagement[AppTask](sessionConfig)
+ val sfMW = SecurityFilterMiddleware
+ .securityFilter[AppTask](pac4jConfig, contextBuilder)
+
+ def authedProtectedPages(appPath: String): HttpRoutes[AppTask] =
+ smMW.compose(sfMW)(
+ filesService(appPath).local(
+ (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req
+ )
+ )
+
+ val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of {
+ case req @ GET -> Root / "callback" =>
+ callbackService.callback(req)
+ case req @ POST -> Root / "callback" =>
+ callbackService.callback(req)
+ case req @ GET -> Root / "logout" =>
+ localLogoutService.logout(req)
+ case req @ GET -> Root / "centralLogout" =>
+ centralLogoutService.logout(req)
+ }
+
+ def httpApp(appPath: String): HttpRoutes[AppTask] =
+ Router(
+ "/mdr/pdb/auth" -> smMW(rootRoutes),
+ "/mdr" -> authedProtectedPages(appPath)
+ )
+
+ override def routes(): UIO[HttpRoutes[AppTask]] =
+ ZIO.succeed(httpApp(config.appPath))
diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala
new file mode 100644
index 0000000..1d78408
--- /dev/null
+++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala
@@ -0,0 +1,50 @@
+package mdr.pdb.server
+
+import zio.*
+import zio.interop.catz.*
+import zio.interop.catz.implicits.{*, given}
+import org.http4s.blaze.server.BlazeServerBuilder
+import org.http4s.HttpRoutes
+
+trait HttpServer:
+ def serve(): UIO[ExitCode]
+
+object BlazeHttpServer {
+ import zio.config.*
+
+ case class BlazeServerConf(host: String, port: Int)
+
+ val blazeServerConfig: ConfigDescriptor[BlazeServerConf] =
+ import ConfigDescriptor.*
+ nested("BLAZE")(
+ string("HOST").default("localhost") zip int("PORT").default(8080)
+ ).to[BlazeServerConf]
+
+ val layer: RLayer[System & HttpApplication, HttpServer] =
+ val configLayer = ZConfig.fromSystemEnv(
+ blazeServerConfig,
+ keyDelimiter = Some('_'),
+ valueDelimiter = Some(',')
+ )
+ val routesLayer = ZLayer
+ .environment[HttpApplication]
+ .flatMap(a => ZLayer.fromZIO(a.get.routes()))
+ val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer]
+ (configLayer ++ routesLayer) >>> blazeLayer
+}
+
+import BlazeHttpServer.*
+
+case class BlazeHttpServer(
+ config: BlazeServerConf,
+ httpApp: HttpRoutes[AppTask]
+) extends HttpServer:
+ override def serve(): UIO[ExitCode] =
+ BlazeServerBuilder[AppTask]
+ .bindHttp(config.port, config.host)
+ .withHttpApp(httpApp.orNotFound)
+ .serve
+ .compile
+ .drain
+ .fold(_ => ExitCode.failure, _ => ExitCode.success)
+ .provideEnvironment(ZEnvironment.default)
diff --git a/build.sbt b/build.sbt
index 39c0f95..442e737 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,5 @@
import org.scalajs.linker.interface.ModuleSplitStyle
import scala.sys.process._
-import sbt.nio.file.FileTreeView
import com.typesafe.sbt.packager.docker._
import NativePackagerHelper._
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}
@@ -9,32 +8,22 @@
ThisBuild / scalaVersion := scala3Version
-// TODO: integrate vite build and Docker publishing
-// Taken from mdr-app, moving to plugin would be nice
-lazy val viteBuild = taskKey[File]("Vite build")
-lazy val viteMonitoredFiles =
- taskKey[Seq[File]]("Files monitored for vite build")
-lazy val viteDist = settingKey[File]("Vite dist directory")
-lazy val caddyFile = settingKey[File]("Caddyfile for caddy docker image")
-
lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
lazy val app = (project in file("app"))
- .enablePlugins(ScalaJSPlugin, MockDataExport, DockerPlugin)
+ .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport)
.settings(
IWDeps.useZIO(Test),
IWDeps.laminar,
IWDeps.zioJson,
- libraryDependencies ++= Seq(
- "com.raquo" %%% "waypoint" % "0.5.0",
- "be.doeraene" %%% "url-dsl" % "0.4.0",
- "io.laminext" %%% "core" % IWVersions.laminar,
- "io.laminext" %%% "ui" % IWVersions.laminar,
- "io.laminext" %%% "tailwind" % IWVersions.laminar,
- "io.laminext" %%% "validation-core" % IWVersions.laminar
- )
+ IWDeps.waypoint,
+ IWDeps.urlDsl,
+ IWDeps.laminextCore,
+ IWDeps.laminextUI,
+ IWDeps.laminextTailwind,
+ IWDeps.laminextValidationCore
)
.settings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
@@ -44,66 +33,38 @@
scalaJSLinkerConfig ~= { _.withSourceMap(false) },
scalaJSUseMainModuleInitializer := true
)
- .settings(
- caddyFile := baseDirectory.value / "Caddyfile",
- dockerRepository := Some("docker.e-bs.cz"),
- dockerUsername := Some("cmi/posuzovani-mdr-pdb"),
- dockerExposedPorts += 80,
- Docker / mappings ++= directory(viteBuild.value),
- Docker / mappings += caddyFile.value -> "Caddyfile",
- dockerCommands := Seq(
- Cmd("FROM", "caddy:2.4.6"),
- Cmd("COPY", "Caddyfile", "/etc/caddy/Caddyfile"),
- Cmd("COPY", "vite", "/srv/mdr/pdb")
- ),
- viteDist := target.value / "vite",
- viteMonitoredFiles := {
- val baseGlob = baseDirectory.value.toGlob
- def baseFiles(pattern: String): Glob = baseGlob / pattern
- val viteConfigs =
- FileTreeView.default.list(
- List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
- )
- val linkerDirectory =
- (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
- val viteInputs = FileTreeView.default.list(
- linkerDirectory.toGlob / "*.js"
- )
- (viteConfigs ++ viteInputs).map(_._1.toFile)
- },
- viteBuild := {
- val s = streams.value
- val dist = viteDist.value
- val files = viteMonitoredFiles.value
- // We depend on fullLinkJS
- val _ = (Compile / fullLinkJS).value
- def doBuild() = Process(
- "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
- baseDirectory.value
- ) ! s.log
- val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
- doBuild()
- Set(dist)
- }
- cachedFun(files.toSet).head
- }
- )
.dependsOn(core.js)
-lazy val server = (project in file("server")).settings(
- IWDeps.useZIO(),
- libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.23.10",
- "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio" % "0.20.0-M10",
- "com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "0.20.0-M10",
- "dev.zio" %% "zio-interop-cats" % "3.3.0-RC2",
- "dev.zio" %% "zio-logging-slf4j" % "2.0.0-RC5",
- "ch.qos.logback" % "logback-classic" % "1.2.10" % Runtime,
- "org.pac4j" %% "http4s-pac4j" % "4.0.0",
- "org.pac4j" % "pac4j-oidc" % "5.2.0"
+lazy val server = (project in file("server"))
+ .enablePlugins(DockerPlugin, JavaServerAppPackaging)
+ .settings(
+ IWDeps.useZIO(),
+ IWDeps.zioConfig,
+ IWDeps.zioConfigTypesafe,
+ IWDeps.zioConfigMagnolia,
+ IWDeps.zioLoggingSlf4j,
+ IWDeps.zioInteropCats,
+ IWDeps.tapirCore,
+ IWDeps.tapirZIO,
+ IWDeps.tapirZIOHttp4sServer,
+ IWDeps.http4sBlazeServer,
+ IWDeps.logbackClassic,
+ IWDeps.http4sPac4J,
+ IWDeps.pac4jOIDC,
+ Docker / mappings ++= directory((app / viteBuild).value).map {
+ case (f, p) => f -> s"/opt/docker/${p}"
+ },
+ dockerBaseImage := "openjdk:11",
+ dockerRepository := Some("docker.e-bs.cz"),
+ dockerExposedPorts := Seq(8080),
+ Docker / packageName := "mdr-pdb-frontend-server",
+ dockerEnvVars := Map(
+ "BLAZE_HOST" -> "0.0.0.0",
+ "BLAZE_PORT" -> "8080",
+ "APP_PATH" -> "/opt/docker/vite"
+ ),
+ reStart / envVars := Map("APP_PATH" -> "../app/target/vite")
)
-)
lazy val root = (project in file("."))
.settings(name := "mdr-personnel-db", publish / skip := true)
diff --git a/deployment/staging/.env b/deployment/staging/.env
new file mode 100644
index 0000000..ac7c844
--- /dev/null
+++ b/deployment/staging/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=staging_mdrpdb
diff --git a/deployment/staging/docker-compose.yml b/deployment/staging/docker-compose.yml
new file mode 100644
index 0000000..78a67e7
--- /dev/null
+++ b/deployment/staging/docker-compose.yml
@@ -0,0 +1,8 @@
+version: "3"
+services:
+ front:
+ image: docker.e-bs.cz/mdr-pdb-frontend-server:0.1.0-SNAPSHOT
+ environment:
+ APP_BASE: https://tc163.cmi.cz
+ ports:
+ - "19003:8080"
diff --git a/project/MockDataExport.scala b/project/MockDataExport.scala
index 73ee24a..bee338e 100644
--- a/project/MockDataExport.scala
+++ b/project/MockDataExport.scala
@@ -1,10 +1,13 @@
import sbt._
import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
import scala.xml.XML
import scala.xml.Elem
object MockDataExport extends AutoPlugin {
- override def trigger = noTrigger
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
object autoImport {
lazy val generateOrgDbData =
@@ -23,15 +26,26 @@
orgDbHeliosExportFile := orgDbExportDir.value / "HeliosData.xml",
generateOrgDbData := {
val file = orgDbOutputFile.value
- val heliosData =
- XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
- IO.write(
- file,
- userData(heliosData)
- )
- Seq(file)
- }
- // TODO: cached run & auto run on fastLinkJS
+ val heliosFile = orgDbHeliosExportFile.value
+ def doExport() = {
+ val heliosData =
+ XML.loadFile(orgDbHeliosExportFile.value.getAbsolutePath())
+ IO.write(
+ file,
+ userData(heliosData)
+ )
+ }
+ val cachedFun =
+ FileFunction.cached(streams.value.cacheDirectory / "orgdb_export") {
+ _ =>
+ doExport()
+ Set(file)
+ }
+ cachedFun(Set(heliosFile)).toSeq
+ },
+ (Compile / fastLinkJS) := (Compile / fastLinkJS)
+ .dependsOn(generateOrgDbData)
+ .value
)
def escaped(v: String): String = v.replaceAll("\"", "\\\"")
diff --git a/project/VitePlugin.scala b/project/VitePlugin.scala
new file mode 100644
index 0000000..1a20989
--- /dev/null
+++ b/project/VitePlugin.scala
@@ -0,0 +1,118 @@
+import sbt._
+import Keys._
+import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+import org.scalajs.sbtplugin.ScalaJSPlugin
+import scala.sys.process._
+import sbt.nio.file.FileTreeView
+
+class ViteDevServer() {
+ private var worker: Option[Worker] = None
+
+ def start(workDir: File, logger: Logger, globalLogger: Logger): Unit =
+ this.synchronized {
+ stop()
+ worker = Some(new Worker(workDir, logger, globalLogger))
+ }
+
+ def stop(): Unit = this.synchronized {
+ worker.foreach { w =>
+ w.stop()
+ worker = None
+ }
+ }
+
+ private class Worker(
+ workDir: File,
+ logger: Logger,
+ globalLogger: Logger
+ ) {
+ logger.info("Starting vite dev server")
+ val command = Seq("yarn", "dev")
+ val process = Process(command, workDir).run(
+ ProcessLogger(globalLogger.info(_), globalLogger.error(_))
+ )
+
+ def stop(): Unit = {
+ logger.info("Stopping vite dev server")
+ process.destroy()
+ }
+ }
+
+ override def finalize() = stop()
+}
+
+object VitePlugin extends AutoPlugin {
+ override lazy val requires = ScalaJSPlugin
+ override lazy val trigger = noTrigger
+
+ object autoImport {
+ lazy val viteBuild = taskKey[File]("Vite build")
+ lazy val viteMonitoredFiles =
+ taskKey[Seq[File]]("Files monitored for vite build")
+ lazy val startViteDev = taskKey[Unit]("Start vite dev mode")
+ lazy val stopViteDev = taskKey[Unit]("Stop vite dev mode")
+ }
+
+ import autoImport._
+
+ private val viteDist =
+ SettingKey[File]("viteDist", "Vite dist directory", KeyRanks.Invisible)
+
+ private val viteDevServer = SettingKey[ViteDevServer](
+ "viteDevServer",
+ "Global vite dev server",
+ KeyRanks.Invisible
+ )
+
+ override def projectSettings = Seq(
+ viteDist := target.value / "vite",
+ viteDevServer := new ViteDevServer(),
+ startViteDev := {
+ val workDir = baseDirectory.value
+ val log = streams.value.log
+ val globalLog = state.value.globalLogging.full
+ val server = viteDevServer.value
+ server.start(workDir, log, globalLog)
+ },
+ stopViteDev := {
+ viteDevServer.value.stop()
+ },
+ viteMonitoredFiles := {
+ val baseGlob = baseDirectory.value.toGlob
+ def baseFiles(pattern: String): Glob = baseGlob / pattern
+ val viteConfigs =
+ FileTreeView.default.list(
+ List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
+ )
+ val linkerDirectory =
+ (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
+ val viteInputs = FileTreeView.default.list(
+ linkerDirectory.toGlob / "*.js"
+ )
+ (viteConfigs ++ viteInputs).map(_._1.toFile)
+ },
+ viteBuild := {
+ val s = streams.value
+ val dist = viteDist.value
+ val files = viteMonitoredFiles.value
+ // We depend on fullLinkJS
+ val _ = (Compile / fullLinkJS).value
+ def doBuild() = Process(
+ "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
+ baseDirectory.value
+ ) ! s.log
+ val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
+ doBuild()
+ Set(dist)
+ }
+ cachedFun(files.toSet).head
+ },
+ (onLoad in Global) := {
+ (onLoad in Global).value.compose(
+ _.addExitHook {
+ viteDevServer.value.stop()
+ }
+ )
+ }
+ )
+}
diff --git a/project/plugins.sbt b/project/plugins.sbt
index cf3faa4..8169a90 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,3 +1,5 @@
addIWProjects
addScalaJSSupport
+
+addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml
new file mode 100644
index 0000000..08867d4
--- /dev/null
+++ b/server/src/main/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala
new file mode 100644
index 0000000..6b00719
--- /dev/null
+++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala
@@ -0,0 +1,171 @@
+package mdr.pdb.server
+
+import zio.*
+
+import zio.interop.catz.*
+import zio.interop.catz.implicits.{*, given}
+
+import org.http4s.*
+import org.http4s.dsl.Http4sDsl
+import org.http4s.dsl.io.*
+import org.http4s.implicits.{*, given}
+import org.http4s.server.Router
+import org.http4s.syntax.all.{*, given}
+
+import sttp.tapir.*
+import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
+
+import org.pac4j.http4s.*
+
+import org.pac4j.core.authorization.generator.AuthorizationGenerator
+import org.pac4j.core.client.Clients
+import org.pac4j.core.config.Config
+import org.pac4j.core.context.WebContext
+import org.pac4j.core.context.session.SessionStore
+import org.pac4j.core.profile.CommonProfile
+import org.pac4j.core.profile.UserProfile
+import org.pac4j.oidc.client.OidcClient
+import org.pac4j.oidc.config.OidcConfiguration
+
+import scala.concurrent.duration.{*, given}
+import java.util.Optional
+
+trait HttpApplication {
+ def routes(): UIO[HttpRoutes[AppTask]]
+}
+
+object HttpApplicationLive {
+ import zio.config.*
+
+ case class AppConfig(appPath: String, urlBase: String)
+
+ val appConfigDesc: ConfigDescriptor[AppConfig] =
+ import ConfigDescriptor.*
+ nested("APP")(
+ string("PATH") zip string("BASE").default("http://localhost:8080")
+ ).to[AppConfig]
+
+ def layer(
+ contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask]
+ ): RLayer[System, HttpApplication] =
+ val configLayer = ZConfig.fromSystemEnv(
+ appConfigDesc,
+ keyDelimiter = Some('_'),
+ valueDelimiter = Some(',')
+ )
+ val appLayer =
+ (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication]
+ configLayer >>> appLayer
+}
+
+import HttpApplicationLive.AppConfig
+
+case class HttpApplicationLive(
+ config: AppConfig,
+ contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask]
+) extends HttpApplication:
+ val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {}
+ import dsl.*
+
+ // TODO: zio-config
+ def oidcClient(): OidcClient = {
+ val oidcConfiguration = new OidcConfiguration()
+ oidcConfiguration.setClientId("mdrpdbtest")
+ oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02")
+ oidcConfiguration.setDiscoveryURI(
+ "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration"
+ )
+ oidcConfiguration.setUseNonce(true)
+ // oidcConfiguration.addCustomParam("prompt", "consent")
+ val oidcClient = new OidcClient(oidcConfiguration)
+
+ val authorizationGenerator = new AuthorizationGenerator {
+ override def generate(
+ context: WebContext,
+ sessionStore: SessionStore,
+ profile: UserProfile
+ ): Optional[UserProfile] = {
+ profile.addRole("ROLE_ADMIN")
+ Optional.of(profile)
+ }
+ }
+ oidcClient.setAuthorizationGenerator(authorizationGenerator)
+ oidcClient
+ }
+
+ val pac4jConfig =
+ val clients =
+ Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient())
+ val conf = org.pac4j.core.config.Config(clients)
+ conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]())
+ conf.setSessionStore(Http4sCacheSessionStore[AppTask]())
+ conf
+
+ private val sessionConfig = SessionConfig(
+ cookieName = "session",
+ mkCookie = ResponseCookie(_, _, path = Some("/")),
+ secret = "This is a secret",
+ maxAge = 5.minutes
+ )
+
+ val callbackService =
+ CallbackService[AppTask](pac4jConfig, contextBuilder)
+
+ val localLogoutService = LogoutService[AppTask](
+ pac4jConfig,
+ contextBuilder,
+ Some(config.urlBase),
+ destroySession = true
+ )
+ val centralLogoutService = LogoutService[AppTask](
+ pac4jConfig,
+ contextBuilder,
+ defaultUrl = Some(config.urlBase),
+ logoutUrlPattern = Some(s"${config.urlBase}.*"),
+ localLogout = false,
+ destroySession = true,
+ centralLogout = true
+ )
+
+ def filesService(appPath: String): HttpRoutes[AppTask] =
+ ZHttp4sServerInterpreter()
+ .from(
+ List(
+ fileGetServerEndpoint("pdb" / "app")(
+ s"${appPath}/index.html"
+ ),
+ filesGetServerEndpoint("pdb")(appPath)
+ )
+ )
+ .toRoutes
+
+ val smMW = Session.sessionManagement[AppTask](sessionConfig)
+ val sfMW = SecurityFilterMiddleware
+ .securityFilter[AppTask](pac4jConfig, contextBuilder)
+
+ def authedProtectedPages(appPath: String): HttpRoutes[AppTask] =
+ smMW.compose(sfMW)(
+ filesService(appPath).local(
+ (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req
+ )
+ )
+
+ val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of {
+ case req @ GET -> Root / "callback" =>
+ callbackService.callback(req)
+ case req @ POST -> Root / "callback" =>
+ callbackService.callback(req)
+ case req @ GET -> Root / "logout" =>
+ localLogoutService.logout(req)
+ case req @ GET -> Root / "centralLogout" =>
+ centralLogoutService.logout(req)
+ }
+
+ def httpApp(appPath: String): HttpRoutes[AppTask] =
+ Router(
+ "/mdr/pdb/auth" -> smMW(rootRoutes),
+ "/mdr" -> authedProtectedPages(appPath)
+ )
+
+ override def routes(): UIO[HttpRoutes[AppTask]] =
+ ZIO.succeed(httpApp(config.appPath))
diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala
new file mode 100644
index 0000000..1d78408
--- /dev/null
+++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala
@@ -0,0 +1,50 @@
+package mdr.pdb.server
+
+import zio.*
+import zio.interop.catz.*
+import zio.interop.catz.implicits.{*, given}
+import org.http4s.blaze.server.BlazeServerBuilder
+import org.http4s.HttpRoutes
+
+trait HttpServer:
+ def serve(): UIO[ExitCode]
+
+object BlazeHttpServer {
+ import zio.config.*
+
+ case class BlazeServerConf(host: String, port: Int)
+
+ val blazeServerConfig: ConfigDescriptor[BlazeServerConf] =
+ import ConfigDescriptor.*
+ nested("BLAZE")(
+ string("HOST").default("localhost") zip int("PORT").default(8080)
+ ).to[BlazeServerConf]
+
+ val layer: RLayer[System & HttpApplication, HttpServer] =
+ val configLayer = ZConfig.fromSystemEnv(
+ blazeServerConfig,
+ keyDelimiter = Some('_'),
+ valueDelimiter = Some(',')
+ )
+ val routesLayer = ZLayer
+ .environment[HttpApplication]
+ .flatMap(a => ZLayer.fromZIO(a.get.routes()))
+ val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer]
+ (configLayer ++ routesLayer) >>> blazeLayer
+}
+
+import BlazeHttpServer.*
+
+case class BlazeHttpServer(
+ config: BlazeServerConf,
+ httpApp: HttpRoutes[AppTask]
+) extends HttpServer:
+ override def serve(): UIO[ExitCode] =
+ BlazeServerBuilder[AppTask]
+ .bindHttp(config.port, config.host)
+ .withHttpApp(httpApp.orNotFound)
+ .serve
+ .compile
+ .drain
+ .fold(_ => ExitCode.failure, _ => ExitCode.success)
+ .provideEnvironment(ZEnvironment.default)
diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala
index 77952f4..37f509d 100644
--- a/server/src/main/scala/mdr/pdb/server/Main.scala
+++ b/server/src/main/scala/mdr/pdb/server/Main.scala
@@ -1,42 +1,16 @@
package mdr.pdb.server
-import zio._
+import zio.*
import zio.interop.catz.*
import zio.interop.catz.implicits.{*, given}
-import org.http4s.HttpRoutes
-import org.http4s.*
-import org.http4s.dsl.io.*
-import org.http4s.implicits.{*, given}
-import org.http4s.blaze.server.*
-import org.http4s.syntax.all.{*, given}
-import scala.concurrent.ExecutionContext.global
-import org.http4s.dsl.Http4sDsl
-import sttp.tapir.*
-import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
+import org.http4s.Request
+import org.pac4j.http4s.Http4sWebContext
-import org.pac4j.oidc.client.OidcClient
-import org.pac4j.oidc.config.OidcConfiguration
-import org.pac4j.core.authorization.generator.AuthorizationGenerator
-import org.pac4j.core.context.WebContext
-import org.pac4j.core.context.session.SessionStore
-import org.pac4j.core.profile.UserProfile
-import java.util.Optional
-import org.pac4j.core.client.Clients
-import org.pac4j.http4s.Http4sCacheSessionStore
-import org.pac4j.http4s.DefaultHttpActionAdapter
-import org.pac4j.http4s.SessionConfig
-import org.http4s.ResponseCookie
-import org.pac4j.http4s.CallbackService
-import scala.concurrent.duration.{*, given}
-import org.pac4j.http4s.{Http4sWebContext, *}
-import org.pac4j.core.profile.CommonProfile
-import org.http4s.server.Router
+type AppTask = RIO[ZEnv, *]
object Main extends ZIOAppDefault:
- type AppTask = RIO[ZEnv, *]
- protected val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {}
- import dsl.*
+ // TODO: move inside HttpApplication (using ZIO.runtime)
private val contextBuilder =
(req: Request[AppTask], conf: org.pac4j.core.config.Config) =>
new Http4sWebContext[AppTask](
@@ -45,117 +19,12 @@
runtime.unsafeRun(_)
)
- // TODO: zio-config
- def oidcClient(): OidcClient = {
- val oidcConfiguration = new OidcConfiguration()
- oidcConfiguration.setClientId("mdrpdbtest")
- oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02")
- oidcConfiguration.setDiscoveryURI(
- "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration"
- )
- oidcConfiguration.setUseNonce(true)
- // oidcConfiguration.addCustomParam("prompt", "consent")
- val oidcClient = new OidcClient(oidcConfiguration)
-
- val authorizationGenerator = new AuthorizationGenerator {
- override def generate(
- context: WebContext,
- sessionStore: SessionStore,
- profile: UserProfile
- ): Optional[UserProfile] = {
- profile.addRole("ROLE_ADMIN")
- Optional.of(profile)
- }
- }
- oidcClient.setAuthorizationGenerator(authorizationGenerator)
- oidcClient
- }
-
- val pac4jConfig =
- val clients = Clients("http://localhost:8080/callback", oidcClient())
- val config = org.pac4j.core.config.Config(clients)
- config.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]())
- config.setSessionStore(Http4sCacheSessionStore[AppTask]())
- config
-
- private val sessionConfig = SessionConfig(
- cookieName = "session",
- mkCookie = ResponseCookie(_, _, path = Some("/")),
- secret = "This is a secret",
- maxAge = 5.minutes
- )
-
- val callbackService =
- CallbackService[AppTask](pac4jConfig, contextBuilder)
-
- val localLogoutService = LogoutService[AppTask](
- pac4jConfig,
- contextBuilder,
- Some("/?defaulturlafterlogout"),
- destroySession = true
- )
- val centralLogoutService = LogoutService[AppTask](
- pac4jConfig,
- contextBuilder,
- defaultUrl = Some("http://localhost:8080/?defaulturlafterlogoutafteridp"),
- logoutUrlPattern = Some("http://localhost:8080/.*"),
- localLogout = false,
- destroySession = true,
- centralLogout = true
- )
-
- val filesService: HttpRoutes[AppTask] =
- ZHttp4sServerInterpreter()
- .from(
- List(
- fileGetServerEndpoint("pdb" / "app")(
- "app/target/vite/index.html"
- ),
- filesGetServerEndpoint("pdb")("app/target/vite")
- )
- )
- .toRoutes
-
- val authedProtectedPages: HttpRoutes[AppTask] =
- Session
- .sessionManagement[AppTask](sessionConfig)
- .compose(
- SecurityFilterMiddleware
- .securityFilter[AppTask](pac4jConfig, contextBuilder)
- ) {
- filesService.local(
- (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req
- )
- }
-
- val root: HttpRoutes[AppTask] = HttpRoutes.of {
- case req @ GET -> Root / "callback" =>
- callbackService.callback(req)
- case req @ POST -> Root / "callback" =>
- callbackService.callback(req)
- case req @ GET -> Root / "logout" =>
- localLogoutService.logout(req)
- case req @ GET -> Root / "centralLogout" =>
- centralLogoutService.logout(req)
- }
-
- def serve: URIO[ZEnv, ExitCode] =
- BlazeServerBuilder[AppTask]
- .bindHttp(8080, "localhost")
- .withHttpApp(
- Router(
- "/mdr" -> authedProtectedPages,
- "/" -> (Session
- .sessionManagement[AppTask](sessionConfig)
- .apply) { root }
- ).orNotFound
- )
- .serve
- .compile
- .drain
- .fold(_ => ExitCode.failure, _ => ExitCode.success)
-
override def run =
for {
- _ <- serve
+ server <- ZIO
+ .service[HttpServer]
+ .provideCustom(
+ HttpApplicationLive.layer(contextBuilder) >>> BlazeHttpServer.layer
+ )
+ _ <- server.serve()
} yield ()