Merge pull request #1060 from eikek/fix/delete-user

Fix/delete user
This commit is contained in:
mergify[bot] 2021-09-08 19:11:50 +00:00 committed by GitHub
commit 84e16f65f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 410 additions and 60 deletions

View File

@ -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))

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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)
} }

View File

@ -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] =

View 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
}

View File

@ -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)
} }

View File

@ -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))

View File

@ -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)
} }

View File

@ -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

View File

@ -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
]
]
] ]

View File

@ -85,6 +85,7 @@ getUser model =
, email = model.email , email = model.email
, state = state , state = state
, password = model.password , password = model.password
, source = "local"
} }

View File

@ -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

View File

@ -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."
} }

View File

@ -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