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 ()