forked from vikunja/vikunja
User account deletion (#937)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: vikunja/api#937 Co-authored-by: konrad <konrad@kola-entertainments.de> Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
cd21c5fc6e
commit
27119ad6d4
@ -43,6 +43,10 @@ service:
|
||||
# If enabled, vikunja will send an email to everyone who is either assigned to a task or created it when a task reminder
|
||||
# is due.
|
||||
enableemailreminders: true
|
||||
# If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
||||
# for user deletion.
|
||||
enableuserdeletion: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
|
@ -108,6 +108,13 @@ return
|
||||
The `mail` package provides a `Fake()` method which you should call in the `MainTest` functions of your package.
|
||||
If it was called, no mails are being sent and you can instead assert they have been sent with the `AssertSent` method.
|
||||
|
||||
When testing, you should call the `notifications.Fake()` method in the `TestMain` function of the package you want to test.
|
||||
This prevents any notifications from being sent and lets you assert a notifications has been sent like this:
|
||||
|
||||
{{< highlight golang >}}
|
||||
notifications.AssertSent(t, &ReminderDueNotification{})
|
||||
{{< /highlight >}}
|
||||
|
||||
## Example
|
||||
|
||||
Take a look at the [pkg/user/notifications.go](https://code.vikunja.io/api/src/branch/main/pkg/user/notifications.go) file for a good example.
|
||||
|
@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@ -42,6 +43,7 @@ var (
|
||||
userFlagResetPasswordDirectly bool
|
||||
userFlagEnableUser bool
|
||||
userFlagDisableUser bool
|
||||
userFlagDeleteNow bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -66,7 +68,10 @@ func init() {
|
||||
userChangeEnabledCmd.Flags().BoolVarP(&userFlagDisableUser, "disable", "d", false, "Disable the user.")
|
||||
userChangeEnabledCmd.Flags().BoolVarP(&userFlagEnableUser, "enable", "e", false, "Enable the user.")
|
||||
|
||||
userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd)
|
||||
// User deletion flags
|
||||
userDeleteCmd.Flags().BoolVarP(&userFlagDeleteNow, "now", "n", false, "If provided, deletes the user immediately instead of sending them an email first.")
|
||||
|
||||
userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
|
||||
rootCmd.AddCommand(userCmd)
|
||||
}
|
||||
|
||||
@ -300,3 +305,61 @@ var userChangeEnabledCmd = &cobra.Command{
|
||||
fmt.Printf("User status successfully changed, status is now \"%s\"\n", u.Status)
|
||||
},
|
||||
}
|
||||
|
||||
var userDeleteCmd = &cobra.Command{
|
||||
Use: "delete [user id]",
|
||||
Short: "Delete an existing user.",
|
||||
Long: "Kick off the user deletion process. If call without the --now flag, this command will only trigger an email to the user in order for them to confirm and start the deletion process. With the flag the user is deleted immediately. USE WITH CAUTION.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initialize.FullInit()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if userFlagDeleteNow {
|
||||
fmt.Println("You requested to delete the user immediately. Are you sure?")
|
||||
fmt.Println(`To confirm, please type "yes, I confirm" in all uppercase:`)
|
||||
|
||||
cr := bufio.NewReader(os.Stdin)
|
||||
text, err := cr.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Fatalf("could not read confirmation message: %s", err)
|
||||
}
|
||||
if text != "YES, I CONFIRM\n" {
|
||||
log.Fatalf("invalid confirmation message")
|
||||
}
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
if err := s.Begin(); err != nil {
|
||||
log.Fatalf("Count not start transaction: %s", err)
|
||||
}
|
||||
|
||||
u := getUserFromArg(s, args[0])
|
||||
|
||||
if userFlagDeleteNow {
|
||||
err := models.DeleteUser(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Error removing the user: %s", err)
|
||||
}
|
||||
} else {
|
||||
err := user.RequestDeletion(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Could not request user deletion: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
log.Fatalf("Error saving everything: %s", err)
|
||||
}
|
||||
|
||||
if userFlagDeleteNow {
|
||||
fmt.Println("User deleted successfully.")
|
||||
} else {
|
||||
fmt.Println("User scheduled for deletion successfully.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ const (
|
||||
ServiceSentryDsn Key = `service.sentrydsn`
|
||||
ServiceTestingtoken Key = `service.testingtoken`
|
||||
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
||||
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
|
||||
|
||||
AuthLocalEnabled Key = `auth.local.enabled`
|
||||
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
||||
@ -246,6 +247,7 @@ func InitDefaultConfig() {
|
||||
ServiceEnableTaskComments.setDefault(true)
|
||||
ServiceEnableTotp.setDefault(true)
|
||||
ServiceEnableEmailReminders.setDefault(true)
|
||||
ServiceEnableUserDeletion.setDefault(true)
|
||||
|
||||
// Auth
|
||||
AuthLocalEnabled.setDefault(true)
|
||||
|
@ -232,3 +232,13 @@
|
||||
position: 23
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 24
|
||||
title: Test24
|
||||
description: Lorem Ipsum
|
||||
identifier: test6
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 7
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -95,6 +95,8 @@ func FullInit() {
|
||||
models.RegisterReminderCron()
|
||||
models.RegisterOverdueReminderCron()
|
||||
user.RegisterTokenCleanupCron()
|
||||
user.RegisterDeletionNotificationCron()
|
||||
models.RegisterUserDeletionCron()
|
||||
|
||||
// Start processing events
|
||||
go func() {
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
type users20210713213622 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
ID int64 `xorm:"bigint autoincr not null" json:"id"`
|
||||
IsActive bool `xorm:"null" json:"-"`
|
||||
Status int `xorm:"default 0" json:"-"`
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
type tasks20210725153703 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
ID int64 `xorm:"bigint autoincr not null" json:"id" param:"listtask"`
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
KanbanPosition float64 `xorm:"double null" json:"kanban_position"`
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
type lists20210727204942 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"`
|
||||
ID int64 `xorm:"bigint autoincr not null" json:"id" param:"list"`
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
type buckets20210727211037 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"`
|
||||
ID int64 `xorm:"bigint autoincr not null" json:"id" param:"list"`
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
}
|
||||
|
||||
|
46
pkg/migration/20210802081716.go
Normal file
46
pkg/migration/20210802081716.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type users20210802081716 struct {
|
||||
DeletionScheduledAt time.Time `xorm:"datetime null" json:"-"`
|
||||
DeletionLastReminderSent time.Time `xorm:"datetime null" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210802081716) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210802081716",
|
||||
Description: "Add account deletion schedule timestamps",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210802081716{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
@ -762,11 +762,19 @@ func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
// Delete all tasks on that list
|
||||
_, err = s.Where("list_id = ?", l.ID).Delete(&Task{})
|
||||
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
|
||||
tasks, _, _, err := getRawTasksForLists(s, []*List{l}, a, &taskOptions{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
err = task.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListDeletedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
|
@ -650,7 +650,10 @@ func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id} [delete]
|
||||
func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
return deleteNamespace(s, n, a, true)
|
||||
}
|
||||
|
||||
func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withLists bool) (err error) {
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
@ -663,6 +666,15 @@ func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
namespaceDeleted := &NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
}
|
||||
|
||||
if !withLists {
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Delete all lists with their tasks
|
||||
lists, err := GetListsByNamespaceID(s, n.ID, &user.User{})
|
||||
if err != nil {
|
||||
@ -670,43 +682,18 @@ func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
if len(lists) == 0 {
|
||||
return events.Dispatch(&NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
var listIDs []int64
|
||||
// We need to do that for here because we need the list ids to delete two times:
|
||||
// 1) to delete the lists itself
|
||||
// 2) to delete the list tasks
|
||||
for _, l := range lists {
|
||||
listIDs = append(listIDs, l.ID)
|
||||
// Looping over all lists to let the list handle properly cleaning up the tasks and everything else associated with it.
|
||||
for _, list := range lists {
|
||||
err = list.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(listIDs) == 0 {
|
||||
return events.Dispatch(&NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete tasks
|
||||
_, err = s.In("list_id", listIDs).Delete(&Task{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the lists
|
||||
_, err = s.In("id", listIDs).Delete(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
return events.Dispatch(namespaceDeleted)
|
||||
}
|
||||
|
||||
// Update implements the update method via the interface
|
||||
|
275
pkg/models/user_delete.go
Normal file
275
pkg/models/user_delete.go
Normal file
@ -0,0 +1,275 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// User deletion must happen here in this packaage because we want to delete everything associated to this user.
|
||||
// Because most of these things are managed in the models package, using them has to happen here.
|
||||
|
||||
// RegisterUserDeletionCron registers the cron job that actually removes users who are scheduled to delete.
|
||||
func RegisterUserDeletionCron() {
|
||||
err := cron.Schedule("0 * * * *", deleteUsers)
|
||||
if err != nil {
|
||||
log.Errorf("Could not register deletion cron: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func deleteUsers() {
|
||||
s := db.NewSession()
|
||||
users := []*user.User{}
|
||||
err := s.Where(builder.Lt{"deletion_scheduled_at": time.Now()}).
|
||||
Find(&users)
|
||||
if err != nil {
|
||||
log.Errorf("Could not get users scheduled for deletion: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
if u.DeletionScheduledAt.Before(time.Now()) {
|
||||
log.Debugf("User %d is not yet scheduled for deletion.", u.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.Begin()
|
||||
if err != nil {
|
||||
log.Errorf("Could not start transaction: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = DeleteUser(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Errorf("Could not delete u %d: %s", u.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
log.Errorf("Could not commit transaction: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser completely removes a user and all their associated lists, namespaces and tasks.
|
||||
// This action is irrevocable.
|
||||
// Public to allow deletion from the CLI.
|
||||
func DeleteUser(s *xorm.Session, u *user.User) (err error) {
|
||||
namespacesToDelete := []*Namespace{}
|
||||
// Get all namespaces and lists this u has access to
|
||||
nm := &Namespace{IsArchived: true}
|
||||
res, _, _, err := nm.ReadAll(s, u, "", 1, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespaces := res.([]*NamespaceWithLists)
|
||||
for _, n := range namespaces {
|
||||
if n.ID < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hadUsers, err := ensureNamespaceAdminUser(s, &n.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hadUsers {
|
||||
continue
|
||||
}
|
||||
hadTeams, err := ensureNamespaceAdminTeam(s, &n.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hadTeams {
|
||||
continue
|
||||
}
|
||||
|
||||
namespacesToDelete = append(namespacesToDelete, &n.Namespace)
|
||||
}
|
||||
|
||||
// Get all lists to delete
|
||||
listsToDelete := []*List{}
|
||||
lm := &List{IsArchived: true}
|
||||
res, _, _, err = lm.ReadAll(s, u, "", 0, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lists := res.([]*List)
|
||||
for _, l := range lists {
|
||||
if l.ID < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hadUsers, err := ensureListAdminUser(s, l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hadUsers {
|
||||
continue
|
||||
}
|
||||
hadTeams, err := ensureListAdminTeam(s, l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hadTeams {
|
||||
continue
|
||||
}
|
||||
|
||||
listsToDelete = append(listsToDelete, l)
|
||||
}
|
||||
|
||||
// Delete everything not shared with anybody else
|
||||
for _, n := range namespacesToDelete {
|
||||
err = deleteNamespace(s, n, u, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range listsToDelete {
|
||||
err = l.Delete(s, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.Where("id = ?", u.ID).Delete(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return notifications.Notify(u, &user.AccountDeletedNotification{})
|
||||
}
|
||||
|
||||
func ensureNamespaceAdminUser(s *xorm.Session, n *Namespace) (hadUsers bool, err error) {
|
||||
namespaceUsers := []*NamespaceUser{}
|
||||
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceUsers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(namespaceUsers) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range namespaceUsers {
|
||||
if lu.Right == RightAdmin {
|
||||
// List already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstUser := namespaceUsers[0]
|
||||
firstUser.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstUser.ID).
|
||||
Cols("right").
|
||||
Update(firstUser)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func ensureNamespaceAdminTeam(s *xorm.Session, n *Namespace) (hadTeams bool, err error) {
|
||||
namespaceTeams := []*TeamNamespace{}
|
||||
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceTeams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(namespaceTeams) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range namespaceTeams {
|
||||
if lu.Right == RightAdmin {
|
||||
// List already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstTeam := namespaceTeams[0]
|
||||
firstTeam.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstTeam.ID).
|
||||
Cols("right").
|
||||
Update(firstTeam)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func ensureListAdminUser(s *xorm.Session, l *List) (hadUsers bool, err error) {
|
||||
listUsers := []*ListUser{}
|
||||
err = s.Where("list_id = ?", l.ID).Find(&listUsers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(listUsers) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range listUsers {
|
||||
if lu.Right == RightAdmin {
|
||||
// List already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstUser := listUsers[0]
|
||||
firstUser.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstUser.ID).
|
||||
Cols("right").
|
||||
Update(firstUser)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func ensureListAdminTeam(s *xorm.Session, l *List) (hadTeams bool, err error) {
|
||||
listTeams := []*TeamList{}
|
||||
err = s.Where("list_id = ?", l.ID).Find(&listTeams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(listTeams) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, lu := range listTeams {
|
||||
if lu.Right == RightAdmin {
|
||||
// List already has more than one admin, no need to do anything
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
firstTeam := listTeams[0]
|
||||
firstTeam.Right = RightAdmin
|
||||
_, err = s.Where("id = ?", firstTeam.ID).
|
||||
Cols("right").
|
||||
Update(firstTeam)
|
||||
return true, err
|
||||
}
|
47
pkg/models/user_delete_test.go
Normal file
47
pkg/models/user_delete_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
notifications.Fake()
|
||||
|
||||
u := &user.User{ID: 6}
|
||||
err := DeleteUser(s, u)
|
||||
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "users", map[string]interface{}{"id": u.ID})
|
||||
db.AssertMissing(t, "lists", map[string]interface{}{"id": 24}) // only user6 had access to this list
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 6}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 7}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 8}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 9}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 10}, false)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{"id": 11}, false)
|
||||
}
|
@ -48,6 +48,10 @@ type Notifiable interface {
|
||||
|
||||
// Notify notifies a notifiable of a notification
|
||||
func Notify(notifiable Notifiable, notification Notification) (err error) {
|
||||
if isUnderTest {
|
||||
sentTestNotifications = append(sentTestNotifications, notification)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = notifyMail(notifiable, notification)
|
||||
if err != nil {
|
||||
|
46
pkg/notifications/testing.go
Normal file
46
pkg/notifications/testing.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
isUnderTest bool
|
||||
sentTestNotifications []Notification
|
||||
)
|
||||
|
||||
func Fake() {
|
||||
isUnderTest = true
|
||||
sentTestNotifications = nil
|
||||
}
|
||||
|
||||
// AssertSent asserts a notification has been sent
|
||||
func AssertSent(t *testing.T, n Notification) {
|
||||
var found bool
|
||||
for _, testNotification := range sentTestNotifications {
|
||||
if n.Name() == testNotification.Name() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found, "Failed to assert "+n.Name()+" has been sent.")
|
||||
}
|
@ -48,6 +48,7 @@ type vikunjaInfos struct {
|
||||
CaldavEnabled bool `json:"caldav_enabled"`
|
||||
AuthInfo authInfo `json:"auth"`
|
||||
EmailRemindersEnabled bool `json:"email_reminders_enabled"`
|
||||
UserDeletionEnabled bool `json:"user_deletion_enabled"`
|
||||
}
|
||||
|
||||
type authInfo struct {
|
||||
@ -89,6 +90,7 @@ func Info(c echo.Context) error {
|
||||
TotpEnabled: config.ServiceEnableTotp.GetBool(),
|
||||
CaldavEnabled: config.ServiceEnableCaldav.GetBool(),
|
||||
EmailRemindersEnabled: config.ServiceEnableEmailReminders.GetBool(),
|
||||
UserDeletionEnabled: config.ServiceEnableUserDeletion.GetBool(),
|
||||
Legal: legalInfo{
|
||||
ImprintURL: config.LegalImprintURL.GetString(),
|
||||
PrivacyPolicyURL: config.LegalPrivacyURL.GetString(),
|
||||
|
201
pkg/routes/api/v1/user_deletion.go
Normal file
201
pkg/routes/api/v1/user_deletion.go
Normal file
@ -0,0 +1,201 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type UserDeletionRequest struct {
|
||||
Password string `json:"password" valid:"required"`
|
||||
}
|
||||
|
||||
type UserDeletionRequestConfirm struct {
|
||||
Token string `json:"token" valid:"required"`
|
||||
}
|
||||
|
||||
// UserRequestDeletion is the handler to request a user deletion process (sends a mail)
|
||||
// @Summary Request the deletion of the user
|
||||
// @Description Requests the deletion of the current user. It will trigger an email which has to be confirmed to start the deletion.
|
||||
// @tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param credentials body v1.UserDeletionRequest true "The user password."
|
||||
// @Success 200 {object} models.Message
|
||||
// @Failure 412 {object} web.HTTPError "Bad password provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /user/deletion/request [post]
|
||||
func UserRequestDeletion(c echo.Context) error {
|
||||
var deletionRequest UserDeletionRequest
|
||||
if err := c.Bind(&deletionRequest); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
|
||||
}
|
||||
|
||||
err := c.Validate(deletionRequest)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err = s.Begin()
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
u, err := user.GetCurrentUserFromDB(s, c)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = user.CheckUserPassword(u, deletionRequest.Password)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = user.RequestDeletion(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{Message: "Successfully requested deletion."})
|
||||
}
|
||||
|
||||
// UserConfirmDeletion is the handler to confirm a user deletion process and start it
|
||||
// @Summary Confirm a user deletion request
|
||||
// @Description Confirms the deletion request of a user sent via email.
|
||||
// @tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param credentials body v1.UserDeletionRequestConfirm true "The token."
|
||||
// @Success 200 {object} models.Message
|
||||
// @Failure 412 {object} web.HTTPError "Bad token provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /user/deletion/confirm [post]
|
||||
func UserConfirmDeletion(c echo.Context) error {
|
||||
var deleteConfirmation UserDeletionRequestConfirm
|
||||
if err := c.Bind(&deleteConfirmation); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No token provided.")
|
||||
}
|
||||
|
||||
err := c.Validate(deleteConfirmation)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err = s.Begin()
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
u, err := user.GetCurrentUserFromDB(s, c)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = user.ConfirmDeletion(s, u, deleteConfirmation.Token)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusNoContent, models.Message{Message: "Successfully confirmed the deletion request."})
|
||||
}
|
||||
|
||||
// UserCancelDeletion is the handler to abort a user deletion process
|
||||
// @Summary Abort a user deletion request
|
||||
// @Description Aborts an in-progress user deletion.
|
||||
// @tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param credentials body v1.UserDeletionRequest true "The user password to confirm."
|
||||
// @Success 200 {object} models.Message
|
||||
// @Failure 412 {object} web.HTTPError "Bad password provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /user/deletion/cancel [post]
|
||||
func UserCancelDeletion(c echo.Context) error {
|
||||
var deletionRequest UserDeletionRequest
|
||||
if err := c.Bind(&deletionRequest); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
|
||||
}
|
||||
|
||||
err := c.Validate(deletionRequest)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err = s.Begin()
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
u, err := user.GetCurrentUserFromDB(s, c)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = user.CheckUserPassword(u, deletionRequest.Password)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = user.CancelDeletion(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusNoContent, models.Message{Message: "Successfully confirmed the deletion request."})
|
||||
}
|
@ -18,6 +18,7 @@ package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
@ -32,7 +33,8 @@ import (
|
||||
|
||||
type userWithSettings struct {
|
||||
user.User
|
||||
Settings *UserSettings `json:"settings"`
|
||||
Settings *UserSettings `json:"settings"`
|
||||
DeletionScheduledAt time.Time `json:"deletion_scheduled_at"`
|
||||
}
|
||||
|
||||
// UserShow gets all informations about the current user
|
||||
@ -71,6 +73,7 @@ func UserShow(c echo.Context) error {
|
||||
DefaultListID: u.DefaultListID,
|
||||
WeekStart: u.WeekStart,
|
||||
},
|
||||
DeletionScheduledAt: u.DeletionScheduledAt,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, us)
|
||||
|
@ -312,6 +312,13 @@ func registerAPIRoutes(a *echo.Group) {
|
||||
u.GET("/settings/totp/qrcode", apiv1.UserTOTPQrCode)
|
||||
}
|
||||
|
||||
// User deletion
|
||||
if config.ServiceEnableUserDeletion.GetBool() {
|
||||
u.POST("/deletion/request", apiv1.UserRequestDeletion)
|
||||
u.POST("/deletion/confirm", apiv1.UserConfirmDeletion)
|
||||
u.POST("/deletion/cancel", apiv1.UserCancelDeletion)
|
||||
}
|
||||
|
||||
listHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.List{}
|
||||
|
@ -6315,6 +6315,144 @@ var doc = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/deletion/cancel": {
|
||||
"post": {
|
||||
"description": "Aborts an in-progress user deletion.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Abort a user deletion request",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The user password to confirm.",
|
||||
"name": "credentials",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserDeletionRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "Bad password provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/deletion/confirm": {
|
||||
"post": {
|
||||
"description": "Confirms the deletion request of a user sent via email.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Confirm a user deletion request",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The token.",
|
||||
"name": "credentials",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserDeletionRequestConfirm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "Bad token provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/deletion/request": {
|
||||
"post": {
|
||||
"description": "Requests the deletion of the current user. It will trigger an email which has to be confirmed to start the deletion.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Request the deletion of the user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The user password.",
|
||||
"name": "credentials",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserDeletionRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "Bad password provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/password": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -8556,6 +8694,22 @@ var doc = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserDeletionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserDeletionRequestConfirm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserPassword": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8695,6 +8849,9 @@ var doc = `{
|
||||
"totp_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user_deletion_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -6298,6 +6298,144 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/deletion/cancel": {
|
||||
"post": {
|
||||
"description": "Aborts an in-progress user deletion.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Abort a user deletion request",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The user password to confirm.",
|
||||
"name": "credentials",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserDeletionRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "Bad password provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/deletion/confirm": {
|
||||
"post": {
|
||||
"description": "Confirms the deletion request of a user sent via email.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Confirm a user deletion request",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The token.",
|
||||
"name": "credentials",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserDeletionRequestConfirm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "Bad token provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/deletion/request": {
|
||||
"post": {
|
||||
"description": "Requests the deletion of the current user. It will trigger an email which has to be confirmed to start the deletion.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Request the deletion of the user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The user password.",
|
||||
"name": "credentials",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserDeletionRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "Bad password provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/password": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -8539,6 +8677,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserDeletionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserDeletionRequestConfirm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserPassword": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8678,6 +8832,9 @@
|
||||
"totp_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user_deletion_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -1185,6 +1185,16 @@ definitions:
|
||||
email), `upload`, `initials`, `default`.
|
||||
type: string
|
||||
type: object
|
||||
v1.UserDeletionRequest:
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
type: object
|
||||
v1.UserDeletionRequestConfirm:
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
v1.UserPassword:
|
||||
properties:
|
||||
new_password:
|
||||
@ -1284,6 +1294,8 @@ definitions:
|
||||
type: boolean
|
||||
totp_enabled:
|
||||
type: boolean
|
||||
user_deletion_enabled:
|
||||
type: boolean
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
@ -5531,6 +5543,97 @@ paths:
|
||||
summary: Confirm the email of a new user
|
||||
tags:
|
||||
- user
|
||||
/user/deletion/cancel:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Aborts an in-progress user deletion.
|
||||
parameters:
|
||||
- description: The user password to confirm.
|
||||