Account deletion

This commit is contained in:
2024-09-19 22:18:22 +08:00
parent 02bffc062f
commit 3031f61ea4
8 changed files with 154 additions and 7 deletions

View File

@ -8,6 +8,7 @@ const (
ConfirmMagicToken = MagicTokenType(iota)
RegistrationMagicToken
ResetPasswordMagicToken
DeleteAccountMagicToken
)
type MagicToken struct {

View File

@ -216,3 +216,34 @@ func doRegisterConfirm(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
func requestDeleteAccount(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
if err := services.CheckAbleToDeleteAccount(user); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if err = services.RequestDeleteAccount(user); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}
func confirmDeleteAccount(c *fiber.Ctx) error {
var data struct {
Code string `json:"code" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
if err := services.ConfirmDeleteAccount(data.Code); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -79,6 +79,9 @@ func MapAPIs(app *fiber.App, baseURL string) {
relations.Post("/:relatedId/accept", acceptFriend)
relations.Post("/:relatedId/decline", declineFriend)
}
me.Post("/deletion", requestDeleteAccount)
me.Post("/deletion/confirm", confirmDeleteAccount)
}
directory := api.Group("/users/:alias").Name("User Directory")

View File

@ -1,7 +1,10 @@
package services
import (
"context"
"fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"git.solsynth.dev/hydrogen/passport/pkg/internal/gap"
"time"
"unicode"
@ -179,6 +182,61 @@ func ForceConfirmAccount(user models.Account) error {
return nil
}
func CheckAbleToDeleteAccount(user models.Account) error {
if user.AutomatedID != nil {
return fmt.Errorf("bot cannot request delete account, head to developer portal and dispose bot")
}
var count int64
if err := database.C.
Where("account_id = ?", user.ID).
Where("expired_at < ?", time.Now()).
Where("type = ?", models.ResetPasswordMagicToken).
Model(&models.MagicToken{}).
Count(&count).Error; err != nil {
return fmt.Errorf("unable to check delete account ability: %v", err)
} else if count > 0 {
return fmt.Errorf("you requested delete account recently")
}
return nil
}
func RequestDeleteAccount(user models.Account) error {
if tk, err := NewMagicToken(
models.DeleteAccountMagicToken,
&user,
lo.ToPtr(time.Now().Add(24*time.Hour)),
); err != nil {
return err
} else if err := NotifyMagicToken(tk); err != nil {
log.Error().
Err(err).
Str("code", tk.Code).
Uint("user", user.ID).
Msg("Failed to notify delete account magic token...")
}
return nil
}
func ConfirmDeleteAccount(code string) error {
token, err := ValidateMagicToken(code, models.DeleteAccountMagicToken)
if err != nil {
return err
} else if token.AccountID == nil {
return fmt.Errorf("magic token didn't assign a valid account")
}
if err := DeleteAccount(*token.AccountID); err != nil {
return err
} else {
database.C.Delete(&token)
}
return nil
}
func CheckAbleToResetPassword(user models.Account) error {
var count int64
if err := database.C.
@ -232,7 +290,13 @@ func ConfirmResetPassword(code, newPassword string) error {
factor.Secret = HashPassword(newPassword)
}
return database.C.Save(&factor).Error
if err = database.C.Save(&factor).Error; err != nil {
return err
} else {
database.C.Delete(&token)
}
return nil
}
func DeleteAccount(id uint) error {
@ -243,7 +307,17 @@ func DeleteAccount(id uint) error {
return err
}
return tx.Commit().Error
if err := tx.Commit().Error; err != nil {
return err
} else {
InvalidAuthCacheWithUser(id)
_, _ = proto.NewServiceDirectoryClient(gap.H.GetDealerGrpcConn()).BroadcastDeletion(context.Background(), &proto.DeletionRequest{
ResourceType: "account",
ResourceId: fmt.Sprintf("%d", id),
})
}
return nil
}
func RecycleUnConfirmAccount() {

View File

@ -45,6 +45,23 @@ If you have any questions or need further assistance, please do not hesitate to
Best regards,
%s`
const DeleteAccountTemplate = `Dear %s,
We received a request to delete your account at %s. If you did not request a account deletion, please change your account password right now.
If you changed your mind, please ignore this email.
To confirm your account deletion request, please use the link below:
%s
This link will expire in 24 hours. If you do not use that link within this time frame, you will need to submit an account deletion request.
If you have any questions or need further assistance, please do not hesitate to contact our support team.
Also, if you want to let us know why you decided to delete your account, send email us (lily@solsynth.dev) and tell us how could we improve our user experience.
Best regards,
%s`
func ValidateMagicToken(code string, mode models.MagicTokenType) (models.MagicToken, error) {
var tk models.MagicToken
if err := database.C.Where(models.MagicToken{Code: code, Type: mode}).First(&tk).Error; err != nil {
@ -112,6 +129,16 @@ func NotifyMagicToken(token models.MagicToken) error {
link,
viper.GetString("maintainer"),
)
case models.DeleteAccountMagicToken:
link := fmt.Sprintf("%s/flow/accounts/account-delete?code=%s", viper.GetString("frontend_app"), token.Code)
subject = fmt.Sprintf("[%s] Confirm your account deletion", viper.GetString("name"))
content = fmt.Sprintf(
DeleteAccountTemplate,
user.Name,
viper.GetString("name"),
link,
viper.GetString("maintainer"),
)
default:
return fmt.Errorf("unsupported magic token type to notify")
}