mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-11-03 18:00:11 +00:00 
			
		
		
		
	@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user