Account deletion

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

17
.idea/workspace.xml generated
View File

@ -4,7 +4,16 @@
<option name="autoReloadType" value="ALL" /> <option name="autoReloadType" value="ALL" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Realm avatar, banner and access policy" /> <list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Account deletion">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/go.mod" beforeDir="false" afterPath="$PROJECT_DIR$/go.mod" afterDir="false" />
<change beforePath="$PROJECT_DIR$/go.sum" beforeDir="false" afterPath="$PROJECT_DIR$/go.sum" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/models/tokens.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/models/tokens.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/api/accounts_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/accounts_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/api/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/index.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/services/accounts.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/services/accounts.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/services/tokens.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/services/tokens.go" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -56,7 +65,7 @@
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;run.code.analysis.last.selected.profile&quot;: &quot;pProject Default&quot;, &quot;run.code.analysis.last.selected.profile&quot;: &quot;pProject Default&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.lookFeel&quot;, &quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
&quot;ts.external.directory.path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib&quot;, &quot;ts.external.directory.path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot; &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}, },
@ -150,7 +159,6 @@
</option> </option>
</component> </component>
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value=":card_file_box: Update modeling" />
<MESSAGE value=":sparkles: Bot token aka. API token" /> <MESSAGE value=":sparkles: Bot token aka. API token" />
<MESSAGE value=":sparkles: Bots aka. automated accounts" /> <MESSAGE value=":sparkles: Bots aka. automated accounts" />
<MESSAGE value=":sparkles: Return affiliated to and automated by in userinfo grpc call" /> <MESSAGE value=":sparkles: Return affiliated to and automated by in userinfo grpc call" />
@ -175,7 +183,8 @@
<MESSAGE value=":bug: Fix daily sign batch list query issue" /> <MESSAGE value=":bug: Fix daily sign batch list query issue" />
<MESSAGE value=":bug: Fix daily sign random panic" /> <MESSAGE value=":bug: Fix daily sign random panic" />
<MESSAGE value=":sparkles: Realm avatar, banner and access policy" /> <MESSAGE value=":sparkles: Realm avatar, banner and access policy" />
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: Realm avatar, banner and access policy" /> <MESSAGE value=":sparkles: Account deletion" />
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: Account deletion" />
</component> </component>
<component name="VgoProject"> <component name="VgoProject">
<settings-migrated>true</settings-migrated> <settings-migrated>true</settings-migrated>

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.21.6
toolchain go1.22.1 toolchain go1.22.1
require ( require (
git.solsynth.dev/hydrogen/dealer v0.0.0-20240917083841-b14c0240a75f git.solsynth.dev/hydrogen/dealer v0.0.0-20240919131945-00c52eba6827
github.com/go-playground/validator/v10 v10.17.0 github.com/go-playground/validator/v10 v10.17.0
github.com/gofiber/fiber/v2 v2.52.4 github.com/gofiber/fiber/v2 v2.52.4
github.com/golang-jwt/jwt/v5 v5.2.0 github.com/golang-jwt/jwt/v5 v5.2.0

2
go.sum
View File

@ -2,6 +2,8 @@ git.solsynth.dev/hydrogen/dealer v0.0.0-20240911145828-d734d617bfc8 h1:kWheneSdS
git.solsynth.dev/hydrogen/dealer v0.0.0-20240911145828-d734d617bfc8/go.mod h1:Q51JPkKnV0UoOT/IRmdBh5CyfSlp7s8BRGzgooYHqkI= git.solsynth.dev/hydrogen/dealer v0.0.0-20240911145828-d734d617bfc8/go.mod h1:Q51JPkKnV0UoOT/IRmdBh5CyfSlp7s8BRGzgooYHqkI=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240917083841-b14c0240a75f h1:3jLpcws4/zmNUA60w1RtAtGNjcQd5NZCcbW5HQcUcvw= git.solsynth.dev/hydrogen/dealer v0.0.0-20240917083841-b14c0240a75f h1:3jLpcws4/zmNUA60w1RtAtGNjcQd5NZCcbW5HQcUcvw=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240917083841-b14c0240a75f/go.mod h1:Q51JPkKnV0UoOT/IRmdBh5CyfSlp7s8BRGzgooYHqkI= git.solsynth.dev/hydrogen/dealer v0.0.0-20240917083841-b14c0240a75f/go.mod h1:Q51JPkKnV0UoOT/IRmdBh5CyfSlp7s8BRGzgooYHqkI=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240919131945-00c52eba6827 h1:1ACMPm2ArRpVNYrND/y/R6oPiuMfKe49fP+lG3mcNug=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240919131945-00c52eba6827/go.mod h1:Q51JPkKnV0UoOT/IRmdBh5CyfSlp7s8BRGzgooYHqkI=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=

View File

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

View File

@ -216,3 +216,34 @@ func doRegisterConfirm(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK) 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/accept", acceptFriend)
relations.Post("/:relatedId/decline", declineFriend) relations.Post("/:relatedId/decline", declineFriend)
} }
me.Post("/deletion", requestDeleteAccount)
me.Post("/deletion/confirm", confirmDeleteAccount)
} }
directory := api.Group("/users/:alias").Name("User Directory") directory := api.Group("/users/:alias").Name("User Directory")

View File

@ -1,7 +1,10 @@
package services package services
import ( import (
"context"
"fmt" "fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"git.solsynth.dev/hydrogen/passport/pkg/internal/gap"
"time" "time"
"unicode" "unicode"
@ -179,6 +182,61 @@ func ForceConfirmAccount(user models.Account) error {
return nil 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 { func CheckAbleToResetPassword(user models.Account) error {
var count int64 var count int64
if err := database.C. if err := database.C.
@ -232,7 +290,13 @@ func ConfirmResetPassword(code, newPassword string) error {
factor.Secret = HashPassword(newPassword) 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 { func DeleteAccount(id uint) error {
@ -243,7 +307,17 @@ func DeleteAccount(id uint) error {
return err 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() { func RecycleUnConfirmAccount() {

View File

@ -45,6 +45,23 @@ If you have any questions or need further assistance, please do not hesitate to
Best regards, Best regards,
%s` %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) { func ValidateMagicToken(code string, mode models.MagicTokenType) (models.MagicToken, error) {
var tk models.MagicToken var tk models.MagicToken
if err := database.C.Where(models.MagicToken{Code: code, Type: mode}).First(&tk).Error; err != nil { 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, link,
viper.GetString("maintainer"), 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: default:
return fmt.Errorf("unsupported magic token type to notify") return fmt.Errorf("unsupported magic token type to notify")
} }