User account deletion (#937)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/937 Co-authored-by: konrad <konrad@kola-entertainments.de> Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
131
pkg/user/delete.go
Normal file
131
pkg/user/delete.go
Normal file
@ -0,0 +1,131 @@
|
||||
// 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 user
|
||||
|
||||
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"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func RegisterDeletionNotificationCron() {
|
||||
err := cron.Schedule("0 * * * *", notifyUsersScheduledForDeletion)
|
||||
if err != nil {
|
||||
log.Errorf("Could not register deletion cron: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func notifyUsersScheduledForDeletion() {
|
||||
s := db.NewSession()
|
||||
users := []*User{}
|
||||
err := s.Where(builder.NotNull{"deletion_scheduled_at"}).
|
||||
Find(&users)
|
||||
if err != nil {
|
||||
log.Errorf("Could not get users scheduled for deletion: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Found %d users scheduled for deletion", len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if time.Since(user.DeletionLastReminderSent) < time.Hour*24 {
|
||||
continue
|
||||
}
|
||||
|
||||
var number = 2
|
||||
if user.DeletionLastReminderSent.IsZero() {
|
||||
number = 1
|
||||
}
|
||||
if user.DeletionScheduledAt.Sub(user.DeletionLastReminderSent) < time.Hour*24 {
|
||||
number = 3
|
||||
}
|
||||
|
||||
err = notifications.Notify(user, &AccountDeletionNotification{
|
||||
User: user,
|
||||
NotificationNumber: number,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Could not notify user %d of their deletion: %s", user.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
user.DeletionLastReminderSent = time.Now()
|
||||
_, err = s.Where("id = ?", user.ID).
|
||||
Cols("deletion_last_reminder_sent").
|
||||
Update(user)
|
||||
if err != nil {
|
||||
log.Errorf("Could update user %d last deletion reminder sent date: %s", user.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequestDeletion creates a user deletion confirm token and sends a notification to the user
|
||||
func RequestDeletion(s *xorm.Session, user *User) (err error) {
|
||||
token, err := generateNewToken(s, user, TokenAccountDeletion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return notifications.Notify(user, &AccountDeletionConfirmNotification{
|
||||
User: user,
|
||||
ConfirmToken: token.Token,
|
||||
})
|
||||
}
|
||||
|
||||
// ConfirmDeletion ConformDeletion checks a token and schedules the user for deletion
|
||||
func ConfirmDeletion(s *xorm.Session, user *User, token string) (err error) {
|
||||
tk, err := getToken(s, token, TokenAccountDeletion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tk == nil {
|
||||
// TODO: return invalid token error
|
||||
return
|
||||
}
|
||||
|
||||
err = removeTokens(s, user, TokenAccountDeletion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.DeletionScheduledAt = time.Now().Add(3 * 24 * time.Hour)
|
||||
_, err = s.Where("id = ?", user.ID).
|
||||
Cols("deletion_scheduled_at").
|
||||
Update(user)
|
||||
return err
|
||||
}
|
||||
|
||||
// CancelDeletion cancels the deletion of a user
|
||||
func CancelDeletion(s *xorm.Session, user *User) (err error) {
|
||||
user.DeletionScheduledAt = time.Time{}
|
||||
user.DeletionLastReminderSent = time.Time{}
|
||||
_, err = s.Where("id = ?", user.ID).
|
||||
Cols("deletion_scheduled_at", "deletion_last_reminder_sent").
|
||||
Update(user)
|
||||
return
|
||||
}
|
@ -17,6 +17,8 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
)
|
||||
@ -186,3 +188,86 @@ func (n *FailedLoginAttemptNotification) ToDB() interface{} {
|
||||
func (n *FailedLoginAttemptNotification) Name() string {
|
||||
return "failed.login.attempt"
|
||||
}
|
||||
|
||||
// AccountDeletionConfirmNotification represents a AccountDeletionConfirmNotification notification
|
||||
type AccountDeletionConfirmNotification struct {
|
||||
User *User
|
||||
ConfirmToken string
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for AccountDeletionConfirmNotification
|
||||
func (n *AccountDeletionConfirmNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject("Please confirm the deletion of your Vikunja account").
|
||||
Greeting("Hi "+n.User.GetName()+",").
|
||||
Line("You have requested the deletion of your account. To confirm this, please click the link below:").
|
||||
Action("Confirm the deletion of my account", config.ServiceFrontendurl.GetString()+"?accountDeletionConfirm="+n.ConfirmToken).
|
||||
Line("This link will be valid for 24 hours.").
|
||||
Line("Once you confirm the deletion we will schedule the deletion of your account in three days and send you another email until then.").
|
||||
Line("If you proceed with the deletion of your account, we will remove all of your namespaces, lists and tasks you created. Everything you shared with another user or team will transfer ownership to them.").
|
||||
Line("If you did not requested the deletion or changed your mind, you can simply ignore this email.").
|
||||
Line("Have a nice day!")
|
||||
}
|
||||
|
||||
// ToDB returns the AccountDeletionConfirmNotification notification in a format which can be saved in the db
|
||||
func (n *AccountDeletionConfirmNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *AccountDeletionConfirmNotification) Name() string {
|
||||
return "user.deletion.confirm"
|
||||
}
|
||||
|
||||
// AccountDeletionNotification represents a AccountDeletionNotification notification
|
||||
type AccountDeletionNotification struct {
|
||||
User *User
|
||||
NotificationNumber int
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for AccountDeletionNotification
|
||||
func (n *AccountDeletionNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject("Your Vikunja account will be deleted in "+strconv.Itoa(n.NotificationNumber)+" days").
|
||||
Greeting("Hi "+n.User.GetName()+",").
|
||||
Line("You recently requested the deletion of your Vikunja account.").
|
||||
Line("We will delete your account in "+strconv.Itoa(n.NotificationNumber)+" days.").
|
||||
Line("If you changed your mind, simply click the link below to cancel the deletion and follow the instructions there:").
|
||||
Action("Abort the deletion", config.ServiceFrontendurl.GetString()).
|
||||
Line("Have a nice day!")
|
||||
}
|
||||
|
||||
// ToDB returns the AccountDeletionNotification notification in a format which can be saved in the db
|
||||
func (n *AccountDeletionNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *AccountDeletionNotification) Name() string {
|
||||
return "user.deletion"
|
||||
}
|
||||
|
||||
// AccountDeletedNotification represents a AccountDeletedNotification notification
|
||||
type AccountDeletedNotification struct {
|
||||
User *User
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for AccountDeletedNotification
|
||||
func (n *AccountDeletedNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject("Your Vikunja Account has been deleted").
|
||||
Greeting("Hi " + n.User.GetName() + ",").
|
||||
Line("As requested, we've deleted your Vikunja account.").
|
||||
Line("This deletion is permanent. If did not create a backup and need your data back now, talk to your administrator.").
|
||||
Line("Have a nice day!")
|
||||
}
|
||||
|
||||
// ToDB returns the AccountDeletedNotification notification in a format which can be saved in the db
|
||||
func (n *AccountDeletedNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *AccountDeletedNotification) Name() string {
|
||||
return "user.deleted"
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ const (
|
||||
TokenUnknown TokenKind = iota
|
||||
TokenPasswordReset
|
||||
TokenEmailConfirm
|
||||
TokenAccountDeletion
|
||||
|
||||
tokenSize = 64
|
||||
)
|
||||
@ -88,7 +89,7 @@ func RegisterTokenCleanupCron() {
|
||||
defer s.Close()
|
||||
|
||||
deleted, err := s.
|
||||
Where("created > ? AND kind = ?", time.Now().Add(time.Hour*24*-1), TokenPasswordReset).
|
||||
Where("created > ? AND (kind = ? OR kind = ?)", time.Now().Add(time.Hour*24*-1), TokenPasswordReset, TokenAccountDeletion).
|
||||
Delete(&Token{})
|
||||
if err != nil {
|
||||
log.Errorf(logPrefix+"Error removing old password reset tokens: %s", err)
|
||||
|
@ -95,6 +95,9 @@ type User struct {
|
||||
DefaultListID int64 `xorm:"bigint null index" json:"-"`
|
||||
WeekStart int `xorm:"null" json:"-"`
|
||||
|
||||
DeletionScheduledAt time.Time `xorm:"datetime null" json:"-"`
|
||||
DeletionLastReminderSent time.Time `xorm:"datetime null" json:"-"`
|
||||
|
||||
// A timestamp when this task was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this task was last updated. You cannot change this value.
|
||||
@ -367,6 +370,16 @@ func CheckUserPassword(user *User, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentUserFromDB gets a user from jwt claims and returns the full user from the db.
|
||||
func GetCurrentUserFromDB(s *xorm.Session, c echo.Context) (user *User, err error) {
|
||||
u, err := GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return GetUserByID(s, u.ID)
|
||||
}
|
||||
|
||||
// GetCurrentUser returns the current user based on its jwt token
|
||||
func GetCurrentUser(c echo.Context) (user *User, err error) {
|
||||
jwtinf := c.Get("user").(*jwt.Token)
|
||||
|
Reference in New Issue
Block a user