mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Adopt store module to new collective table
This commit is contained in:
@ -18,12 +18,13 @@ import scodec.bits.ByteVector
|
||||
|
||||
case class AuthToken(
|
||||
nowMillis: Long,
|
||||
account: AccountId,
|
||||
account: AccountInfo,
|
||||
requireSecondFactor: Boolean,
|
||||
valid: Option[Duration],
|
||||
salt: String,
|
||||
sig: String
|
||||
) {
|
||||
|
||||
def asString =
|
||||
valid match {
|
||||
case Some(v) =>
|
||||
@ -63,7 +64,7 @@ object AuthToken {
|
||||
for {
|
||||
millis <- TokenUtil.asInt(ms).toRight("Cannot read authenticator data")
|
||||
acc <- TokenUtil.b64dec(as).toRight("Cannot read authenticator data")
|
||||
accId <- AccountId.parse(acc)
|
||||
accId <- AccountInfo.parse(acc)
|
||||
twofac <- Right[String, Boolean](java.lang.Boolean.parseBoolean(fa))
|
||||
valid <- TokenUtil
|
||||
.asInt(vs)
|
||||
@ -75,7 +76,7 @@ object AuthToken {
|
||||
for {
|
||||
millis <- TokenUtil.asInt(ms).toRight("Cannot read authenticator data")
|
||||
acc <- TokenUtil.b64dec(as).toRight("Cannot read authenticator data")
|
||||
accId <- AccountId.parse(acc)
|
||||
accId <- AccountInfo.parse(acc)
|
||||
twofac <- Right[String, Boolean](java.lang.Boolean.parseBoolean(fa))
|
||||
} yield AuthToken(millis, accId, twofac, None, salt, sig)
|
||||
|
||||
@ -84,7 +85,7 @@ object AuthToken {
|
||||
}
|
||||
|
||||
def user[F[_]: Sync](
|
||||
accountId: AccountId,
|
||||
accountId: AccountInfo,
|
||||
requireSecondFactor: Boolean,
|
||||
key: ByteVector,
|
||||
valid: Option[Duration]
|
||||
|
@ -96,10 +96,12 @@ object Login {
|
||||
for {
|
||||
data <- store.transact(QLogin.findUser(accountId))
|
||||
_ <- logF.trace(s"Account lookup: $data")
|
||||
res <-
|
||||
if (data.exists(checkNoPassword(_, Set(AccountSource.OpenId))))
|
||||
doLogin(config, accountId, false)
|
||||
else Result.invalidAuth.pure[F]
|
||||
res <- data match {
|
||||
case Some(d) if checkNoPassword(d, Set(AccountSource.OpenId)) =>
|
||||
doLogin(config, d.account, false)
|
||||
case _ =>
|
||||
Result.invalidAuth.pure[F]
|
||||
}
|
||||
} yield res
|
||||
|
||||
def loginSession(config: Config)(sessionKey: String): F[Result] =
|
||||
@ -122,9 +124,12 @@ object Login {
|
||||
for {
|
||||
data <- store.transact(QLogin.findUser(acc))
|
||||
_ <- logF.trace(s"Account lookup: $data")
|
||||
res <-
|
||||
if (data.exists(check(up.pass))) doLogin(config, acc, up.rememberMe)
|
||||
else Result.invalidAuth.pure[F]
|
||||
res <- data match {
|
||||
case Some(d) if check(up.pass)(d) =>
|
||||
doLogin(config, d.account, up.rememberMe)
|
||||
case _ =>
|
||||
Result.invalidAuth.pure[F]
|
||||
}
|
||||
} yield res
|
||||
case Left(_) =>
|
||||
logF.info(s"User authentication failed for: ${up.hidePass}") *>
|
||||
@ -162,7 +167,7 @@ object Login {
|
||||
(for {
|
||||
_ <- validateToken
|
||||
key <- EitherT.fromOptionF(
|
||||
store.transact(RTotp.findEnabledByLogin(sf.token.account, true)),
|
||||
store.transact(RTotp.findEnabledByLogin(sf.token.account.userId, true)),
|
||||
Result.invalidAuth
|
||||
)
|
||||
now <- EitherT.right[Result](Timestamp.current[F])
|
||||
@ -175,13 +180,13 @@ object Login {
|
||||
}
|
||||
|
||||
def loginRememberMe(config: Config)(token: String): F[Result] = {
|
||||
def okResult(acc: AccountId) =
|
||||
def okResult(acc: AccountInfo) =
|
||||
for {
|
||||
_ <- store.transact(RUser.updateLogin(acc))
|
||||
token <- AuthToken.user(acc, false, config.serverSecret, None)
|
||||
} yield Result.ok(token, None)
|
||||
|
||||
def doLogin(rid: Ident) =
|
||||
def rememberedLogin(rid: Ident) =
|
||||
(for {
|
||||
now <- OptionT.liftF(Timestamp.current[F])
|
||||
minTime = now - config.rememberMe.valid
|
||||
@ -214,7 +219,7 @@ object Login {
|
||||
else if (rt.isExpired(config.rememberMe.valid))
|
||||
logF.info(s"RememberMe cookie expired ($rt).") *> Result.invalidTime
|
||||
.pure[F]
|
||||
else doLogin(rt.rememberId)
|
||||
else rememberedLogin(rt.rememberId)
|
||||
case Left(err) =>
|
||||
logF.info(s"RememberMe cookie was invalid: $err") *> Result.invalidAuth
|
||||
.pure[F]
|
||||
@ -245,11 +250,11 @@ object Login {
|
||||
|
||||
private def doLogin(
|
||||
config: Config,
|
||||
acc: AccountId,
|
||||
acc: AccountInfo,
|
||||
rememberMe: Boolean
|
||||
): F[Result] =
|
||||
for {
|
||||
require2FA <- store.transact(RTotp.isEnabled(acc))
|
||||
require2FA <- store.transact(RTotp.isEnabled(acc.userId))
|
||||
_ <-
|
||||
if (require2FA) ().pure[F]
|
||||
else store.transact(RUser.updateLogin(acc))
|
||||
@ -263,13 +268,11 @@ object Login {
|
||||
|
||||
private def insertRememberToken(
|
||||
store: Store[F],
|
||||
acc: AccountId,
|
||||
acc: AccountInfo,
|
||||
config: Config
|
||||
): F[RememberToken] =
|
||||
for {
|
||||
uid <- OptionT(store.transact(RUser.findIdByAccount(acc)))
|
||||
.getOrRaise(new IllegalStateException(s"No user_id found for account: $acc"))
|
||||
rme <- RRememberMe.generate[F](uid)
|
||||
rme <- RRememberMe.generate[F](acc.userId)
|
||||
_ <- store.transact(RRememberMe.insert(rme))
|
||||
token <- RememberToken.user(rme.id, config.serverSecret)
|
||||
} yield token
|
||||
|
@ -6,7 +6,6 @@
|
||||
|
||||
package docspell.backend.ops
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
@ -19,19 +18,19 @@ import docspell.store.records._
|
||||
|
||||
trait OQueryBookmarks[F[_]] {
|
||||
|
||||
def getAll(account: AccountId): F[Vector[OQueryBookmarks.Bookmark]]
|
||||
def getAll(account: AccountInfo): F[Vector[OQueryBookmarks.Bookmark]]
|
||||
|
||||
def findOne(account: AccountId, nameOrId: String): F[Option[OQueryBookmarks.Bookmark]]
|
||||
def findOne(account: AccountInfo, nameOrId: String): F[Option[OQueryBookmarks.Bookmark]]
|
||||
|
||||
def create(account: AccountId, bookmark: OQueryBookmarks.NewBookmark): F[AddResult]
|
||||
def create(account: AccountInfo, bookmark: OQueryBookmarks.NewBookmark): F[AddResult]
|
||||
|
||||
def update(
|
||||
account: AccountId,
|
||||
account: AccountInfo,
|
||||
id: Ident,
|
||||
bookmark: OQueryBookmarks.NewBookmark
|
||||
): F[UpdateResult]
|
||||
|
||||
def delete(account: AccountId, bookmark: Ident): F[Unit]
|
||||
def delete(account: AccountInfo, bookmark: Ident): F[Unit]
|
||||
}
|
||||
|
||||
object OQueryBookmarks {
|
||||
@ -53,39 +52,43 @@ object OQueryBookmarks {
|
||||
|
||||
def apply[F[_]: Sync](store: Store[F]): Resource[F, OQueryBookmarks[F]] =
|
||||
Resource.pure(new OQueryBookmarks[F] {
|
||||
def getAll(account: AccountId): F[Vector[Bookmark]] =
|
||||
def getAll(account: AccountInfo): F[Vector[Bookmark]] =
|
||||
store
|
||||
.transact(RQueryBookmark.allForUser(account))
|
||||
.transact(RQueryBookmark.allForUser(account.collectiveId, account.userId))
|
||||
.map(_.map(convert.toModel))
|
||||
|
||||
def findOne(
|
||||
account: AccountId,
|
||||
account: AccountInfo,
|
||||
nameOrId: String
|
||||
): F[Option[OQueryBookmarks.Bookmark]] =
|
||||
store
|
||||
.transact(RQueryBookmark.findByNameOrId(account, nameOrId))
|
||||
.transact(
|
||||
RQueryBookmark.findByNameOrId(account.collectiveId, account.userId, nameOrId)
|
||||
)
|
||||
.map(_.map(convert.toModel))
|
||||
|
||||
def create(account: AccountId, b: NewBookmark): F[AddResult] = {
|
||||
def create(account: AccountInfo, b: NewBookmark): F[AddResult] = {
|
||||
val uid = if (b.personal) account.userId.some else None
|
||||
val record =
|
||||
RQueryBookmark.createNew(account, b.name, b.label, b.query, b.personal)
|
||||
store.transact(RQueryBookmark.insertIfNotExists(account, record))
|
||||
RQueryBookmark.createNew(
|
||||
account.collectiveId,
|
||||
uid,
|
||||
b.name,
|
||||
b.label,
|
||||
b.query
|
||||
)
|
||||
store.transact(
|
||||
RQueryBookmark.insertIfNotExists(account.collectiveId, account.userId, record)
|
||||
)
|
||||
}
|
||||
|
||||
def update(account: AccountId, id: Ident, b: NewBookmark): F[UpdateResult] =
|
||||
def update(acc: AccountInfo, id: Ident, b: NewBookmark): F[UpdateResult] =
|
||||
UpdateResult.fromUpdate(
|
||||
store.transact {
|
||||
(for {
|
||||
userId <- OptionT(RUser.findIdByAccount(account))
|
||||
n <- OptionT.liftF(
|
||||
RQueryBookmark.update(convert.toRecord(account, id, userId, b))
|
||||
)
|
||||
} yield n).getOrElse(0)
|
||||
}
|
||||
store.transact(RQueryBookmark.update(convert.toRecord(acc, id, b)))
|
||||
)
|
||||
|
||||
def delete(account: AccountId, bookmark: Ident): F[Unit] =
|
||||
store.transact(RQueryBookmark.deleteById(account.collective, bookmark)).as(())
|
||||
def delete(account: AccountInfo, bookmark: Ident): F[Unit] =
|
||||
store.transact(RQueryBookmark.deleteById(account.collectiveId, bookmark)).as(())
|
||||
})
|
||||
|
||||
private object convert {
|
||||
@ -94,17 +97,16 @@ object OQueryBookmarks {
|
||||
Bookmark(r.id, r.name, r.label, r.query, r.isPersonal, r.created)
|
||||
|
||||
def toRecord(
|
||||
account: AccountId,
|
||||
account: AccountInfo,
|
||||
id: Ident,
|
||||
userId: Ident,
|
||||
b: NewBookmark
|
||||
): RQueryBookmark =
|
||||
RQueryBookmark(
|
||||
id,
|
||||
b.name,
|
||||
b.label,
|
||||
if (b.personal) userId.some else None,
|
||||
account.collective,
|
||||
if (b.personal) account.userId.some else None,
|
||||
account.collectiveId,
|
||||
b.query,
|
||||
Timestamp.Epoch
|
||||
)
|
||||
|
@ -6,29 +6,29 @@
|
||||
|
||||
package docspell.backend.ops
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.ops.OTotp.{ConfirmResult, InitResult, OtpState}
|
||||
import docspell.common._
|
||||
import docspell.store.records.{RTotp, RUser}
|
||||
import docspell.store.records.RTotp
|
||||
import docspell.store.{AddResult, Store, UpdateResult}
|
||||
import docspell.totp.{Key, OnetimePassword, Totp}
|
||||
|
||||
trait OTotp[F[_]] {
|
||||
|
||||
/** Return whether TOTP is enabled for this account or not. */
|
||||
def state(accountId: AccountId): F[OtpState]
|
||||
def state(accountId: AccountInfo): F[OtpState]
|
||||
|
||||
/** Initializes TOTP by generating a secret and storing it in the database. TOTP is
|
||||
* still disabled, it must be confirmed in order to be active.
|
||||
*/
|
||||
def initialize(accountId: AccountId): F[InitResult]
|
||||
def initialize(accountId: AccountInfo): F[InitResult]
|
||||
|
||||
/** Confirms and finishes initialization. TOTP is active after this for the given
|
||||
* account.
|
||||
*/
|
||||
def confirmInit(accountId: AccountId, otp: OnetimePassword): F[ConfirmResult]
|
||||
def confirmInit(accountId: AccountInfo, otp: OnetimePassword): F[ConfirmResult]
|
||||
|
||||
/** Disables TOTP and removes the shared secret. If a otp is specified, it must be
|
||||
* valid.
|
||||
@ -57,7 +57,7 @@ object OTotp {
|
||||
|
||||
sealed trait InitResult
|
||||
object InitResult {
|
||||
final case class Success(accountId: AccountId, key: Key) extends InitResult {
|
||||
final case class Success(accountId: AccountInfo, key: Key) extends InitResult {
|
||||
def authenticatorUrl(issuer: String): LenientUri =
|
||||
LenientUri.unsafe(
|
||||
s"otpauth://totp/$issuer:${accountId.asString}?secret=${key.data.toBase32}&issuer=$issuer"
|
||||
@ -67,7 +67,7 @@ object OTotp {
|
||||
case object NotFound extends InitResult
|
||||
final case class Failed(ex: Throwable) extends InitResult
|
||||
|
||||
def success(accountId: AccountId, key: Key): InitResult =
|
||||
def success(accountId: AccountInfo, key: Key): InitResult =
|
||||
Success(accountId, key)
|
||||
|
||||
def alreadyExists: InitResult = AlreadyExists
|
||||
@ -85,47 +85,41 @@ object OTotp {
|
||||
Resource.pure[F, OTotp[F]](new OTotp[F] {
|
||||
val log = docspell.logging.getLogger[F]
|
||||
|
||||
def initialize(accountId: AccountId): F[InitResult] =
|
||||
def initialize(accountId: AccountInfo): F[InitResult] =
|
||||
for {
|
||||
_ <- log.info(s"Initializing TOTP for account ${accountId.asString}")
|
||||
userId <- store.transact(RUser.findIdByAccount(accountId))
|
||||
result <- userId match {
|
||||
case Some(uid) =>
|
||||
for {
|
||||
record <- RTotp.generate[F](uid, totp.settings.mac)
|
||||
un <- store.transact(RTotp.updateDisabled(record))
|
||||
an <-
|
||||
if (un != 0)
|
||||
AddResult.entityExists("Entity exists, but update was ok").pure[F]
|
||||
else store.add(RTotp.insert(record), RTotp.existsByLogin(accountId))
|
||||
innerResult <-
|
||||
if (un != 0) InitResult.success(accountId, record.secret).pure[F]
|
||||
else
|
||||
an match {
|
||||
case AddResult.EntityExists(msg) =>
|
||||
log.warn(
|
||||
s"A totp record already exists for account '${accountId.asString}': $msg!"
|
||||
) *>
|
||||
InitResult.alreadyExists.pure[F]
|
||||
case AddResult.Failure(ex) =>
|
||||
log.warn(
|
||||
s"Failed to setup totp record for '${accountId.asString}': ${ex.getMessage}"
|
||||
) *>
|
||||
InitResult.failed(ex).pure[F]
|
||||
case AddResult.Success =>
|
||||
InitResult.success(accountId, record.secret).pure[F]
|
||||
}
|
||||
} yield innerResult
|
||||
case None =>
|
||||
log.warn(s"No user found for account: ${accountId.asString}!") *>
|
||||
InitResult.NotFound.pure[F]
|
||||
}
|
||||
result <- for {
|
||||
record <- RTotp.generate[F](accountId.userId, totp.settings.mac)
|
||||
un <- store.transact(RTotp.updateDisabled(record))
|
||||
an <-
|
||||
if (un != 0)
|
||||
AddResult.entityExists("Entity exists, but update was ok").pure[F]
|
||||
else store.add(RTotp.insert(record), RTotp.existsByUserId(accountId.userId))
|
||||
innerResult <-
|
||||
if (un != 0) InitResult.success(accountId, record.secret).pure[F]
|
||||
else
|
||||
an match {
|
||||
case AddResult.EntityExists(msg) =>
|
||||
log.warn(
|
||||
s"A totp record already exists for account '${accountId.asString}': $msg!"
|
||||
) *>
|
||||
InitResult.alreadyExists.pure[F]
|
||||
case AddResult.Failure(ex) =>
|
||||
log.warn(
|
||||
s"Failed to setup totp record for '${accountId.asString}': ${ex.getMessage}"
|
||||
) *>
|
||||
InitResult.failed(ex).pure[F]
|
||||
case AddResult.Success =>
|
||||
InitResult.success(accountId, record.secret).pure[F]
|
||||
}
|
||||
} yield innerResult
|
||||
|
||||
} yield result
|
||||
|
||||
def confirmInit(accountId: AccountId, otp: OnetimePassword): F[ConfirmResult] =
|
||||
def confirmInit(accountId: AccountInfo, otp: OnetimePassword): F[ConfirmResult] =
|
||||
for {
|
||||
_ <- log.info(s"Confirm TOTP setup for account ${accountId.asString}")
|
||||
key <- store.transact(RTotp.findEnabledByLogin(accountId, false))
|
||||
key <- store.transact(RTotp.findEnabledByUserId(accountId.userId, false))
|
||||
now <- Timestamp.current[F]
|
||||
res <- key match {
|
||||
case None =>
|
||||
@ -134,7 +128,7 @@ object OTotp {
|
||||
val check = totp.checkPassword(r.secret, otp, now.value)
|
||||
if (check)
|
||||
store
|
||||
.transact(RTotp.setEnabled(accountId, true))
|
||||
.transact(RTotp.setEnabled(accountId.userId, true))
|
||||
.map(_ => ConfirmResult.Success)
|
||||
else ConfirmResult.Failed.pure[F]
|
||||
}
|
||||
@ -154,7 +148,7 @@ object OTotp {
|
||||
val check = totp.checkPassword(r.secret, pw, now.value)
|
||||
if (check)
|
||||
UpdateResult.fromUpdate(
|
||||
store.transact(RTotp.setEnabled(accountId, false))
|
||||
store.transact(RTotp.setEnabled(r.userId, false))
|
||||
)
|
||||
else
|
||||
log.info(s"TOTP code was invalid. Not disabling it.") *> UpdateResult
|
||||
@ -163,12 +157,17 @@ object OTotp {
|
||||
}
|
||||
} yield res
|
||||
case None =>
|
||||
UpdateResult.fromUpdate(store.transact(RTotp.setEnabled(accountId, false)))
|
||||
UpdateResult.fromUpdate {
|
||||
(for {
|
||||
key <- OptionT(RTotp.findEnabledByLogin(accountId, true))
|
||||
n <- OptionT.liftF(RTotp.setEnabled(key.userId, false))
|
||||
} yield n).mapK(store.transform).getOrElse(0)
|
||||
}
|
||||
}
|
||||
|
||||
def state(accountId: AccountId): F[OtpState] =
|
||||
def state(acc: AccountInfo): F[OtpState] =
|
||||
for {
|
||||
record <- store.transact(RTotp.findEnabledByLogin(accountId, true))
|
||||
record <- store.transact(RTotp.findEnabledByUserId(acc.userId, true))
|
||||
result = record match {
|
||||
case Some(r) =>
|
||||
OtpState.Enabled(r.created)
|
||||
|
Reference in New Issue
Block a user