From d35a61212ade6c79cb266a637d7a6069f2ad4d0d Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sat, 28 Jan 2023 21:00:48 +0100 Subject: [PATCH] fixed some aspects in login page handler and working on register page handler --- Core/resources/application.conf | 2 +- Core/resources/log4j2.xml | 6 +- Core/src/module-info.java | 2 - Core/src/org/tbasket/Main.scala | 12 +- .../org/tbasket/auth/Authentificator.scala | 71 ++++++++++-- Core/src/org/tbasket/data/Database.scala | 2 +- .../org/tbasket/data/DatabaseContext.scala | 11 ++ Core/src/org/tbasket/data/User.scala | 5 +- Core/src/org/tbasket/endpoint/Endpoint.scala | 48 ++++++-- .../src/org/tbasket/error/AuthException.scala | 9 ++ .../src/org/tbasket/error/ExceptionEnum.scala | 5 + Core/src/org/tbasket/error/JwtException.scala | 10 ++ .../org/tbasket/error/RegularException.scala | 9 ++ .../org/tbasket/handler/HandlerUtils.scala | 15 ++- .../org/tbasket/handler/LoginHandler.scala | 104 ------------------ .../tbasket/handler/LoginPageHandler.scala | 97 ++++++++++++++++ .../src/org/tbasket/handler/PageHandler.scala | 8 ++ .../tbasket/handler/RegisterPageHandler.scala | 40 +++++++ .../src/org/tbasket/jwt/JwtGenerator.scala | 2 - build.sc | 2 + 20 files changed, 318 insertions(+), 142 deletions(-) delete mode 100644 Core/src/module-info.java create mode 100644 Core/src/org/tbasket/data/DatabaseContext.scala create mode 100644 Core/src/org/tbasket/error/AuthException.scala create mode 100644 Core/src/org/tbasket/error/ExceptionEnum.scala create mode 100644 Core/src/org/tbasket/error/JwtException.scala create mode 100644 Core/src/org/tbasket/error/RegularException.scala delete mode 100644 Core/src/org/tbasket/handler/LoginHandler.scala create mode 100644 Core/src/org/tbasket/handler/LoginPageHandler.scala create mode 100644 Core/src/org/tbasket/handler/PageHandler.scala create mode 100644 Core/src/org/tbasket/handler/RegisterPageHandler.scala diff --git a/Core/resources/application.conf b/Core/resources/application.conf index 1afc42a..1fe5c54 100644 --- a/Core/resources/application.conf +++ b/Core/resources/application.conf @@ -1,6 +1,6 @@ database { dataSourceClassName = org.sqlite.SQLiteDataSource dataSource { - url = "jdbc:sqlite:database.sqlite;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'classpath:table_init.sql'" + url = "jdbc:sqlite:database.sqlite" } } \ No newline at end of file diff --git a/Core/resources/log4j2.xml b/Core/resources/log4j2.xml index f045e3d..e305b25 100644 --- a/Core/resources/log4j2.xml +++ b/Core/resources/log4j2.xml @@ -4,7 +4,7 @@ + pattern="%style{[%d{HH:mm:ss,SSS}]}{magenta} [%highlight{%-5p}{FATAL=red, ERROR=red, WARN=yellow, INFO=blue, DEBUG=green, TRACE=normal} _ %-30c{1.}] %style{-}{normal} %highlight{%m%n}{FATAL=red, ERROR=red, WARN=yellow, INFO=blue, DEBUG=green, TRACE=normal}"/> + + + + diff --git a/Core/src/module-info.java b/Core/src/module-info.java deleted file mode 100644 index 26182a6..0000000 --- a/Core/src/module-info.java +++ /dev/null @@ -1,2 +0,0 @@ -module core { -} \ No newline at end of file diff --git a/Core/src/org/tbasket/Main.scala b/Core/src/org/tbasket/Main.scala index a8dd308..9c45278 100644 --- a/Core/src/org/tbasket/Main.scala +++ b/Core/src/org/tbasket/Main.scala @@ -4,22 +4,22 @@ import org.apache.logging.log4j.LogManager import org.tbasket.auth.Authentificator import org.tbasket.data.Database import org.tbasket.endpoint.Endpoint +import pdi.jwt.algorithms.JwtAsymmetricAlgorithm 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.security.spec.{KeySpec, PKCS8EncodedKeySpec, RSAPrivateKeySpec, X509EncodedKeySpec} 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} +import scala.util.{Failure, Success} object Main extends ZIOAppDefault: - final val LOG = LogManager.getLogger("Core") + final val LOG = LogManager.getLogger("Core") override def run = ZIO.serviceWithZIO[ZIOAppArgs] { args => for @@ -43,11 +43,11 @@ object Main extends ZIOAppDefault: private def setupAuth(config: ServerConfig) = ZIO.attempt { val publicKey = config.emitterCertificate.getPublicKey - val auth = new Authentificator(config.emitterURL, publicKey, config.emitterCertificateAlgorithm) + val auth = new Authentificator(config.emitterURL, publicKey, config.emitterCertificateAlgorithm) ZLayer.succeed(auth) } - + private def retrieveConfig(args: ZIOAppArgs): Task[ServerConfig] = ZIO.attempt { val configFile = Path.of("server.properties") if Files.notExists(configFile) then diff --git a/Core/src/org/tbasket/auth/Authentificator.scala b/Core/src/org/tbasket/auth/Authentificator.scala index 9dae525..3ce7bf8 100644 --- a/Core/src/org/tbasket/auth/Authentificator.scala +++ b/Core/src/org/tbasket/auth/Authentificator.scala @@ -7,8 +7,14 @@ import io.circe.syntax.* import io.getquill.* import io.getquill.context.qzio.ZioJdbcContext import io.getquill.context.sql.idiom.SqlIdiom +import org.apache.logging.log4j.LogManager import org.tbasket.InternalBasketServerException -import org.tbasket.data.User +import org.tbasket.auth.Authentificator.LOG +import org.tbasket.data.{DatabaseContext, User} +import org.tbasket.error.AuthException.* +import org.tbasket.error.ExceptionEnum +import org.tbasket.error.JwtException.* +import org.tbasket.error.RegularException.* import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import pdi.jwt.{JwtClaim, JwtZIOJson} import zio.* @@ -24,14 +30,15 @@ import java.util.UUID import javax.sql.DataSource import scala.collection.immutable.HashMap -enum AuthentificatorError: - case ExpiredToken - case InvalidEmitterResponse case class JwtContent(uuid: UUID) +object Authentificator: + private final val LOG = LogManager.getLogger("Authentification") + class Authentificator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm) { + private def defineCustomClaims(user: User): String = { JwtContent(user.id).asJson.noSpaces.toString } @@ -41,7 +48,7 @@ class Authentificator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorith Request(Body.fromString(custom), Headers.empty, Method.GET, url, Http_1_1, None) } - def requestJwt(user: User) = { + def requestNewJwt(user: User) = { Client.request(mkRequest(user)) .flatMap { case Response(Ok, _, body, _, _) => @@ -53,22 +60,68 @@ class Authentificator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorith } } + private def hashPassword(password: String) = { + password.hashCode //TODO user BCrypt or argon2id + } + + private def findByMail(mail: String) = ZIO.serviceWithZIO[DatabaseContext] { ctx => + import ctx.v.* + run(query[User].filter(_.mailAddress == lift(mail))).map(_.headOption) + } + + def loginUser(mail: String, password: String) = ZIO.serviceWithZIO[DatabaseContext] { ctx => + import ctx.v.* + findByMail(mail) + .someOrFail(UserNotFound) + .filterOrFail(_.passwordHash == hashPassword(password))(InvalidPassword) + } + + private inline def insert(user: User) = quote { + query[User].insert( + _.id -> user.id, + _.name -> user.name, + _.forename -> user.forename, + _.passwordHash -> user.passwordHash, + _.mailAddress -> user.mailAddress, + ) + } + + def registerUser(name: String, forename: String, mail: String, password: String) = ZIO.serviceWithZIO[DatabaseContext] { ctx => + import ctx.* + lazy val user = { + val uuid = UUID.randomUUID() + val hash = hashPassword(password) + User(uuid, name, forename, hash, mail) + } + + for + _ <- findByMail(mail).none.orElse(ZIO.fail(UserAlreadyRegistered)) + _ <- run(insert(user)).fork + yield user + } + + def validateAndGetUser(jwt: String) = { for //decoding token - claims <- ZIO.fromTry(JwtZIOJson.decode(jwt, key, Seq(algorithm))) + claims <- + ZIO.fromTry(JwtZIOJson.decode(jwt, key, Seq(algorithm))) + .mapError(InternalError.apply) + //ensure that the token is not expired (or else fail) _ <- ZIO.attempt(claims.expiration) - .someOrFail("Received invalid jwt token (missing expiration date)") - .filterOrFail(_ <= java.lang.System.currentTimeMillis())("Expired token") + .someOrElseZIO(ZIO.dieMessage("Invalid token")) + .filterOrFail(_ <= java.lang.System.currentTimeMillis())(ExpiredJwt) + // uuid <- ZIO.attempt(claims.content) .mapAttempt(decode[JwtContent](_)) .flatMap(ZIO.fromEither(_)) .map(_.uuid) + .mapError(InternalError.apply) user <- ZIO.serviceWithZIO[ZioJdbcContext[SqlIdiom, NamingStrategy]] { ctx => - import 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 index 166ed2f..b911099 100644 --- a/Core/src/org/tbasket/data/Database.scala +++ b/Core/src/org/tbasket/data/Database.scala @@ -15,7 +15,7 @@ import javax.sql class Database(config: ServerConfig): - val contextLayer = ZLayer.succeed(new SqliteZioJdbcContext(SnakeCase)) + val contextLayer = ZLayer.succeed(DatabaseContext(new SqliteZioJdbcContext(SnakeCase))) val datasourceLayer = Quill.DataSource.fromPrefix("database") diff --git a/Core/src/org/tbasket/data/DatabaseContext.scala b/Core/src/org/tbasket/data/DatabaseContext.scala new file mode 100644 index 0000000..bb36295 --- /dev/null +++ b/Core/src/org/tbasket/data/DatabaseContext.scala @@ -0,0 +1,11 @@ +package org.tbasket.data + +import io.getquill.NamingStrategy +import io.getquill.context.qzio.ZioJdbcContext +import io.getquill.context.sql.idiom.SqlIdiom + +case class DatabaseContext(v: ZioJdbcContext[SqlIdiom, NamingStrategy]): + export v.* + + + diff --git a/Core/src/org/tbasket/data/User.scala b/Core/src/org/tbasket/data/User.scala index 3a7d2af..f39b752 100644 --- a/Core/src/org/tbasket/data/User.scala +++ b/Core/src/org/tbasket/data/User.scala @@ -4,10 +4,11 @@ import io.getquill.* import java.util.UUID -case class User( +final case class User( id : UUID, name : String, forename : String, passwordHash: Int, mailAddress : String - ) \ No newline at end of file + ) + diff --git a/Core/src/org/tbasket/endpoint/Endpoint.scala b/Core/src/org/tbasket/endpoint/Endpoint.scala index 0a0b743..203b7ed 100644 --- a/Core/src/org/tbasket/endpoint/Endpoint.scala +++ b/Core/src/org/tbasket/endpoint/Endpoint.scala @@ -1,23 +1,24 @@ package org.tbasket.endpoint -import io.getquill.{Literal, NamingStrategy, SqliteDialect} import io.getquill.context.qzio.{ZioContext, ZioJdbcContext} import io.getquill.context.sql.idiom.SqlIdiom +import io.getquill.idiom.Idiom +import io.getquill.{Literal, NamingStrategy, SqliteDialect} import org.apache.logging.log4j.LogManager import org.tbasket.auth.Authentificator +import org.tbasket.data.DatabaseContext import org.tbasket.endpoint.Endpoint.LOG -import org.tbasket.handler.LoginHandler +import org.tbasket.handler.LoginPageHandler import zio.* import zio.http.* import zio.http.ServerConfig.LeakDetectionLevel import zio.http.model.Method.{GET, POST} import zio.http.model.Status - -import scala.collection.mutable +import zio.http.model.Status.InternalServerError import zio.http.netty.client.ConnectionPool -import io.getquill.idiom.Idiom import javax.sql.DataSource +import scala.collection.mutable class Endpoint(port: Int): @@ -26,10 +27,10 @@ class Endpoint(port: Int): private def applyGenerics(response: Response): Response = response.withAccessControlAllowOrigin("*") - private val app = Http.collectZIO[Request] { + private val app = Http.collectZIO[Request] { case r@POST -> _ / "login" => - LoginHandler.post(r) - + LoginPageHandler.post(r) + case r@method -> path => val ipInsights = r.remoteAddress .map(ip => s": request received from $ip.") @@ -38,7 +39,34 @@ class Endpoint(port: Int): s"Was unable to find a handler for request '$path' with method $method ${ipInsights}" ) ZIO.succeed(Response(Status.NotFound)) - }.map(applyGenerics) + }.catchAllCause(handleUnexpectedError) + .map(applyGenerics) + + private def handleUnexpectedError(cause: Cause[Throwable]): HttpApp[Any, Throwable] = { + def report(kind: String, value: Throwable = null, trace: StackTrace = StackTrace.none) = + LOG.error(s"Received unhandled $kind cause ${if value == null then "" else ": " + value}") + LOG.error(trace) + + cause match + case Cause.Empty => report("empty") + case Cause.Fail(e, trace) => report("failure", e, trace) + case Cause.Die(e, trace) => report("die", e, trace) + case Cause.Interrupt(fiberId, e) => report(s"interruption of $fiberId", null, e) + case Cause.Stackless(cause, _) => + LOG.error("stackless error :") + handleUnexpectedError(cause) + case Cause.Then(left, right) => + handleUnexpectedError(left) + LOG.error("**THEN this error occurred : **") + handleUnexpectedError(right) + case Cause.Both(left, right) => + handleUnexpectedError(left) + LOG.error("**AND this error also occurred (async) : **") + handleUnexpectedError(right) + + + Http.succeed(Response.status(InternalServerError)) + } val run = val config = ServerConfig.default @@ -49,7 +77,7 @@ class Endpoint(port: Int): Server.install(app).flatMap { port => LOG.info(s"Listening API entries on $port") ZIO.never - }.provideSome[ZioJdbcContext[SqlIdiom, NamingStrategy] & Authentificator & DataSource]( + }.provideSome[DatabaseContext & Authentificator & DataSource]( Scope.default, serverConfigLayer, ConnectionPool.fixed(4), diff --git a/Core/src/org/tbasket/error/AuthException.scala b/Core/src/org/tbasket/error/AuthException.scala new file mode 100644 index 0000000..3a4795a --- /dev/null +++ b/Core/src/org/tbasket/error/AuthException.scala @@ -0,0 +1,9 @@ +package org.tbasket.error + +import org.tbasket.error.ExceptionEnum + +enum AuthException extends ExceptionEnum { + case InvalidPassword + case UserNotFound + case UserAlreadyRegistered +} \ No newline at end of file diff --git a/Core/src/org/tbasket/error/ExceptionEnum.scala b/Core/src/org/tbasket/error/ExceptionEnum.scala new file mode 100644 index 0000000..a9c539d --- /dev/null +++ b/Core/src/org/tbasket/error/ExceptionEnum.scala @@ -0,0 +1,5 @@ +package org.tbasket.error + +trait ExceptionEnum extends Exception { + +} diff --git a/Core/src/org/tbasket/error/JwtException.scala b/Core/src/org/tbasket/error/JwtException.scala new file mode 100644 index 0000000..df61cbf --- /dev/null +++ b/Core/src/org/tbasket/error/JwtException.scala @@ -0,0 +1,10 @@ +package org.tbasket.error + +enum JwtException extends ExceptionEnum { + case InvalidJwt(cause: String) + case ExpiredJwt + + + case InvalidEmitterResponse + case EmitterInternalError +} \ No newline at end of file diff --git a/Core/src/org/tbasket/error/RegularException.scala b/Core/src/org/tbasket/error/RegularException.scala new file mode 100644 index 0000000..5d31a3f --- /dev/null +++ b/Core/src/org/tbasket/error/RegularException.scala @@ -0,0 +1,9 @@ +package org.tbasket.error + +enum RegularException extends ExceptionEnum { + case InternalError(cause: Throwable) + case InvalidArgumentError(cause: String) + + + case InvalidRequest(msg: String, cause: String) +} diff --git a/Core/src/org/tbasket/handler/HandlerUtils.scala b/Core/src/org/tbasket/handler/HandlerUtils.scala index c8201d4..7139554 100644 --- a/Core/src/org/tbasket/handler/HandlerUtils.scala +++ b/Core/src/org/tbasket/handler/HandlerUtils.scala @@ -1,11 +1,18 @@ package org.tbasket.handler -import zio.ZIO +import org.tbasket.error.RegularException.InvalidRequest +import zio.{Task, ZIO} import zio.http.Body -import zio.json.ast.Json -import zio.json.ast.JsonCursor +import zio.json.* +import zio.json.ast.{Json, JsonCursor} + +import scala.language.reflectiveCalls object HandlerUtils { - def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": $errorType,"msg": "$msg"}""") + def parseAttribute[V, T <: Json {def value: V}](json: Json, name: String, cursor: JsonCursor[Json, T]): Task[V] = + ZIO.fromEither(json.get[T](cursor).map(_.value)) + .mapError(InvalidRequest(s"Missing or invalid field $name.", _)) + + def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": $errorType,"msg": "$msg"}""") } diff --git a/Core/src/org/tbasket/handler/LoginHandler.scala b/Core/src/org/tbasket/handler/LoginHandler.scala deleted file mode 100644 index 49d751e..0000000 --- a/Core/src/org/tbasket/handler/LoginHandler.scala +++ /dev/null @@ -1,104 +0,0 @@ -package org.tbasket.handler - -import io.getquill.* -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.handler.HandlerUtils.errorBody -import org.tbasket.handler.LoginError.* -import org.tbasket.data.User -import zio.http.* -import zio.http.model.{Cookie, Header, Headers, Status} -import zio.json.* -import zio.json.ast.Json.Str -import zio.json.ast.{Json, JsonCursor} -import zio.{ZEnvironment, ZIO, *} - -import java.sql.SQLException -import java.util.UUID - -enum LoginError: - case TokenNotFound(token: UUID) - case UserNotFound(user: User) - case InvalidPassword - case InvalidRequest(msg: String, cause: String) - case InternalError(t: Throwable) - - -object LoginHandler: - - private def getUser(json: Json) = - 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.hashCode)) - .mapError(InvalidRequest("Missing or invalid field password", _)) - - result <- run(quote { // TODO use argon2id - query[User] - .filter(usr => usr.mailAddress == lift(mail)) - .filter(usr => usr.passwordHash == lift(password)) - }).mapError(InternalError.apply) - yield result.headOption - }.someOrFail(InvalidPassword) - - def post(request: Request) = - val bindSession = - for - body <- request - .body - .asString - .tapError(Console.printError(_)) - .mapError(s => - InvalidRequest("Wrong request body", s.getMessage) - ) - - json <- ZIO.fromEither(body.fromJson[Json]) - .mapError(InvalidRequest("Invalid JSON body", _)) - - user <- getUser(json) - jwt <- ZIO.serviceWithZIO[Authentificator](_.requestJwt(user)) - yield (user, jwt) - - bindSession.map { case (_, jwt) => - Response( - status = Status.Found, - headers = Headers.location("/") ++ //login successful, go back to main page - Headers.setCookie(Cookie("JWT", jwt)) //and set the token cookie - ) - } fold( { - _ match - case TokenNotFound(_) => Response( - status = Status.Unauthorized, - body = errorBody("unauthorized", "unknown token"), - headers = - Headers( - Headers.location("/login") - ) // send back caller to login panel - ) - case UserNotFound(_) => Response( - status = Status.Unauthorized, - body = errorBody("unauthorized", "unknown user email"), - headers = - Headers( - Headers.location("/register") - ) // send back caller to register panel - ) - case InvalidPassword => Response( - status = Status.Unauthorized, - body = errorBody("unauthorized", "invalid password") - ) - case InvalidRequest(msg, cause) => Response( - status = Status.Unauthorized, - body = errorBody("wrong request", s"$cause: $msg") - ) - case InternalError(_) => Response( - status = Status.InternalServerError, - body = errorBody("internal", "internal error, please contact support") - ) - }, x => x) diff --git a/Core/src/org/tbasket/handler/LoginPageHandler.scala b/Core/src/org/tbasket/handler/LoginPageHandler.scala new file mode 100644 index 0000000..2ad4f02 --- /dev/null +++ b/Core/src/org/tbasket/handler/LoginPageHandler.scala @@ -0,0 +1,97 @@ +package org.tbasket.handler + +import io.getquill.* +import io.getquill.context.ZioJdbc.* +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.data.{DatabaseContext, User} +import org.tbasket.error.AuthException.* +import org.tbasket.error.ExceptionEnum +import org.tbasket.error.JwtException.* +import org.tbasket.error.RegularException.* +import org.tbasket.handler.HandlerUtils.errorBody +import zio.http.* +import zio.http.model.{Cookie, Header, Headers, Status} +import zio.json.* +import zio.json.ast.Json.Str +import zio.json.ast.{Json, JsonCursor} +import zio.{ZEnvironment, ZIO, *} + +import java.sql.SQLException +import java.util.UUID + + +object LoginPageHandler extends PageHandler: + + private val LOG = LogManager.getLogger("Login") + + private def getUser(json: Json) = + ZIO.serviceWithZIO[Authentificator] { auth => + for + mail <- HandlerUtils.parseAttribute(json, "mail", JsonCursor.field("name").isString) + password <- HandlerUtils.parseAttribute(json, "mail", JsonCursor.field("name").isString) + user <- auth.loginUser(mail, password) + yield user + } + + def post(request: Request) = + val bindSession = + for + body <- request + .body + .asString + .mapError(s => + InvalidRequest("Unparseable request body", s.getMessage) + ) + + json <- ZIO.fromEither(body.fromJson[Json]) + .mapError(InvalidRequest("Invalid JSON body", _)) + + user <- getUser(json) + jwt <- ZIO.serviceWithZIO[Authentificator](_.requestNewJwt(user)) + yield jwt + + bindSession.map { jwt => + Response( + status = Status.Found, + headers = Headers.location("/") ++ //login successful, go back to main page + Headers.setCookie(Cookie("JWT", jwt)) //and set the token cookie + ) + }.catchAll { + case UserNotFound => ZIO.attempt(Response( + status = Status.Unauthorized, + body = errorBody("unauthorized", "unknown user email"), + headers = + Headers( + Headers.location("/register") + ) // send back caller to register panel + )) + + case InvalidPassword => ZIO.attempt(Response( + status = Status.Unauthorized, + body = errorBody("unauthorized", "invalid password") + )) + + case InvalidRequest(msg, cause) => ZIO.attempt(Response( + status = Status.Unauthorized, + body = errorBody("invalid request", s"$cause: $msg") + )) + + case InternalError(e) => + LOG.error("Internal error : ") + LOG.throwing(e) + ZIO.attempt(Response( + status = Status.InternalServerError, + body = errorBody("internal", "internal error, please contact support") + )) + + case other => + LOG.error("Unhandle exception : ") + LOG.throwing(other) + ZIO.attempt(Response( + status = Status.InternalServerError, + body = errorBody("internal", "internal error, please contact support") + )) + } diff --git a/Core/src/org/tbasket/handler/PageHandler.scala b/Core/src/org/tbasket/handler/PageHandler.scala new file mode 100644 index 0000000..7b70205 --- /dev/null +++ b/Core/src/org/tbasket/handler/PageHandler.scala @@ -0,0 +1,8 @@ +package org.tbasket.handler + +/** + * tag interface for page handlers + * */ +trait PageHandler { + +} diff --git a/Core/src/org/tbasket/handler/RegisterPageHandler.scala b/Core/src/org/tbasket/handler/RegisterPageHandler.scala new file mode 100644 index 0000000..9315cba --- /dev/null +++ b/Core/src/org/tbasket/handler/RegisterPageHandler.scala @@ -0,0 +1,40 @@ +package org.tbasket.handler + +import org.tbasket.auth.Authentificator +import org.tbasket.data.User +import org.tbasket.error.RegularException.InvalidRequest +import org.tbasket.handler.HandlerUtils.parseAttribute +import zio.ZIO +import zio.http.model.{Cookie, Headers, Status} +import zio.http.{Request, Response, model} +import zio.json.ast.{Json, JsonCursor} +import zio.json.* + +object RegisterPageHandler extends PageHandler { + + private def tryPost(request: Request) = + for + body <- request.body.asString + .mapError(e => InvalidRequest("Invalid request body", e.getMessage)) + + json <- ZIO.fromEither(body.fromJson[Json]) + .mapError(InvalidRequest("Invalid JSON body", _)) + + name <- parseAttribute(json, "name", JsonCursor.field("name").isString) + forename <- parseAttribute(json, "forename", JsonCursor.field("forename").isString) + mail <- parseAttribute(json, "mail", JsonCursor.field("mail").isString) + password <- parseAttribute(json, "password", JsonCursor.field("password").isString) + + user <- ZIO.serviceWithZIO[Authentificator](_.registerUser(name, forename, mail, password)) + jwt <- ZIO.serviceWithZIO[Authentificator](_.requestNewJwt(user)) + yield Response( + status = Status.Found, + headers = Headers.location("/") ++ //register successful, go back to main page + Headers.setCookie(Cookie("JWT", jwt)) + ) + + def post(request: Request) = tryPost(request).catchSome { + case x => ??? + } + +} diff --git a/JWTEmitter/src/org/tbasket/jwt/JwtGenerator.scala b/JWTEmitter/src/org/tbasket/jwt/JwtGenerator.scala index 1180ae0..34c3cf8 100644 --- a/JWTEmitter/src/org/tbasket/jwt/JwtGenerator.scala +++ b/JWTEmitter/src/org/tbasket/jwt/JwtGenerator.scala @@ -25,7 +25,6 @@ import javax.crypto.SecretKey class JwtGenerator(tokenLifespan: Duration, key: PrivateKey, algorithm: JwtAsymmetricAlgorithm): - private def claims(content: String) = JwtClaim( expiration = Some(currentTimeMillis() + tokenLifespan.toMillis), issuedAt = Some(currentTimeMillis()), @@ -33,7 +32,6 @@ class JwtGenerator(tokenLifespan: Duration, key: PrivateKey, algorithm: JwtAsymm content = content ) - def generateTokenResponse(request: Request): Task[Response] = for claims <- request.body.asString.map(claims) diff --git a/build.sc b/build.sc index 5608c5e..161dee8 100644 --- a/build.sc +++ b/build.sc @@ -46,4 +46,6 @@ object Core extends HttpModule { //also handles http ivy"org.xerial:sqlite-jdbc:3.40.0.0", ) + //override def scalacOptions = T { Seq("-explain") } + }