mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-11 03:29:33 +00:00
commit
84e16f65f9
modules
backend/src/main/scala/docspell/backend/ops
restapi/src/main/resources
restserver/src/main/scala/docspell/restserver/routes
store/src/main/scala/docspell/store
webapp/src/main/elm
@ -15,7 +15,7 @@ import docspell.backend.PasswordCrypt
|
||||
import docspell.backend.ops.OCollective._
|
||||
import docspell.common._
|
||||
import docspell.store.UpdateResult
|
||||
import docspell.store.queries.QCollective
|
||||
import docspell.store.queries.{QCollective, QUser}
|
||||
import docspell.store.queue.JobQueue
|
||||
import docspell.store.records._
|
||||
import docspell.store.usertask.{UserTask, UserTaskScope, UserTaskStore}
|
||||
@ -37,7 +37,11 @@ trait OCollective[F[_]] {
|
||||
|
||||
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]
|
||||
|
||||
@ -91,6 +95,9 @@ object OCollective {
|
||||
type EmptyTrash = REmptyTrashSetting.EmptyTrash
|
||||
val EmptyTrash = REmptyTrashSetting.EmptyTrash
|
||||
|
||||
type DeleteUserData = QUser.UserData
|
||||
val DeleteUserData = QUser.UserData
|
||||
|
||||
sealed trait PassResetResult
|
||||
object PassResetResult {
|
||||
case class Success(newPw: Password) extends PassResetResult
|
||||
@ -207,16 +214,24 @@ object OCollective {
|
||||
store.transact(RUser.findAll(collective, _.login))
|
||||
|
||||
def add(s: RUser): F[AddResult] =
|
||||
store.add(
|
||||
RUser.insert(s.copy(password = PasswordCrypt.crypt(s.password))),
|
||||
RUser.exists(s.login)
|
||||
)
|
||||
if (s.source != AccountSource.Local)
|
||||
AddResult.failure(new Exception("Only local accounts can be created!")).pure[F]
|
||||
else
|
||||
store.add(
|
||||
RUser.insert(s.copy(password = PasswordCrypt.crypt(s.password))),
|
||||
RUser.exists(s.login)
|
||||
)
|
||||
|
||||
def update(s: RUser): F[AddResult] =
|
||||
store.add(RUser.update(s), RUser.exists(s.login))
|
||||
|
||||
def deleteUser(login: Ident, collective: Ident): F[AddResult] =
|
||||
store.transact(RUser.delete(login, collective)).attempt.map(AddResult.fromUpdate)
|
||||
def getDeleteUserData(accountId: AccountId): F[DeleteUserData] =
|
||||
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] =
|
||||
store.transact(QCollective.getInsights(collective))
|
||||
|
@ -1309,9 +1309,9 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
/sec/user/{id}:
|
||||
/sec/user/{username}:
|
||||
delete:
|
||||
operationId: "sec-user-delete-by-id"
|
||||
operationId: "sec-user-delete-by-username"
|
||||
tags: [ Collective ]
|
||||
summary: Delete a user.
|
||||
description: |
|
||||
@ -1319,7 +1319,7 @@ paths:
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
- $ref: "#/components/parameters/username"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
@ -1327,6 +1327,27 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
post:
|
||||
operationId: "sec-user-change-password"
|
||||
@ -4068,6 +4089,23 @@ paths:
|
||||
|
||||
components:
|
||||
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:
|
||||
description: |
|
||||
Provide a second factor for login.
|
||||
@ -6206,6 +6244,13 @@ components:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
username:
|
||||
name: username
|
||||
in: path
|
||||
required: true
|
||||
description: The username of a user of this collective
|
||||
schema:
|
||||
type: string
|
||||
itemId:
|
||||
name: itemId
|
||||
in: path
|
||||
|
@ -66,6 +66,14 @@ object UserRoutes {
|
||||
ar <- backend.collective.deleteUser(id, user.account.collective)
|
||||
resp <- Ok(basicResult(ar, "User deleted."))
|
||||
} 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 =
|
||||
fc(this)
|
||||
}
|
||||
def failure(ex: Exception): AddResult =
|
||||
Failure(ex)
|
||||
}
|
||||
|
@ -8,19 +8,20 @@ package docspell.store.qb
|
||||
|
||||
import cats.data.{NonEmptyList => Nel}
|
||||
|
||||
import docspell.store.impl.DoobieMeta
|
||||
import docspell.store.qb.impl._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object DML {
|
||||
object DML extends DoobieMeta {
|
||||
private val comma = fr","
|
||||
|
||||
def delete(table: TableDef, cond: Condition): ConnectionIO[Int] =
|
||||
deleteFragment(table, cond).update.run
|
||||
|
||||
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)
|
||||
|
||||
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] =
|
||||
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)
|
||||
}
|
||||
|
||||
private val T = Table(None)
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
|
@ -168,4 +168,7 @@ object RUser {
|
||||
val t = Table(None)
|
||||
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
|
||||
, getContacts
|
||||
, getCustomFields
|
||||
, getDeleteUserData
|
||||
, getEquipment
|
||||
, getEquipments
|
||||
, getFolderDetail
|
||||
@ -162,6 +163,7 @@ import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
||||
import Api.Model.ContactList exposing (ContactList)
|
||||
import Api.Model.CustomFieldList exposing (CustomFieldList)
|
||||
import Api.Model.CustomFieldValue exposing (CustomFieldValue)
|
||||
import Api.Model.DeleteUserData exposing (DeleteUserData)
|
||||
import Api.Model.DirectionValue exposing (DirectionValue)
|
||||
import Api.Model.EmailSettings exposing (EmailSettings)
|
||||
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
|
||||
|
||||
|
@ -6,7 +6,9 @@
|
||||
|
||||
|
||||
module Comp.Basic exposing
|
||||
( editLinkLabel
|
||||
( contentDimmer
|
||||
, deleteButton
|
||||
, editLinkLabel
|
||||
, editLinkTableCell
|
||||
, genericButton
|
||||
, 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 :
|
||||
{ x
|
||||
| label : String
|
||||
@ -182,18 +205,27 @@ linkLabel model =
|
||||
|
||||
loadingDimmer : { label : String, active : Bool } -> Html msg
|
||||
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
|
||||
[ classList
|
||||
[ ( "hidden", not cfg.active )
|
||||
[ ( "hidden", not active )
|
||||
]
|
||||
, class S.dimmer
|
||||
]
|
||||
[ div [ class "text-gray-200" ]
|
||||
[ i [ class "fa fa-circle-notch animate-spin" ] []
|
||||
, span [ class "ml-2" ]
|
||||
[ text cfg.label
|
||||
]
|
||||
]
|
||||
[ content
|
||||
]
|
||||
|
||||
|
||||
|
@ -85,6 +85,7 @@ getUser model =
|
||||
, email = model.email
|
||||
, state = state
|
||||
, password = model.password
|
||||
, source = "local"
|
||||
}
|
||||
|
||||
|
||||
|
@ -15,18 +15,18 @@ module Comp.UserManage exposing
|
||||
|
||||
import Api
|
||||
import Api.Model.BasicResult exposing (BasicResult)
|
||||
import Api.Model.DeleteUserData exposing (DeleteUserData)
|
||||
import Api.Model.User
|
||||
import Api.Model.UserList exposing (UserList)
|
||||
import Comp.Basic as B
|
||||
import Comp.MenuBar as MB
|
||||
import Comp.UserForm
|
||||
import Comp.UserTable
|
||||
import Comp.YesNoDimmer
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onSubmit)
|
||||
import Html.Events exposing (onClick, onSubmit)
|
||||
import Http
|
||||
import Messages.Comp.UserManage exposing (Texts)
|
||||
import Styles as S
|
||||
@ -39,10 +39,16 @@ type alias Model =
|
||||
, viewMode : ViewMode
|
||||
, formError : FormError
|
||||
, loading : Bool
|
||||
, deleteConfirm : Comp.YesNoDimmer.Model
|
||||
, deleteConfirm : DimmerMode
|
||||
}
|
||||
|
||||
|
||||
type DimmerMode
|
||||
= DimmerOff
|
||||
| DimmerLoading
|
||||
| DimmerUserData DeleteUserData
|
||||
|
||||
|
||||
type ViewMode
|
||||
= Table
|
||||
| Form
|
||||
@ -53,6 +59,7 @@ type FormError
|
||||
| FormErrorSubmit String
|
||||
| FormErrorHttp Http.Error
|
||||
| FormErrorInvalid
|
||||
| FormErrorCurrentUser
|
||||
|
||||
|
||||
emptyModel : Model
|
||||
@ -62,7 +69,7 @@ emptyModel =
|
||||
, viewMode = Table
|
||||
, formError = FormErrorNone
|
||||
, loading = False
|
||||
, deleteConfirm = Comp.YesNoDimmer.emptyModel
|
||||
, deleteConfirm = DimmerOff
|
||||
}
|
||||
|
||||
|
||||
@ -75,8 +82,10 @@ type Msg
|
||||
| InitNewUser
|
||||
| Submit
|
||||
| SubmitResp (Result Http.Error BasicResult)
|
||||
| YesNoMsg Comp.YesNoDimmer.Msg
|
||||
| RequestDelete
|
||||
| GetDeleteDataResp (Result Http.Error DeleteUserData)
|
||||
| DeleteUserNow String
|
||||
| CancelDelete
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
@ -183,12 +192,44 @@ update flags msg model =
|
||||
( m3, c3 ) =
|
||||
update flags LoadUsers m2
|
||||
in
|
||||
( { m3 | loading = False }, Cmd.batch [ c2, c3 ] )
|
||||
( { m3 | loading = False, deleteConfirm = DimmerOff }, Cmd.batch [ c2, c3 ] )
|
||||
|
||||
else
|
||||
( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none )
|
||||
( { model
|
||||
| formError = FormErrorSubmit res.message
|
||||
, loading = False
|
||||
, deleteConfirm = DimmerOff
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
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
|
||||
| formError = FormErrorHttp err
|
||||
, loading = False
|
||||
@ -196,29 +237,15 @@ update flags msg model =
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
RequestDelete ->
|
||||
update flags (YesNoMsg Comp.YesNoDimmer.activate) model
|
||||
CancelDelete ->
|
||||
( { model | deleteConfirm = DimmerOff }, Cmd.none )
|
||||
|
||||
YesNoMsg m ->
|
||||
let
|
||||
( 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 )
|
||||
DeleteUserNow login ->
|
||||
( { model | deleteConfirm = DimmerLoading }, Api.deleteUser flags login SubmitResp )
|
||||
|
||||
|
||||
|
||||
--- View2
|
||||
--- View
|
||||
|
||||
|
||||
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 settings model =
|
||||
let
|
||||
newUser =
|
||||
Comp.UserForm.isNewUser model.formModel
|
||||
|
||||
dimmerSettings : Comp.YesNoDimmer.Settings
|
||||
dimmerSettings =
|
||||
Comp.YesNoDimmer.defaultSettings texts.reallyDeleteUser
|
||||
texts.basics.yes
|
||||
texts.basics.no
|
||||
in
|
||||
Html.form
|
||||
[ class "flex flex-col md:relative"
|
||||
, onSubmit Submit
|
||||
]
|
||||
[ Html.map YesNoMsg
|
||||
(Comp.YesNoDimmer.viewN True
|
||||
dimmerSettings
|
||||
model.deleteConfirm
|
||||
)
|
||||
[ renderDeleteConfirm texts settings model
|
||||
, if newUser then
|
||||
h3 [ class S.header2 ]
|
||||
[ text texts.createNewUser
|
||||
@ -331,6 +413,9 @@ viewForm2 texts settings model =
|
||||
|
||||
FormErrorInvalid ->
|
||||
text texts.pleaseCorrectErrors
|
||||
|
||||
FormErrorCurrentUser ->
|
||||
text texts.notDeleteCurrentUser
|
||||
]
|
||||
, B.loadingDimmer
|
||||
{ active = model.loading
|
||||
|
@ -30,6 +30,7 @@ type alias Texts =
|
||||
, basics : Messages.Basics.Texts
|
||||
, deleteThisUser : String
|
||||
, pleaseCorrectErrors : String
|
||||
, notDeleteCurrentUser : String
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +47,7 @@ gb =
|
||||
, createNewUser = "Create new user"
|
||||
, deleteThisUser = "Delete this user"
|
||||
, 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"
|
||||
, deleteThisUser = "Benutzer löschen"
|
||||
, pleaseCorrectErrors = "Bitte korrigiere die Fehler im Formular."
|
||||
, notDeleteCurrentUser = "Der aktuelle Benutzer kann nicht gelöscht werden."
|
||||
}
|
||||
|
@ -197,7 +197,17 @@ secondaryBasicButtonHover =
|
||||
|
||||
deleteButton : String
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user