mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-04 22:25:58 +00:00
commit
84e16f65f9
@ -15,7 +15,7 @@ import docspell.backend.PasswordCrypt
|
|||||||
import docspell.backend.ops.OCollective._
|
import docspell.backend.ops.OCollective._
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store.UpdateResult
|
import docspell.store.UpdateResult
|
||||||
import docspell.store.queries.QCollective
|
import docspell.store.queries.{QCollective, QUser}
|
||||||
import docspell.store.queue.JobQueue
|
import docspell.store.queue.JobQueue
|
||||||
import docspell.store.records._
|
import docspell.store.records._
|
||||||
import docspell.store.usertask.{UserTask, UserTaskScope, UserTaskStore}
|
import docspell.store.usertask.{UserTask, UserTaskScope, UserTaskStore}
|
||||||
@ -37,7 +37,11 @@ trait OCollective[F[_]] {
|
|||||||
|
|
||||||
def update(s: RUser): F[AddResult]
|
def update(s: RUser): F[AddResult]
|
||||||
|
|
||||||
def deleteUser(login: Ident, collective: Ident): F[AddResult]
|
/** Deletes the user and all its data. */
|
||||||
|
def deleteUser(login: Ident, collective: Ident): F[UpdateResult]
|
||||||
|
|
||||||
|
/** Return an excerpt of what would be deleted, when the user is deleted. */
|
||||||
|
def getDeleteUserData(accountId: AccountId): F[DeleteUserData]
|
||||||
|
|
||||||
def insights(collective: Ident): F[InsightData]
|
def insights(collective: Ident): F[InsightData]
|
||||||
|
|
||||||
@ -91,6 +95,9 @@ object OCollective {
|
|||||||
type EmptyTrash = REmptyTrashSetting.EmptyTrash
|
type EmptyTrash = REmptyTrashSetting.EmptyTrash
|
||||||
val EmptyTrash = REmptyTrashSetting.EmptyTrash
|
val EmptyTrash = REmptyTrashSetting.EmptyTrash
|
||||||
|
|
||||||
|
type DeleteUserData = QUser.UserData
|
||||||
|
val DeleteUserData = QUser.UserData
|
||||||
|
|
||||||
sealed trait PassResetResult
|
sealed trait PassResetResult
|
||||||
object PassResetResult {
|
object PassResetResult {
|
||||||
case class Success(newPw: Password) extends PassResetResult
|
case class Success(newPw: Password) extends PassResetResult
|
||||||
@ -207,16 +214,24 @@ object OCollective {
|
|||||||
store.transact(RUser.findAll(collective, _.login))
|
store.transact(RUser.findAll(collective, _.login))
|
||||||
|
|
||||||
def add(s: RUser): F[AddResult] =
|
def add(s: RUser): F[AddResult] =
|
||||||
store.add(
|
if (s.source != AccountSource.Local)
|
||||||
RUser.insert(s.copy(password = PasswordCrypt.crypt(s.password))),
|
AddResult.failure(new Exception("Only local accounts can be created!")).pure[F]
|
||||||
RUser.exists(s.login)
|
else
|
||||||
)
|
store.add(
|
||||||
|
RUser.insert(s.copy(password = PasswordCrypt.crypt(s.password))),
|
||||||
|
RUser.exists(s.login)
|
||||||
|
)
|
||||||
|
|
||||||
def update(s: RUser): F[AddResult] =
|
def update(s: RUser): F[AddResult] =
|
||||||
store.add(RUser.update(s), RUser.exists(s.login))
|
store.add(RUser.update(s), RUser.exists(s.login))
|
||||||
|
|
||||||
def deleteUser(login: Ident, collective: Ident): F[AddResult] =
|
def getDeleteUserData(accountId: AccountId): F[DeleteUserData] =
|
||||||
store.transact(RUser.delete(login, collective)).attempt.map(AddResult.fromUpdate)
|
store.transact(QUser.getUserData(accountId))
|
||||||
|
|
||||||
|
def deleteUser(login: Ident, collective: Ident): F[UpdateResult] =
|
||||||
|
UpdateResult.fromUpdate(
|
||||||
|
store.transact(QUser.deleteUserAndData(AccountId(collective, login)))
|
||||||
|
)
|
||||||
|
|
||||||
def insights(collective: Ident): F[InsightData] =
|
def insights(collective: Ident): F[InsightData] =
|
||||||
store.transact(QCollective.getInsights(collective))
|
store.transact(QCollective.getInsights(collective))
|
||||||
|
@ -1309,9 +1309,9 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$ref: "#/components/schemas/BasicResult"
|
||||||
/sec/user/{id}:
|
/sec/user/{username}:
|
||||||
delete:
|
delete:
|
||||||
operationId: "sec-user-delete-by-id"
|
operationId: "sec-user-delete-by-username"
|
||||||
tags: [ Collective ]
|
tags: [ Collective ]
|
||||||
summary: Delete a user.
|
summary: Delete a user.
|
||||||
description: |
|
description: |
|
||||||
@ -1319,7 +1319,7 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- authTokenHeader: []
|
- authTokenHeader: []
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/id"
|
- $ref: "#/components/parameters/username"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Ok
|
description: Ok
|
||||||
@ -1327,6 +1327,27 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$ref: "#/components/schemas/BasicResult"
|
||||||
|
/sec/user/{username}/deleteData:
|
||||||
|
get:
|
||||||
|
operationId: "sec-user-delete-data"
|
||||||
|
tags: [ Collective ]
|
||||||
|
summary: Shows some data that would be deleted if the user is deleted
|
||||||
|
description: |
|
||||||
|
Gets some data that would be deleted, when the user with the
|
||||||
|
given username is deleted. The `username` must be part of this
|
||||||
|
collective.
|
||||||
|
security:
|
||||||
|
- authTokenHeader: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/username"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/DeleteUserData"
|
||||||
|
|
||||||
/sec/user/changePassword:
|
/sec/user/changePassword:
|
||||||
post:
|
post:
|
||||||
operationId: "sec-user-change-password"
|
operationId: "sec-user-change-password"
|
||||||
@ -4068,6 +4089,23 @@ paths:
|
|||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
DeleteUserData:
|
||||||
|
description: |
|
||||||
|
An excerpt of data that would be deleted when deleting the
|
||||||
|
associated user.
|
||||||
|
required:
|
||||||
|
- folders
|
||||||
|
- sentMails
|
||||||
|
properties:
|
||||||
|
folders:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: ident
|
||||||
|
sentMails:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
|
||||||
SecondFactor:
|
SecondFactor:
|
||||||
description: |
|
description: |
|
||||||
Provide a second factor for login.
|
Provide a second factor for login.
|
||||||
@ -6206,6 +6244,13 @@ components:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
username:
|
||||||
|
name: username
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The username of a user of this collective
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
itemId:
|
itemId:
|
||||||
name: itemId
|
name: itemId
|
||||||
in: path
|
in: path
|
||||||
|
@ -66,6 +66,14 @@ object UserRoutes {
|
|||||||
ar <- backend.collective.deleteUser(id, user.account.collective)
|
ar <- backend.collective.deleteUser(id, user.account.collective)
|
||||||
resp <- Ok(basicResult(ar, "User deleted."))
|
resp <- Ok(basicResult(ar, "User deleted."))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
|
case GET -> Root / Ident(username) / "deleteData" =>
|
||||||
|
for {
|
||||||
|
data <- backend.collective.getDeleteUserData(
|
||||||
|
AccountId(user.account.collective, username)
|
||||||
|
)
|
||||||
|
resp <- Ok(DeleteUserData(data.ownedFolders.map(_.id), data.sentMails))
|
||||||
|
} yield resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,4 +50,6 @@ object AddResult {
|
|||||||
def fold[A](fa: Success.type => A, fb: EntityExists => A, fc: Failure => A): A =
|
def fold[A](fa: Success.type => A, fb: EntityExists => A, fc: Failure => A): A =
|
||||||
fc(this)
|
fc(this)
|
||||||
}
|
}
|
||||||
|
def failure(ex: Exception): AddResult =
|
||||||
|
Failure(ex)
|
||||||
}
|
}
|
||||||
|
@ -8,19 +8,20 @@ package docspell.store.qb
|
|||||||
|
|
||||||
import cats.data.{NonEmptyList => Nel}
|
import cats.data.{NonEmptyList => Nel}
|
||||||
|
|
||||||
|
import docspell.store.impl.DoobieMeta
|
||||||
import docspell.store.qb.impl._
|
import docspell.store.qb.impl._
|
||||||
|
|
||||||
import doobie._
|
import doobie._
|
||||||
import doobie.implicits._
|
import doobie.implicits._
|
||||||
|
|
||||||
object DML {
|
object DML extends DoobieMeta {
|
||||||
private val comma = fr","
|
private val comma = fr","
|
||||||
|
|
||||||
def delete(table: TableDef, cond: Condition): ConnectionIO[Int] =
|
def delete(table: TableDef, cond: Condition): ConnectionIO[Int] =
|
||||||
deleteFragment(table, cond).update.run
|
deleteFragment(table, cond).update.run
|
||||||
|
|
||||||
def deleteFragment(table: TableDef, cond: Condition): Fragment =
|
def deleteFragment(table: TableDef, cond: Condition): Fragment =
|
||||||
fr"DELETE FROM" ++ FromExprBuilder.buildTable(table) ++ fr"WHERE" ++ ConditionBuilder
|
fr"DELETE FROM" ++ FromExprBuilder.buildTable(table) ++ fr" WHERE" ++ ConditionBuilder
|
||||||
.build(cond)
|
.build(cond)
|
||||||
|
|
||||||
def insert(table: TableDef, cols: Nel[Column[_]], values: Fragment): ConnectionIO[Int] =
|
def insert(table: TableDef, cols: Nel[Column[_]], values: Fragment): ConnectionIO[Int] =
|
||||||
|
131
modules/store/src/main/scala/docspell/store/queries/QUser.scala
Normal file
131
modules/store/src/main/scala/docspell/store/queries/QUser.scala
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Docspell Contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package docspell.store.queries
|
||||||
|
|
||||||
|
import cats.implicits._
|
||||||
|
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.store.qb.DML
|
||||||
|
import docspell.store.qb.DSL._
|
||||||
|
import docspell.store.records._
|
||||||
|
|
||||||
|
import doobie._
|
||||||
|
|
||||||
|
object QUser {
|
||||||
|
private val logger = Logger.log4s[ConnectionIO](org.log4s.getLogger)
|
||||||
|
|
||||||
|
final case class UserData(
|
||||||
|
ownedFolders: List[Ident],
|
||||||
|
sentMails: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
def getUserData(accountId: AccountId): ConnectionIO[UserData] = {
|
||||||
|
val folder = RFolder.as("f")
|
||||||
|
val mail = RSentMail.as("m")
|
||||||
|
val mitem = RSentMailItem.as("mi")
|
||||||
|
val user = RUser.as("u")
|
||||||
|
|
||||||
|
for {
|
||||||
|
uid <- loadUserId(accountId).map(_.getOrElse(Ident.unsafe("")))
|
||||||
|
folders <- run(
|
||||||
|
select(folder.name),
|
||||||
|
from(folder),
|
||||||
|
folder.owner === uid && folder.collective === accountId.collective
|
||||||
|
).query[Ident].to[List]
|
||||||
|
mails <- run(
|
||||||
|
select(count(mail.id)),
|
||||||
|
from(mail)
|
||||||
|
.innerJoin(mitem, mail.id === mitem.sentMailId)
|
||||||
|
.innerJoin(user, user.uid === mail.uid),
|
||||||
|
user.login === accountId.user && user.cid === accountId.collective
|
||||||
|
).query[Int].unique
|
||||||
|
} yield UserData(folders, mails)
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteUserAndData(accountId: AccountId): ConnectionIO[Int] =
|
||||||
|
for {
|
||||||
|
uid <- loadUserId(accountId).map(_.getOrElse(Ident.unsafe("")))
|
||||||
|
_ <- logger.info(s"Remove user ${accountId.asString} (uid=${uid.id})")
|
||||||
|
|
||||||
|
n1 <- deleteUserFolders(uid)
|
||||||
|
|
||||||
|
n2 <- deleteUserSentMails(uid)
|
||||||
|
_ <- logger.info(s"Removed $n2 sent mails")
|
||||||
|
|
||||||
|
n3 <- deleteRememberMe(accountId)
|
||||||
|
_ <- logger.info(s"Removed $n3 remember me tokens")
|
||||||
|
|
||||||
|
n4 <- deleteTotp(uid)
|
||||||
|
_ <- logger.info(s"Removed $n4 totp secrets")
|
||||||
|
|
||||||
|
n5 <- deleteMailSettings(uid)
|
||||||
|
_ <- logger.info(s"Removed $n5 mail settings")
|
||||||
|
|
||||||
|
nu <- RUser.deleteById(uid)
|
||||||
|
} yield nu + n1 + n2 + n3 + n4 + n5
|
||||||
|
|
||||||
|
def deleteUserFolders(uid: Ident): ConnectionIO[Int] = {
|
||||||
|
val folder = RFolder.as("f")
|
||||||
|
val member = RFolderMember.as("fm")
|
||||||
|
for {
|
||||||
|
folders <- run(
|
||||||
|
select(folder.id),
|
||||||
|
from(folder),
|
||||||
|
folder.owner === uid
|
||||||
|
).query[Ident].to[List]
|
||||||
|
_ <- logger.info(s"Removing folders: ${folders.map(_.id)}")
|
||||||
|
|
||||||
|
ri <- folders.traverse(RItem.removeFolder)
|
||||||
|
_ <- logger.info(s"Removed folders from items: $ri")
|
||||||
|
rs <- folders.traverse(RSource.removeFolder)
|
||||||
|
_ <- logger.info(s"Removed folders from sources: $rs")
|
||||||
|
rf <- folders.traverse(RFolderMember.deleteAll)
|
||||||
|
_ <- logger.info(s"Removed folders from members: $rf")
|
||||||
|
|
||||||
|
n1 <- DML.delete(member, member.user === uid)
|
||||||
|
_ <- logger.info(s"Removed $n1 members for owning folders.")
|
||||||
|
n2 <- DML.delete(folder, folder.owner === uid)
|
||||||
|
_ <- logger.info(s"Removed $n2 folders.")
|
||||||
|
|
||||||
|
} yield n1 + n2 + ri.sum + rs.sum + rf.sum
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteUserSentMails(uid: Ident): ConnectionIO[Int] = {
|
||||||
|
val mail = RSentMail.as("m")
|
||||||
|
for {
|
||||||
|
ids <- run(select(mail.id), from(mail), mail.uid === uid).query[Ident].to[List]
|
||||||
|
n1 <- ids.traverse(RSentMailItem.deleteMail)
|
||||||
|
n2 <- ids.traverse(RSentMail.delete)
|
||||||
|
} yield n1.sum + n2.sum
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteRememberMe(id: AccountId): ConnectionIO[Int] =
|
||||||
|
DML.delete(
|
||||||
|
RRememberMe.T,
|
||||||
|
RRememberMe.T.cid === id.collective && RRememberMe.T.username === id.user
|
||||||
|
)
|
||||||
|
|
||||||
|
def deleteTotp(uid: Ident): ConnectionIO[Int] =
|
||||||
|
DML.delete(RTotp.T, RTotp.T.userId === uid)
|
||||||
|
|
||||||
|
def deleteMailSettings(uid: Ident): ConnectionIO[Int] = {
|
||||||
|
val smtp = RUserEmail.as("ms")
|
||||||
|
val imap = RUserImap.as("mi")
|
||||||
|
for {
|
||||||
|
n1 <- DML.delete(smtp, smtp.uid === uid)
|
||||||
|
n2 <- DML.delete(imap, imap.uid === uid)
|
||||||
|
} yield n1 + n2
|
||||||
|
}
|
||||||
|
|
||||||
|
private def loadUserId(id: AccountId): ConnectionIO[Option[Ident]] =
|
||||||
|
run(
|
||||||
|
select(RUser.T.uid),
|
||||||
|
from(RUser.T),
|
||||||
|
RUser.T.cid === id.collective && RUser.T.login === id.user
|
||||||
|
).query[Ident].option
|
||||||
|
|
||||||
|
}
|
@ -64,4 +64,7 @@ object RFolderMember {
|
|||||||
|
|
||||||
def deleteAll(folderId: Ident): ConnectionIO[Int] =
|
def deleteAll(folderId: Ident): ConnectionIO[Int] =
|
||||||
DML.delete(T, T.folder === folderId)
|
DML.delete(T, T.folder === folderId)
|
||||||
|
|
||||||
|
def deleteMemberships(userId: Ident): ConnectionIO[Int] =
|
||||||
|
DML.delete(T, T.user === userId)
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ object RRememberMe {
|
|||||||
val all = NonEmptyList.of[Column[_]](id, cid, username, created, uses)
|
val all = NonEmptyList.of[Column[_]](id, cid, username, created, uses)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val T = Table(None)
|
val T = Table(None)
|
||||||
def as(alias: String): Table =
|
def as(alias: String): Table =
|
||||||
Table(Some(alias))
|
Table(Some(alias))
|
||||||
|
|
||||||
|
@ -168,4 +168,7 @@ object RUser {
|
|||||||
val t = Table(None)
|
val t = Table(None)
|
||||||
DML.delete(t, t.cid === coll && t.login === user)
|
DML.delete(t, t.cid === coll && t.login === user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def deleteById(uid: Ident): ConnectionIO[Int] =
|
||||||
|
DML.delete(T, T.uid === uid)
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ module Api exposing
|
|||||||
, getCollectiveSettings
|
, getCollectiveSettings
|
||||||
, getContacts
|
, getContacts
|
||||||
, getCustomFields
|
, getCustomFields
|
||||||
|
, getDeleteUserData
|
||||||
, getEquipment
|
, getEquipment
|
||||||
, getEquipments
|
, getEquipments
|
||||||
, getFolderDetail
|
, getFolderDetail
|
||||||
@ -162,6 +163,7 @@ import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
|||||||
import Api.Model.ContactList exposing (ContactList)
|
import Api.Model.ContactList exposing (ContactList)
|
||||||
import Api.Model.CustomFieldList exposing (CustomFieldList)
|
import Api.Model.CustomFieldList exposing (CustomFieldList)
|
||||||
import Api.Model.CustomFieldValue exposing (CustomFieldValue)
|
import Api.Model.CustomFieldValue exposing (CustomFieldValue)
|
||||||
|
import Api.Model.DeleteUserData exposing (DeleteUserData)
|
||||||
import Api.Model.DirectionValue exposing (DirectionValue)
|
import Api.Model.DirectionValue exposing (DirectionValue)
|
||||||
import Api.Model.EmailSettings exposing (EmailSettings)
|
import Api.Model.EmailSettings exposing (EmailSettings)
|
||||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
|
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
|
||||||
@ -1467,6 +1469,15 @@ deleteUser flags user receive =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getDeleteUserData : Flags -> String -> (Result Http.Error DeleteUserData -> msg) -> Cmd msg
|
||||||
|
getDeleteUserData flags username receive =
|
||||||
|
Http2.authGet
|
||||||
|
{ url = flags.config.baseUrl ++ "/api/v1/sec/user/" ++ username ++ "/deleteData"
|
||||||
|
, account = getAccount flags
|
||||||
|
, expect = Http.expectJson receive Api.Model.DeleteUserData.decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--- Job Queue
|
--- Job Queue
|
||||||
|
|
||||||
|
@ -6,7 +6,9 @@
|
|||||||
|
|
||||||
|
|
||||||
module Comp.Basic exposing
|
module Comp.Basic exposing
|
||||||
( editLinkLabel
|
( contentDimmer
|
||||||
|
, deleteButton
|
||||||
|
, editLinkLabel
|
||||||
, editLinkTableCell
|
, editLinkTableCell
|
||||||
, genericButton
|
, genericButton
|
||||||
, horizontalDivider
|
, horizontalDivider
|
||||||
@ -89,6 +91,27 @@ secondaryButton model =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
deleteButton :
|
||||||
|
{ x
|
||||||
|
| label : String
|
||||||
|
, icon : String
|
||||||
|
, disabled : Bool
|
||||||
|
, handler : Attribute msg
|
||||||
|
, attrs : List (Attribute msg)
|
||||||
|
}
|
||||||
|
-> Html msg
|
||||||
|
deleteButton model =
|
||||||
|
genericButton
|
||||||
|
{ label = model.label
|
||||||
|
, icon = model.icon
|
||||||
|
, handler = model.handler
|
||||||
|
, disabled = model.disabled
|
||||||
|
, attrs = model.attrs
|
||||||
|
, baseStyle = S.deleteButtonMain
|
||||||
|
, activeStyle = S.deleteButtonHover
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
secondaryBasicButton :
|
secondaryBasicButton :
|
||||||
{ x
|
{ x
|
||||||
| label : String
|
| label : String
|
||||||
@ -182,18 +205,27 @@ linkLabel model =
|
|||||||
|
|
||||||
loadingDimmer : { label : String, active : Bool } -> Html msg
|
loadingDimmer : { label : String, active : Bool } -> Html msg
|
||||||
loadingDimmer cfg =
|
loadingDimmer cfg =
|
||||||
|
let
|
||||||
|
content =
|
||||||
|
div [ class "text-gray-200" ]
|
||||||
|
[ i [ class "fa fa-circle-notch animate-spin" ] []
|
||||||
|
, span [ class "ml-2" ]
|
||||||
|
[ text cfg.label
|
||||||
|
]
|
||||||
|
]
|
||||||
|
in
|
||||||
|
contentDimmer cfg.active content
|
||||||
|
|
||||||
|
|
||||||
|
contentDimmer : Bool -> Html msg -> Html msg
|
||||||
|
contentDimmer active content =
|
||||||
div
|
div
|
||||||
[ classList
|
[ classList
|
||||||
[ ( "hidden", not cfg.active )
|
[ ( "hidden", not active )
|
||||||
]
|
]
|
||||||
, class S.dimmer
|
, class S.dimmer
|
||||||
]
|
]
|
||||||
[ div [ class "text-gray-200" ]
|
[ content
|
||||||
[ i [ class "fa fa-circle-notch animate-spin" ] []
|
|
||||||
, span [ class "ml-2" ]
|
|
||||||
[ text cfg.label
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,6 +85,7 @@ getUser model =
|
|||||||
, email = model.email
|
, email = model.email
|
||||||
, state = state
|
, state = state
|
||||||
, password = model.password
|
, password = model.password
|
||||||
|
, source = "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,18 +15,18 @@ module Comp.UserManage exposing
|
|||||||
|
|
||||||
import Api
|
import Api
|
||||||
import Api.Model.BasicResult exposing (BasicResult)
|
import Api.Model.BasicResult exposing (BasicResult)
|
||||||
|
import Api.Model.DeleteUserData exposing (DeleteUserData)
|
||||||
import Api.Model.User
|
import Api.Model.User
|
||||||
import Api.Model.UserList exposing (UserList)
|
import Api.Model.UserList exposing (UserList)
|
||||||
import Comp.Basic as B
|
import Comp.Basic as B
|
||||||
import Comp.MenuBar as MB
|
import Comp.MenuBar as MB
|
||||||
import Comp.UserForm
|
import Comp.UserForm
|
||||||
import Comp.UserTable
|
import Comp.UserTable
|
||||||
import Comp.YesNoDimmer
|
|
||||||
import Data.Flags exposing (Flags)
|
import Data.Flags exposing (Flags)
|
||||||
import Data.UiSettings exposing (UiSettings)
|
import Data.UiSettings exposing (UiSettings)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (onSubmit)
|
import Html.Events exposing (onClick, onSubmit)
|
||||||
import Http
|
import Http
|
||||||
import Messages.Comp.UserManage exposing (Texts)
|
import Messages.Comp.UserManage exposing (Texts)
|
||||||
import Styles as S
|
import Styles as S
|
||||||
@ -39,10 +39,16 @@ type alias Model =
|
|||||||
, viewMode : ViewMode
|
, viewMode : ViewMode
|
||||||
, formError : FormError
|
, formError : FormError
|
||||||
, loading : Bool
|
, loading : Bool
|
||||||
, deleteConfirm : Comp.YesNoDimmer.Model
|
, deleteConfirm : DimmerMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type DimmerMode
|
||||||
|
= DimmerOff
|
||||||
|
| DimmerLoading
|
||||||
|
| DimmerUserData DeleteUserData
|
||||||
|
|
||||||
|
|
||||||
type ViewMode
|
type ViewMode
|
||||||
= Table
|
= Table
|
||||||
| Form
|
| Form
|
||||||
@ -53,6 +59,7 @@ type FormError
|
|||||||
| FormErrorSubmit String
|
| FormErrorSubmit String
|
||||||
| FormErrorHttp Http.Error
|
| FormErrorHttp Http.Error
|
||||||
| FormErrorInvalid
|
| FormErrorInvalid
|
||||||
|
| FormErrorCurrentUser
|
||||||
|
|
||||||
|
|
||||||
emptyModel : Model
|
emptyModel : Model
|
||||||
@ -62,7 +69,7 @@ emptyModel =
|
|||||||
, viewMode = Table
|
, viewMode = Table
|
||||||
, formError = FormErrorNone
|
, formError = FormErrorNone
|
||||||
, loading = False
|
, loading = False
|
||||||
, deleteConfirm = Comp.YesNoDimmer.emptyModel
|
, deleteConfirm = DimmerOff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -75,8 +82,10 @@ type Msg
|
|||||||
| InitNewUser
|
| InitNewUser
|
||||||
| Submit
|
| Submit
|
||||||
| SubmitResp (Result Http.Error BasicResult)
|
| SubmitResp (Result Http.Error BasicResult)
|
||||||
| YesNoMsg Comp.YesNoDimmer.Msg
|
|
||||||
| RequestDelete
|
| RequestDelete
|
||||||
|
| GetDeleteDataResp (Result Http.Error DeleteUserData)
|
||||||
|
| DeleteUserNow String
|
||||||
|
| CancelDelete
|
||||||
|
|
||||||
|
|
||||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||||
@ -183,12 +192,44 @@ update flags msg model =
|
|||||||
( m3, c3 ) =
|
( m3, c3 ) =
|
||||||
update flags LoadUsers m2
|
update flags LoadUsers m2
|
||||||
in
|
in
|
||||||
( { m3 | loading = False }, Cmd.batch [ c2, c3 ] )
|
( { m3 | loading = False, deleteConfirm = DimmerOff }, Cmd.batch [ c2, c3 ] )
|
||||||
|
|
||||||
else
|
else
|
||||||
( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none )
|
( { model
|
||||||
|
| formError = FormErrorSubmit res.message
|
||||||
|
, loading = False
|
||||||
|
, deleteConfirm = DimmerOff
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
SubmitResp (Err err) ->
|
SubmitResp (Err err) ->
|
||||||
|
( { model
|
||||||
|
| formError = FormErrorHttp err
|
||||||
|
, loading = False
|
||||||
|
, deleteConfirm = DimmerOff
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
RequestDelete ->
|
||||||
|
let
|
||||||
|
login =
|
||||||
|
Maybe.map .user flags.account
|
||||||
|
|> Maybe.withDefault ""
|
||||||
|
in
|
||||||
|
if model.formModel.user.login == login then
|
||||||
|
( { model | formError = FormErrorCurrentUser }, Cmd.none )
|
||||||
|
|
||||||
|
else
|
||||||
|
( { model | deleteConfirm = DimmerLoading }
|
||||||
|
, Api.getDeleteUserData flags model.formModel.user.login GetDeleteDataResp
|
||||||
|
)
|
||||||
|
|
||||||
|
GetDeleteDataResp (Ok data) ->
|
||||||
|
( { model | deleteConfirm = DimmerUserData data }, Cmd.none )
|
||||||
|
|
||||||
|
GetDeleteDataResp (Err err) ->
|
||||||
( { model
|
( { model
|
||||||
| formError = FormErrorHttp err
|
| formError = FormErrorHttp err
|
||||||
, loading = False
|
, loading = False
|
||||||
@ -196,29 +237,15 @@ update flags msg model =
|
|||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
RequestDelete ->
|
CancelDelete ->
|
||||||
update flags (YesNoMsg Comp.YesNoDimmer.activate) model
|
( { model | deleteConfirm = DimmerOff }, Cmd.none )
|
||||||
|
|
||||||
YesNoMsg m ->
|
DeleteUserNow login ->
|
||||||
let
|
( { model | deleteConfirm = DimmerLoading }, Api.deleteUser flags login SubmitResp )
|
||||||
( cm, confirmed ) =
|
|
||||||
Comp.YesNoDimmer.update m model.deleteConfirm
|
|
||||||
|
|
||||||
user =
|
|
||||||
Comp.UserForm.getUser model.formModel
|
|
||||||
|
|
||||||
cmd =
|
|
||||||
if confirmed then
|
|
||||||
Api.deleteUser flags user.login SubmitResp
|
|
||||||
|
|
||||||
else
|
|
||||||
Cmd.none
|
|
||||||
in
|
|
||||||
( { model | deleteConfirm = cm }, cmd )
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--- View2
|
--- View
|
||||||
|
|
||||||
|
|
||||||
view2 : Texts -> UiSettings -> Model -> Html Msg
|
view2 : Texts -> UiSettings -> Model -> Html Msg
|
||||||
@ -253,27 +280,82 @@ viewTable2 texts model =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
renderDeleteConfirm : Texts -> UiSettings -> Model -> Html Msg
|
||||||
|
renderDeleteConfirm texts settings model =
|
||||||
|
case model.deleteConfirm of
|
||||||
|
DimmerOff ->
|
||||||
|
span [ class "hidden" ] []
|
||||||
|
|
||||||
|
DimmerLoading ->
|
||||||
|
B.loadingDimmer
|
||||||
|
{ label = "Loading..."
|
||||||
|
, active = True
|
||||||
|
}
|
||||||
|
|
||||||
|
DimmerUserData data ->
|
||||||
|
let
|
||||||
|
empty =
|
||||||
|
List.isEmpty data.folders && data.sentMails == 0
|
||||||
|
|
||||||
|
folderNames =
|
||||||
|
String.join ", " data.folders
|
||||||
|
in
|
||||||
|
B.contentDimmer True <|
|
||||||
|
div [ class "flex flex-col" ] <|
|
||||||
|
(if empty then
|
||||||
|
[ div []
|
||||||
|
[ text texts.reallyDeleteUser
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
[ div []
|
||||||
|
[ text texts.reallyDeleteUser
|
||||||
|
, text " "
|
||||||
|
, text "The following data will be deleted:"
|
||||||
|
]
|
||||||
|
, ul [ class "list-inside list-disc" ]
|
||||||
|
[ li [ classList [ ( "hidden", List.isEmpty data.folders ) ] ]
|
||||||
|
[ text "Folders: "
|
||||||
|
, text folderNames
|
||||||
|
]
|
||||||
|
, li [ classList [ ( "hidden", data.sentMails == 0 ) ] ]
|
||||||
|
[ text (String.fromInt data.sentMails)
|
||||||
|
, text " sent mails"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
++ [ div [ class "mt-4 flex flex-row items-center" ]
|
||||||
|
[ B.deleteButton
|
||||||
|
{ label = texts.basics.yes
|
||||||
|
, icon = "fa fa-check"
|
||||||
|
, disabled = False
|
||||||
|
, handler = onClick (DeleteUserNow model.formModel.user.login)
|
||||||
|
, attrs = [ href "#" ]
|
||||||
|
}
|
||||||
|
, B.secondaryButton
|
||||||
|
{ label = texts.basics.no
|
||||||
|
, icon = "fa fa-times"
|
||||||
|
, disabled = False
|
||||||
|
, handler = onClick CancelDelete
|
||||||
|
, attrs = [ href "#", class "ml-2" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
viewForm2 : Texts -> UiSettings -> Model -> Html Msg
|
viewForm2 : Texts -> UiSettings -> Model -> Html Msg
|
||||||
viewForm2 texts settings model =
|
viewForm2 texts settings model =
|
||||||
let
|
let
|
||||||
newUser =
|
newUser =
|
||||||
Comp.UserForm.isNewUser model.formModel
|
Comp.UserForm.isNewUser model.formModel
|
||||||
|
|
||||||
dimmerSettings : Comp.YesNoDimmer.Settings
|
|
||||||
dimmerSettings =
|
|
||||||
Comp.YesNoDimmer.defaultSettings texts.reallyDeleteUser
|
|
||||||
texts.basics.yes
|
|
||||||
texts.basics.no
|
|
||||||
in
|
in
|
||||||
Html.form
|
Html.form
|
||||||
[ class "flex flex-col md:relative"
|
[ class "flex flex-col md:relative"
|
||||||
, onSubmit Submit
|
, onSubmit Submit
|
||||||
]
|
]
|
||||||
[ Html.map YesNoMsg
|
[ renderDeleteConfirm texts settings model
|
||||||
(Comp.YesNoDimmer.viewN True
|
|
||||||
dimmerSettings
|
|
||||||
model.deleteConfirm
|
|
||||||
)
|
|
||||||
, if newUser then
|
, if newUser then
|
||||||
h3 [ class S.header2 ]
|
h3 [ class S.header2 ]
|
||||||
[ text texts.createNewUser
|
[ text texts.createNewUser
|
||||||
@ -331,6 +413,9 @@ viewForm2 texts settings model =
|
|||||||
|
|
||||||
FormErrorInvalid ->
|
FormErrorInvalid ->
|
||||||
text texts.pleaseCorrectErrors
|
text texts.pleaseCorrectErrors
|
||||||
|
|
||||||
|
FormErrorCurrentUser ->
|
||||||
|
text texts.notDeleteCurrentUser
|
||||||
]
|
]
|
||||||
, B.loadingDimmer
|
, B.loadingDimmer
|
||||||
{ active = model.loading
|
{ active = model.loading
|
||||||
|
@ -30,6 +30,7 @@ type alias Texts =
|
|||||||
, basics : Messages.Basics.Texts
|
, basics : Messages.Basics.Texts
|
||||||
, deleteThisUser : String
|
, deleteThisUser : String
|
||||||
, pleaseCorrectErrors : String
|
, pleaseCorrectErrors : String
|
||||||
|
, notDeleteCurrentUser : String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ gb =
|
|||||||
, createNewUser = "Create new user"
|
, createNewUser = "Create new user"
|
||||||
, deleteThisUser = "Delete this user"
|
, deleteThisUser = "Delete this user"
|
||||||
, pleaseCorrectErrors = "Please correct the errors in the form."
|
, pleaseCorrectErrors = "Please correct the errors in the form."
|
||||||
|
, notDeleteCurrentUser = "You can't delete the user you are currently logged in with."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -62,4 +64,5 @@ de =
|
|||||||
, createNewUser = "Neuen Benutzer erstellen"
|
, createNewUser = "Neuen Benutzer erstellen"
|
||||||
, deleteThisUser = "Benutzer löschen"
|
, deleteThisUser = "Benutzer löschen"
|
||||||
, pleaseCorrectErrors = "Bitte korrigiere die Fehler im Formular."
|
, pleaseCorrectErrors = "Bitte korrigiere die Fehler im Formular."
|
||||||
|
, notDeleteCurrentUser = "Der aktuelle Benutzer kann nicht gelöscht werden."
|
||||||
}
|
}
|
||||||
|
@ -197,7 +197,17 @@ secondaryBasicButtonHover =
|
|||||||
|
|
||||||
deleteButton : String
|
deleteButton : String
|
||||||
deleteButton =
|
deleteButton =
|
||||||
" rounded my-auto whitespace-nowrap border border-red-500 dark:border-lightred-500 text-red-500 dark:text-orange-500 text-center px-4 py-2 shadow-none focus:outline-none focus:ring focus:ring-opacity-75 hover:bg-red-600 hover:text-white dark:hover:text-white dark:hover:bg-orange-500 dark:hover:text-bluegray-900 "
|
deleteButtonMain ++ deleteButtonHover
|
||||||
|
|
||||||
|
|
||||||
|
deleteButtonMain : String
|
||||||
|
deleteButtonMain =
|
||||||
|
" rounded my-auto whitespace-nowrap border border-red-500 dark:border-lightred-500 text-red-500 dark:text-orange-500 text-center px-4 py-2 shadow-none focus:outline-none focus:ring focus:ring-opacity-75 "
|
||||||
|
|
||||||
|
|
||||||
|
deleteButtonHover : String
|
||||||
|
deleteButtonHover =
|
||||||
|
" hover:bg-red-600 hover:text-white dark:hover:bg-orange-500 dark:hover:text-bluegray-900 "
|
||||||
|
|
||||||
|
|
||||||
undeleteButton : String
|
undeleteButton : String
|
||||||
|
Loading…
x
Reference in New Issue
Block a user