diff --git a/Core/src/org/tbasket/endpoint/Endpoint.scala b/Core/src/org/tbasket/endpoint/Endpoint.scala index a0efbc6..70a1a44 100644 --- a/Core/src/org/tbasket/endpoint/Endpoint.scala +++ b/Core/src/org/tbasket/endpoint/Endpoint.scala @@ -11,8 +11,8 @@ import org.tbasket.data.DatabaseContext import org.tbasket.dispatch.StaticWebService import org.tbasket.endpoint.Endpoint.{Log, app} import org.tbasket.error.* -import org.tbasket.handler.HandlerUtils.errorBody -import org.tbasket.handler.{HandlerUtils, LoginPageHandler, RegisterPageHandler} +import EndpointUtils.errorBody +import org.tbasket.endpoint.auth.{LoginHandler, RegisterHandler} import zio.* import zio.http.* import zio.http.ServerConfig.LeakDetectionLevel @@ -51,10 +51,10 @@ object Endpoint: private def tryHandle(r: Request) = r match case r@POST -> _ / "login" => - LoginPageHandler.post(r) + LoginHandler.login(r) case r@POST -> _ / "register" => - RegisterPageHandler.post(r) + RegisterHandler.register(r) case r@GET -> _ => diff --git a/Core/src/org/tbasket/endpoint/EndpointUtils.scala b/Core/src/org/tbasket/endpoint/EndpointUtils.scala new file mode 100644 index 0000000..76e46b8 --- /dev/null +++ b/Core/src/org/tbasket/endpoint/EndpointUtils.scala @@ -0,0 +1,29 @@ +package org.tbasket.endpoint + +import io.netty.handler.codec.http.QueryStringDecoder +import org.tbasket.error.* +import zio.http.Body +import zio.http.api.openapi.OpenAPI.Parameter.QueryStyle.Form +import zio.json.* +import zio.json.ast.{Json, JsonCursor} +import zio.{Task, ZIO, http} + +import java.net.URLDecoder +import java.nio.charset.StandardCharsets +import scala.deriving.Mirror +import scala.jdk.CollectionConverters.* +import scala.language.{implicitConversions, reflectiveCalls} + +object EndpointUtils { + + + def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": "$errorType","msg": "$msg"}""") + + inline def decode[A <: Product](body: Body)(using mirror: Mirror.Of[A]) = + for + str <- body.asString + decoded <- ZIO.fromEither(str.fromJson(DeriveJsonDecoder.gen[A])) + .mapError(InvalidRequest("Invalid request body", _)) + yield decoded + +} \ No newline at end of file diff --git a/Core/src/org/tbasket/handler/PageHandler.scala b/Core/src/org/tbasket/endpoint/PageHandler.scala similarity index 71% rename from Core/src/org/tbasket/handler/PageHandler.scala rename to Core/src/org/tbasket/endpoint/PageHandler.scala index 7b70205..e5c1864 100644 --- a/Core/src/org/tbasket/handler/PageHandler.scala +++ b/Core/src/org/tbasket/endpoint/PageHandler.scala @@ -1,4 +1,4 @@ -package org.tbasket.handler +package org.tbasket.endpoint /** * tag interface for page handlers diff --git a/Core/src/org/tbasket/handler/LoginPageHandler.scala b/Core/src/org/tbasket/endpoint/auth/LoginHandler.scala similarity index 69% rename from Core/src/org/tbasket/handler/LoginPageHandler.scala rename to Core/src/org/tbasket/endpoint/auth/LoginHandler.scala index 29fb235..b759061 100644 --- a/Core/src/org/tbasket/handler/LoginPageHandler.scala +++ b/Core/src/org/tbasket/endpoint/auth/LoginHandler.scala @@ -1,4 +1,4 @@ -package org.tbasket.handler +package org.tbasket.endpoint.auth import io.getquill.* import io.getquill.context.ZioJdbc.* @@ -7,8 +7,10 @@ import io.getquill.context.sql.idiom.SqlIdiom import org.apache.logging.log4j.{LogManager, Logger} import org.tbasket.auth.Authenticator import org.tbasket.data.{DatabaseContext, User} +import org.tbasket.endpoint.EndpointUtils.{decode, errorBody} +import org.tbasket.endpoint.PageHandler +import org.tbasket.endpoint.request.LoginRequest import org.tbasket.error.* -import org.tbasket.handler.HandlerUtils.* import zio.* import zio.http.* import zio.http.model.{Cookie, Header, Headers, Status} @@ -20,24 +22,14 @@ import java.sql.SQLException import java.util.UUID -object LoginPageHandler extends PageHandler: +object LoginHandler extends PageHandler: implicit private final val Log: Logger = LogManager.getLogger("/login") - private def getUser(params: Map[String, String]) = - ZIO.serviceWithZIO[Authenticator] { auth => - val zio = for - mail <- search(params)("email") - password <- search(params)("password") - user <- auth.loginUser(mail, password) - yield user - zio - } - - private def tryPost(request: Request) = + private def tryLogin(request: Request) = for - params <- parseRequestForm(request.body) - user <- getUser(params) + request <- decode[LoginRequest](request.body) + user <- ZIO.serviceWithZIO[Authenticator](_.loginUser(request.email, request.password)) jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user)) yield Response( @@ -46,8 +38,8 @@ object LoginPageHandler extends PageHandler: Headers.setCookie(Cookie("JWT", jwt)) //and set the token cookie ) - def post(request: Request) = - tryPost(request).catchSome { + def login(request: Request) = + tryLogin(request).catchSome { case UserNotFound(msg) => ZIO.attempt(Response( status = Status.Found, body = errorBody("unauthorized", msg), @@ -62,3 +54,4 @@ object LoginPageHandler extends PageHandler: body = errorBody("unauthorized", msg) )) } + diff --git a/Core/src/org/tbasket/handler/RegisterPageHandler.scala b/Core/src/org/tbasket/endpoint/auth/RegisterHandler.scala similarity index 70% rename from Core/src/org/tbasket/handler/RegisterPageHandler.scala rename to Core/src/org/tbasket/endpoint/auth/RegisterHandler.scala index 11a44b7..ad41869 100644 --- a/Core/src/org/tbasket/handler/RegisterPageHandler.scala +++ b/Core/src/org/tbasket/endpoint/auth/RegisterHandler.scala @@ -1,30 +1,26 @@ -package org.tbasket.handler +package org.tbasket.endpoint.auth import org.apache.logging.log4j.{LogManager, Logger} import org.tbasket.auth.Authenticator import org.tbasket.data.User +import org.tbasket.endpoint.EndpointUtils.* +import org.tbasket.endpoint.PageHandler +import org.tbasket.endpoint.request.RegisterRequest import org.tbasket.error.* -import org.tbasket.handler.HandlerUtils.{errorBody, parseRequestForm, search} import zio.ZIO import zio.http.model.{Cookie, Headers, Status} import zio.http.{Body, Request, Response, model} import zio.json.* import zio.json.ast.{Json, JsonCursor} -object RegisterPageHandler extends PageHandler { +object RegisterHandler extends PageHandler { - implicit private final val Log: Logger = LogManager.getLogger("/register") - private def tryPost(request: Request) = + private def tryRegister(request: Request) = for - params <- parseRequestForm(request.body) + request <- decode[RegisterRequest](request.body) - name <- search(params)("name") - forename <- search(params)("forename") - mail <- search(params)("email") - password <- search(params)("password") - - user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(name, forename, mail, password)) + user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(request.name, request.forename, request.email, request.password)) jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user)) yield Response( status = Status.Found, @@ -32,7 +28,7 @@ object RegisterPageHandler extends PageHandler { Headers.setCookie(Cookie("JWT", jwt)) ) - def post(request: Request) = tryPost(request) + def register(request: Request) = tryRegister(request) .catchSome { case InvalidEmail(msg) => ZIO.attempt(Response( status = Status.ExpectationFailed, diff --git a/Core/src/org/tbasket/endpoint/auth/TokenLoginHandler.scala b/Core/src/org/tbasket/endpoint/auth/TokenLoginHandler.scala new file mode 100644 index 0000000..41b3d96 --- /dev/null +++ b/Core/src/org/tbasket/endpoint/auth/TokenLoginHandler.scala @@ -0,0 +1,11 @@ +package org.tbasket.endpoint.auth + +import zio.http.Request + +class TokenLoginHandler { + + def login(request: Request) = { + + } + +} diff --git a/Core/src/org/tbasket/endpoint/request/AuthRequests.scala b/Core/src/org/tbasket/endpoint/request/AuthRequests.scala new file mode 100644 index 0000000..7c42dd0 --- /dev/null +++ b/Core/src/org/tbasket/endpoint/request/AuthRequests.scala @@ -0,0 +1,6 @@ +package org.tbasket.endpoint.request + + +case class RegisterRequest(name: String, forename: String, email: String, password: String) + +case class LoginRequest(email: String, password: String) \ No newline at end of file diff --git a/Core/src/org/tbasket/handler/HandlerUtils.scala b/Core/src/org/tbasket/handler/HandlerUtils.scala deleted file mode 100644 index 29394f7..0000000 --- a/Core/src/org/tbasket/handler/HandlerUtils.scala +++ /dev/null @@ -1,31 +0,0 @@ -package org.tbasket.handler - -import io.netty.handler.codec.http.QueryStringDecoder -import org.tbasket.error.* -import zio.http.Body -import zio.http.api.openapi.OpenAPI.Parameter.QueryStyle.Form -import zio.json.* -import zio.json.ast.{Json, JsonCursor} -import zio.{Task, ZIO} - -import java.net.URLDecoder -import java.nio.charset.StandardCharsets -import scala.jdk.CollectionConverters.* -import scala.language.reflectiveCalls - -object HandlerUtils { - - - def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": "$errorType","msg": "$msg"}""") - - def parseRequestForm(body: Body) = - (for - decoded <- body.asString.map(str => URLDecoder.decode(str, StandardCharsets.UTF_8.name())) - params = decoded.split('&').collect { case s"$k=$v" => (k.stripPrefix("?"), v)}.toMap - yield params) - .mapError(s => InvalidRequest("Invalid request body", s.getMessage)) - - def search(map: Map[String, String])(name: String) = - ZIO.attempt(map.get(name)).someOrFail(InvalidRequest(s"Missing or invalid field $name.")) - -} \ No newline at end of file diff --git a/drone/image-deploy.sh b/drone/image-deploy.sh deleted file mode 100644 index 9ddcce8..0000000 --- a/drone/image-deploy.sh +++ /dev/null @@ -1,27 +0,0 @@ - -FILES_COMMITTED=$(git diff --name-only HEAD^) -IMAGES_COMMITTED=$(echo "$FILES_COMMITTED" | grep -E "^.*\.dockerfile$") - -REPOSITORY="hub.codefirst.iut.uca.fr/maxime.batista/codefirst-docdeployer" - - -dockerd - -push_image() ({ - local IMAGE_NAME=$(basename "$1" | rev | cut -d . -f2- | rev) - cd "$(dirname "$1")" - - - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" "$REPOSITORY" - echo "Building $IMAGE_NAME..." - docker build -f "$(basename "$1")" . - docker tag "$IMAGE_NAME" "$REPOSITORY/$IMAGE_NAME:latest" - echo "Pushing $IMAGE_NAME..." - docker push "$REPOSITORY/$IMAGE_NAME" -}) - -echo $IMAGES_COMMITTED - -for IMAGE in $IMAGES_COMMITTED; do - push_image "$IMAGE" -done \ No newline at end of file diff --git a/tests/src/org/tbasket/test/TestUtils.scala b/tests/src/org/tbasket/test/TestUtils.scala index d7c59a7..ae33d11 100644 --- a/tests/src/org/tbasket/test/TestUtils.scala +++ b/tests/src/org/tbasket/test/TestUtils.scala @@ -25,10 +25,4 @@ object TestUtils { yield json } - implicit def makeFormBody(params: (String, String)*): Body = { - val qse = new QueryStringEncoder("") - params.foreach(qse.addParam(_, _)) - Body.fromString(qse.toString) - } - } diff --git a/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala b/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala index ace7a54..bbefe6b 100644 --- a/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala +++ b/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala @@ -6,8 +6,8 @@ import org.tbasket.auth.Authenticator import org.tbasket.data.{Database, DatabaseContext} import org.tbasket.endpoint.Endpoint import org.tbasket.endpoint.Endpoint.handle +import org.tbasket.endpoint.auth.LoginHandler import org.tbasket.error.* -import org.tbasket.handler.LoginPageHandler import org.tbasket.test.TestUtils.* import org.tbasket.test.{TestLayers, TestUtils} import zio.* @@ -26,9 +26,9 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") { private def requestsSpec = suite("bad request tests")( ZIO.attempt(Map( "empty packet" -> Body.empty, - "with no mail attribute" -> makeFormBody("password" -> "bouhours"), - "with no password attribute" -> makeFormBody("email" -> "valid.email@not.very"), - "with invalid form data" -> Body.fromString("""this is a corrupted form data""") + "with no mail attribute" -> Body.fromString("""{"password":"bouhours"}"""), + "with no password attribute" -> Body.fromString("""{"email":"valid.email@not.very"}"""), + "with invalid form data" -> Body.fromString("""{this is a corrupted form data}""") )).map(_.map { case (name, body) => test(name) { for @@ -46,7 +46,7 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") { suite("login situation tests")( test("login with unknown account") { for - response <- handle(Request.post(makeFormBody("password" -> "bouhours", "email" -> "unknownaccount@gmail.com"), url)) + response <- handle(Request.post(Body.fromString("""{"password":"bouhours","email":"unknownaccount@gmail.com"}"""), url)) json <- getJsonBody(response) errorType <- parseAttribute(json, JsonCursor.field("error").isString) yield @@ -58,16 +58,16 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") { test("login with known account") { for - response <- handle(Request.post(makeFormBody("password" -> "123456", "email" -> "maximebatista18@gmail.com"), url)) + response <- handle(Request.post(Body.fromString("""{"password":"123456","email":"maximebatista18@gmail.com"}"""), url)) yield assert(response)(hasField("status", _.status, equalTo(Status.Found))) - && assert(response)(hasField("body", _.body, equalTo(Body.empty))) //TODO assert that the cookie name is JWT + && assert(response)(hasField("body", _.body, equalTo(Body.empty))) && assert(response)(hasField("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie))))) }, test("login with known account wrong password") { for - fiber <- handle(Request.post(makeFormBody("password" -> "wrong", "email" -> "maximebatista18@gmail.com"), url)).fork + fiber <- handle(Request.post(Body.fromString("""{"password":"wrong","email":"maximebatista18@gmail.com"}"""), url)).fork _ <- TestClock.adjust(1.seconds) response <- ZIO.fromFiber(fiber) json <- getJsonBody(response) diff --git a/tests/src/org/tbasket/test/pages/RegisterPageHandlerTests.scala b/tests/src/org/tbasket/test/pages/RegisterPageHandlerTests.scala index 8d1e0be..c62db8a 100644 --- a/tests/src/org/tbasket/test/pages/RegisterPageHandlerTests.scala +++ b/tests/src/org/tbasket/test/pages/RegisterPageHandlerTests.scala @@ -17,9 +17,9 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") { private def requestsSpec = suite("bad request tests")( ZIO.attempt(Map( //TODO test all wrong combinations "empty packet" -> Body.empty, - "with no mail attribute" -> makeFormBody("password" -> "123445678"), - "with no password attribute" -> makeFormBody("email" -> "valid.mail@x.y"), - "with invalid form data" -> Body.fromString("""this is a corrupted form data""") + "with no mail attribute" -> Body.fromString("""{"password": "123445678"}"""), + "with no password attribute" -> Body.fromString("""{"email":"valid.mail@x.y"}"""), + "with invalid form data" -> Body.fromString("""{this is a corrupted form data}""") )).map(_.map { case (name, body) => test(name) { for @@ -36,7 +36,7 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") { private def registerSpec = suite("register tests")( test("register then try register again") { (for - resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url)) + resp <- handle(Request.post(Body.fromString("""{"name":"tuaillon","forename":"leo","email":"leo.tuaillon@etu.uca.fr","password":"bouhours"}"""), url)) yield assert(resp)(hasField("status", _.status, equalTo(Status.Found))) && assert(resp)(hasField("body", _.body, equalTo(Body.empty))) //TODO assert that the cookie name is JWT @@ -45,7 +45,7 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") { ) *> (for - resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url)) + resp <- handle(Request.post(Body.fromString("""{"name":"tuaillon","forename":"leo","email":"leo.tuaillon@etu.uca.fr","password":"bouhours"}"""), url)) json <- getJsonBody(resp) errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString) @@ -56,7 +56,7 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") { }, test("register bad email") { for - resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillonbadmail", "password" -> "bouhours"), url)) + resp <- handle(Request.post(Body.fromString("""{"name":"tuaillon","forename":"leo","email":"leo.tuaillonbadmail","password":"bouhours"}"""), url)) json <- getJsonBody(resp) errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString) yield @@ -65,7 +65,7 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") { }, test("register bad password") { for - resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "1234"), url)) + resp <- handle(Request.post(Body.fromString("""{"name":"tuaillon","forename":"leo","email":"leo.tuaillon@etu.uca.fr","password":"1234"}"""), url)) json <- getJsonBody(resp) errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString) yield @@ -74,7 +74,7 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") { }, test("register bad name") { for - resp <- handle(Request.post(makeFormBody("name" -> "", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url)) + resp <- handle(Request.post(Body.fromString("""{"name":"","forename":"leo","email":"leo.tuaillon@etu.uca.fr","password":"123456"}"""), url)) json <- getJsonBody(resp) errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString) yield @@ -83,7 +83,7 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") { }, test("register bad forename") { for - resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url)) + resp <- handle(Request.post(Body.fromString("""{"name":"tuaillon","forename":"","email":"leo.tuaillon@etu.uca.fr","password":"bouhours"}"""), url)) json <- getJsonBody(resp) errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString) yield diff --git a/tests/src/org/tbasket/test/pages/TBasketPageSpec.scala b/tests/src/org/tbasket/test/pages/TBasketPageSpec.scala index 45ba226..7be53f2 100644 --- a/tests/src/org/tbasket/test/pages/TBasketPageSpec.scala +++ b/tests/src/org/tbasket/test/pages/TBasketPageSpec.scala @@ -6,7 +6,7 @@ import io.getquill.context.sql.idiom.SqlIdiom import org.tbasket.auth.Authenticator import org.tbasket.data.DatabaseContext import org.tbasket.dispatch.StaticWebService -import org.tbasket.handler.LoginPageHandler.post +import org.tbasket.endpoint.auth.LoginHandler.login import org.tbasket.test.TestLayers.* import zio.* import zio.http.netty.client.ConnectionPool @@ -18,8 +18,8 @@ import javax.sql.DataSource abstract class TBasketPageSpec(location: String) extends ZIOSpecDefault { protected val url = URL.fromString(s"http://localhost$location") match - case Left(value) => throw value - case Right(url) => url + case Left(exception) => throw exception + case Right(url) => url protected def tspec: Spec[DataSource & ClientConfig & Authenticator & ConnectionPool & Scope & DatabaseContext & Client & StaticWebService, Any]