Compare commits

..

16 Commits

Author SHA1 Message Date
Override-6 8692ed55c3 writting some tests
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 b94b4493e2 modified drone script
continuous-integration/drone/push Build is failing Details
continuous-integration/drone Build is passing Details
2 years ago
Override-6 d7c57f06fa fixing tests and added script to generate key pairs for tests
continuous-integration/drone/push Build was killed Details
2 years ago
Override-6 023f847b06 fixing tests and drone
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 708c3308d5 fix
continuous-integration/drone/push Build is passing Details
2 years ago
Override-6 01eebf9bc1 fix
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 b1380d2d5a fix
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 7b7eed885d fix
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 c4fae28f43 fix
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 87accaf7a2 fixing scripts
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 21249c1b82 triggering
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 05288f089f fixing scripts
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 a0652bb3d0 test commit
continuous-integration/drone/push Build was killed Details
continuous-integration/drone Build is failing Details
2 years ago
Override-6 2c5b54d5ff added git and docker to drone Deploy docker images pipeline
continuous-integration/drone/push Build was killed Details
2 years ago
Override-6 fb83280c79 fixing drone
continuous-integration/drone/push Build is failing Details
2 years ago
Override-6 a4196fae1b setting up docker image auto push
continuous-integration/drone/push Build was killed Details
2 years ago

4
.gitignore vendored

@ -1,15 +1,13 @@
build build
.idea .idea
out out
target
*/.gradle/ */.gradle/
.bsp .bsp
keys keys
www
log
server.properties server.properties
log
*.sqlite *.sqlite
*.$* *.$*

@ -4,6 +4,4 @@ emitter.cert=<x509 certificate path here>
endpoint.port=<enter port here> endpoint.port=<enter port here>
database.prefix=<database config prefix> database.prefix=database
pages.location=<location to html content>

@ -0,0 +1,7 @@
package org.tbasket
/**
* exception thrown when an error occurs due to an external fault from the server.
*/
class ExternalBasketServerException(msg: String, cause: Throwable = null)
extends Exception(msg, cause) {}

@ -0,0 +1,7 @@
package org.tbasket
/**
* exception thrown when an error occurs due to an internal fault of the server or its maintainers.
*/
class InternalBasketServerException(msg: String, cause: Throwable = null)
extends Exception(msg, cause) {}

@ -23,13 +23,16 @@ object Main extends ZIOAppDefault:
final val LOG = LogManager.getLogger("Core") final val LOG = LogManager.getLogger("Core")
override def run = ZIO.serviceWithZIO[ZIOAppArgs] { args => override def run = ZIO.serviceWithZIO[ZIOAppArgs] { args =>
retrieveConfig(args) for
.flatMap(config => setupAuth(config) <&> setupDatabase(config) <&> setupEndpoint(config)) config <- retrieveConfig(args)
.flatMap { res <- setupAuth(config) <&> setupDatabase(config) <&> setupEndpoint(config)
case (auth, db, ep) => yield
ep.run.provide(db.datasourceLayer, db.contextLayer, auth) 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 { private def setupEndpoint(config: ServerConfig) = ZIO.attempt {
new Endpoint(config.endpointPort) new Endpoint(config.endpointPort)

@ -8,9 +8,13 @@ import io.getquill.*
import io.getquill.context.qzio.ZioJdbcContext import io.getquill.context.qzio.ZioJdbcContext
import io.getquill.context.sql.idiom.SqlIdiom import io.getquill.context.sql.idiom.SqlIdiom
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.tbasket.InternalBasketServerException
import org.tbasket.auth.Authenticator.* import org.tbasket.auth.Authenticator.*
import org.tbasket.data.{DatabaseContext, User} import org.tbasket.data.{DatabaseContext, User}
import org.tbasket.error.* 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.algorithms.JwtAsymmetricAlgorithm
import pdi.jwt.{JwtClaim, JwtZIOJson} import pdi.jwt.{JwtClaim, JwtZIOJson}
import zio.* import zio.*
@ -27,11 +31,10 @@ import javax.sql.DataSource
import scala.collection.immutable.HashMap import scala.collection.immutable.HashMap
object Authenticator: object Authenticator:
private final val LOG = LogManager.getLogger("Authentification") private final val LOG = LogManager.getLogger("Authentification")
private final val ValidMailPattern = "^\\w+([.-]?\\w+)*@\\w+([.-]?\\w+)*(\\.\\w{2,3})+$".r private final val ValidMailPattern = "^\\w+([.-]?\\w+)*@\\w+([.-]?\\w+)*(\\.\\w{2,3})+$".r
private final val ValidForenamePattern = "^[a-zA-Z\\u00C0-\\u017F-']+$".r
private final val ValidNamePattern = ValidForenamePattern
private final val ValidPasswordPattern = ".{6,}".r private final val ValidPasswordPattern = ".{6,}".r
case class JwtContent(uuid: UUID) case class JwtContent(uuid: UUID)
@ -52,12 +55,12 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm)
def requestNewJwt(user: User) = { def requestNewJwt(user: User) = {
Client.request(mkRequest(user)) Client.request(mkRequest(user))
.flatMap { .flatMap {
case Response(Ok, _, body, _, _) => case Response(Ok, _, body, _, _) =>
body.asString body.asString
case Response(InternalServerError, _, body, _, _) => case Response(InternalServerError, _, _, _, _) =>
ZIO.fail(EmitterInternalError(_)).flatMap(f => body.asString.map(f)) ZIO.fail(new InternalBasketServerException("internal server error"))
case r => case _ =>
ZIO.fail(UnrecognizedEmitterResponse(s"Received unknown response from emitter ${r}")) ZIO.fail(new InternalBasketServerException("Received unknown response from emitter"))
} }
} }
@ -74,11 +77,19 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm)
import ctx.v.* import ctx.v.*
findByMail(mail) findByMail(mail)
.someOrFail(UserNotFound(s"user not found for email address $mail")) .someOrFail(UserNotFound)// await one second if password fails to reduce bruteforce
// await one second if password fails to reduce bruteforce //FIXME this wont actually reduce bruteforce .filterOrElse(_.passwordHash == hashPassword(password))(ZIO.sleep(1.second) *> ZIO.fail(InvalidPassword))
.filterOrElse(_.passwordHash == hashPassword(password))(Clock.sleep(1.seconds) *> ZIO.fail(InvalidPassword("invalid password")))
} }
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 => def registerUser(name: String, forename: String, mail: String, password: String) = ZIO.serviceWithZIO[DatabaseContext] { ctx =>
import ctx.* import ctx.*
@ -87,26 +98,13 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm)
val hash = hashPassword(password) val hash = hashPassword(password)
User(uuid, name, forename, hash, mail) User(uuid, name, forename, hash, mail)
} }
if (!ValidMailPattern.matches(mail)) if (!ValidMailPattern.matches(mail))
ZIO.fail(InvalidEmail(s"email address did not satisfy regex pattern")) ZIO.fail(InvalidEmail)
else if (!ValidPasswordPattern.matches(password)) else if (!ValidPasswordPattern.matches(password))
ZIO.fail(InvalidPassword(s"password did not satisfy regex pattern")) ZIO.fail(InvalidPassword)
else if (!ValidForenamePattern.matches(forename))
ZIO.fail(InvalidForename(s"email address did not satisfy regex pattern"))
else if (!ValidNamePattern.matches(name))
ZIO.fail(InvalidName(s"password did not satisfy regex pattern"))
else for else for
_ <- findByMail(mail).none.orElse(ZIO.fail(UserAlreadyRegistered(s"an email address already exists for '$mail'"))) _ <- findByMail(mail).none.orElse(ZIO.fail(UserAlreadyRegistered))
_ <- run(quote { _ <- run(insert(user)).fork
query[User].insert(
_.id -> lift(user).id,
_.name -> lift(user).name,
_.forename -> lift(user).forename,
_.passwordHash -> lift(user).passwordHash,
_.mailAddress -> lift(user).mailAddress,
)
}).fork
yield user yield user
} }

@ -62,9 +62,6 @@ final class FileServerConfig private(userProperties: Properties, schema: Propert
override val databaseConfigName: String = getPropertySafe("database.prefix") override val databaseConfigName: String = getPropertySafe("database.prefix")
override def pagesLocation: Option[Path] = Some(Path.of(getPropertySafe("pages.location")))
private def schemaString = { private def schemaString = {
schema.stringPropertyNames() schema.stringPropertyNames()
.toArray(new Array[String](_)) .toArray(new Array[String](_))

@ -4,7 +4,6 @@ import pdi.jwt.JwtAlgorithm
import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import pdi.jwt.algorithms.JwtAsymmetricAlgorithm
import zio.http.URL import zio.http.URL
import java.nio.file.Path
import java.security.cert.Certificate import java.security.cert.Certificate
trait ServerConfig { trait ServerConfig {
@ -18,6 +17,4 @@ trait ServerConfig {
def databaseConfigName: String def databaseConfigName: String
def pagesLocation: Option[Path]
} }

@ -4,14 +4,11 @@ import io.getquill.context.qzio.{ZioContext, ZioJdbcContext}
import io.getquill.context.sql.idiom.SqlIdiom import io.getquill.context.sql.idiom.SqlIdiom
import io.getquill.idiom.Idiom import io.getquill.idiom.Idiom
import io.getquill.{Literal, NamingStrategy, SqliteDialect} import io.getquill.{Literal, NamingStrategy, SqliteDialect}
import io.netty.handler.codec.http.HttpMethod import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.{LogManager, Logger}
import org.tbasket.auth.Authenticator import org.tbasket.auth.Authenticator
import org.tbasket.data.DatabaseContext import org.tbasket.data.DatabaseContext
import org.tbasket.endpoint.Endpoint.{Log, app} import org.tbasket.endpoint.Endpoint.LOG
import org.tbasket.error.* import org.tbasket.handler.LoginPageHandler
import EndpointUtils.errorBody
import org.tbasket.endpoint.auth.{LoginHandler, RegisterHandler, TokenLoginHandler}
import zio.* import zio.*
import zio.http.* import zio.http.*
import zio.http.ServerConfig.LeakDetectionLevel import zio.http.ServerConfig.LeakDetectionLevel
@ -20,108 +17,74 @@ import zio.http.model.Status
import zio.http.model.Status.InternalServerError import zio.http.model.Status.InternalServerError
import zio.http.netty.client.ConnectionPool import zio.http.netty.client.ConnectionPool
import java.sql.Timestamp
import javax.sql.DataSource import javax.sql.DataSource
import scala.collection.mutable import scala.collection.mutable
class Endpoint(port: Int): class Endpoint(port: Int):
val run = // set generic required headers
val config = ServerConfig.default private def applyGenerics(response: Response): Response =
.port(port) response.withAccessControlAllowOrigin("*")
.leakDetection(LeakDetectionLevel.PARANOID)
val serverConfigLayer = ServerConfig.live(config)
Server.install(app).flatMap { port =>
Log.info(s"Listening API entries on $port")
ZIO.never
}.provideSome[DatabaseContext & Authenticator & DataSource](
Scope.default,
serverConfigLayer,
ConnectionPool.fixed(4),
ClientConfig.default,
Server.live,
Client.live
)
object Endpoint:
final val Log = LogManager.getLogger("API")
private def tryHandle(r: Request) = r match
case r@POST -> _ / "auth" / "login" =>
LoginHandler.login(r)
case r@POST -> _ / "auth" / "register" => private val app = Http.collectZIO[Request] {
RegisterHandler.register(r) case r@POST -> _ / "login" =>
LoginPageHandler.post(r)
case r@method -> path => case r@method -> path =>
val ipInsights = r.remoteAddress val ipInsights = r.remoteAddress
.map(ip => s": request received from $ip.") .map(ip => s": request received from $ip.")
.getOrElse("") .getOrElse("")
Log.error( LOG.error(
s"Was unable to find a handler for request '$path' with method $method ${ipInsights}" s"Was unable to find a handler for request '$path' with method $method ${ipInsights}"
) )
ZIO.succeed(Response(Status.NotFound)) ZIO.succeed(Response(Status.NotFound))
}.catchAllCause(handleUnexpectedError)
def handle(r: Request) = tryHandle(r)
.catchSome(respondToRegulars)
.catchAllCause(handleUnexpectedError)
.map(applyGenerics) .map(applyGenerics)
val app = Http.collectZIO[Request] { r => private def handleUnexpectedError(cause: Cause[Throwable]): HttpApp[Any, Throwable] = {
handle(r)
}
// set generic required headers
private def applyGenerics(response: Response): Response =
response.withAccessControlAllowOrigin("*")
private def respondToRegulars: PartialFunction[Object, Task[Response]] = {
case InvalidRequest(msg, cause) => ZIO.attempt(Response(
status = Status.BadRequest,
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("invalid request", s"internal error: please contact support, timestamp : $timestamp")
))
}
private def timestamp = new Timestamp(java.lang.System.currentTimeMillis())
private def handleUnexpectedError(cause: Object): Task[Response] = {
def report(kind: String, value: Throwable = null, trace: StackTrace = StackTrace.none) = 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(s"Received unhandled $kind cause ${if value == null then "" else ": " + value}")
Log.error(trace) LOG.error(trace)
cause match cause match
case e: Throwable => report(e.getMessage, e) case Cause.Empty => report("empty")
case Cause.Empty => report("empty") case Cause.Fail(e, trace) => report("failure", e, trace)
case Cause.Fail(e: Throwable, trace) => report("failure", e, trace) case Cause.Die(e, trace) => report("die", e, trace)
case Cause.Fail(_, trace) => report("failure", null, trace) case Cause.Interrupt(fiberId, e) => report(s"interruption of $fiberId", null, e)
case Cause.Die(e, trace) => report("die", e, trace) case Cause.Stackless(cause, _) =>
case Cause.Interrupt(fiberId, e) => report(s"interruption of $fiberId", null, e) LOG.error("stackless error :")
case Cause.Stackless(cause, _) =>
Log.error("stackless error :")
handleUnexpectedError(cause) handleUnexpectedError(cause)
case Cause.Then(left, right) => case Cause.Then(left, right) =>
handleUnexpectedError(left) handleUnexpectedError(left)
Log.error("**THEN this error occurred : **") LOG.error("**THEN this error occurred : **")
handleUnexpectedError(right) handleUnexpectedError(right)
case Cause.Both(left, right) => case Cause.Both(left, right) =>
handleUnexpectedError(left) handleUnexpectedError(left)
Log.error("**AND this error also occurred (async) : **") LOG.error("**AND this error also occurred (async) : **")
handleUnexpectedError(right) handleUnexpectedError(right)
ZIO.attempt(Response( Http.succeed(Response.status(InternalServerError))
status = Status.InternalServerError,
body = errorBody("internal", s"internal error, please contact support (timestamp: $timestamp")
))
} }
val run =
val config = ServerConfig.default
.port(port)
.leakDetection(LeakDetectionLevel.PARANOID)
val serverConfigLayer = ServerConfig.live(config)
Server.install(app).flatMap { port =>
LOG.info(s"Listening API entries on $port")
ZIO.never
}.provideSome[DatabaseContext & Authenticator & DataSource](
Scope.default,
serverConfigLayer,
ConnectionPool.fixed(4),
ClientConfig.default,
Server.live,
Client.live
)
object Endpoint:
final val LOG = LogManager.getLogger("API")

@ -1,29 +0,0 @@
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
}

@ -1,57 +0,0 @@
package org.tbasket.endpoint.auth
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, 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 zio.*
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 java.sql.SQLException
import java.util.UUID
object LoginHandler extends PageHandler:
implicit private final val Log: Logger = LogManager.getLogger("/login")
private def tryLogin(request: Request) =
for
request <- decode[LoginRequest](request.body)
user <- ZIO.serviceWithZIO[Authenticator](_.loginUser(request.email, request.password))
jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user))
yield Response(
status = Status.Found,
headers = Headers.location("/") ++ //login successful, go back to main page
Headers.setCookie(Cookie("JWT", jwt)) //and set the token cookie
)
def login(request: Request) =
tryLogin(request).catchSome {
case UserNotFound(msg) => ZIO.attempt(Response(
status = Status.Found,
body = errorBody("unauthorized", msg),
headers =
Headers(
Headers.location("/register")
) // send back caller to register panel
))
case InvalidPassword(msg) => ZIO.attempt(Response(
status = Status.Unauthorized,
body = errorBody("unauthorized", msg)
))
}

@ -1,57 +0,0 @@
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 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 RegisterHandler extends PageHandler {
private def tryRegister(request: Request) =
for
request <- decode[RegisterRequest](request.body)
user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(request.name, request.forename, request.email, request.password))
jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user))
yield Response(
status = Status.Found,
headers = Headers.location("/") ++ //register successful, go back to main page
Headers.setCookie(Cookie("JWT", jwt))
)
def register(request: Request) = tryRegister(request)
.catchSome {
case InvalidEmail(msg) => ZIO.attempt(Response(
status = Status.ExpectationFailed,
body = errorBody("invalid email", msg)
))
case InvalidPassword(msg) => ZIO.attempt(Response(
status = Status.ExpectationFailed,
body = errorBody("invalid password", msg)
))
case InvalidName(msg) => ZIO.attempt(Response(
status = Status.ExpectationFailed,
body = errorBody("invalid name", msg)
))
case InvalidForename(msg) => ZIO.attempt(Response(
status = Status.ExpectationFailed,
body = errorBody("invalid forename", msg)
))
case UserAlreadyRegistered(msg) =>
ZIO.attempt(Response(
status = Status.NotAcceptable,
body = errorBody("already registered", msg),
headers = Headers.location("/login") //the account already exists so we move the user to login page
))
}
}

@ -1,11 +0,0 @@
package org.tbasket.endpoint.auth
import zio.http.Request
object TokenLoginHandler {
def login(request: Request) = {
}
}

@ -1,6 +0,0 @@
package org.tbasket.endpoint.request
case class RegisterRequest(name: String, forename: String, email: String, password: String)
case class LoginRequest(email: String, password: String)

@ -1,17 +1,10 @@
package org.tbasket.error package org.tbasket.error
sealed class AuthException(msg: String) extends Exception(msg) with UserException import org.tbasket.error.ExceptionEnum
case class InvalidPassword(msg: String) extends AuthException(msg) enum AuthException extends ExceptionEnum {
case InvalidPassword
case class InvalidEmail(msg: String) extends AuthException(msg) case InvalidEmail
case UserNotFound
case class UserNotFound(msg: String) extends AuthException(msg) case UserAlreadyRegistered
}
case class UserAlreadyRegistered(msg: String) extends AuthException(msg)
case class InvalidName(msg: String) extends AuthException(msg)
case class InvalidForename(msg: String) extends AuthException(msg)

@ -0,0 +1,5 @@
package org.tbasket.error
trait ExceptionEnum extends Exception {
}

@ -1,12 +1,10 @@
package org.tbasket.error package org.tbasket.error
sealed class JwtException(msg: String) extends Exception(msg) enum JwtException extends ExceptionEnum {
case InvalidJwt(cause: String)
case ExpiredJwt
case class InvalidJwt(msg: String) extends JwtException(msg) with UserException
case class ExpiredJwt(msg: String) extends JwtException(msg) with UserException case InvalidEmitterResponse
case EmitterInternalError
}
case class UnrecognizedEmitterResponse(msg: String) extends JwtException(msg)
case class EmitterInternalError(msg: String) extends JwtException(msg)

@ -1,17 +0,0 @@
package org.tbasket.error
import org.apache.logging.log4j.{LogManager, Logger}
import zio.http.Response
import zio.http.model.Status
import zio.{Task, ZIO}
sealed class RegularException(msg: String, cause: Throwable = null) extends Exception(msg, cause)
case class InternalError(cause: Throwable) extends Exception(cause)
case class InvalidArgumentError(cause: String) extends Exception(cause)
case class InvalidRequest(msg: String, cause: String = "") extends Exception(msg + (if cause.nonEmpty then ":" + cause else "")) with UserException

@ -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)
}

@ -1,10 +0,0 @@
package org.tbasket.error
/**
* User exceptions are a special type of exception where stack trace is not filled as those exceptions are not meant to be printed in the console
* because they are predictable error.
*
* */
trait UserException extends Exception {
override def fillInStackTrace(): Throwable = this //do not fill stack trace
}

@ -0,0 +1,18 @@
package org.tbasket.handler
import org.tbasket.error.RegularException.InvalidRequest
import zio.{Task, ZIO}
import zio.http.Body
import zio.json.*
import zio.json.ast.{Json, JsonCursor}
import scala.language.reflectiveCalls
object HandlerUtils {
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"}""")
}

@ -0,0 +1,95 @@
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.Authenticator
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.*
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[Authenticator] { auth =>
for
mail <- HandlerUtils.parseAttribute(json, "email", JsonCursor.field("email").isString)
password <- HandlerUtils.parseAttribute(json, "password", JsonCursor.field("password").isString)
user <- auth.loginUser(mail, password)
yield user
}
private def tryPost(request: Request) =
for
body <- request
.body
.asString
.mapError(s =>
InvalidRequest("Invalid request body", s.getMessage)
)
json <- ZIO.fromEither(body.fromJson[Json])
.mapError(InvalidRequest("Invalid JSON body", _))
user <- getUser(json)
jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user))
yield Response(
status = Status.Found,
headers = Headers.location("/") ++ //login successful, go back to main page
Headers.setCookie(Cookie("JWT", jwt)) //and set the token cookie
)
def post(request: Request) =
tryPost(request).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")
))
}

@ -1,4 +1,4 @@
package org.tbasket.endpoint package org.tbasket.handler
/** /**
* tag interface for page handlers * tag interface for page handlers

@ -0,0 +1,40 @@
package org.tbasket.handler
import org.tbasket.auth.Authenticator
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, "email", JsonCursor.field("email").isString)
password <- parseAttribute(json, "password", JsonCursor.field("password").isString)
user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(name, forename, mail, password))
jwt <- ZIO.serviceWithZIO[Authenticator](_.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 => ???
}
}

@ -19,11 +19,11 @@
</Appenders> </Appenders>
<Loggers> <Loggers>
<Logger level="WARN" name="JWTEmitter" additivity="false" includeLocation="false"> <Logger name="JWTEmitter" additivity="false" includeLocation="false">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
<AppenderRef ref="LogFile"/> <AppenderRef ref="LogFile"/>
</Logger> </Logger>
<Root level="INFO" includeLocation="false"> <Root level="ALL" includeLocation="false">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
<AppenderRef ref="LogFile"/> <AppenderRef ref="LogFile"/>
</Root> </Root>

@ -18,13 +18,12 @@ import scala.util.chaining.scalaUtilChainingOps
object Main extends ZIOAppDefault: object Main extends ZIOAppDefault:
private val KeyFactory = java.security.KeyFactory.getInstance("RSA") private val KeyFactory = java.security.KeyFactory.getInstance("RSA")
private val EmitterPresenceHook = Path.of("/tmp/emitter.presence")
private val app = Http.collectZIO[Request] { private val app = Http.collectZIO[Request] {
case r@(Method.GET | Method.POST) -> _ => case r@(Method.GET | Method.POST) -> _ / "jwt" =>
ZIO.serviceWithZIO[JwtGenerator](_.generateTokenResponse(r)) ZIO.serviceWithZIO[JwtGenerator](_.generateTokenResponse(r))
case _ => case _ =>
ZIO.succeed(Response(status = Status.MethodNotAllowed)) ZIO.succeed(Response(status = Status.NotFound))
} }
private def parsePort(port: Option[String]): Task[Int] = private def parsePort(port: Option[String]): Task[Int] =
@ -55,11 +54,7 @@ object Main extends ZIOAppDefault:
} }
parsePort(port) <&> loadKey(keyFile) parsePort(port) <&> loadKey(keyFile)
private def onStart(port: Int) =
Console.printLine(s"JWT AppToken open on port $port") *> ZIO.attempt {
Files.deleteIfExists(EmitterPresenceHook)
Files.createFile(EmitterPresenceHook)
}
private def startServer(port: Int, key: PrivateKey) = private def startServer(port: Int, key: PrivateKey) =
val config = ServerConfig.default val config = ServerConfig.default
@ -69,12 +64,10 @@ object Main extends ZIOAppDefault:
val generator = val generator =
new JwtGenerator(Duration.ofDays(15), key, JwtAlgorithm.RS256) new JwtGenerator(Duration.ofDays(15), key, JwtAlgorithm.RS256)
val configLayer = ServerConfig.live(config) val configLayer = ServerConfig.live(config)
(Server.install(app) *> onStart(port) *> ZIO.never) (Server.install(
app
) *> Console.printLine(s"JWT AppToken open on port $port") *> ZIO.never)
.provide(configLayer, Server.live, ZLayer.succeed(generator)) .provide(configLayer, Server.live, ZLayer.succeed(generator))
.catchAllCause(c => {
Files.writeString(EmitterPresenceHook, "A")
ZIO.failCause(c)
})
val run = val run =
ZIO.serviceWithZIO[ZIOAppArgs](args => parseArgs(args.getArgs)) ZIO.serviceWithZIO[ZIOAppArgs](args => parseArgs(args.getArgs))

@ -15,9 +15,6 @@ trait ServerModule extends ScalaModule with ScalafmtModule {
} }
trait HttpModule extends ServerModule { trait HttpModule extends ServerModule {
override def ivyDeps = super.ivyDeps() ++ Agg( override def ivyDeps = super.ivyDeps() ++ Agg(
ivy"dev.zio::zio-http:0.0.3", ivy"dev.zio::zio-http:0.0.3",
ivy"dev.zio::zio-streams:2.0.6", ivy"dev.zio::zio-streams:2.0.6",

@ -10,22 +10,22 @@ steps:
- name: build - name: build
path: ./build path: ./build
commands: commands:
- ./mill --disable-ticker __.compile - ./mill tests.test
- ./mill --disable-ticker tests.test
- name: Deploy # - name: Deploy
image: override6/tbasket-backend:latest # image: override6/tbasket-backend:latest
depends_on: # depends_on:
- 'Unit tests' # - 'Unit tests'
environment: # environment:
SSH_PRIVATE_KEY: # SSH_PRIVATE_KEY:
from_secret: SSH_PRIVATE # from_secret: SSH_PRIVATE
volumes: # PASSWD:
- name: build # from_secret: PASSWD
path: ./build # volumes:
commands: # - name: build
- ./mill Core.assembly # path: ./build
- ./mill JWTEmitter.assembly # commands:
- chmod 777 drone/deliver.sh # - ./mill Core.compile
- drone/deliver.sh # - chmod 777 drone/deliver.sh
# - drone/deliver.sh

@ -5,6 +5,5 @@ source ./drone/prepare-deliver.sh
echo "making delivery onto '$USER@$IP:$DIR/backend'" echo "making delivery onto '$USER@$IP:$DIR/backend'"
mv "out/JWTEmitter/assembly.dest/out.jar" "out/JWTEmitter/assembly.dest/JWTEmitter.jar" scp -o "StrictHostKeyChecking no" "build/libs/server-all.jar" "drone/deploy.sh" "drone/start.sh" "$USER@$IP:$DIR/backend"
scp -o "StrictHostKeyChecking no" "out/Core/assembly.dest/out.jar" "out/JWTEmitter/assembly.dest/JWTEmitter.jar" "drone/deploy.sh" "drone/start.sh" "$USER@$IP:$DIR/backend"
echo "chmod 700 $DIR/backend/*; cd $DIR/backend; ./deploy.sh" | ssh -o "StrictHostKeyChecking no" "$USER@$IP" echo "chmod 700 $DIR/backend/*; cd $DIR/backend; ./deploy.sh" | ssh -o "StrictHostKeyChecking no" "$USER@$IP"

@ -2,35 +2,30 @@
DIR=$(readlink -e "$(dirname "$0")") DIR=$(readlink -e "$(dirname "$0")")
PROD_SERVER_JAR_NAME="server-prod.jar" PROD_SERVER_JAR_NAME="server-prod.jar"
NEW_SERVER_JAR_NAME="out.jar" NEW_SERVER_JAR_NAME="server-all.jar"
PROD_EMITTER_JAR_NAME="JWTEmitter-prod.jar" prod_pid() {
NEW_EMITTER_JAR_NAME="JWTEmitter.jar" ps -aux | tr -s " " | grep -E "\bjava -jar .*$PROD_SERVER_JAR_NAME\b" | cut -d " " -f2
find_pid() {
ps -aux | tr -s " " | grep -E "\bjava -jar .*$1\b" | cut -d " " -f2
} }
SERVER_PROD_PID=$(find_pid $PROD_SERVER_JAR_NAME) PROD_PID=$(prod_pid)
EMITTER_PROD_PID=$(find_pid $PROD_EMITTER_JAR_NAME)
try_shutdown() {
if [ "$1" ]; then
#will cause the old server to gracefully shutdown
echo "shutting down old $2 version ..."
kill "$PROD_PID"
while [ ! "$(find_pid $2)" ]; do sleep 1; done #sleep until process ends
echo "$2 shut down"
fi
}
try_shutdown "$SERVER_PROD_PID" "$PROD_SERVER_JAR_NAME" #if $PROD_PID is not empty but isn't a number, something went wrong
try_shutdown "$EMITTER_PROD_PID" "$PROD_EMITTER_JAR_NAME" if [ "$PROD_PID" ] && ! echo "$PROD_PID" | grep -E -q "^[0-9]+$"; then
echo "error, unable to retrieve old server pid: $PROD_PID" >&2
exit 2
fi
if [ "$PROD_PID" ]; then
#will cause the old server to gracefully shutdown
echo "shutting down old server version ..."
kill "$PROD_PID"
while [ ! "$(prod_pid)" ]; do sleep 1; done #sleep until process ends
echo "server shut down"
fi
rm "$DIR/$PROD_SERVER_JAR_NAME" rm "$DIR/$PROD_SERVER_JAR_NAME"
mv "$DIR/$NEW_SERVER_JAR_NAME" "$DIR/$PROD_SERVER_JAR_NAME" || ls mv "$DIR/$NEW_SERVER_JAR_NAME" "$DIR/$PROD_SERVER_JAR_NAME" || ls
mv "$DIR/$NEW_EMITTER_JAR_NAME" "$DIR/$PROD_EMITTER_JAR_NAME" || ls
SCREEN="basket" SCREEN="basket"
@ -39,6 +34,5 @@ if ! screen -ls | grep -q -E "\b[0-9]+\.$SCREEN\b"; then
screen -S "$SCREEN" -d -m screen -S "$SCREEN" -d -m
fi fi
chmod 755 /run/screen
screen -d -r "$SCREEN" -X stuff $"$DIR/start.sh\n" screen -d -r "$SCREEN" -X stuff $"$DIR/start.sh\n"
echo "server is started into $SCREEN screen." echo "server is started into $SCREEN screen."

@ -0,0 +1,27 @@
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

@ -1,9 +1,3 @@
DIR=$(readlink -e "$(dirname "$0")") DIR=$(readlink -e "$(dirname "$0")")
echo "starting emitter"
java -jar "$DIR/JWTEmitter-prod.jar"&
echo "starting server" echo "starting server"
java -jar "$DIR/server-prod.jar" java -jar "$DIR/server-prod.jar"

@ -0,0 +1,53 @@
{
"results": [
{
"name" : "Test Task name not available here/\/login page handler/login situation tests/login with unknown account",
"status" : "Success",
"durationMillis" : "4103",
"annotations" : "",
"fullyQualifiedClassName" : "Test Task name not available here",
"labels" : ["\/login page handler", "login situation tests", "login with unknown account"]
},
{
"name" : "Test Task name not available here/\/login page handler/login situation tests/login with known account",
"status" : "Failure",
"durationMillis" : "1",
"annotations" : "",
"fullyQualifiedClassName" : "Test Task name not available here",
"labels" : ["\/login page handler", "login situation tests", "login with known account"]
},
{
"name" : "Test Task name not available here/\/login page handler/erroned request body tests/with no password attribute",
"status" : "Success",
"durationMillis" : "2681",
"annotations" : "",
"fullyQualifiedClassName" : "Test Task name not available here",
"labels" : ["\/login page handler", "erroned request body tests", "with no password attribute"]
},
{
"name" : "Test Task name not available here/\/login page handler/erroned request body tests/empty packet",
"status" : "Success",
"durationMillis" : "2697",
"annotations" : "",
"fullyQualifiedClassName" : "Test Task name not available here",
"labels" : ["\/login page handler", "erroned request body tests", "empty packet"]
},
{
"name" : "Test Task name not available here/\/login page handler/erroned request body tests/with no mail attribute",
"status" : "Success",
"durationMillis" : "2702",
"annotations" : "",
"fullyQualifiedClassName" : "Test Task name not available here",
"labels" : ["\/login page handler", "erroned request body tests", "with no mail attribute"]
},
{
"name" : "Test Task name not available here/\/login page handler/erroned request body tests/with invalid json",
"status" : "Success",
"durationMillis" : "2717",
"annotations" : "",
"fullyQualifiedClassName" : "Test Task name not available here",
"labels" : ["\/login page handler", "erroned request body tests", "with invalid json"]
}
]
}

@ -5,21 +5,23 @@ echo GENERATING TEMPORARY KEY PAIRS FOR TESTS
rm -r /tmp/keys &> /dev/null rm -r /tmp/keys &> /dev/null
mkdir -p /tmp/keys mkdir -p /tmp/keys
cd /tmp/keys cd /tmp/keys
keytool -genkeypair \ keytool -genkey -noprompt \
-alias key \ -alias key \
-keyalg RSA \ -keyalg RSA \
-keysize 4069 \ -validity 2 \
-sigalg SHA256withRSA \ -keystore test.keystore \
-storetype PKCS12 \ -storetype PKCS12 \
-keystore store.p12 \
-dname "CN=x.y.com, OU=TB, O=TBA, L=dzqdz, S=dqzdzq, C=GB" \ -dname "CN=x.y.com, OU=TB, O=TBA, L=dzqdz, S=dqzdzq, C=GB" \
-storepass 123456789 \ -storepass 123456789 \
-keypass 123456789 -keypass 123456789
keytool -v -export -file public.cert -keystore store.p12 -alias key -storepass 123456789 keytool -noprompt -export \
-alias key \
openssl pkcs12 -in store.p12 -nodes -nocerts -out key.pem -passin pass:123456789 -keystore test.keystore \
openssl pkcs8 -topk8 -inform PEM -outform DER -in key.pem -out key.pcqks -nocrypt -rfc \
-file public.cert \
-keypass 123456789 \
-storepass 123456789
#openssl pkcs8 -topk8 -inform PEM -outform DER -in test.keystore -out private.pcks -passin pass:123456789 openssl pkcs12 -in test.keystore -nodes -nocerts -out private.pcks -passin pass:123456789
#openssl pkcs8 -topk8 -inform PEM -outform DER -in test.keystore -out public.cert -passin pass:123456789 openssl pkcs12 -in test.keystore -nokeys -out public.cert -passin pass:123456789

@ -1,36 +1,8 @@
package org.tbasket.test package org.tbasket.test
import com.sun.nio.file.ExtendedOpenOption
import io.netty.buffer.ByteBuf
import java.nio.channels.Pipe
import java.nio.file.{Files, Path, StandardOpenOption}
object TestEmitter: object TestEmitter:
val PORT = 5457 val PORT = 5455
def start(): Unit = {
println("SETTING UP LOCAL JWT EMITTER FOR TESTS")
val emitterPresence = Path.of("/tmp/emitter.presence")
Files.deleteIfExists(emitterPresence)
val process = new ProcessBuilder(
"bash",
"./mill", "--disable-ticker", "JWTEmitter.run",
"-k", "/tmp/keys/key.pcqks",
"-p", TestServerConfig.emitterURL.port.get.toString
)
.inheritIO()
.start()
Runtime.getRuntime.addShutdownHook(new Thread((() => process.destroy()): Runnable))
//the emitter will create the /tmp/emitter.presence file once it started, this is to inform us that the server is OP
while (Files.notExists(emitterPresence))
Thread.sleep(500)
if (Files.readString(emitterPresence) == "A") {
System.err.println("Emitter did not start successfully")
System.exit(1)
}
Files.delete(emitterPresence)
println("EMITTER PRESENCE DETECTED AND INITIALIZED, CONTINUING...")
}

@ -15,7 +15,6 @@ object TestLayers {
val auth = { val auth = {
TestEmitter.start()
val publicKey = TestServerConfig.emitterCertificate.getPublicKey val publicKey = TestServerConfig.emitterCertificate.getPublicKey
val auth = new Authenticator(TestServerConfig.emitterURL, publicKey, TestServerConfig.emitterKeyAlgorithm) val auth = new Authenticator(TestServerConfig.emitterURL, publicKey, TestServerConfig.emitterKeyAlgorithm)
ZLayer.succeed(auth) ZLayer.succeed(auth)

@ -8,7 +8,6 @@ import pdi.jwt.algorithms.JwtAsymmetricAlgorithm
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}
import java.security.cert.{Certificate, CertificateFactory} import java.security.cert.{Certificate, CertificateFactory}
import scala.reflect.io
object TestServerConfig extends ServerConfig { object TestServerConfig extends ServerConfig {
new ProcessBuilder("bash", "./tests/resources/generate_keys.sh") new ProcessBuilder("bash", "./tests/resources/generate_keys.sh")
@ -18,7 +17,7 @@ object TestServerConfig extends ServerConfig {
private final val CertFactory = CertificateFactory.getInstance("X509") private final val CertFactory = CertificateFactory.getInstance("X509")
override def emitterURL: URL = URL.fromString(s"http://localhost:$PORT").getOrElse(null) override def emitterURL: URL = URL.fromString(s"http://localhost/$PORT").getOrElse(null)
override def emitterCertificate: Certificate = override def emitterCertificate: Certificate =
CertFactory.generateCertificate(Files.newInputStream(Path.of("/tmp/keys/public.cert"))) CertFactory.generateCertificate(Files.newInputStream(Path.of("/tmp/keys/public.cert")))
@ -28,6 +27,4 @@ object TestServerConfig extends ServerConfig {
override def endpointPort: Int = 5454 override def endpointPort: Int = 5454
override def databaseConfigName: String = "test-database" override def databaseConfigName: String = "test-database"
override def pagesLocation: Option[Path] = None
} }

@ -1,28 +0,0 @@
package org.tbasket.test
import io.netty.handler.codec.http.QueryStringEncoder
import org.tbasket.error.InvalidRequest
import zio.*
import zio.http.{Body, Response}
import zio.json.*
import zio.json.ast.{Json, JsonCursor}
import scala.language.{implicitConversions, reflectiveCalls}
object TestUtils {
def parseAttribute[V, T <: Json {def value: V}](json: Json, cursor: JsonCursor[Json, T]): Task[V] =
ZIO.fromEither(json.get[T](cursor).map(_.value))
.mapError(InvalidRequest(s"Missing or invalid field $cursor.", _))
def parseAttributeOpt[V, T <: Json {def value: V}](json: Json, cursor: JsonCursor[Json, T]) =
ZIO.fromEither(json.get[T](cursor).map(_.value)).option
def getJsonBody(r: Response): Task[Json] = {
for
body <- r.body.asString
json <- ZIO.fromEither(body.fromJson[Json]).orElseSucceed(Json.Obj())
yield json
}
}

@ -1,36 +0,0 @@
package org.tbasket.test.endpoint
import io.getquill.NamingStrategy
import io.getquill.context.qzio.ZioJdbcContext
import io.getquill.context.sql.idiom.SqlIdiom
import org.tbasket.auth.Authenticator
import org.tbasket.data.DatabaseContext
import org.tbasket.endpoint.auth.LoginHandler.login
import org.tbasket.test.TestLayers.*
import zio.*
import zio.http.netty.client.ConnectionPool
import zio.http.{Client, ClientConfig, URL}
import zio.test.{Spec, TestEnvironment, ZIOSpecDefault}
import javax.sql.DataSource
abstract class TBasketPageSpec(location: String) extends ZIOSpecDefault {
protected val url = URL.fromString(s"http://localhost$location") match
case Left(exception) => throw exception
case Right(url) => url
protected def tspec: Spec[
DataSource & ClientConfig & Authenticator & ConnectionPool &
Scope & DatabaseContext & Client, Any
]
final override def spec = tspec.provide(
db.datasourceLayer,
db.contextLayer,
auth,
ConnectionPool.fixed(1),
Scope.default,
ClientConfig.default,
Client.live)
}

@ -1,87 +0,0 @@
package org.tbasket.test.endpoint.auth
import io.getquill.jdbczio.Quill
import io.getquill.{SnakeCase, SqliteZioJdbcContext}
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.test.TestUtils.*
import org.tbasket.test.endpoint.TBasketPageSpec
import org.tbasket.test.{TestLayers, TestUtils}
import zio.*
import zio.http.*
import zio.http.model.Headers.{Header, empty}
import zio.http.model.{HeaderNames, Headers, Status}
import zio.http.netty.client.ConnectionPool
import zio.json.*
import zio.json.ast.{Json, JsonCursor}
import zio.test.*
import zio.test.Assertion.*
object LoginHandlerTests extends TBasketPageSpec("/auth/login") {
private def requestsSpec = suite("bad request tests")(
ZIO.attempt(Map(
"empty packet" -> Body.empty,
"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
response <- handle(Request.post(body, url))
json <- getJsonBody(response)
errorType <- parseAttribute(json, JsonCursor.field("error").isString)
yield
assert(response)(hasField("status", _.status, equalTo(Status.BadRequest)))
&& assertTrue(errorType == "invalid request")
}
})
)
private def loginSpec = {
suite("login situation tests")(
test("login with unknown account") {
for
response <- handle(Request.post(Body.fromString("""{"password":"bouhours","email":"unknownaccount@gmail.com"}"""), url))
json <- getJsonBody(response)
errorType <- parseAttribute(json, JsonCursor.field("error").isString)
yield
//assert that the response error is of type unauthorized and headers are Location: /register
assert(response)(hasField("status", _.status, equalTo(Status.Found)))
&& assert(errorType)(equalTo("unauthorized"))
&& assert(response)(hasField("headers", _.headers, contains(Headers.location("/register"))))
},
test("login with known account") {
for
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)))
&& assert(response)(hasField("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie)))))
},
test("login with known account wrong password") {
for
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)
errorType <- parseAttribute(json, JsonCursor.field("error").isString)
yield
assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized)))
&& assert(errorType)(equalTo("unauthorized"))
}
)
}
override def tspec = suite("/login page handler")(
requestsSpec,
loginSpec
)
}

@ -1,100 +0,0 @@
package org.tbasket.test.endpoint.auth
import org.tbasket.endpoint.Endpoint.handle
import org.tbasket.test.TestUtils
import org.tbasket.test.TestUtils.*
import org.tbasket.test.endpoint.TBasketPageSpec
import org.tbasket.test.endpoint.auth.RegisterHandlerTests.test
import zio.*
import zio.http.*
import zio.http.model.{HeaderNames, Headers, Status}
import zio.json.ast.JsonCursor
import zio.test.*
import zio.test.Assertion.*
object RegisterHandlerTests extends TBasketPageSpec("/auth/register") {
private def requestsSpec = suite("bad request tests")(
ZIO.attempt(Map( //TODO test all wrong combinations
"empty packet" -> Body.empty,
"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
response <- handle(Request.post(body, url))
json <- getJsonBody(response)
errorType <- parseAttribute(json, JsonCursor.field("error").isString)
yield
assert(response)(hasField("status", _.status, equalTo(Status.BadRequest)))
&& assertTrue(errorType.startsWith("invalid"))
}
})
)
private def registerSpec = suite("register tests")(
test("register then try register again") {
(for
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
&& assert(resp)(hasField("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie)))))
&& assert(resp)(hasField("headers", _.headers, contains(Headers.location("/"))))
)
*>
(for
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)
yield
assert(resp)(hasField("status", _.status, equalTo(Status.NotAcceptable)))
&& assert(errorType)(isSome(equalTo("already registered")))
&& assert(resp)(hasField("headers", _.headers, contains(Headers.location("/login")))))
},
test("register bad email") {
for
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
assert(resp)(hasField("status", _.status, equalTo(Status.ExpectationFailed)))
&& assert(errorType)(isSome(equalTo("invalid email")))
},
test("register bad password") {
for
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
assert(resp)(hasField("status", _.status, equalTo(Status.ExpectationFailed)))
&& assert(errorType)(isSome(equalTo("invalid password")))
},
test("register bad name") {
for
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
assert(resp)(hasField("status", _.status, equalTo(Status.ExpectationFailed)))
&& assert(errorType)(isSome(equalTo("invalid name")))
},
test("register bad forename") {
for
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
assert(resp)(hasField("status", _.status, equalTo(Status.ExpectationFailed)))
&& assert(errorType)(isSome(equalTo("invalid forename")))
}
)
override def tspec = suite("/register page handler")(
requestsSpec,
registerSpec
)
}

@ -0,0 +1,87 @@
package org.tbasket.test.pages
import io.getquill.jdbczio.Quill
import io.getquill.{SnakeCase, SqliteZioJdbcContext}
import org.tbasket.auth.Authenticator
import org.tbasket.data.{Database, DatabaseContext}
import org.tbasket.error.RegularException.InvalidRequest
import org.tbasket.handler.HandlerUtils.parseAttribute
import org.tbasket.handler.LoginPageHandler
import org.tbasket.handler.LoginPageHandler.post
import org.tbasket.test.TestLayers
import org.tbasket.test.pages.LoginPageHandlerTests.test
import zio.*
import zio.http.netty.client.ConnectionPool
import zio.http.*
import zio.http.model.{HeaderNames, Headers}
import zio.http.model.Headers.Header
import zio.json.*
import zio.json.ast.{Json, JsonCursor}
import zio.test.*
import zio.test.Assertion.*
object LoginPageHandlerTests extends ZIOSpecDefault {
import LoginPageHandler.post
import TestLayers.*
private def getJsonBody(r: Response): Task[Json] = {
for
body <- r.body.asString
json <- ZIO.fromEither(body.fromJson[Json]).mapError(new Exception(_))
yield json
}
private def requestsSpec = suite("erroned request body tests")(
ZIO.attempt(Map(
"empty packet" -> Body.empty,
"with no mail attribute" -> Body.fromString("""{"password":"1234"}"""),
"with no password attribute" -> Body.fromString("""{"email":"valid.mail@x.y"}"""),
"with invalid json" -> Body.fromString("""this is a corrupted json""")
)).map(_.map((name, body) =>
test(name) {
for
response <- post(Request.post(body, URL.empty))
json <- getJsonBody(response)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString)
yield assertTrue(errorType == "invalid request")
}
))
)
private def loginSpec = {
suite("login situation tests")(
test("login with unknown account") {
for
response <- post(Request.post(Body.fromString("""{"password":"123456","email":"unknownaccount@gmail.com"}"""), URL.empty))
json <- getJsonBody(response)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString)
yield
//assert that the response error is of type unauthorized and headers are Location: /register
assert(errorType)(equalTo("unauthorized"))
&& assert(response)(hasField("headers", _.headers, hasSameElements(Headers.location("/register"))))
},
test("login with known account") {
for
response <- post(Request.post(Body.fromString("""{"password":"123456","email":"maximebatista18@gmail.com"}"""), URL.empty))
json <- getJsonBody(response)
//errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString)
yield
assert(response)(hasField("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie)))))
}
)
}
override def spec = suite("/login page handler")(
requestsSpec,
loginSpec
).provide(
db.datasourceLayer,
db.contextLayer,
auth,
ConnectionPool.fixed(1),
Scope.default,
ClientConfig.default,
Client.live)
}