From 99f182502ad53dcc0ad1218fd44039824e01a66d Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sat, 28 Jan 2023 14:45:09 +0100 Subject: [PATCH] removed DB module as well (useless) fixing problems occured with ZIO, the app now compiles --- .gitignore | 2 + {DB => Core}/datastructure.drawio | 0 Core/resources/application.conf | 6 ++ Core/resources/server.properties | 6 +- {DB => Core}/resources/table_init.sql | 2 +- Core/src/org/tbasket/EndpointSetup.scala | 28 ------- Core/src/org/tbasket/Main.scala | 51 +++++++++--- Core/src/org/tbasket/ServerConfig.scala | 83 +++++++++++++++++++ .../org/tbasket/ServerConfigException.scala | 5 ++ .../org/tbasket/auth/Authentificator.scala | 8 +- Core/src/org/tbasket/data/Database.scala | 22 +++++ Core/src/org/tbasket/data/Member.scala | 5 ++ Core/src/org/tbasket/data/Tactic.scala | 6 ++ Core/src/org/tbasket/data/Team.scala | 5 ++ .../src/org/tbasket/data}/User.scala | 3 +- Core/src/org/tbasket/endpoint/Endpoint.scala | 6 +- .../org/tbasket/handler/LoginHandler.scala | 25 +++--- DB/resources/database.properties | 1 - DB/src/org/tbasket/db/Database.scala | 21 ----- DB/src/org/tbasket/db/schemas/Member.scala | 19 ----- DB/src/org/tbasket/db/schemas/Tactic.scala | 19 ----- DB/src/org/tbasket/db/schemas/Team.scala | 17 ---- build.sc | 15 +--- 23 files changed, 204 insertions(+), 151 deletions(-) rename {DB => Core}/datastructure.drawio (100%) create mode 100644 Core/resources/application.conf rename {DB => Core}/resources/table_init.sql (99%) delete mode 100644 Core/src/org/tbasket/EndpointSetup.scala create mode 100644 Core/src/org/tbasket/ServerConfig.scala create mode 100644 Core/src/org/tbasket/ServerConfigException.scala create mode 100644 Core/src/org/tbasket/data/Database.scala create mode 100644 Core/src/org/tbasket/data/Member.scala create mode 100644 Core/src/org/tbasket/data/Tactic.scala create mode 100644 Core/src/org/tbasket/data/Team.scala rename {DB/src/org/tbasket/db/schemas => Core/src/org/tbasket/data}/User.scala (76%) delete mode 100644 DB/resources/database.properties delete mode 100644 DB/src/org/tbasket/db/Database.scala delete mode 100644 DB/src/org/tbasket/db/schemas/Member.scala delete mode 100644 DB/src/org/tbasket/db/schemas/Tactic.scala delete mode 100644 DB/src/org/tbasket/db/schemas/Team.scala diff --git a/.gitignore b/.gitignore index cba59ec..6774166 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ out .bsp keys +server.properties + log *.sqlite diff --git a/DB/datastructure.drawio b/Core/datastructure.drawio similarity index 100% rename from DB/datastructure.drawio rename to Core/datastructure.drawio diff --git a/Core/resources/application.conf b/Core/resources/application.conf new file mode 100644 index 0000000..1afc42a --- /dev/null +++ b/Core/resources/application.conf @@ -0,0 +1,6 @@ +database { + dataSourceClassName = org.sqlite.SQLiteDataSource + dataSource { + url = "jdbc:sqlite:database.sqlite;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'classpath:table_init.sql'" + } +} \ No newline at end of file diff --git a/Core/resources/server.properties b/Core/resources/server.properties index 14d538d..11d1e86 100644 --- a/Core/resources/server.properties +++ b/Core/resources/server.properties @@ -1,3 +1,5 @@ #may not be a public url -emitter.url=localhost:4454 -eme=dzq=d=qzd=qz=d \ No newline at end of file +emitter.url= +emitter.cert= + +endpoint.port= \ No newline at end of file diff --git a/DB/resources/table_init.sql b/Core/resources/table_init.sql similarity index 99% rename from DB/resources/table_init.sql rename to Core/resources/table_init.sql index 7b12d58..85b8bc3 100644 --- a/DB/resources/table_init.sql +++ b/Core/resources/table_init.sql @@ -1,8 +1,8 @@ CREATE TABLE user ( id varchar(32) PRIMARY KEY, - name varchar(30) NOT NULL, mail_address varchar NOT NULL UNIQUE, + name varchar(30) NOT NULL, forename varchar(30) NOT NULL, password_hash varchar ); diff --git a/Core/src/org/tbasket/EndpointSetup.scala b/Core/src/org/tbasket/EndpointSetup.scala deleted file mode 100644 index b9efb44..0000000 --- a/Core/src/org/tbasket/EndpointSetup.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.tbasket - -import io.getquill.{Literal, SqliteDialect} -import io.getquill.context.qzio.ZioContext -import org.tbasket.endpoint.Endpoint -import org.tbasket.handler.LoginHandler - -import java.util.Properties - -object EndpointSetup: - - private final val EndpointPort = "endpoint.port" - private final val EndpointPortDefault = "48485" - - def setupEndpoint(config: Properties): Endpoint = - Main.LOG.debug("Initializing API endpoint...") - - createEndpoint(config) - - private def createEndpoint(config: Properties): Endpoint = - val port = config - .getProperty(EndpointPort, EndpointPortDefault) match - case s"$port" if port.toIntOption.isDefined => port.toInt - case v => - throw new InternalBasketServerException( - s"$EndpointPort property value is wrong: $v must be integer" - ) - new Endpoint(port) diff --git a/Core/src/org/tbasket/Main.scala b/Core/src/org/tbasket/Main.scala index fc76da7..a8dd308 100644 --- a/Core/src/org/tbasket/Main.scala +++ b/Core/src/org/tbasket/Main.scala @@ -1,34 +1,65 @@ package org.tbasket import org.apache.logging.log4j.LogManager -import org.tbasket.db.Database +import org.tbasket.auth.Authentificator +import org.tbasket.data.Database +import org.tbasket.endpoint.Endpoint import zio.* +import zio.http.URL +import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import java.lang import java.nio.file.{Files, Path} +import java.security.PublicKey +import java.security.cert.CertificateFactory import java.util.Properties import scala.io.StdIn import scala.util.{Failure, Success} import scala.util.control.NonFatal +import java.security.spec.{KeySpec, PKCS8EncodedKeySpec, RSAPrivateKeySpec, X509EncodedKeySpec} object Main extends ZIOAppDefault: - final val LOG = LogManager.getLogger("Core") + final val LOG = LogManager.getLogger("Core") + + override def run = ZIO.serviceWithZIO[ZIOAppArgs] { args => + for + config <- retrieveConfig(args) + res <- setupAuth(config) <&> setupDatabase(config) <&> setupEndpoint(config) + yield + val (auth, db, ep) = res + (auth, db, ep) + }.flatMap((auth, db, ep) => + ep.run.provide(db.datasourceLayer, db.contextLayer, auth) + ) + + + private def setupEndpoint(config: ServerConfig) = ZIO.attempt { + new Endpoint(config.endpointPort) + } + + private def setupDatabase(config: ServerConfig) = ZIO.attempt { + new Database(config) + } + + private def setupAuth(config: ServerConfig) = ZIO.attempt { + val publicKey = config.emitterCertificate.getPublicKey + val auth = new Authentificator(config.emitterURL, publicKey, config.emitterCertificateAlgorithm) + ZLayer.succeed(auth) + } + - override def run = - val config = retrieveConfig - val db = new Database(config) - EndpointSetup.setupEndpoint(config).run.provideLayer(db.layer) - - private def retrieveConfig: Properties = + private def retrieveConfig(args: ZIOAppArgs): Task[ServerConfig] = ZIO.attempt { val configFile = Path.of("server.properties") if Files.notExists(configFile) then val in = getClass.getResourceAsStream("/server.properties") Files.writeString(configFile, new String(in.readAllBytes())) - val in = Files.newInputStream(configFile) + + val in = Files.newInputStream(configFile) val properties = new Properties() properties.load(in) properties - + }.flatMap(p => ServerConfig(p, args.getArgs)) + // add a shutdown hook to log when the server is about to get killed lang.Runtime.getRuntime.addShutdownHook(new Thread(() => LOG.info("Server shutdowns") diff --git a/Core/src/org/tbasket/ServerConfig.scala b/Core/src/org/tbasket/ServerConfig.scala new file mode 100644 index 0000000..c4275b7 --- /dev/null +++ b/Core/src/org/tbasket/ServerConfig.scala @@ -0,0 +1,83 @@ +package org.tbasket + +import org.tbasket.ServerConfig.CertFactory +import pdi.jwt.JwtAlgorithm +import zio.{Chunk, Task, ZIO, ZIOAppArgs} +import zio.http.URL +import zio.stream.ZStream +import pdi.jwt.algorithms.JwtAsymmetricAlgorithm + +import java.nio.file.{Files, Path} +import java.security.cert.{Certificate, CertificateFactory} +import java.util.Properties + +class ServerConfig private(userProperties: Properties, schema: Properties, arguments: Map[String, String]) { + + private def getPropertySafe(name: String) = + if (schema.getProperty(name) == null) { + throw new ServerConfigException( + s""" + | current config seems expired : property $name should not be present in your config + | (maybe this server has a newer version than the configuration and its was modified) + | config schema : + | + |$schemaString + | + | You may try to adapt your old config version with new schema + |""".stripMargin) + } + Option(userProperties.getProperty(name)) match + case Some(value) => value + case None => + arguments.getOrElse(name, throw new ServerConfigException( + s""" + | could not find property in server configuration : $name + | config schema : + | + |$schemaString + | + | This property is required. + |""".stripMargin)) + + val emitterURL: URL = URL.fromString(getPropertySafe("emitter.url")) match + case Left(exception) => throw exception + case Right(value) => value + + val emitterCertificate: Certificate = { + val path = Path.of(getPropertySafe("emitter.cert")) + val in = Files.newInputStream(path) + CertFactory.generateCertificate(in) + } + + val emitterCertificateAlgorithm = JwtAlgorithm.RS256 + + val endpointPort: Int = + getPropertySafe("endpoint.port") + .toIntOption + .getOrElse(throw new ServerConfigException("endpoint.port is not an integer")) + + private def schemaString = { + schema.stringPropertyNames() + .toArray(new Array[String](_)) + .map(key => s"$key=${schema.getProperty(key)}") + .mkString("\n") + } + +} + +object ServerConfig { + //TODO make certificate type configurable + final val CertFactory = CertificateFactory.getInstance("X509") + + def apply(userProperties: Properties, arguments: Seq[String]): Task[ServerConfig] = ZIO.attempt { + val args = arguments.map { + case s"$key=$value" => (key, value) + case x => throw new ServerConfigException(s"configuration parameter must be of format 'property.name=value' received : ${x}") + }.toMap + val schemaIn = getClass.getClassLoader.getResourceAsStream("server.properties") + val schema = new Properties() + schema.load(schemaIn) + new ServerConfig(userProperties, schema, args) + } + +} diff --git a/Core/src/org/tbasket/ServerConfigException.scala b/Core/src/org/tbasket/ServerConfigException.scala new file mode 100644 index 0000000..1e292f5 --- /dev/null +++ b/Core/src/org/tbasket/ServerConfigException.scala @@ -0,0 +1,5 @@ +package org.tbasket + +class ServerConfigException(msg: String) extends Exception(msg) { + +} diff --git a/Core/src/org/tbasket/auth/Authentificator.scala b/Core/src/org/tbasket/auth/Authentificator.scala index 99a8e6b..9dae525 100644 --- a/Core/src/org/tbasket/auth/Authentificator.scala +++ b/Core/src/org/tbasket/auth/Authentificator.scala @@ -6,8 +6,9 @@ import io.circe.parser.* import io.circe.syntax.* import io.getquill.* import io.getquill.context.qzio.ZioJdbcContext +import io.getquill.context.sql.idiom.SqlIdiom import org.tbasket.InternalBasketServerException -import org.tbasket.db.schemas.User +import org.tbasket.data.User import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import pdi.jwt.{JwtClaim, JwtZIOJson} import zio.* @@ -66,9 +67,8 @@ class Authentificator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorith .flatMap(ZIO.fromEither(_)) .map(_.uuid) - user <- ZIO.serviceWithZIO[ZioJdbcContext[_,_]] { ds => - import org.tbasket.db.Database.ctx.* - + user <- ZIO.serviceWithZIO[ZioJdbcContext[SqlIdiom, NamingStrategy]] { ctx => + import ctx._ run(quote { query[User].filter(_.id == lift(uuid)) }).map(_.headOption) diff --git a/Core/src/org/tbasket/data/Database.scala b/Core/src/org/tbasket/data/Database.scala new file mode 100644 index 0000000..166ed2f --- /dev/null +++ b/Core/src/org/tbasket/data/Database.scala @@ -0,0 +1,22 @@ +package org.tbasket.data + +import io.getquill.context.ZioJdbc.{DataSourceLayer, QuillZioExt} +import io.getquill.context.qzio.ZioContext +import io.getquill.idiom.Idiom +import io.getquill.jdbczio.Quill +import io.getquill.* +import org.sqlite.SQLiteDataSource +import org.tbasket.ServerConfig +import zio.* + +import java.io.Closeable +import java.util.Properties +import javax.sql + +class Database(config: ServerConfig): + + val contextLayer = ZLayer.succeed(new SqliteZioJdbcContext(SnakeCase)) + val datasourceLayer = Quill.DataSource.fromPrefix("database") + + + diff --git a/Core/src/org/tbasket/data/Member.scala b/Core/src/org/tbasket/data/Member.scala new file mode 100644 index 0000000..57e74d2 --- /dev/null +++ b/Core/src/org/tbasket/data/Member.scala @@ -0,0 +1,5 @@ +package org.tbasket.data + +import io.getquill.* + +case class Member(team: Team, user: User, admin: Boolean) \ No newline at end of file diff --git a/Core/src/org/tbasket/data/Tactic.scala b/Core/src/org/tbasket/data/Tactic.scala new file mode 100644 index 0000000..55aa192 --- /dev/null +++ b/Core/src/org/tbasket/data/Tactic.scala @@ -0,0 +1,6 @@ +package org.tbasket.data + +import io.getquill.* + +case class Tactic(id: Int, name: String, owner: User, filePath: String) + diff --git a/Core/src/org/tbasket/data/Team.scala b/Core/src/org/tbasket/data/Team.scala new file mode 100644 index 0000000..2850f3f --- /dev/null +++ b/Core/src/org/tbasket/data/Team.scala @@ -0,0 +1,5 @@ +package org.tbasket.data + +import io.getquill.* + +case class Team(id: Int, name: String, clubName: String) diff --git a/DB/src/org/tbasket/db/schemas/User.scala b/Core/src/org/tbasket/data/User.scala similarity index 76% rename from DB/src/org/tbasket/db/schemas/User.scala rename to Core/src/org/tbasket/data/User.scala index ddb3b3a..3a7d2af 100644 --- a/DB/src/org/tbasket/db/schemas/User.scala +++ b/Core/src/org/tbasket/data/User.scala @@ -1,7 +1,6 @@ -package org.tbasket.db.schemas +package org.tbasket.data import io.getquill.* -import org.tbasket.db.Database import java.util.UUID diff --git a/Core/src/org/tbasket/endpoint/Endpoint.scala b/Core/src/org/tbasket/endpoint/Endpoint.scala index ee905de..0a0b743 100644 --- a/Core/src/org/tbasket/endpoint/Endpoint.scala +++ b/Core/src/org/tbasket/endpoint/Endpoint.scala @@ -1,7 +1,8 @@ package org.tbasket.endpoint import io.getquill.{Literal, NamingStrategy, SqliteDialect} -import io.getquill.context.qzio.ZioContext +import io.getquill.context.qzio.{ZioContext, ZioJdbcContext} +import io.getquill.context.sql.idiom.SqlIdiom import org.apache.logging.log4j.LogManager import org.tbasket.auth.Authentificator import org.tbasket.endpoint.Endpoint.LOG @@ -15,6 +16,7 @@ import zio.http.model.Status import scala.collection.mutable import zio.http.netty.client.ConnectionPool import io.getquill.idiom.Idiom + import javax.sql.DataSource class Endpoint(port: Int): @@ -47,7 +49,7 @@ class Endpoint(port: Int): Server.install(app).flatMap { port => LOG.info(s"Listening API entries on $port") ZIO.never - }.provideSome[ZioContext[Idiom, NamingStrategy] & Authentificator]( + }.provideSome[ZioJdbcContext[SqlIdiom, NamingStrategy] & Authentificator & DataSource]( Scope.default, serverConfigLayer, ConnectionPool.fixed(4), diff --git a/Core/src/org/tbasket/handler/LoginHandler.scala b/Core/src/org/tbasket/handler/LoginHandler.scala index fce63f8..49d751e 100644 --- a/Core/src/org/tbasket/handler/LoginHandler.scala +++ b/Core/src/org/tbasket/handler/LoginHandler.scala @@ -1,14 +1,15 @@ package org.tbasket.handler import io.getquill.* -import io.getquill.context.qzio.ZioContext +import io.getquill.context.ZioJdbc.* +import io.getquill.context.qzio.{ZioContext, ZioJdbcContext} +import io.getquill.context.sql.idiom.SqlIdiom import org.tbasket.auth.Authentificator -import org.tbasket.db.Database.ctx.* -import org.tbasket.db.schemas.User import org.tbasket.handler.HandlerUtils.errorBody -import org.tbasket.handler.LoginError.{InvalidRequest, *} +import org.tbasket.handler.LoginError.* +import org.tbasket.data.User +import zio.http.* import zio.http.model.{Cookie, Header, Headers, Status} -import zio.http.{Body, Client, Request, Response, URL} import zio.json.* import zio.json.ast.Json.Str import zio.json.ast.{Json, JsonCursor} @@ -16,8 +17,6 @@ import zio.{ZEnvironment, ZIO, *} import java.sql.SQLException import java.util.UUID -import io.getquill._ -import io.getquill.context.ZioJdbc._ enum LoginError: case TokenNotFound(token: UUID) @@ -30,25 +29,25 @@ enum LoginError: object LoginHandler: private def getUser(json: Json) = - ZIO.serviceWithZIO[SqliteZioJdbcContext[_]] { ctx => + ZIO.serviceWithZIO[ZioJdbcContext[SqlIdiom, NamingStrategy]] { ctx => import ctx.* for mail <- ZIO.fromEither(json.get[Str](JsonCursor.field("mail").isString).map(_.value)) .mapError(InvalidRequest("Missing or invalid field mail", _)) password <- - ZIO.fromEither(json.get[Str](JsonCursor.field("password").isString).map(_.value)) + ZIO.fromEither(json.get[Str](JsonCursor.field("password").isString).map(_.value.hashCode)) .mapError(InvalidRequest("Missing or invalid field password", _)) - result <- run(quote { // TODO use argon2id + result <- run(quote { // TODO use argon2id query[User] .filter(usr => usr.mailAddress == lift(mail)) - .filter(usr => usr.passwordHash == lift(password.hashCode)) + .filter(usr => usr.passwordHash == lift(password)) }).mapError(InternalError.apply) yield result.headOption }.someOrFail(InvalidPassword) - override def post(request: Request) = + def post(request: Request) = val bindSession = for body <- request @@ -66,7 +65,7 @@ object LoginHandler: jwt <- ZIO.serviceWithZIO[Authentificator](_.requestJwt(user)) yield (user, jwt) - bindSession.map { case (user, jwt) => + bindSession.map { case (_, jwt) => Response( status = Status.Found, headers = Headers.location("/") ++ //login successful, go back to main page diff --git a/DB/resources/database.properties b/DB/resources/database.properties deleted file mode 100644 index c31abb1..0000000 --- a/DB/resources/database.properties +++ /dev/null @@ -1 +0,0 @@ -path=database.sqlite \ No newline at end of file diff --git a/DB/src/org/tbasket/db/Database.scala b/DB/src/org/tbasket/db/Database.scala deleted file mode 100644 index e4334fd..0000000 --- a/DB/src/org/tbasket/db/Database.scala +++ /dev/null @@ -1,21 +0,0 @@ -package org.tbasket.db - -import io.getquill.context.ZioJdbc.{DataSourceLayer, QuillZioExt} -import io.getquill.context.qzio.ZioContext -import io.getquill.{Literal, SnakeCase, SqliteDialect, SqliteZioJdbcContext} -import org.sqlite.SQLiteDataSource -import zio.* - -import java.io.Closeable -import java.util.Properties -import javax.sql - -class Database(config: Properties): - - val layer = ZLayer.succeed(new SqliteZioJdbcContext(SnakeCase)) - -object Database: - val ctx = new SqliteZioJdbcContext(SnakeCase) - - import ctx.* - diff --git a/DB/src/org/tbasket/db/schemas/Member.scala b/DB/src/org/tbasket/db/schemas/Member.scala deleted file mode 100644 index 6b1c19e..0000000 --- a/DB/src/org/tbasket/db/schemas/Member.scala +++ /dev/null @@ -1,19 +0,0 @@ -package org.tbasket.db.schemas - -import io.getquill.* -import org.tbasket.db.Database - -case class Member(team: Team, user: User, admin: Boolean) - -object Member: - - import Database.ctx.* - - val schema = quote { - querySchema[Member]( - "member", - _.team.id -> "team_id", - _.user.id -> "user_id", - _.admin -> "is_admin" - ) - } diff --git a/DB/src/org/tbasket/db/schemas/Tactic.scala b/DB/src/org/tbasket/db/schemas/Tactic.scala deleted file mode 100644 index b050cc5..0000000 --- a/DB/src/org/tbasket/db/schemas/Tactic.scala +++ /dev/null @@ -1,19 +0,0 @@ -package org.tbasket.db.schemas - -import io.getquill.* - -import org.tbasket.db.Database -case class Tactic(id: Int, name: String, owner: User, filePath: String) - -object Tactic: - import Database.ctx.* - - val schema = quote { - querySchema[Tactic]( - "tactic", - _.id -> "id", - _.name -> "name", - _.owner.id -> "owner_id", - _.filePath -> "file_path" - ) - } diff --git a/DB/src/org/tbasket/db/schemas/Team.scala b/DB/src/org/tbasket/db/schemas/Team.scala deleted file mode 100644 index 6b19ac2..0000000 --- a/DB/src/org/tbasket/db/schemas/Team.scala +++ /dev/null @@ -1,17 +0,0 @@ -package org.tbasket.db.schemas - -import io.getquill.* - -case class Team(id: Int, name: String, clubName: String) -object Team: - - import org.tbasket.db.Database.ctx.* - - val schema = quote { - querySchema[Team]( - "team", - _.id -> "id", - _.name -> "name", - _.clubName -> "club_name" - ) - } diff --git a/build.sc b/build.sc index d01378c..d32a34c 100644 --- a/build.sc +++ b/build.sc @@ -38,18 +38,9 @@ object Core extends HttpModule { //also handles http ivy"io.circe::circe-core:0.15.0-M1", ivy"io.circe::circe-parser:0.14.3", ivy"io.circe::circe-generic:0.14.3", - ) - - override def moduleDeps = Seq(DB) -} - -/** - * Database module - * */ -object DB extends ServerModule { - override def ivyDeps = super.ivyDeps() ++ Agg( - ivy"io.getquill::quill-jdbc-zio:4.6.0", + ivy"io.getquill::quill-jdbc-zio:4.6.0", ivy"org.xerial:sqlite-jdbc:3.40.0.0", ) -} \ No newline at end of file + +}