Create user complete, added impl-specific types.
This commit is contained in:
parent
5c7c33d1b5
commit
5481686e08
8 changed files with 419 additions and 46 deletions
|
|
@ -2,6 +2,7 @@ package gs.smolban.auth
|
|||
|
||||
import cats.effect.Sync
|
||||
import cats.syntax.all.*
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters
|
||||
|
|
@ -28,13 +29,19 @@ final class Argon2[F[_]: Sync](
|
|||
* @return
|
||||
* The calculated hash.
|
||||
*/
|
||||
def calculateHash(input: String): F[Argon2Hash] =
|
||||
def calculateHash(
|
||||
input: String,
|
||||
charset: Charset = StandardCharsets.UTF_8
|
||||
): F[Argon2Hash] =
|
||||
calculateHash(input.getBytes(charset))
|
||||
|
||||
def calculateHash(input: Array[Byte]): F[Argon2Hash] =
|
||||
randomSalt().map { salt =>
|
||||
val ap = buildAlgorithmParams(salt)
|
||||
val a2 = new Argon2BytesGenerator()
|
||||
val _ = a2.init(ap)
|
||||
val bytes = new Array[Byte](config.hashLengthInBytes)
|
||||
val _ = a2.generateBytes(input.getBytes(StandardCharsets.UTF_8), bytes)
|
||||
val _ = a2.generateBytes(input, bytes)
|
||||
new Argon2Hash(
|
||||
algorithmVersion = config.algorithmVersion,
|
||||
algorithmType = config.algorithmType,
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ import gs.smolban.model.account.AiAgent
|
|||
import gs.smolban.model.account.PermissionSet
|
||||
import gs.smolban.model.account.ServiceAccount
|
||||
import gs.smolban.model.account.User
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
|
||||
/** Database interface for [[Account]] and related management activities.
|
||||
*/
|
||||
trait AuthDb[F[_]]:
|
||||
trait AuthDb[DBIO[_], F[_]]:
|
||||
|
||||
/** Get the user account with the given name.
|
||||
*
|
||||
|
|
@ -28,7 +29,7 @@ trait AuthDb[F[_]]:
|
|||
*/
|
||||
def getUser(
|
||||
name: AccountName
|
||||
): F[Option[User]]
|
||||
): DBIO[Option[User]]
|
||||
|
||||
/** Create a new [[User]].
|
||||
*
|
||||
|
|
@ -44,8 +45,9 @@ trait AuthDb[F[_]]:
|
|||
def createUser(
|
||||
name: AccountName,
|
||||
initialPassword: Password,
|
||||
initialPermissions: PermissionSet
|
||||
): EitherT[F, DbError, User]
|
||||
initialPermissions: PermissionSet,
|
||||
createdAt: CreatedAt
|
||||
): F[DBIO[Either[DbError, User]]]
|
||||
|
||||
/** Update the password for an existing [[User]].
|
||||
*
|
||||
|
|
@ -59,12 +61,12 @@ trait AuthDb[F[_]]:
|
|||
def setUserPassword(
|
||||
id: AccountId,
|
||||
newPassword: Password
|
||||
): EitherT[F, DbError, User]
|
||||
): EitherT[DBIO, DbError, User]
|
||||
|
||||
/** @return
|
||||
* List of all active [[User]].
|
||||
*/
|
||||
def listActiveUsers(): F[List[User]]
|
||||
def listActiveUsers(): fs2.Stream[DBIO, User]
|
||||
|
||||
/** Get the [[ServiceAccount]] with the given name.
|
||||
*
|
||||
|
|
@ -76,7 +78,7 @@ trait AuthDb[F[_]]:
|
|||
*/
|
||||
def getServiceAccount(
|
||||
name: AccountName
|
||||
): F[Option[ServiceAccount]]
|
||||
): DBIO[Option[ServiceAccount]]
|
||||
|
||||
/** Create a new [[ServiceAccount]].
|
||||
*
|
||||
|
|
@ -93,12 +95,12 @@ trait AuthDb[F[_]]:
|
|||
name: AccountName,
|
||||
owner: AccountId,
|
||||
initialPermissions: PermissionSet
|
||||
): EitherT[F, DbError, NewServiceAccount]
|
||||
): EitherT[DBIO, DbError, NewServiceAccount]
|
||||
|
||||
/** @return
|
||||
* List of all active [[ServiceAccount]].
|
||||
*/
|
||||
def listActiveServiceAccounts(): F[List[ServiceAccount]]
|
||||
def listActiveServiceAccounts(): DBIO[List[ServiceAccount]]
|
||||
|
||||
/** Get the [[AiAgent]] with the given name.
|
||||
*
|
||||
|
|
@ -109,7 +111,7 @@ trait AuthDb[F[_]]:
|
|||
*/
|
||||
def getAgentAccount(
|
||||
name: AccountName
|
||||
): F[Option[AiAgent]]
|
||||
): DBIO[Option[AiAgent]]
|
||||
|
||||
/** Create a new [[AiAgent]].
|
||||
*
|
||||
|
|
@ -126,12 +128,12 @@ trait AuthDb[F[_]]:
|
|||
name: AccountName,
|
||||
owner: AccountId,
|
||||
initialPermissions: PermissionSet
|
||||
): EitherT[F, DbError, NewAiAgentAccount]
|
||||
): EitherT[DBIO, DbError, NewAiAgentAccount]
|
||||
|
||||
/** @return
|
||||
* List of all active [[AiAgent]].
|
||||
*/
|
||||
def listActiveAgentAccounts(): F[List[AiAgent]]
|
||||
def listActiveAgentAccounts(): DBIO[List[AiAgent]]
|
||||
|
||||
/** Rotate the specified secret and return the new secret. This is the only
|
||||
* time the new value will ever be exposed.
|
||||
|
|
@ -153,7 +155,7 @@ trait AuthDb[F[_]]:
|
|||
accountId: AccountId,
|
||||
credentialId: CredentialId,
|
||||
overlapHours: Int
|
||||
): EitherT[F, DbError, Base64]
|
||||
): EitherT[DBIO, DbError, Base64]
|
||||
|
||||
/** Replace the permissions assigned to some [[Account]].
|
||||
*
|
||||
|
|
@ -167,7 +169,7 @@ trait AuthDb[F[_]]:
|
|||
def setAccountPermissions(
|
||||
id: AccountId,
|
||||
newPermissions: PermissionSet
|
||||
): EitherT[F, DbError, Account]
|
||||
): EitherT[DBIO, DbError, Account]
|
||||
|
||||
/** List all credentials that exist for the given [[AccountId]].
|
||||
*
|
||||
|
|
@ -178,7 +180,7 @@ trait AuthDb[F[_]]:
|
|||
*/
|
||||
def listAccountCredentials(
|
||||
id: AccountId
|
||||
): F[List[Credential]]
|
||||
): DBIO[List[Credential]]
|
||||
|
||||
/** Revoke a credential, making it unusable.
|
||||
*
|
||||
|
|
@ -189,7 +191,7 @@ trait AuthDb[F[_]]:
|
|||
*/
|
||||
def revokeCredential(
|
||||
id: CredentialId
|
||||
): EitherT[F, DbError, Unit]
|
||||
): EitherT[DBIO, DbError, Unit]
|
||||
|
||||
/** Expire a credential, making it unusable.
|
||||
*
|
||||
|
|
@ -200,4 +202,4 @@ trait AuthDb[F[_]]:
|
|||
*/
|
||||
def expireCredential(
|
||||
id: CredentialId
|
||||
): EitherT[F, DbError, Unit]
|
||||
): EitherT[DBIO, DbError, Unit]
|
||||
|
|
|
|||
|
|
@ -1,47 +1,161 @@
|
|||
package gs.smolban.db.doobie
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.Sync
|
||||
import cats.syntax.all.*
|
||||
import doobie.*
|
||||
import doobie.implicits.*
|
||||
import gs.smolban.auth.Argon2
|
||||
import gs.smolban.auth.Argon2Hash
|
||||
import gs.smolban.auth.Base64
|
||||
import gs.smolban.auth.Credential
|
||||
import gs.smolban.auth.CredentialEffectivity
|
||||
import gs.smolban.auth.CredentialId
|
||||
import gs.smolban.auth.CredentialStatus
|
||||
import gs.smolban.auth.CredentialType
|
||||
import gs.smolban.auth.NewAiAgentAccount
|
||||
import gs.smolban.auth.NewServiceAccount
|
||||
import gs.smolban.auth.Password
|
||||
import gs.smolban.auth.RsaDecryption
|
||||
import gs.smolban.auth.RsaEncryptedBytes
|
||||
import gs.smolban.db.AuthDb
|
||||
import gs.smolban.db.DbError
|
||||
import gs.smolban.db.doobie.DoobieTypes.*
|
||||
import gs.smolban.model.account.Account
|
||||
import gs.smolban.model.account.AccountId
|
||||
import gs.smolban.model.account.AccountName
|
||||
import gs.smolban.model.account.AccountStatus
|
||||
import gs.smolban.model.account.AccountType
|
||||
import gs.smolban.model.account.AiAgent
|
||||
import gs.smolban.model.account.Permission
|
||||
import gs.smolban.model.account.PermissionSet
|
||||
import gs.smolban.model.account.ServiceAccount
|
||||
import gs.smolban.model.account.User
|
||||
import gs.smolban.model.group.GroupName
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
|
||||
final class DoobieAuthDb extends AuthDb[ConnectionIO] {
|
||||
final class DoobieAuthDb[F[_]: Sync](
|
||||
val argon2: Argon2[F],
|
||||
val rsaDecryption: RsaDecryption[F],
|
||||
val sqlStates: CuratedSqlStates,
|
||||
val implTypes: ImplTypes
|
||||
) extends AuthDb[ConnectionIO, F]:
|
||||
private val Sql = new DoobieAuthDb.Sql(implTypes)
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def getUser(name: AccountName): ConnectionIO[Option[User]] = ???
|
||||
override def getUser(name: AccountName): ConnectionIO[Option[User]] =
|
||||
Sql.getUser(name).option.flatMap {
|
||||
case Some((id, status, createdAt)) =>
|
||||
getUserPermissions(id).map { permissions =>
|
||||
Some(
|
||||
new User(
|
||||
id = id,
|
||||
name = name,
|
||||
status = status,
|
||||
permissions = permissions,
|
||||
createdAt = createdAt
|
||||
)
|
||||
)
|
||||
}
|
||||
case None => None.pure[ConnectionIO]
|
||||
}
|
||||
|
||||
private def getUserPermissions(id: AccountId): ConnectionIO[PermissionSet] =
|
||||
for
|
||||
global <- Sql.getUserGlobalPermissions(id).to[Set]
|
||||
group <- Sql.getUserGroupPermissions(id).to[Set]
|
||||
yield PermissionSet(
|
||||
global = global,
|
||||
group = group.groupMap(_._1)(_._2)
|
||||
)
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def createUser(
|
||||
name: AccountName,
|
||||
initialPassword: Password,
|
||||
initialPermissions: PermissionSet
|
||||
): EitherT[[A] =>> ConnectionIO[A], DbError, User] = ???
|
||||
initialPermissions: PermissionSet,
|
||||
createdAt: CreatedAt
|
||||
): F[ConnectionIO[Either[DbError, User]]] =
|
||||
// Prepare Password
|
||||
exchangeEncryptedForHash(initialPassword.unwrap()).map { passwordHash =>
|
||||
// Insert the base user record.
|
||||
val accountId = AccountId.generate()
|
||||
Sql
|
||||
.insertUser(accountId, name, AccountStatus.Active, createdAt)
|
||||
.run
|
||||
.attemptSqlState
|
||||
.flatMap {
|
||||
case Left(sqlState) =>
|
||||
if sqlState.value == sqlStates.uniqueViolation.value then
|
||||
Left(DbError.AccountAlreadyExists(name, AccountType.User))
|
||||
.pure[ConnectionIO]
|
||||
else Left(DbError.GenericDatabaseError(sqlState)).pure[ConnectionIO]
|
||||
case Right(_) =>
|
||||
for
|
||||
// Insert the user's credential.
|
||||
_ <- Sql
|
||||
.insertCredential(
|
||||
id = CredentialId.generate(),
|
||||
hash = passwordHash,
|
||||
accountId = accountId,
|
||||
accountType = AccountType.User,
|
||||
credentialType = CredentialType.Password,
|
||||
status = CredentialStatus.Active,
|
||||
effective = None,
|
||||
createdAt = createdAt
|
||||
)
|
||||
.run
|
||||
// Insert the user's global permissions.
|
||||
_ <- Sql.insertUserGlobalPermissions(
|
||||
accountId,
|
||||
initialPermissions.global
|
||||
)
|
||||
// Insert the user's group permissions.
|
||||
_ <- Sql.insertUserGroupPermissions(
|
||||
accountId,
|
||||
initialPermissions.group
|
||||
)
|
||||
yield Right(
|
||||
new User(
|
||||
id = accountId,
|
||||
name = name,
|
||||
status = AccountStatus.Active,
|
||||
permissions = initialPermissions,
|
||||
createdAt = createdAt
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def exchangeEncryptedForHash(rsa: RsaEncryptedBytes): F[Argon2Hash] =
|
||||
rsaDecryption.decrypt(rsa).flatMap(argon2.calculateHash)
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def setUserPassword(
|
||||
id: AccountId,
|
||||
newPassword: Password
|
||||
): EitherT[[A] =>> ConnectionIO[A], DbError, User] = ???
|
||||
): EitherT[ConnectionIO, DbError, User] = ???
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def listActiveUsers(): ConnectionIO[List[User]] = ???
|
||||
override def listActiveUsers(): fs2.Stream[ConnectionIO, User] =
|
||||
Sql
|
||||
.listUsersForStatus(AccountStatus.Active)
|
||||
.stream
|
||||
.evalMap { case (id, name, createdAt) =>
|
||||
getUserPermissions(id).map { permissions =>
|
||||
new User(
|
||||
id = id,
|
||||
name = name,
|
||||
status = AccountStatus.Active,
|
||||
permissions = permissions,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
|
|
@ -54,7 +168,7 @@ final class DoobieAuthDb extends AuthDb[ConnectionIO] {
|
|||
name: AccountName,
|
||||
owner: AccountId,
|
||||
initialPermissions: PermissionSet
|
||||
): EitherT[[A] =>> ConnectionIO[A], DbError, NewServiceAccount] = ???
|
||||
): EitherT[ConnectionIO, DbError, NewServiceAccount] = ???
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
|
|
@ -108,4 +222,94 @@ final class DoobieAuthDb extends AuthDb[ConnectionIO] {
|
|||
override def expireCredential(id: CredentialId)
|
||||
: EitherT[[A] =>> ConnectionIO[A], DbError, Unit] = ???
|
||||
|
||||
}
|
||||
object DoobieAuthDb:
|
||||
|
||||
class Sql(implTypes: ImplTypes):
|
||||
import implTypes.given
|
||||
|
||||
def getUser(name: AccountName)
|
||||
: Query0[(AccountId, AccountStatus, CreatedAt)] =
|
||||
sql"""
|
||||
SELECT id, status, created_at
|
||||
FROM user
|
||||
WHERE name = $name
|
||||
""".query[(AccountId, AccountStatus, CreatedAt)]
|
||||
|
||||
def getUserGlobalPermissions(id: AccountId): Query0[Permission] =
|
||||
sql"""
|
||||
SELECT permission
|
||||
FROM user_permission_global
|
||||
WHERE user_id = $id
|
||||
""".query[Permission]
|
||||
|
||||
def getUserGroupPermissions(id: AccountId)
|
||||
: Query0[(GroupName, Permission)] =
|
||||
sql"""
|
||||
SELECT group_name, permission
|
||||
FROM user_permission_group
|
||||
WHERE user_id = $id
|
||||
""".query[(GroupName, Permission)]
|
||||
|
||||
def listUsersForStatus(status: AccountStatus)
|
||||
: Query0[(AccountId, AccountName, CreatedAt)] =
|
||||
sql"""
|
||||
SELECT id, name, created_at
|
||||
FROM user
|
||||
WHERE status = $status
|
||||
""".query[(AccountId, AccountName, CreatedAt)]
|
||||
|
||||
def insertUser(
|
||||
id: AccountId,
|
||||
name: AccountName,
|
||||
status: AccountStatus,
|
||||
createdAt: CreatedAt
|
||||
): Update0 =
|
||||
sql"""
|
||||
INSERT INTO user(id, name, status, created_at)
|
||||
VALUES ($id, $name, $status, $createdAt)
|
||||
""".update
|
||||
|
||||
def insertCredential(
|
||||
id: CredentialId,
|
||||
hash: Argon2Hash,
|
||||
accountId: AccountId,
|
||||
accountType: AccountType,
|
||||
credentialType: CredentialType,
|
||||
status: CredentialStatus,
|
||||
effective: Option[CredentialEffectivity],
|
||||
createdAt: CreatedAt
|
||||
): Update0 =
|
||||
sql"""
|
||||
INSERT INTO credential(credential_id, credential_hash, account_id,
|
||||
account_type, credential_type, status, effective_at, effective_through,
|
||||
created_at)
|
||||
VALUES ($id, $hash, $accountId, $accountType, $credentialType, $status,
|
||||
${effective.map(_.startDate)}, ${effective.flatMap(_.endDate)},
|
||||
$createdAt)
|
||||
""".update
|
||||
|
||||
def insertUserGlobalPermissions(
|
||||
id: AccountId,
|
||||
permissions: Set[Permission]
|
||||
): ConnectionIO[Int] =
|
||||
val command = """
|
||||
INSERT INTO user_permission_global (user_id, permission) VALUES (?, ?)
|
||||
""".stripMargin
|
||||
Update[(AccountId, Permission)](command)
|
||||
.updateMany(permissions.map(p => id -> p).toList)
|
||||
|
||||
def insertUserGroupPermissions(
|
||||
id: AccountId,
|
||||
permissions: Map[GroupName, Set[Permission]]
|
||||
): ConnectionIO[Int] =
|
||||
val command = """
|
||||
INSERT INTO user_permission_group (user_id, group_name, permission) VALUES (?, ?, ?)
|
||||
""".stripMargin
|
||||
Update[(AccountId, GroupName, Permission)](command)
|
||||
.updateMany(permissions.flatMap { case (gn, ps) =>
|
||||
ps.map(p => (id, gn, p))
|
||||
}.toList)
|
||||
|
||||
end Sql
|
||||
|
||||
end DoobieAuthDb
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ import gs.smolban.model.metadata.Tag
|
|||
import gs.smolban.model.metadata.TagValue
|
||||
|
||||
final class DoobieTagDb(
|
||||
sqlStates: CuratedSqlStates
|
||||
sqlStates: CuratedSqlStates,
|
||||
implTypes: ImplTypes
|
||||
) extends TagDb[ConnectionIO]:
|
||||
|
||||
import DoobieTagDb.Sql
|
||||
private val Sql = new DoobieTagDb.Sql(implTypes)
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
|
|
@ -46,7 +46,8 @@ final class DoobieTagDb(
|
|||
|
||||
object DoobieTagDb:
|
||||
|
||||
private object Sql:
|
||||
private class Sql(implTypes: ImplTypes):
|
||||
import implTypes.given
|
||||
|
||||
def createTag(
|
||||
tag: TagValue,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,80 @@ package gs.smolban.db.doobie
|
|||
|
||||
import DoobieTypes.ErrorMessages
|
||||
import doobie.*
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import gs.smolban.auth.Argon2Hash
|
||||
import gs.smolban.auth.CredentialId
|
||||
import gs.smolban.auth.CredentialStatus
|
||||
import gs.smolban.auth.CredentialType
|
||||
import gs.smolban.model.account.AccountId
|
||||
import gs.smolban.model.account.AccountName
|
||||
import gs.smolban.model.account.AccountStatus
|
||||
import gs.smolban.model.account.AccountType
|
||||
import gs.smolban.model.account.Permission
|
||||
import gs.smolban.model.group.GroupName
|
||||
import gs.smolban.model.metadata.TagValue
|
||||
import gs.smolban.model.ticket.CommentId
|
||||
import gs.smolban.model.ticket.TicketStatus
|
||||
import gs.uuid.v0.UUID
|
||||
import gs.uuid.v0.UUIDFormat
|
||||
import java.time.Instant
|
||||
|
||||
trait DoobieTypes:
|
||||
|
||||
implicit val accountIdGet: Get[AccountId] =
|
||||
Get[Array[Byte]].tmap(bytes => AccountId(UUID(UUIDFormat.fromBytes(bytes))))
|
||||
|
||||
implicit val accountIdPut: Put[AccountId] =
|
||||
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
||||
|
||||
implicit val accountNameGet: Get[AccountName] =
|
||||
Get[String].temap { value =>
|
||||
AccountName.validate(value) match
|
||||
case None => Left(ErrorMessages.invalidAccountName(value))
|
||||
case Some(s) => Right(s)
|
||||
}
|
||||
|
||||
implicit val accountNamePut: Put[AccountName] =
|
||||
Put[String].tcontramap(_.toString())
|
||||
|
||||
implicit val accountStatusGet: Get[AccountStatus] =
|
||||
Get[String].temap(dbValue =>
|
||||
AccountStatus.parse(dbValue) match
|
||||
case None => Left(ErrorMessages.invalidAccountStatus(dbValue))
|
||||
case Some(status) => Right(status)
|
||||
)
|
||||
|
||||
implicit val accountStatusPut: Put[AccountStatus] =
|
||||
Put[String].tcontramap(_.name)
|
||||
|
||||
implicit val accountTypeGet: Get[AccountType] =
|
||||
Get[String].temap(dbValue =>
|
||||
AccountType.parse(dbValue) match
|
||||
case None => Left(ErrorMessages.invalidAccountType(dbValue))
|
||||
case Some(status) => Right(status)
|
||||
)
|
||||
|
||||
implicit val accountTypePut: Put[AccountType] =
|
||||
Put[String].tcontramap(_.name)
|
||||
|
||||
implicit val permissionGet: Get[Permission] =
|
||||
Get[String].temap(dbValue =>
|
||||
Permission.parse(dbValue) match
|
||||
case None => Left(ErrorMessages.invalidPermission(dbValue))
|
||||
case Some(status) => Right(status)
|
||||
)
|
||||
|
||||
implicit val permissionPut: Put[Permission] =
|
||||
Put[String].tcontramap(_.name)
|
||||
|
||||
implicit val groupNameGet: Get[GroupName] =
|
||||
Get[String].temap { value =>
|
||||
GroupName.validate(value) match
|
||||
case None => Left(ErrorMessages.invalidGroupName(value))
|
||||
case Some(s) => Right(s)
|
||||
}
|
||||
|
||||
implicit val groupNamePut: Put[GroupName] =
|
||||
Put[String].tcontramap(_.toString())
|
||||
|
||||
implicit val ticketStatusGet: Get[TicketStatus] =
|
||||
Get[String].temap(dbValue =>
|
||||
TicketStatus.parse(dbValue) match
|
||||
|
|
@ -28,18 +92,6 @@ trait DoobieTypes:
|
|||
implicit val commentIdPut: Put[CommentId] =
|
||||
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
||||
|
||||
implicit val createdAtGet: Get[CreatedAt] =
|
||||
Get[Long].tmap(CreatedAt.fromMilliseconds)
|
||||
|
||||
implicit val createdAtPut: Put[CreatedAt] =
|
||||
Put[Long].tcontramap(_.toMilliseconds())
|
||||
|
||||
implicit val instantGet: Get[Instant] =
|
||||
Get[Long].tmap(Instant.ofEpochMilli)
|
||||
|
||||
implicit val instantPut: Put[Instant] =
|
||||
Put[Long].tcontramap(_.toEpochMilli())
|
||||
|
||||
implicit val tagValueGet: Get[TagValue] =
|
||||
Get[String].temap { value =>
|
||||
TagValue.validate(value) match
|
||||
|
|
@ -50,16 +102,78 @@ trait DoobieTypes:
|
|||
implicit val tagValuePut: Put[TagValue] =
|
||||
Put[String].tcontramap(_.toString())
|
||||
|
||||
implicit val credentialIdGet: Get[CredentialId] =
|
||||
Get[Array[Byte]].tmap(bytes =>
|
||||
CredentialId(UUID(UUIDFormat.fromBytes(bytes)))
|
||||
)
|
||||
|
||||
implicit val credentialIdPut: Put[CredentialId] =
|
||||
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
||||
|
||||
implicit val argon2HashPut: Put[Argon2Hash] =
|
||||
Put[String].tcontramap(_.encode())
|
||||
|
||||
implicit val argon2HashGet: Get[Argon2Hash] =
|
||||
Get[String].temap(dbValue =>
|
||||
Argon2Hash.decode(dbValue) match
|
||||
case None => Left(ErrorMessages.invalidAccountStatus(dbValue))
|
||||
case Some(hash) => Right(hash)
|
||||
)
|
||||
|
||||
implicit val credentialTypeGet: Get[CredentialType] =
|
||||
Get[String].temap(dbValue =>
|
||||
CredentialType.parse(dbValue) match
|
||||
case None => Left(ErrorMessages.invalidCredentialType(dbValue))
|
||||
case Some(status) => Right(status)
|
||||
)
|
||||
|
||||
implicit val credentialTypePut: Put[CredentialType] =
|
||||
Put[String].tcontramap(_.name)
|
||||
|
||||
implicit val credentialStatusGet: Get[CredentialStatus] =
|
||||
Get[String].temap(dbValue =>
|
||||
CredentialStatus.parse(dbValue) match
|
||||
case None => Left(ErrorMessages.invalidCredentialStatus(dbValue))
|
||||
case Some(status) => Right(status)
|
||||
)
|
||||
|
||||
implicit val credentialStatusPut: Put[CredentialStatus] =
|
||||
Put[String].tcontramap(_.name)
|
||||
|
||||
object DoobieTypes extends DoobieTypes:
|
||||
|
||||
object ErrorMessages:
|
||||
|
||||
def invalidAccountName(candidate: String): String =
|
||||
s"'$candidate' is not a valid account name."
|
||||
|
||||
def invalidAccountStatus(candidate: String): String =
|
||||
s"'$candidate' is not a valid account status."
|
||||
|
||||
def invalidAccountType(candidate: String): String =
|
||||
s"'$candidate' is not a valid account type."
|
||||
|
||||
def invalidPermission(candidate: String): String =
|
||||
s"'$candidate' is not a valid permission."
|
||||
|
||||
def invalidGroupName(candidate: String): String =
|
||||
s"'$candidate' is not a valid group name."
|
||||
|
||||
def invalidTagValue(candidate: String): String =
|
||||
s"'$candidate' is not a valid tag value."
|
||||
|
||||
def invalidTicketStatus(candidate: String): String =
|
||||
s"'$candidate' is not a valid ticket status."
|
||||
|
||||
def invalidArgon2Hash(candidate: String): String =
|
||||
s"'$candidate' is not a valid Argon2 hash."
|
||||
|
||||
def invalidCredentialType(candidate: String): String =
|
||||
s"'$candidate' is not a valid credential type."
|
||||
|
||||
def invalidCredentialStatus(candidate: String): String =
|
||||
s"'$candidate' is not a valid credential status."
|
||||
|
||||
end ErrorMessages
|
||||
|
||||
end DoobieTypes
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package gs.smolban.db.doobie
|
||||
|
||||
import doobie.*
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
/** Abstraction for supporting implementation-specific types.
|
||||
*/
|
||||
trait ImplTypes:
|
||||
given createdAtGet: Get[CreatedAt]
|
||||
given createdAtPut: Put[CreatedAt]
|
||||
given instantGet: Get[Instant]
|
||||
given instantPut: Put[Instant]
|
||||
given localDateGet: Get[LocalDate]
|
||||
given localDatePut: Put[LocalDate]
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package gs.smolban.db.doobie
|
||||
|
||||
import doobie.*
|
||||
import gs.smolban.model.metadata.CreatedAt
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
/** SQLite implementation of specific types.
|
||||
*/
|
||||
final class SqliteImplTypes extends ImplTypes:
|
||||
|
||||
given createdAtGet: Get[CreatedAt] =
|
||||
Get[Long].tmap(CreatedAt.fromMilliseconds)
|
||||
|
||||
given createdAtPut: Put[CreatedAt] =
|
||||
Put[Long].tcontramap(_.toMilliseconds())
|
||||
|
||||
given instantGet: Get[Instant] =
|
||||
Get[Long].tmap(Instant.ofEpochMilli)
|
||||
|
||||
given instantPut: Put[Instant] =
|
||||
Put[Long].tcontramap(_.toEpochMilli())
|
||||
|
||||
given localDateGet: Get[LocalDate] =
|
||||
Get[String].tmap(LocalDate.parse)
|
||||
|
||||
given localDatePut: Put[LocalDate] =
|
||||
Put[String].tcontramap(_.toString())
|
||||
|
|
@ -29,7 +29,8 @@ class DoobieTagDbTests extends munit.FunSuite:
|
|||
test(name)(f.unsafeRunSync())
|
||||
|
||||
private val tagDb: TagDb[ConnectionIO] = new DoobieTagDb(
|
||||
CuratedSqlStates.sqlite
|
||||
CuratedSqlStates.sqlite,
|
||||
new SqliteImplTypes
|
||||
)
|
||||
|
||||
private val clock = Clock.systemDefaultZone()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue