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)) } } 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 }