From 27b808abe33c0e81301b31a5c56b652854b266e3 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sat, 21 Jan 2023 23:05:06 +0100 Subject: [PATCH] working on backend --- .gitignore | 4 +- .../main/scala/org/tbasket/api/Endpoint.scala | 43 ++++++++------- .../api/compute/APIRequestHandler.scala | 11 ++-- DB/build.gradle | 6 ++- DB/src/main/resources/database.properties | 1 + DB/src/main/resources/table_init.sql | 44 ++++++++++++++++ .../main/scala/org/tbasket/db/Database.scala | 46 ++++++++++++++++ .../scala/org/tbasket/db/schemas/Member.scala | 18 +++++++ .../scala/org/tbasket/db/schemas/Tactic.scala | 16 ++++++ .../scala/org/tbasket/db/schemas/Team.scala | 17 ++++++ .../scala/org/tbasket/db/schemas/User.scala | 23 ++++++++ DB/table_init.sql | 39 -------------- build.gradle | 7 +-- src/main/scala/org/tbasket/Main.scala | 38 +++++--------- src/main/scala/org/tbasket/data/User.scala | 10 ---- .../org/tbasket/handler/LoginHandler.scala | 50 +++++++++++++----- .../org/tbasket/session/UserSession.scala | 9 ++++ .../tbasket/session/UserSessionHandler.scala | 52 +++++++++++++++++++ 18 files changed, 312 insertions(+), 122 deletions(-) create mode 100644 DB/src/main/resources/database.properties create mode 100644 DB/src/main/resources/table_init.sql create mode 100644 DB/src/main/scala/org/tbasket/db/Database.scala create mode 100644 DB/src/main/scala/org/tbasket/db/schemas/Member.scala create mode 100644 DB/src/main/scala/org/tbasket/db/schemas/Tactic.scala create mode 100644 DB/src/main/scala/org/tbasket/db/schemas/Team.scala create mode 100644 DB/src/main/scala/org/tbasket/db/schemas/User.scala delete mode 100644 DB/table_init.sql delete mode 100644 src/main/scala/org/tbasket/data/User.scala create mode 100644 src/main/scala/org/tbasket/session/UserSession.scala create mode 100644 src/main/scala/org/tbasket/session/UserSessionHandler.scala diff --git a/.gitignore b/.gitignore index 6ee10aa..95e6a36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ build .gradle .idea -*.sqlite */.gradle/ log + +*.sqlite + .$* diff --git a/API/src/main/scala/org/tbasket/api/Endpoint.scala b/API/src/main/scala/org/tbasket/api/Endpoint.scala index ef99857..a8a1800 100644 --- a/API/src/main/scala/org/tbasket/api/Endpoint.scala +++ b/API/src/main/scala/org/tbasket/api/Endpoint.scala @@ -11,48 +11,47 @@ import zio.http.model.Status import scala.collection.mutable -class Endpoint(port: Int) extends ZIOAppDefault { - - +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.collect[Request] { - case r@GET -> _ / path if handlers.contains(path) => - transform(handlers(path).get(r)) - case r@POST -> _ / path if handlers.contains(path) => - transform(handlers(path).post(r)) - case r@method -> path => + + + 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("") + .map(ip => s": request received from $ip.") + .getOrElse("") LOG.error(s"Was unable to find a handler for request '$path' with method $method ${ipInsights}") - Response(Status.NotFound) + ZIO.succeed(Response(Status.NotFound)) } - + val run = { val config = ServerConfig.default - .port(port) - .leakDetection(LeakDetectionLevel.PARANOID) - + .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 - }.provide(configLayer, Server.live) + }.provideSome(configLayer, Server.live) } } object Endpoint { final val LOG = LogManager.getLogger("API") - + } diff --git a/API/src/main/scala/org/tbasket/api/compute/APIRequestHandler.scala b/API/src/main/scala/org/tbasket/api/compute/APIRequestHandler.scala index a0807d4..f5c4a33 100644 --- a/API/src/main/scala/org/tbasket/api/compute/APIRequestHandler.scala +++ b/API/src/main/scala/org/tbasket/api/compute/APIRequestHandler.scala @@ -1,12 +1,13 @@ package org.tbasket.api.compute +import zio.ZIO import zio.http.model.Status import zio.http.{Request, Response} trait APIRequestHandler { - - def get(request: Request): Response = Response(Status.MethodNotAllowed) - - def post(request: Request): Response = Response(Status.MethodNotAllowed) - + + def get: ZIO[Request, Throwable, Response] = ZIO.succeed(Response(Status.MethodNotAllowed)) + + def post: ZIO[Request, Throwable, Response] = ZIO.succeed(Response(Status.MethodNotAllowed)) + } diff --git a/DB/build.gradle b/DB/build.gradle index 2f7f270..23aa6b3 100644 --- a/DB/build.gradle +++ b/DB/build.gradle @@ -1,5 +1,9 @@ group "org.tbasket.api" dependencies { - implementation 'io.getquill:quill_2.12:3.2.0' + + implementation 'org.xerial:sqlite-jdbc:3.40.0.0' + + implementation 'io.getquill:quill-jdbc-zio_2.13:3.9.0' + implementation 'io.getquill:quill-zio_2.13:3.9.0' } diff --git a/DB/src/main/resources/database.properties b/DB/src/main/resources/database.properties new file mode 100644 index 0000000..c31abb1 --- /dev/null +++ b/DB/src/main/resources/database.properties @@ -0,0 +1 @@ +path=database.sqlite \ No newline at end of file diff --git a/DB/src/main/resources/table_init.sql b/DB/src/main/resources/table_init.sql new file mode 100644 index 0000000..249c982 --- /dev/null +++ b/DB/src/main/resources/table_init.sql @@ -0,0 +1,44 @@ +CREATE TABLE user +( + id int PRIMARY KEY, + name varchar(30) NOT NULL, + mail_address varchar NOT NULL UNIQUE, + forename varchar(30) NOT NULL, + password_hash varchar +); + +CREATE TABLE team +( + id int, + name varchar(30), + club_name varchar(30), + PRIMARY KEY (id) +); + +CREATE TABLE tactic_group +( + tactic_id int, + team_id int, + PRIMARY KEY (tactic_id, team_id), + FOREIGN KEY (tactic_id) REFERENCES tactic (id), + FOREIGN KEY (team_id) REFERENCES team (id) +); + +CREATE TABLE member +( + team_id int, + user_id int, + is_admin boolean NOT NULL, + PRIMARY KEY (team_id, user_id), + FOREIGN KEY (team_id) REFERENCES team (id), + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE tactic +( + id int PRIMARY KEY, + name varchar(30) NOT NULL, + owner_id int, + file_path varchar NOT NULL, + FOREIGN KEY (owner_id) REFERENCES user (id) +); \ No newline at end of file diff --git a/DB/src/main/scala/org/tbasket/db/Database.scala b/DB/src/main/scala/org/tbasket/db/Database.scala new file mode 100644 index 0000000..93c03ce --- /dev/null +++ b/DB/src/main/scala/org/tbasket/db/Database.scala @@ -0,0 +1,46 @@ +package org.tbasket.db + +import io.getquill.context.ZioJdbc.{DataSourceLayer, QuillZioExt} +import io.getquill.{Literal, SqliteZioJdbcContext} +import org.sqlite.SQLiteDataSource +import org.tbasket.db.schemas.User +import zio._ +import zio.console._ + +import java.io.Closeable +import java.util.Properties +import javax.sql + +class Database(config: Properties) { + + private val source = new SQLiteDataSource() with Closeable { + override def close(): Unit = () + } + + source.setUrl(config.getProperty("database.url")) + val layer = DataSourceLayer.fromDataSource(source) + +} + +object Database { + val ctx = new SqliteZioJdbcContext(Literal) + + import ctx._ + + + val app: ZIO[Console with Has[sql.DataSource with Closeable], Exception, Unit] = + for { + _ <- ctx.run( + quote { + liftQuery(List(User(4, "batista", "maxime", 788872465, "myemail@gmail.com"))) + .foreach(r => User.schema.insert(r)) + } + ).onDS + result <- ctx.run( + quote(User.schema.filter(_.name == "batista")) + ).onDS + _ <- putStrLn(s"result : $result") + } yield () + +} + diff --git a/DB/src/main/scala/org/tbasket/db/schemas/Member.scala b/DB/src/main/scala/org/tbasket/db/schemas/Member.scala new file mode 100644 index 0000000..c56b771 --- /dev/null +++ b/DB/src/main/scala/org/tbasket/db/schemas/Member.scala @@ -0,0 +1,18 @@ +package org.tbasket.db.schemas + +import org.tbasket.db.Database + +case class Member(team: Team, user: User, admin: Boolean) + +object Member { + + import Database.ctx._ + + val schema = quote { + querySchema[Member]("member", + _.team.id -> "team_id", + _.user.id -> "user_id", + _.admin -> "is_admin" + ) + } +} diff --git a/DB/src/main/scala/org/tbasket/db/schemas/Tactic.scala b/DB/src/main/scala/org/tbasket/db/schemas/Tactic.scala new file mode 100644 index 0000000..b2269bd --- /dev/null +++ b/DB/src/main/scala/org/tbasket/db/schemas/Tactic.scala @@ -0,0 +1,16 @@ +package org.tbasket.db.schemas +import org.tbasket.db.Database +case class Tactic(id: Int, name: String, owner: User, filePath: String) + +object Tactic { + import Database.ctx._ + + val schema = quote { + querySchema[Tactic]("tactic", + _.id -> "id", + _.name -> "name", + _.owner.id -> "owner_id", + _.filePath -> "file_path" + ) + } +} diff --git a/DB/src/main/scala/org/tbasket/db/schemas/Team.scala b/DB/src/main/scala/org/tbasket/db/schemas/Team.scala new file mode 100644 index 0000000..310bf12 --- /dev/null +++ b/DB/src/main/scala/org/tbasket/db/schemas/Team.scala @@ -0,0 +1,17 @@ +package org.tbasket.db.schemas + +import org.tbasket.db.Database + +case class Team(id: Int, name: String, clubName: String) +object Team { + import Database.ctx._ + + val schema = quote { + querySchema[Team]("team", + _.id -> "id", + _.name -> "name", + _.clubName -> "club_name" + ) + } + +} diff --git a/DB/src/main/scala/org/tbasket/db/schemas/User.scala b/DB/src/main/scala/org/tbasket/db/schemas/User.scala new file mode 100644 index 0000000..fef0ebb --- /dev/null +++ b/DB/src/main/scala/org/tbasket/db/schemas/User.scala @@ -0,0 +1,23 @@ +package org.tbasket.db.schemas + +import org.tbasket.db.Database + +case class User(id : Int, + name : String, + forename : String, + passwordHash: Int, + 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", + ) + } +} \ No newline at end of file diff --git a/DB/table_init.sql b/DB/table_init.sql deleted file mode 100644 index 37cb552..0000000 --- a/DB/table_init.sql +++ /dev/null @@ -1,39 +0,0 @@ - - -CREATE TABLE user( - id int PRIMARY KEY, - name varchar(30) NOT NULL , - forename varchar(30) NOT NULL -); - -CREATE TABLE team( - id int, - name varchar(30), - club_name varchar(30), - PRIMARY KEY (id) -); - -CREATE TABLE tactic_group( - tactic_id int, - group_id int, - PRIMARY KEY(tactic_id, group_id), - FOREIGN KEY (tactic_id) REFERENCES tactic(id), - FOREIGN KEY (group_id) REFERENCES team(id) -); - -CREATE TABLE member( - group_id int, - user_id int, - is_manager int NOT NULL, - PRIMARY KEY (group_id, user_id), - FOREIGN KEY (group_id) REFERENCES team(id), - FOREIGN KEY (user_id) REFERENCES user(id) -); - -CREATE TABLE tactic( - id int PRIMARY KEY , - name varchar(30) NOT NULL, - file_path varchar NOT NULL, - owner_id int, - FOREIGN KEY (owner_id) REFERENCES user(id) -); \ No newline at end of file diff --git a/build.gradle b/build.gradle index cf428d6..b0b6317 100644 --- a/build.gradle +++ b/build.gradle @@ -22,13 +22,10 @@ shadowJar { } dependencies { + implementation project(':API') implementation project(':DB') - implementation 'io.getquill:quill-jdbc_2.13:4.6.0' - implementation 'com.typesafe.play:play-json_2.13:2.10.0-RC7' - - testImplementation "io.circe:circe-core_$scalaVersion:0.15.0-M1" - testImplementation "io.circe:circe-parser_$scalaVersion:0.15.0-M1" + implementation 'dev.zio:zio_2.13:2.0.6' } diff --git a/src/main/scala/org/tbasket/Main.scala b/src/main/scala/org/tbasket/Main.scala index 2486558..fe32996 100644 --- a/src/main/scala/org/tbasket/Main.scala +++ b/src/main/scala/org/tbasket/Main.scala @@ -2,41 +2,27 @@ package org.tbasket import org.apache.logging.log4j.LogManager +import org.tbasket.db.Database import zio._ import java.lang import java.nio.file.{Files, Path} import java.util.Properties import scala.io.StdIn +import scala.util.{Failure, Success} import scala.util.control.NonFatal -object Main { +object Main extends ZIOAppDefault { final val LOG = LogManager.getLogger("Core") - - def main(args: Array[String]): Unit = { - LOG.info("Starting server") + + override def run: ZIO[Main.Environment with ZIOAppArgs with Scope, Any, Any] = { val config = retrieveConfig - db(config) - api(config) - LOG.info("Server successfully started") + val db = new Database(config) + EndpointSetup.setupEndpoint(config).run + .provide(db.layer) } - - private def db(config: Properties): Unit = new Thread({ () => - }, "Database").start() - //TODO - - private def api(config: Properties): Unit = new Thread({ () => - val endpoint = EndpointSetup.setupEndpoint(config) - val runtime = Runtime.default - Unsafe.unsafe { implicit u => - runtime.unsafe.run(endpoint.run).catchSome { - case NonFatal(e) => - e.printStackTrace() - throw e - } - } - }: Runnable, "API").start() - + + private def retrieveConfig: Properties = { val configFile = Path.of("server.properties") if (Files.notExists(configFile)) { @@ -48,8 +34,8 @@ object Main { properties.load(in) properties } - + //add a shutdown hook to log when the server is about to get killed lang.Runtime.getRuntime.addShutdownHook(new Thread(() => LOG.info("Server shutdowns"))) - + } diff --git a/src/main/scala/org/tbasket/data/User.scala b/src/main/scala/org/tbasket/data/User.scala deleted file mode 100644 index 0e7af52..0000000 --- a/src/main/scala/org/tbasket/data/User.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.tbasket.data - -class User(val id : Int, - val name : String, - val forename: String, - val passwordHash: Int, - val mailAddress : String) { - - -} diff --git a/src/main/scala/org/tbasket/handler/LoginHandler.scala b/src/main/scala/org/tbasket/handler/LoginHandler.scala index 6ea2b6f..c4351a1 100644 --- a/src/main/scala/org/tbasket/handler/LoginHandler.scala +++ b/src/main/scala/org/tbasket/handler/LoginHandler.scala @@ -1,21 +1,45 @@ package org.tbasket.handler import org.tbasket.api.compute.APIRequestHandler +import org.tbasket.db.Database.ctx._ +import org.tbasket.db.schemas.User +import org.tbasket.session.UserSessionHandler +import zio.ZIO +import zio.http.model.{Cookie, Headers, Status} import zio.http.{Request, Response} -import io.getquill._ -import org.tbasket.data.User -import zio.{ZEnvironment, ZIO} -object LoginHandler extends APIRequestHandler { +import zio.json._ +import zio.json.ast.Json.Str +import zio.json.ast.{Json, JsonCursor} - private val ctx = new SqlMirrorContext(MirrorSqlDialect, Literal) - import ctx._ - private def getAccount(mail: String, passwordHash: Int) = quote { - //query[User].filter(_.mailAddress == mail).filter(_.passwordHash == passwordHash) +class LoginHandler(sessions: UserSessionHandler) extends APIRequestHandler { + + private def getUser(json: Json): Either[String, User] = { + (for { + mail <- json.get[Str](JsonCursor.field("mail").isString).map(_.value) + password <- json.get[Str](JsonCursor.field("password").isString).map(_.value) + } yield quote { // TODO use argon2id + User.schema.filter(usr => usr.mailAddress == mail && usr.passwordHash == password.hashCode) + }).flatMap(_.nested.value.toRight("password or email incorrect")) } - - - override def post(request: Request): Response = { - ??? + + override def post: ZIO[Request, Throwable, Response] = ZIO.serviceWithZIO[Request] { r => + r.body.asString + .map(_.fromJson[Json].flatMap(getUser) match { + case Left(err) => //if account not found or password mismatches + Response.json(Json.Obj("error" -> Json.Str(err)).toJson) + case Right(user) => + val session = sessions.bind(user) + + Response( + status = Status.Found, + headers = Headers.location("/") ++ Headers.setCookie( + Cookie( + name = "token", + content = session.token.toString, + maxAge = Some(session.expirationDate) + )) + ) + } + ) } - } diff --git a/src/main/scala/org/tbasket/session/UserSession.scala b/src/main/scala/org/tbasket/session/UserSession.scala new file mode 100644 index 0000000..12e45ca --- /dev/null +++ b/src/main/scala/org/tbasket/session/UserSession.scala @@ -0,0 +1,9 @@ +package org.tbasket.session + +import org.tbasket.db.schemas.User + +import java.util.UUID + +case class UserSession(user: User, token: UUID, expirationDate: Long) { + +} diff --git a/src/main/scala/org/tbasket/session/UserSessionHandler.scala b/src/main/scala/org/tbasket/session/UserSessionHandler.scala new file mode 100644 index 0000000..e5b5843 --- /dev/null +++ b/src/main/scala/org/tbasket/session/UserSessionHandler.scala @@ -0,0 +1,52 @@ +package org.tbasket.session + +import org.tbasket.db.schemas.User +import org.tbasket.session.UserSessionHandler.SessionLifespan + +import java.time.{Duration, Instant} +import java.util.UUID +import scala.collection.mutable + +object UserSessionHandler { + final val SessionLifespan = Duration.ofDays(31) +} + +class UserSessionHandler { + + private val sessionsUser = mutable.HashMap.empty[User, UserSession] + private val sessionsToken = mutable.HashMap.empty[UUID, UserSession] + + def findSession(token: UUID): Option[UserSession] = { + sessionsToken.get(token) match { + case Some(session) if System.currentTimeMillis() > session.expirationDate => + unbind(token) //session has expired, unbind session + None + + case s => s + } + } + + private def unbind(token: UUID): Unit = { + sessionsToken.remove(token) + .foreach(s => sessionsUser.remove(s.user)) + } + + private def unbind(user: User): Unit = { + sessionsUser.remove(user) + .foreach(s => sessionsToken.remove(s.token)) + } + + def bind(user: User): UserSession = { + if (sessionsUser.contains(user)) { + //if there was a session already bound, then remove it + unbind(user) + } + val uuid = UUID.randomUUID() + val limit = System.currentTimeMillis() + Instant.now().plus(SessionLifespan).toEpochMilli + val session = UserSession(user, uuid, limit) + sessionsUser.put(user, session) + sessionsToken.put(uuid, session) + session + } + +} \ No newline at end of file