83 lines
2.8 KiB
Scala
83 lines
2.8 KiB
Scala
package dao
|
|
|
|
import dao.UsersDAO.{UnexpectedError, UsernameTaken}
|
|
import de.mkammerer.argon2.Argon2Factory
|
|
import execution.CryptoExecutionContext
|
|
import jakarta.inject.Inject
|
|
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
|
import slick.jdbc.PostgresProfile
|
|
import slick.jdbc.PostgresProfile.api.*
|
|
|
|
import scala.concurrent.{ExecutionContext, Future}
|
|
import models.User
|
|
import org.postgresql.util.{PSQLException, PSQLState}
|
|
import schemas.Users
|
|
|
|
import java.nio.charset.StandardCharsets.UTF_8
|
|
import scala.util.Random
|
|
|
|
class UsersDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider, private val cryptoEC: CryptoExecutionContext)
|
|
(using ec: ExecutionContext)
|
|
extends HasDatabaseConfigProvider[PostgresProfile] {
|
|
private val argon2 = Argon2Factory.create()
|
|
|
|
private object ArgonParameters {
|
|
val iterations = 3
|
|
val parallelism = 1
|
|
val memory = 65536
|
|
}
|
|
|
|
private final val dummyDigest = "$argon2i$v=19$m=65536,t=3,p=1$EB9QibLwzqkO7ne2bJoU/Q$ADkdyweXKylJORvWXbBVekURYaazkW/OplTq2wdN1So"
|
|
|
|
def authenticate(username: String, password: String): Future[Option[User]] = {
|
|
val query = Users.filter(_.username === username).take(1).result.headOption
|
|
val userFuture = db.run(query)
|
|
|
|
userFuture.map { userOption =>
|
|
for {
|
|
user <- userOption
|
|
digest <- user.passwordDigest
|
|
} yield (user, digest)
|
|
}.map {
|
|
case Some((user, digest)) => (Some(user), digest)
|
|
case None => (None, dummyDigest)
|
|
}.flatMap { (userOpt, digest) =>
|
|
for {
|
|
validHash <- Future(argon2.verify(digest, password.getBytes(UTF_8)))(using cryptoEC)
|
|
opt = userOpt.flatMap(u => if validHash then Some(u) else None)
|
|
} yield opt
|
|
}
|
|
}
|
|
|
|
def create(username: String, password: String): Future[Either[UsersDAO.RegistrationError, User]] = {
|
|
Future(argonHash(password))(using cryptoEC)
|
|
.flatMap{ passwordDigest =>
|
|
val gid = Random.alphanumeric.take(12).mkString
|
|
val user = User(
|
|
id = None,
|
|
gid = gid,
|
|
username = username,
|
|
passwordDigest = Some(passwordDigest)
|
|
)
|
|
|
|
db.run((Users returning Users) += user).map(Right(_))
|
|
}.recover {
|
|
case ex: PSQLException if ex.getSQLState == PSQLState.UNIQUE_VIOLATION.getState =>
|
|
Left(UsernameTaken)
|
|
case ex: Throwable =>
|
|
Left(UnexpectedError(ex))
|
|
}
|
|
}
|
|
|
|
def find_by_gid(gid: String): Future[Option[User]] = {
|
|
db.run(Users.filter(_.gid === gid).take(1).result.headOption)
|
|
}
|
|
|
|
private def argonHash(data: String) = argon2.hash(ArgonParameters.iterations, ArgonParameters.memory, ArgonParameters.parallelism, data.getBytes(UTF_8))
|
|
}
|
|
|
|
object UsersDAO {
|
|
sealed trait RegistrationError
|
|
case object UsernameTaken extends RegistrationError
|
|
case class UnexpectedError(cause: Throwable) extends RegistrationError
|
|
} |