mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-05 10:59:33 +00:00
Delete the user along its data
This commit is contained in:
parent
3650a7d20c
commit
8df235e9db
@ -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))
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user