|
|
|
@ -1,25 +1,23 @@
|
|
|
|
|
package org.tbasket.handler
|
|
|
|
|
|
|
|
|
|
import io.getquill.*
|
|
|
|
|
import io.getquill.context.qzio.ZioContext
|
|
|
|
|
import org.tbasket.api.compute.APIRequestHandler
|
|
|
|
|
import org.tbasket.auth.JWTClient
|
|
|
|
|
import org.tbasket.db.Database.ctx.*
|
|
|
|
|
import org.tbasket.db.schemas.User
|
|
|
|
|
import org.tbasket.auth.{UserSession, UserSessionHandler}
|
|
|
|
|
import org.tbasket.handler.HandlerUtils.errorBody
|
|
|
|
|
import org.tbasket.handler.LoginError.{InvalidRequest, *}
|
|
|
|
|
import zio.http.model.{Cookie, Header, Headers, Status}
|
|
|
|
|
import zio.http.{Body, Request, Response}
|
|
|
|
|
import zio.http.{Body, Client, Request, Response, URL}
|
|
|
|
|
import zio.json.*
|
|
|
|
|
import zio.json.ast.Json.Str
|
|
|
|
|
import zio.json.ast.{Json, JsonCursor}
|
|
|
|
|
import zio.{ZEnvironment, ZIO}
|
|
|
|
|
import zio.*
|
|
|
|
|
import io.getquill.context.qzio.ZioContext
|
|
|
|
|
import org.tbasket.handler.HandlerUtils.errorBody
|
|
|
|
|
import org.tbasket.handler.LoginError.InvalidRequest
|
|
|
|
|
import zio.{ZEnvironment, ZIO, *}
|
|
|
|
|
|
|
|
|
|
import javax.sql.DataSource
|
|
|
|
|
import java.sql.SQLException
|
|
|
|
|
import java.util.UUID
|
|
|
|
|
|
|
|
|
|
import javax.sql.DataSource
|
|
|
|
|
|
|
|
|
|
enum LoginError:
|
|
|
|
|
case TokenNotFound(token: UUID)
|
|
|
|
@ -29,72 +27,83 @@ enum LoginError:
|
|
|
|
|
case InternalError(t: Throwable)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import LoginError.*
|
|
|
|
|
|
|
|
|
|
class LoginHandler(sessions: UserSessionHandler, ctx: ZioContext[SqliteDialect, Literal]) extends APIRequestHandler:
|
|
|
|
|
class LoginHandler extends APIRequestHandler:
|
|
|
|
|
|
|
|
|
|
import ctx.*
|
|
|
|
|
private def getUser(json: Json) =
|
|
|
|
|
val r =
|
|
|
|
|
ZIO.serviceWithZIO[ZioContext[SqliteDialect, Literal]] { ctx =>
|
|
|
|
|
import ctx.*
|
|
|
|
|
for
|
|
|
|
|
mail <-
|
|
|
|
|
ZIO.fromEither(json.get[Str](JsonCursor.field("mail").isString).map(_.value))
|
|
|
|
|
.mapError(InvalidRequest("Missing or invalid field mail", _))
|
|
|
|
|
password <-
|
|
|
|
|
ZIO.fromEither(json.get[Str](JsonCursor.field("password").isString).map(_.value))
|
|
|
|
|
.mapError(InvalidRequest("Missing or invalid field password", _))
|
|
|
|
|
|
|
|
|
|
private def getUser(json: Json): IO[LoginError, User] =
|
|
|
|
|
val r: ZIO[DataSource, LoginError, Option[User]] = for
|
|
|
|
|
mail <-
|
|
|
|
|
ZIO.fromEither(json.get[Str](JsonCursor.field("mail").isString).map(_.value))
|
|
|
|
|
.mapError(InvalidRequest("Missing or invalid field mail", _))
|
|
|
|
|
password <-
|
|
|
|
|
ZIO.fromEither(json.get[Str](JsonCursor.field("password").isString).map(_.value))
|
|
|
|
|
.mapError(InvalidRequest("Missing or invalid field password", _))
|
|
|
|
|
result <- run(quote { // TODO use argon2id
|
|
|
|
|
User.schema.filter(usr =>
|
|
|
|
|
usr.mailAddress == mail && usr.passwordHash == lift(password.hashCode)
|
|
|
|
|
)
|
|
|
|
|
}).mapError(InternalError.apply)
|
|
|
|
|
yield result.headOption
|
|
|
|
|
}
|
|
|
|
|
r.someOrFail(InvalidPassword)
|
|
|
|
|
|
|
|
|
|
result <- run(quote { // TODO use argon2id
|
|
|
|
|
User.schema.filter(usr =>
|
|
|
|
|
usr.mailAddress == mail && usr.passwordHash == lift(password.hashCode)
|
|
|
|
|
)
|
|
|
|
|
}).mapError(InternalError.apply)
|
|
|
|
|
yield result.headOption
|
|
|
|
|
r.someOrFail(InvalidPassword).provideSome(ZLayer.succeed(ctx))
|
|
|
|
|
|
|
|
|
|
override def post(request: Request): URIO[Console, Response] =
|
|
|
|
|
val bindSession: ZIO[Console, LoginError, UserSession] =
|
|
|
|
|
override def post(request: Request): Task[Response] =
|
|
|
|
|
val bindSession =
|
|
|
|
|
for
|
|
|
|
|
body <- request
|
|
|
|
|
.body
|
|
|
|
|
.asString
|
|
|
|
|
.tapError(Console.printError(_))
|
|
|
|
|
.mapError(s => InvalidRequest("Wrong request body", s.getMessage)): IO[LoginError, String]
|
|
|
|
|
.mapError(s =>
|
|
|
|
|
InvalidRequest("Wrong request body", s.getMessage)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
json <- ZIO.fromEither(body.fromJson[Json])
|
|
|
|
|
.mapError(InvalidRequest("Invalid JSON body", _)): IO[LoginError, Json]
|
|
|
|
|
.mapError(InvalidRequest("Invalid JSON body", _))
|
|
|
|
|
|
|
|
|
|
user <- getUser(json): IO[LoginError, User]
|
|
|
|
|
session <- sessions.bind(user): IO[LoginError, UserSession]
|
|
|
|
|
yield session
|
|
|
|
|
user <- getUser(json)
|
|
|
|
|
jwt <- ZIO.serviceWithZIO[JWTClient](_.requestJwt(user))
|
|
|
|
|
yield (user, jwt)
|
|
|
|
|
|
|
|
|
|
bindSession.map { sess =>
|
|
|
|
|
Response(
|
|
|
|
|
status = Status.Ok,
|
|
|
|
|
body = Body.fromString(s"""{"token": "${sess.token}"}"""),
|
|
|
|
|
)
|
|
|
|
|
}.mapError {
|
|
|
|
|
case TokenNotFound(_) => Response(
|
|
|
|
|
status = Status.Unauthorized,
|
|
|
|
|
body = errorBody("unauthorized", "unknown token"),
|
|
|
|
|
headers = Headers(Headers.location("/login")) //send back caller to login panel
|
|
|
|
|
status = Status.Found,
|
|
|
|
|
headers = Headers.location("/") ++ //login successful, go back to main page
|
|
|
|
|
Headers.setCookie(Cookie(
|
|
|
|
|
"JWT", "Jw"
|
|
|
|
|
))
|
|
|
|
|
)
|
|
|
|
|
case UserNotFound(_) => Response(
|
|
|
|
|
status = Status.Unauthorized,
|
|
|
|
|
body = errorBody("unauthorized", "unknown user email"),
|
|
|
|
|
headers = Headers(Headers.location("/register")) //send back caller to register panel
|
|
|
|
|
)
|
|
|
|
|
case InvalidPassword => Response(
|
|
|
|
|
status = Status.Unauthorized,
|
|
|
|
|
body = errorBody("unauthorized", "invalid password")
|
|
|
|
|
)
|
|
|
|
|
case InvalidRequest(msg, cause) => Response(
|
|
|
|
|
status = Status.Unauthorized,
|
|
|
|
|
body = errorBody("wrong request", s"$cause: $msg")
|
|
|
|
|
)
|
|
|
|
|
case InternalError(_) => Response(
|
|
|
|
|
status = Status.InternalServerError,
|
|
|
|
|
body = errorBody("internal", "internal error, please contact support")
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} fold( {
|
|
|
|
|
_ match
|
|
|
|
|
case TokenNotFound(_) => Response(
|
|
|
|
|
status = Status.Unauthorized,
|
|
|
|
|
body = errorBody("unauthorized", "unknown token"),
|
|
|
|
|
headers =
|
|
|
|
|
Headers(
|
|
|
|
|
Headers.location("/login")
|
|
|
|
|
) // send back caller to login panel
|
|
|
|
|
)
|
|
|
|
|
case UserNotFound(_) => Response(
|
|
|
|
|
status = Status.Unauthorized,
|
|
|
|
|
body = errorBody("unauthorized", "unknown user email"),
|
|
|
|
|
headers =
|
|
|
|
|
Headers(
|
|
|
|
|
Headers.location("/register")
|
|
|
|
|
) // send back caller to register panel
|
|
|
|
|
)
|
|
|
|
|
case InvalidPassword => Response(
|
|
|
|
|
status = Status.Unauthorized,
|
|
|
|
|
body = errorBody("unauthorized", "invalid password")
|
|
|
|
|
)
|
|
|
|
|
case InvalidRequest(msg, cause) => Response(
|
|
|
|
|
status = Status.Unauthorized,
|
|
|
|
|
body = errorBody("wrong request", s"$cause: $msg")
|
|
|
|
|
)
|
|
|
|
|
case InternalError(_) => Response(
|
|
|
|
|
status = Status.InternalServerError,
|
|
|
|
|
body = errorBody("internal", "internal error, please contact support")
|
|
|
|
|
)
|
|
|
|
|
}, x => x)
|
|
|
|
|