working on backend draft
continuous-integration/drone/push Build is failing Details

drone-setup
Override-6 2 years ago
parent e5cd7b897a
commit 1650d472d1

@ -2,9 +2,8 @@ package org.tbasket
import io.getquill.{Literal, SqliteDialect} import io.getquill.{Literal, SqliteDialect}
import io.getquill.context.qzio.ZioContext import io.getquill.context.qzio.ZioContext
import org.tbasket.api.Endpoint import org.tbasket.endpoint.Endpoint
import org.tbasket.handler.{IncrementHandler, LoginHandler} import org.tbasket.handler.LoginHandler
import org.tbasket.auth.UserSessionHandler
import java.util.Properties import java.util.Properties
@ -16,10 +15,7 @@ object EndpointSetup:
def setupEndpoint(config: Properties): Endpoint = def setupEndpoint(config: Properties): Endpoint =
Main.LOG.debug("Initializing API endpoint...") Main.LOG.debug("Initializing API endpoint...")
val endpoint = createEndpoint(config) createEndpoint(config)
endpoint.bind("counter")(IncrementHandler)
endpoint.bind("login")(new LoginHandler())
endpoint
private def createEndpoint(config: Properties): Endpoint = private def createEndpoint(config: Properties): Endpoint =
val port = config val port = config

@ -0,0 +1,79 @@
package org.tbasket.auth
import io.circe.*
import io.circe.generic.auto.*
import io.circe.parser.*
import io.circe.syntax.*
import io.getquill.*
import io.getquill.context.qzio.ZioJdbcContext
import org.tbasket.InternalBasketServerException
import org.tbasket.db.schemas.User
import pdi.jwt.algorithms.JwtAsymmetricAlgorithm
import pdi.jwt.{JwtClaim, JwtZIOJson}
import zio.*
import zio.http.*
import zio.http.api.HttpCodec.Method
import zio.http.model.Status.{InternalServerError, Ok}
import zio.http.model.Version.Http_1_1
import zio.http.model.{Headers, Method}
import java.io.ByteArrayInputStream
import java.security.PublicKey
import java.util.UUID
import javax.sql.DataSource
import scala.collection.immutable.HashMap
enum AuthentificatorError:
case ExpiredToken
case InvalidEmitterResponse
case class JwtContent(uuid: UUID)
class Authentificator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm) {
private def defineCustomClaims(user: User): String = {
JwtContent(user.id).asJson.noSpaces.toString
}
private def mkRequest(user: User): Request = {
val custom = defineCustomClaims(user)
Request(Body.fromString(custom), Headers.empty, Method.GET, url, Http_1_1, None)
}
def requestJwt(user: User) = {
Client.request(mkRequest(user))
.flatMap {
case Response(Ok, _, body, _, _) =>
body.asString
case Response(InternalServerError, _, _, _, _) =>
ZIO.fail(new InternalBasketServerException("internal server error"))
case _ =>
ZIO.fail(new InternalBasketServerException("Received unknown response from emitter"))
}
}
def validateAndGetUser(jwt: String) = {
for
//decoding token
claims <- ZIO.fromTry(JwtZIOJson.decode(jwt, key, Seq(algorithm)))
//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")
//
uuid <- ZIO.attempt(claims.content)
.mapAttempt(decode[JwtContent](_))
.flatMap(ZIO.fromEither(_))
.map(_.uuid)
user <- ZIO.serviceWithZIO[ZioJdbcContext[_,_]] { ds =>
import org.tbasket.db.Database.ctx.*
run(quote {
query[User].filter(_.id == lift(uuid))
}).map(_.headOption)
.someOrFail("uuid not found.")
}
yield user
}
}

@ -1,23 +0,0 @@
package org.tbasket.auth
import io.netty.channel.ChannelHandlerContext
import org.tbasket.db.schemas.User
import pdi.jwt.JwtClaim
import zio.http.{Client, Request, URL}
import zio.*
import java.io.ByteArrayInputStream
class JWTClient(url: URL) {
private def mkRequest(user: User): JwtClaim = {
Request.get(url)
.body
.write(new ByteArrayInputStream(Array.emptyByteArray))
}
def requestJwt(user: User): Task[JwtClaim] = {
}
}

@ -0,0 +1,60 @@
package org.tbasket.endpoint
import io.getquill.{Literal, NamingStrategy, SqliteDialect}
import io.getquill.context.qzio.ZioContext
import org.apache.logging.log4j.LogManager
import org.tbasket.auth.Authentificator
import org.tbasket.endpoint.Endpoint.LOG
import org.tbasket.handler.LoginHandler
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.netty.client.ConnectionPool
import io.getquill.idiom.Idiom
import javax.sql.DataSource
class Endpoint(port: Int):
// set generic required headers
private def applyGenerics(response: Response): Response =
response.withAccessControlAllowOrigin("*")
private val app = Http.collectZIO[Request] {
case r@POST -> _ / "login" =>
LoginHandler.post(r)
case r@method -> path =>
val ipInsights = r.remoteAddress
.map(ip => s": request received from $ip.")
.getOrElse("")
LOG.error(
s"Was unable to find a handler for request '$path' with method $method ${ipInsights}"
)
ZIO.succeed(Response(Status.NotFound))
}.map(applyGenerics)
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[ZioContext[Idiom, NamingStrategy] & Authentificator](
Scope.default,
serverConfigLayer,
ConnectionPool.fixed(4),
ClientConfig.default,
Server.live,
Client.live
)
object Endpoint:
final val LOG = LogManager.getLogger("API")

@ -1,21 +0,0 @@
package org.tbasket.handler
import org.tbasket.Main
import org.tbasket.api.compute.APIRequestHandler
import zio.http.{Request, Response}
import java.util.concurrent.atomic.AtomicInteger
object IncrementHandler extends APIRequestHandler:
private val counter = new AtomicInteger(0)
def getCounter: Int = counter.get()
override def get(request: Request): Response =
Response.json(s"{\"value\": ${counter.get()}}")
override def post(request: Request): Response =
val i = counter.incrementAndGet()
Main.LOG.trace(s"Counter incremented : $i")
Response.ok

@ -2,8 +2,7 @@ package org.tbasket.handler
import io.getquill.* import io.getquill.*
import io.getquill.context.qzio.ZioContext import io.getquill.context.qzio.ZioContext
import org.tbasket.api.compute.APIRequestHandler import org.tbasket.auth.Authentificator
import org.tbasket.auth.JWTClient
import org.tbasket.db.Database.ctx.* import org.tbasket.db.Database.ctx.*
import org.tbasket.db.schemas.User import org.tbasket.db.schemas.User
import org.tbasket.handler.HandlerUtils.errorBody import org.tbasket.handler.HandlerUtils.errorBody
@ -17,7 +16,8 @@ import zio.{ZEnvironment, ZIO, *}
import java.sql.SQLException import java.sql.SQLException
import java.util.UUID import java.util.UUID
import javax.sql.DataSource import io.getquill._
import io.getquill.context.ZioJdbc._
enum LoginError: enum LoginError:
case TokenNotFound(token: UUID) case TokenNotFound(token: UUID)
@ -27,11 +27,10 @@ enum LoginError:
case InternalError(t: Throwable) case InternalError(t: Throwable)
class LoginHandler extends APIRequestHandler: object LoginHandler:
private def getUser(json: Json) = private def getUser(json: Json) =
val r = ZIO.serviceWithZIO[SqliteZioJdbcContext[_]] { ctx =>
ZIO.serviceWithZIO[ZioContext[SqliteDialect, Literal]] { ctx =>
import ctx.* import ctx.*
for for
mail <- mail <-
@ -42,15 +41,14 @@ class LoginHandler extends APIRequestHandler:
.mapError(InvalidRequest("Missing or invalid field password", _)) .mapError(InvalidRequest("Missing or invalid field password", _))
result <- run(quote { // TODO use argon2id result <- run(quote { // TODO use argon2id
User.schema.filter(usr => query[User]
usr.mailAddress == mail && usr.passwordHash == lift(password.hashCode) .filter(usr => usr.mailAddress == lift(mail))
) .filter(usr => usr.passwordHash == lift(password.hashCode))
}).mapError(InternalError.apply) }).mapError(InternalError.apply)
yield result.headOption yield result.headOption
} }.someOrFail(InvalidPassword)
r.someOrFail(InvalidPassword)
override def post(request: Request): Task[Response] = override def post(request: Request) =
val bindSession = val bindSession =
for for
body <- request body <- request
@ -65,16 +63,14 @@ class LoginHandler extends APIRequestHandler:
.mapError(InvalidRequest("Invalid JSON body", _)) .mapError(InvalidRequest("Invalid JSON body", _))
user <- getUser(json) user <- getUser(json)
jwt <- ZIO.serviceWithZIO[JWTClient](_.requestJwt(user)) jwt <- ZIO.serviceWithZIO[Authentificator](_.requestJwt(user))
yield (user, jwt) yield (user, jwt)
bindSession.map { sess => bindSession.map { case (user, jwt) =>
Response( Response(
status = Status.Found, status = Status.Found,
headers = Headers.location("/") ++ //login successful, go back to main page headers = Headers.location("/") ++ //login successful, go back to main page
Headers.setCookie(Cookie( Headers.setCookie(Cookie("JWT", jwt)) //and set the token cookie
"JWT", "Jw"
))
) )
} fold( { } fold( {
_ match _ match

@ -1,19 +0,0 @@
package org.tbasket.handler
import org.tbasket.api.compute.APIRequestHandler
import zio.*
import zio.http.model.Status
import zio.http.{Request, Response}
trait LoginRequiredRequestHandler extends APIRequestHandler:
override final def get: Task[Response] =
ZIO.succeed(Response(Status.MethodNotAllowed))
override final def post: Task[Response] = {
ZIO.succeed(Response(Status.MethodNotAllowed))
}
protected def authGet: Task[Response]
protected def authPost: Task[Response]

@ -1,7 +1,8 @@
package org.tbasket.db package org.tbasket.db
import io.getquill.context.ZioJdbc.{DataSourceLayer, QuillZioExt} import io.getquill.context.ZioJdbc.{DataSourceLayer, QuillZioExt}
import io.getquill.{Literal, SqliteZioJdbcContext} import io.getquill.context.qzio.ZioContext
import io.getquill.{Literal, SnakeCase, SqliteDialect, SqliteZioJdbcContext}
import org.sqlite.SQLiteDataSource import org.sqlite.SQLiteDataSource
import zio.* import zio.*
@ -11,14 +12,10 @@ import javax.sql
class Database(config: Properties): class Database(config: Properties):
private val source = new SQLiteDataSource() val layer = ZLayer.succeed(new SqliteZioJdbcContext(SnakeCase))
source.setUrl(config.getProperty("database.url"))
val layer = ZLayer.succeed(source)
object Database: object Database:
val ctx = new SqliteZioJdbcContext(Literal) val ctx = new SqliteZioJdbcContext(SnakeCase)
import ctx.* import ctx.*

@ -12,18 +12,3 @@ case class User(
passwordHash: Int, passwordHash: Int,
mailAddress : String mailAddress : String
) )
object User:
import Database.ctx.*
val schema = quote {
querySchema[User](
"user",
_.id -> "id",
_.name -> "name",
_.forename -> "forename",
_.passwordHash -> "password_hash",
_.mailAddress -> "mail_address"
)
}

@ -1,3 +0,0 @@
package org.tbasket.api
class APIException(msg: String, cause: Throwable = null) extends Exception

@ -1,54 +0,0 @@
package org.tbasket.api
import org.apache.logging.log4j.LogManager
import org.tbasket.api.Endpoint.LOG
import org.tbasket.api.compute.APIRequestHandler
import zio.*
import zio.http.ServerConfig.LeakDetectionLevel
import zio.http.*
import zio.http.model.Method.{GET, POST}
import zio.http.model.Status
import scala.collection.mutable
class Endpoint(port: Int):
private val handlers = mutable.HashMap.empty[String, APIRequestHandler]
def bind(path: String)(handler: APIRequestHandler): Unit =
handlers.put(path, handler)
// set generic required headers
private def transform(response: Response): Response =
response.withAccessControlAllowOrigin("*")
private val app = Http.collectZIO[Request] {
case GET -> _ / path if handlers.contains(path) =>
handlers(path).get.map(transform)
case POST -> _ / path if handlers.contains(path) =>
handlers(path).post.map(transform)
case r@method -> path =>
val ipInsights = r.remoteAddress
.map(ip => s": request received from $ip.")
.getOrElse("")
LOG.error(
s"Was unable to find a handler for request '$path' with method $method ${ipInsights}"
)
ZIO.succeed(Response(Status.NotFound))
}
val run =
val config = ServerConfig.default
.port(port)
.leakDetection(LeakDetectionLevel.PARANOID)
val configLayer = ServerConfig.live(config)
Server.install(app).flatMap { port =>
LOG.info(s"Listening API entries on $port")
ZIO.never
}.provideSome(configLayer, Server.live)
object Endpoint:
final val LOG = LogManager.getLogger("API")

@ -1,13 +0,0 @@
package org.tbasket.api.compute
import zio.*
import zio.http.model.Status
import zio.http.{Request, Response}
trait APIRequestHandler:
def get: Task[Response] =
ZIO.succeed(Response(Status.MethodNotAllowed))
def post: Task[Response] =
ZIO.succeed(Response(Status.MethodNotAllowed))

@ -3,6 +3,8 @@ package org.tbasket.jwt
import pdi.jwt.* import pdi.jwt.*
import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import pdi.jwt.algorithms.JwtAsymmetricAlgorithm
import zio.* import zio.*
import zio.http.model.HttpError
import zio.http.model.Status.InternalServerError
import zio.http.{Request, Response} import zio.http.{Request, Response}
import zio.json.* import zio.json.*
import zio.json.ast.Json import zio.json.ast.Json
@ -31,8 +33,10 @@ class JwtGenerator(tokenLifespan: Duration, key: PrivateKey, algorithm: JwtAsymm
def generateTokenResponse(request: Request): Task[Response] = def generateTokenResponse(request: Request): Task[Response] =
for for
claims <- request.body.asString.map(claims) claims <- request.body.asString.map(claims)
jwt <- ZIO.attempt(JwtZIOJson.encode(claims, key, algorithm)).catchAll(e => { response <- ZIO.attempt(JwtZIOJson.encode(claims, key, algorithm))
ZIO.attempt(e.printStackTrace()).as("error") .map(Response.json)
.catchAll(e => {
ZIO.attempt(e.printStackTrace()).as(Response.status(InternalServerError))
}) })
yield yield
Response.json(jwt) response

@ -11,11 +11,16 @@ trait ServerModule extends ScalaModule with ScalafmtModule {
ivy"org.apache.logging.log4j:log4j-slf4j-impl:2.19.0" ivy"org.apache.logging.log4j:log4j-slf4j-impl:2.19.0"
) )
} }
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-json:0.4.2",
ivy"dev.zio::zio-streams:2.0.6", ivy"dev.zio::zio-streams:2.0.6",
ivy"io.circe::circe-core:0.14.3",
ivy"io.circe::circe-parser:0.14.3",
ivy"io.circe::circe-generic:0.14.3",
ivy"com.github.jwt-scala::jwt-zio-json:9.1.2" ivy"com.github.jwt-scala::jwt-zio-json:9.1.2"
) )
} }
@ -25,20 +30,17 @@ trait HttpModule extends ServerModule {
* */ * */
object JWTEmitter extends HttpModule object JWTEmitter extends HttpModule
/**
* module that handles the REST API endpoint
* */
object Endpoint extends HttpModule
/** /**
* Business layer of a server * Business layer of a server
* */ * */
object Core extends ServerModule { object Core extends HttpModule { //also handles http
override def ivyDeps = super.ivyDeps() ++ Agg( override def ivyDeps = super.ivyDeps() ++ Agg(
ivy"dev.zio::zio-json:0.4.2", ivy"io.circe::circe-core:0.15.0-M1",
ivy"io.circe::circe-parser:0.14.3",
ivy"io.circe::circe-generic:0.14.3",
) )
override def moduleDeps = Seq(Endpoint, DB) override def moduleDeps = Seq(DB)
} }
/** /**