Delete the user along its data

This commit is contained in:
eikek 2021-09-06 21:54:30 +02:00
parent 3650a7d20c
commit 8df235e9db
6 changed files with 162 additions and 5 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
@ -215,8 +222,13 @@ object OCollective {
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

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

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