Initial impl for totp

This commit is contained in:
eikek
2021-08-30 16:15:13 +02:00
parent 2b46cc7970
commit 309a52393a
17 changed files with 568 additions and 20 deletions

View File

@ -0,0 +1,7 @@
CREATE TABLE "totp" (
"user_id" varchar(254) not null primary key,
"enabled" boolean not null,
"secret" varchar(254) not null,
"created" timestamp not null,
FOREIGN KEY ("user_id") REFERENCES "user_"("uid") ON DELETE CASCADE
);

View File

@ -42,6 +42,7 @@ object AddResult {
def withMsg(msg: String): EntityExists =
EntityExists(msg)
}
def entityExists(msg: String): AddResult = EntityExists(msg)
case class Failure(ex: Throwable) extends AddResult {
def toEither = Left(ex)

View File

@ -11,6 +11,7 @@ import java.time.{Instant, LocalDate}
import docspell.common._
import docspell.common.syntax.all._
import docspell.totp.Key
import com.github.eikek.calev.CalEvent
import doobie._
@ -125,6 +126,9 @@ trait DoobieMeta extends EmilDoobieMeta {
implicit val metaJsonString: Meta[Json] =
Meta[String].timap(DoobieMeta.parseJsonUnsafe)(_.noSpaces)
implicit val metaKey: Meta[Key] =
Meta[String].timap(Key.unsafeFromString)(_.asString)
}
object DoobieMeta extends DoobieMeta {

View File

@ -0,0 +1,99 @@
/*
* Copyright 2020 Docspell Contributors
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package docspell.store.records
import cats.data.{NonEmptyList => Nel}
import cats.effect._
import cats.implicits._
import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import docspell.totp.{Key, Mac}
import doobie._
import doobie.implicits._
final case class RTotp(
userId: Ident,
enabled: Boolean,
secret: Key,
created: Timestamp
) {}
object RTotp {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "totp"
val userId = Column[Ident]("user_id", this)
val enabled = Column[Boolean]("enabled", this)
val secret = Column[Key]("secret", this)
val created = Column[Timestamp]("created", this)
val all = Nel.of(userId, enabled, secret, created)
}
val T = Table(None)
def as(alias: String): Table =
Table(Some(alias))
def generate[F[_]: Sync](userId: Ident, mac: Mac): F[RTotp] =
for {
now <- Timestamp.current[F]
key <- Key.generate[F](mac)
} yield RTotp(userId, false, key, now)
def insert(r: RTotp): ConnectionIO[Int] =
DML.insert(T, T.all, sql"${r.userId},${r.enabled},${r.secret},${r.created}")
def updateDisabled(r: RTotp): ConnectionIO[Int] =
DML.update(
T,
T.enabled === false && T.userId === r.userId,
DML.set(
T.secret.setTo(r.secret),
T.created.setTo(r.created)
)
)
def setEnabled(account: AccountId, enabled: Boolean): ConnectionIO[Int] =
for {
userId <- RUser.findIdByAccount(account)
n <- userId match {
case Some(id) =>
DML.update(T, T.userId === id, DML.set(T.enabled.setTo(enabled)))
case None =>
0.pure[ConnectionIO]
}
} yield n
def findEnabledByLogin(
accountId: AccountId,
enabled: Boolean
): ConnectionIO[Option[RTotp]] = {
val t = RTotp.as("t")
val u = RUser.as("u")
Select(
select(t.all),
from(t).innerJoin(u, t.userId === u.uid),
u.login === accountId.user && u.cid === accountId.collective && t.enabled === enabled
).build.query[RTotp].option
}
def existsByLogin(accountId: AccountId): ConnectionIO[Boolean] = {
val t = RTotp.as("t")
val u = RUser.as("u")
Select(
select(count(t.userId)),
from(t).innerJoin(u, t.userId === u.uid),
u.login === accountId.user && u.cid === accountId.collective
).build
.query[Int]
.unique
.map(_ > 0)
}
}

View File

@ -55,6 +55,8 @@ object RUser {
)
}
val T = Table(None)
def as(alias: String): Table =
Table(Some(alias))
@ -105,6 +107,15 @@ object RUser {
sql.query[RUser].to[Vector]
}
def findIdByAccount(accountId: AccountId): ConnectionIO[Option[Ident]] =
run(
select(T.uid),
from(T),
T.login === accountId.user && T.cid === accountId.collective
)
.query[Ident]
.option
def updateLogin(accountId: AccountId): ConnectionIO[Int] = {
val t = Table(None)
def stmt(now: Timestamp) =