working on backend
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
parent
ebcbc8bab7
commit
27b808abe3
@ -1,8 +1,10 @@
|
|||||||
build
|
build
|
||||||
.gradle
|
.gradle
|
||||||
.idea
|
.idea
|
||||||
*.sqlite
|
|
||||||
*/.gradle/
|
*/.gradle/
|
||||||
log
|
log
|
||||||
|
|
||||||
|
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
.$*
|
.$*
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package org.tbasket.api.compute
|
package org.tbasket.api.compute
|
||||||
|
|
||||||
|
import zio.ZIO
|
||||||
import zio.http.model.Status
|
import zio.http.model.Status
|
||||||
import zio.http.{Request, Response}
|
import zio.http.{Request, Response}
|
||||||
|
|
||||||
trait APIRequestHandler {
|
trait APIRequestHandler {
|
||||||
|
|
||||||
def get(request: Request): Response = Response(Status.MethodNotAllowed)
|
def get: ZIO[Request, Throwable, Response] = ZIO.succeed(Response(Status.MethodNotAllowed))
|
||||||
|
|
||||||
def post(request: Request): Response = Response(Status.MethodNotAllowed)
|
def post: ZIO[Request, Throwable, Response] = ZIO.succeed(Response(Status.MethodNotAllowed))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
group "org.tbasket.api"
|
group "org.tbasket.api"
|
||||||
|
|
||||||
dependencies {
|
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'
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
path=database.sqlite
|
@ -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)
|
||||||
|
);
|
@ -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 ()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
|
||||||
);
|
|
@ -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) {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,21 +1,45 @@
|
|||||||
package org.tbasket.handler
|
package org.tbasket.handler
|
||||||
|
|
||||||
import org.tbasket.api.compute.APIRequestHandler
|
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 zio.http.{Request, Response}
|
||||||
import io.getquill._
|
import zio.json._
|
||||||
import org.tbasket.data.User
|
import zio.json.ast.Json.Str
|
||||||
import zio.{ZEnvironment, ZIO}
|
import zio.json.ast.{Json, JsonCursor}
|
||||||
object LoginHandler extends APIRequestHandler {
|
|
||||||
|
|
||||||
private val ctx = new SqlMirrorContext(MirrorSqlDialect, Literal)
|
class LoginHandler(sessions: UserSessionHandler) extends APIRequestHandler {
|
||||||
import ctx._
|
|
||||||
private def getAccount(mail: String, passwordHash: Int) = quote {
|
private def getUser(json: Json): Either[String, User] = {
|
||||||
//query[User].filter(_.mailAddress == mail).filter(_.passwordHash == passwordHash)
|
(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: ZIO[Request, Throwable, Response] = ZIO.serviceWithZIO[Request] { r =>
|
||||||
override def post(request: Request): Response = {
|
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)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in new issue