mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-03-31 05:15:08 +00:00
Add a new column to distinguish local from external users
This commit is contained in:
parent
b73c252762
commit
aef56233a5
@ -254,6 +254,12 @@ val openapiScalaSettings = Seq(
|
||||
field =>
|
||||
field
|
||||
.copy(typeDef = TypeDef("LenientUri", Imports("docspell.common.LenientUri")))
|
||||
case "accountsource" =>
|
||||
field =>
|
||||
field
|
||||
.copy(typeDef =
|
||||
TypeDef("AccountSource", Imports("docspell.common.AccountSource"))
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -194,7 +194,7 @@ object Login {
|
||||
logF.info(s"Account lookup via remember me: $data")
|
||||
)
|
||||
res <- OptionT.liftF(
|
||||
if (checkNoPassword(data))
|
||||
if (checkNoPassword(data, AccountSource.all.toList.toSet))
|
||||
logF.info("RememberMe auth successful") *> okResult(data.account)
|
||||
else
|
||||
logF.warn("RememberMe auth not successful") *> Result.invalidAuth.pure[F]
|
||||
@ -260,13 +260,17 @@ object Login {
|
||||
|
||||
private def check(given: String)(data: QLogin.Data): Boolean = {
|
||||
val passOk = BCrypt.checkpw(given, data.password.pass)
|
||||
checkNoPassword(data) && passOk
|
||||
checkNoPassword(data, Set(AccountSource.Local)) && passOk
|
||||
}
|
||||
|
||||
private def checkNoPassword(data: QLogin.Data): Boolean = {
|
||||
def checkNoPassword(
|
||||
data: QLogin.Data,
|
||||
expectedSources: Set[AccountSource]
|
||||
): Boolean = {
|
||||
val collOk = data.collectiveState == CollectiveState.Active ||
|
||||
data.collectiveState == CollectiveState.ReadOnly
|
||||
val userOk = data.userState == UserState.Active
|
||||
val userOk =
|
||||
data.userState == UserState.Active && expectedSources.contains(data.source)
|
||||
collOk && userOk
|
||||
}
|
||||
})
|
||||
|
@ -9,7 +9,6 @@ package docspell.backend.ops
|
||||
import cats.effect.{Async, Resource}
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.backend.JobFactory
|
||||
import docspell.backend.PasswordCrypt
|
||||
import docspell.backend.ops.OCollective._
|
||||
@ -20,7 +19,6 @@ import docspell.store.queue.JobQueue
|
||||
import docspell.store.records._
|
||||
import docspell.store.usertask.{UserTask, UserTaskScope, UserTaskStore}
|
||||
import docspell.store.{AddResult, Store}
|
||||
|
||||
import com.github.eikek.calev._
|
||||
|
||||
trait OCollective[F[_]] {
|
||||
@ -95,9 +93,11 @@ object OCollective {
|
||||
object PassResetResult {
|
||||
case class Success(newPw: Password) extends PassResetResult
|
||||
case object NotFound extends PassResetResult
|
||||
case object UserNotLocal extends PassResetResult
|
||||
|
||||
def success(np: Password): PassResetResult = Success(np)
|
||||
def notFound: PassResetResult = NotFound
|
||||
def userNotLocal: PassResetResult = UserNotLocal
|
||||
}
|
||||
|
||||
sealed trait PassChangeResult
|
||||
@ -105,12 +105,14 @@ object OCollective {
|
||||
case object UserNotFound extends PassChangeResult
|
||||
case object PasswordMismatch extends PassChangeResult
|
||||
case object UpdateFailed extends PassChangeResult
|
||||
case object UserNotLocal extends PassChangeResult
|
||||
case object Success extends PassChangeResult
|
||||
|
||||
def userNotFound: PassChangeResult = UserNotFound
|
||||
def passwordMismatch: PassChangeResult = PasswordMismatch
|
||||
def success: PassChangeResult = Success
|
||||
def updateFailed: PassChangeResult = UpdateFailed
|
||||
def userNotLocal: PassChangeResult = UserNotLocal
|
||||
}
|
||||
|
||||
case class RegisterData(
|
||||
@ -245,11 +247,14 @@ object OCollective {
|
||||
def resetPassword(accountId: AccountId): F[PassResetResult] =
|
||||
for {
|
||||
newPass <- Password.generate[F]
|
||||
optUser <- store.transact(RUser.findByAccount(accountId))
|
||||
n <- store.transact(
|
||||
RUser.updatePassword(accountId, PasswordCrypt.crypt(newPass))
|
||||
)
|
||||
res =
|
||||
if (n <= 0) PassResetResult.notFound
|
||||
if (optUser.exists(_.source != AccountSource.Local))
|
||||
PassResetResult.userNotLocal
|
||||
else if (n <= 0) PassResetResult.notFound
|
||||
else PassResetResult.success(newPass)
|
||||
} yield res
|
||||
|
||||
@ -270,6 +275,8 @@ object OCollective {
|
||||
res = check match {
|
||||
case Some(true) =>
|
||||
if (n.getOrElse(0) > 0) PassChangeResult.success
|
||||
else if (optUser.exists(_.source != AccountSource.Local))
|
||||
PassChangeResult.userNotLocal
|
||||
else PassChangeResult.updateFailed
|
||||
case Some(false) =>
|
||||
PassChangeResult.passwordMismatch
|
||||
|
@ -109,6 +109,7 @@ object OSignup {
|
||||
data.collName,
|
||||
PasswordCrypt.crypt(data.password),
|
||||
UserState.Active,
|
||||
AccountSource.Local,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
|
@ -0,0 +1,35 @@
|
||||
package docspell.common
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import io.circe.{Decoder, Encoder}
|
||||
|
||||
sealed trait AccountSource { self: Product =>
|
||||
|
||||
def name: String =
|
||||
self.productPrefix.toLowerCase
|
||||
}
|
||||
|
||||
object AccountSource {
|
||||
|
||||
case object Local extends AccountSource
|
||||
case object OpenId extends AccountSource
|
||||
|
||||
val all: NonEmptyList[AccountSource] =
|
||||
NonEmptyList.of(Local, OpenId)
|
||||
|
||||
def fromString(str: String): Either[String, AccountSource] =
|
||||
str.toLowerCase match {
|
||||
case "local" => Right(Local)
|
||||
case "openid" => Right(OpenId)
|
||||
case _ => Left(s"Invalid account source: $str")
|
||||
}
|
||||
|
||||
def unsafeFromString(str: String): AccountSource =
|
||||
fromString(str).fold(sys.error, identity)
|
||||
|
||||
implicit val jsonDecoder: Decoder[AccountSource] =
|
||||
Decoder.decodeString.emap(fromString)
|
||||
|
||||
implicit val jsonEncoder: Encoder[AccountSource] =
|
||||
Encoder.encodeString.contramap(_.name)
|
||||
}
|
@ -5405,6 +5405,7 @@ components:
|
||||
- id
|
||||
- login
|
||||
- state
|
||||
- source
|
||||
- loginCount
|
||||
- created
|
||||
properties:
|
||||
@ -5420,6 +5421,12 @@ components:
|
||||
enum:
|
||||
- active
|
||||
- disabled
|
||||
source:
|
||||
type: string
|
||||
format: accountsource
|
||||
enum:
|
||||
- local
|
||||
- openid
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
|
@ -522,6 +522,7 @@ trait Conversions {
|
||||
ru.uid,
|
||||
ru.login,
|
||||
ru.state,
|
||||
ru.source,
|
||||
None,
|
||||
ru.email,
|
||||
ru.lastLogin,
|
||||
@ -537,6 +538,7 @@ trait Conversions {
|
||||
cid,
|
||||
u.password.getOrElse(Password.empty),
|
||||
u.state,
|
||||
u.source,
|
||||
u.email,
|
||||
0,
|
||||
None,
|
||||
@ -551,6 +553,7 @@ trait Conversions {
|
||||
cid,
|
||||
u.password.getOrElse(Password.empty),
|
||||
u.state,
|
||||
u.source,
|
||||
u.email,
|
||||
u.loginCount,
|
||||
u.lastLogin,
|
||||
@ -706,6 +709,8 @@ trait Conversions {
|
||||
case PassChangeResult.PasswordMismatch =>
|
||||
BasicResult(false, "The current password is incorrect.")
|
||||
case PassChangeResult.UserNotFound => BasicResult(false, "User not found.")
|
||||
case PassChangeResult.UserNotLocal =>
|
||||
BasicResult(false, "User is not local, passwords are managed externally.")
|
||||
}
|
||||
|
||||
def basicResult(e: Either[Throwable, _], successMsg: String): BasicResult =
|
||||
|
@ -86,6 +86,12 @@ object UserRoutes {
|
||||
Password(""),
|
||||
"Password update failed. User not found."
|
||||
)
|
||||
case OCollective.PassResetResult.UserNotLocal =>
|
||||
ResetPasswordResult(
|
||||
false,
|
||||
Password(""),
|
||||
"Password update failed. User is not local, passwords are managed externally."
|
||||
)
|
||||
})
|
||||
} yield resp
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
ALTER TABLE "user_"
|
||||
ADD COLUMN "account_source" varchar(254);
|
||||
|
||||
UPDATE "user_"
|
||||
SET "account_source" = 'local';
|
||||
|
||||
ALTER TABLE "user_"
|
||||
ALTER COLUMN "account_source" SET NOT NULL;
|
@ -0,0 +1,8 @@
|
||||
ALTER TABLE `user_`
|
||||
ADD COLUMN (`account_source` varchar(254));
|
||||
|
||||
UPDATE `user_`
|
||||
SET `account_source` = 'local';
|
||||
|
||||
ALTER TABLE `user_`
|
||||
MODIFY `account_source` varchar(254) NOT NULL;
|
@ -0,0 +1,8 @@
|
||||
ALTER TABLE "user_"
|
||||
ADD COLUMN "account_source" varchar(254);
|
||||
|
||||
UPDATE "user_"
|
||||
SET "account_source" = 'local';
|
||||
|
||||
ALTER TABLE "user_"
|
||||
ALTER COLUMN "account_source" SET NOT NULL;
|
@ -35,6 +35,9 @@ trait DoobieMeta extends EmilDoobieMeta {
|
||||
e.apply(a).noSpaces
|
||||
)
|
||||
|
||||
implicit val metaAccountSource: Meta[AccountSource] =
|
||||
Meta[String].imap(AccountSource.unsafeFromString)(_.name)
|
||||
|
||||
implicit val metaDuration: Meta[Duration] =
|
||||
Meta[Long].imap(Duration.millis)(_.millis)
|
||||
|
||||
|
@ -24,7 +24,8 @@ object QLogin {
|
||||
account: AccountId,
|
||||
password: Password,
|
||||
collectiveState: CollectiveState,
|
||||
userState: UserState
|
||||
userState: UserState,
|
||||
source: AccountSource
|
||||
)
|
||||
|
||||
def findUser(acc: AccountId): ConnectionIO[Option[Data]] = {
|
||||
@ -32,7 +33,7 @@ object QLogin {
|
||||
val coll = RCollective.as("c")
|
||||
val sql =
|
||||
Select(
|
||||
select(user.cid, user.login, user.password, coll.state, user.state),
|
||||
select(user.cid, user.login, user.password, coll.state, user.state, user.source),
|
||||
from(user).innerJoin(coll, user.cid === coll.id),
|
||||
user.login === acc.user && user.cid === acc.collective
|
||||
).build
|
||||
|
@ -21,6 +21,7 @@ case class RUser(
|
||||
cid: Ident,
|
||||
password: Password,
|
||||
state: UserState,
|
||||
source: AccountSource,
|
||||
email: Option[String],
|
||||
loginCount: Int,
|
||||
lastLogin: Option[Timestamp],
|
||||
@ -36,6 +37,7 @@ object RUser {
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val password = Column[Password]("password", this)
|
||||
val state = Column[UserState]("state", this)
|
||||
val source = Column[AccountSource]("account_source", this)
|
||||
val email = Column[String]("email", this)
|
||||
val loginCount = Column[Int]("logincount", this)
|
||||
val lastLogin = Column[Timestamp]("lastlogin", this)
|
||||
@ -48,6 +50,7 @@ object RUser {
|
||||
cid,
|
||||
password,
|
||||
state,
|
||||
source,
|
||||
email,
|
||||
loginCount,
|
||||
lastLogin,
|
||||
@ -65,7 +68,7 @@ object RUser {
|
||||
DML.insert(
|
||||
t,
|
||||
t.all,
|
||||
fr"${v.uid},${v.login},${v.cid},${v.password},${v.state},${v.email},${v.loginCount},${v.lastLogin},${v.created}"
|
||||
fr"${v.uid},${v.login},${v.cid},${v.password},${v.state},${v.source},${v.email},${v.loginCount},${v.lastLogin},${v.created}"
|
||||
)
|
||||
}
|
||||
|
||||
@ -134,7 +137,7 @@ object RUser {
|
||||
val t = Table(None)
|
||||
DML.update(
|
||||
t,
|
||||
t.cid === accountId.collective && t.login === accountId.user,
|
||||
t.cid === accountId.collective && t.login === accountId.user && t.source === AccountSource.Local,
|
||||
DML.set(t.password.setTo(hashedPass))
|
||||
)
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ view2 texts model =
|
||||
[ th [ class "w-px whitespace-nowrap" ] []
|
||||
, th [ class "text-left" ] [ text texts.login ]
|
||||
, th [ class "text-center" ] [ text texts.state ]
|
||||
, th [ class "text-center" ] [ text texts.source ]
|
||||
, th [ class "hidden md:table-cell text-left" ] [ text texts.email ]
|
||||
, th [ class "hidden md:table-cell text-center" ] [ text texts.logins ]
|
||||
, th [ class "hidden sm:table-cell text-center" ] [ text texts.lastLogin ]
|
||||
@ -92,6 +93,9 @@ renderUserLine2 texts model user =
|
||||
, td [ class "text-center" ]
|
||||
[ text user.state
|
||||
]
|
||||
, td [ class "text-center" ]
|
||||
[ text user.source
|
||||
]
|
||||
, td [ class "hidden md:table-cell text-left" ]
|
||||
[ Maybe.withDefault "" user.email |> text
|
||||
]
|
||||
|
@ -20,6 +20,7 @@ type alias Texts =
|
||||
{ basics : Messages.Basics.Texts
|
||||
, login : String
|
||||
, state : String
|
||||
, source : String
|
||||
, email : String
|
||||
, logins : String
|
||||
, lastLogin : String
|
||||
@ -32,6 +33,7 @@ gb =
|
||||
{ basics = Messages.Basics.gb
|
||||
, login = "Login"
|
||||
, state = "State"
|
||||
, source = "Type"
|
||||
, email = "E-Mail"
|
||||
, logins = "Logins"
|
||||
, lastLogin = "Last Login"
|
||||
@ -44,6 +46,7 @@ de =
|
||||
{ basics = Messages.Basics.de
|
||||
, login = "Benutzername"
|
||||
, state = "Status"
|
||||
, source = "Typ"
|
||||
, email = "E-Mail"
|
||||
, logins = "Anmeldungen"
|
||||
, lastLogin = "Letzte Anmeldung"
|
||||
|
Loading…
x
Reference in New Issue
Block a user