feat(account): deletion orchestration + step-up + gateway edge
Step-up: email accounts confirm with a mailed code (purpose=delete, no deeplink —
ConfirmByToken refuses a delete token so a stray click can't delete); platform-only
accounts type a fixed phrase (anti-impulse). Endpoints /user/delete/{request,confirm};
the confirm orchestration resigns active games, drops all-robot games, tombstones +
anonymizes the account (freeing its creds), and revokes its sessions — the tombstone is
the point of no return, the rest best-effort. Gateway account.delete.{request,confirm}
ops + fbs AccountDeleteConfirm/AccountDeleteRequestResult + branded ru/en delete email.
Integration tests cover the step-up (code + no-email) and the orchestration pieces.
This commit is contained in:
@@ -42,6 +42,7 @@ const (
|
||||
purposeLogin = "login"
|
||||
purposeLink = "link"
|
||||
purposeChange = "change"
|
||||
purposeDelete = "delete"
|
||||
)
|
||||
|
||||
// Errors returned by the email confirm-code flow.
|
||||
@@ -65,6 +66,9 @@ var (
|
||||
// ErrTooManyRequests is returned when confirm-code sends to an address are being
|
||||
// requested too frequently (the resend cooldown or the rolling-hour cap).
|
||||
ErrTooManyRequests = errors.New("account: too many code requests")
|
||||
// ErrNoEmail is returned when an email-code step-up is requested for an account that
|
||||
// holds no confirmed email (the caller must use the typed-phrase path instead).
|
||||
ErrNoEmail = errors.New("account: no confirmed email")
|
||||
)
|
||||
|
||||
// EmailService runs the email confirm-code flow: it issues a 6-digit code over a
|
||||
@@ -115,7 +119,14 @@ func (s *EmailService) issueCode(ctx context.Context, accountID uuid.UUID, email
|
||||
if err := s.store.replacePendingConfirmation(ctx, accountID, email, codeHash, tokenHash, purpose, s.now().Add(emailCodeTTL)); err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := renderConfirmationEmail(purpose, code, s.confirmURL(token, locale), s.baseURL, locale)
|
||||
// Account deletion is never a one-tap link (a prefetch or a stray click must not delete
|
||||
// an account): the delete code is entered in the app only, so the email omits the
|
||||
// deeplink and ConfirmByToken refuses a delete token.
|
||||
deeplink := s.confirmURL(token, locale)
|
||||
if purpose == purposeDelete {
|
||||
deeplink = ""
|
||||
}
|
||||
msg, err := renderConfirmationEmail(purpose, code, deeplink, s.baseURL, locale)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -347,6 +358,9 @@ func (s *EmailService) ConfirmByToken(ctx context.Context, token string) (LinkCo
|
||||
return LinkConfirmation{}, err
|
||||
}
|
||||
return LinkConfirmation{Purpose: purposeChange, Account: pend.accountID}, nil
|
||||
case purposeDelete:
|
||||
// Deletion is confirmed in the app with the code, never via a one-tap link.
|
||||
return LinkConfirmation{}, fmt.Errorf("account: deletion cannot be confirmed by link")
|
||||
default:
|
||||
return LinkConfirmation{}, fmt.Errorf("account: unsupported confirmation purpose %q", pend.purpose)
|
||||
}
|
||||
@@ -380,6 +394,27 @@ func (s *Store) confirmedEmailAccount(ctx context.Context, email string) (uuid.U
|
||||
return row.AccountID, true, nil
|
||||
}
|
||||
|
||||
// confirmedEmailOf returns the account's confirmed email address and true, or ("", false)
|
||||
// when it holds none. It backs the deletion step-up, which mails a code to the account's
|
||||
// own address.
|
||||
func (s *Store) confirmedEmailOf(ctx context.Context, accountID uuid.UUID) (string, bool, error) {
|
||||
stmt := postgres.SELECT(table.Identities.ExternalID).
|
||||
FROM(table.Identities).
|
||||
WHERE(
|
||||
table.Identities.AccountID.EQ(postgres.UUID(accountID)).
|
||||
AND(table.Identities.Kind.EQ(postgres.String(KindEmail))).
|
||||
AND(table.Identities.Confirmed.EQ(postgres.Bool(true))),
|
||||
).LIMIT(1)
|
||||
var row model.Identities
|
||||
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
|
||||
if errors.Is(err, qrm.ErrNoRows) {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, fmt.Errorf("account: confirmed email of %s: %w", accountID, err)
|
||||
}
|
||||
return row.ExternalID, true, nil
|
||||
}
|
||||
|
||||
// replacePendingConfirmation clears any pending code for (accountID, email) and
|
||||
// inserts a fresh one, inside one transaction.
|
||||
func (s *Store) replacePendingConfirmation(ctx context.Context, accountID uuid.UUID, email, codeHash, linkTokenHash, purpose string, expiresAt time.Time) error {
|
||||
|
||||
@@ -98,6 +98,24 @@ var confirmEmailCopy = map[string]map[string]emailCopy{
|
||||
FooterIgnore: "Если вы не запрашивали смену, просто проигнорируйте письмо — адрес останется прежним.",
|
||||
},
|
||||
},
|
||||
purposeDelete: {
|
||||
"en": {
|
||||
Subject: "Confirm your Erudit account deletion",
|
||||
Preheader: "Confirm account deletion",
|
||||
Heading: "Delete your account",
|
||||
Intro: "Enter this code in the app to permanently delete your account:",
|
||||
CTALabel: "",
|
||||
FooterIgnore: "If you didn't request this, ignore it — your account stays as it is.",
|
||||
},
|
||||
"ru": {
|
||||
Subject: "Подтвердите удаление аккаунта Эрудит",
|
||||
Preheader: "Подтверждение удаления аккаунта",
|
||||
Heading: "Удаление аккаунта",
|
||||
Intro: "Введите этот код в приложении, чтобы удалить аккаунт без восстановления:",
|
||||
CTALabel: "",
|
||||
FooterIgnore: "Если вы не запрашивали удаление, проигнорируйте письмо — аккаунт останется.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// emailBrand is the brand wordmark per locale.
|
||||
|
||||
@@ -178,6 +178,49 @@ func (s *EmailService) ConfirmChange(ctx context.Context, accountID uuid.UUID, n
|
||||
return s.store.GetByID(ctx, accountID)
|
||||
}
|
||||
|
||||
// HasEmail reports whether accountID owns a confirmed email. The account-deletion step-up
|
||||
// mails a confirm-code when it does, and falls back to a typed phrase otherwise.
|
||||
func (s *EmailService) HasEmail(ctx context.Context, accountID uuid.UUID) (bool, error) {
|
||||
_, ok, err := s.store.confirmedEmailOf(ctx, accountID)
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// RequestDeleteCode mails an account-deletion confirm-code to the account's own confirmed
|
||||
// email (no deeplink — deletion is confirmed in the app). It returns ErrNoEmail when the
|
||||
// account holds no email, ErrTooManyRequests when throttled.
|
||||
func (s *EmailService) RequestDeleteCode(ctx context.Context, accountID uuid.UUID) error {
|
||||
addr, ok, err := s.store.confirmedEmailOf(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return ErrNoEmail
|
||||
}
|
||||
if !s.allowSend(addr) {
|
||||
return ErrTooManyRequests
|
||||
}
|
||||
return s.issueCode(ctx, accountID, addr, purposeDelete, s.accountLocale(ctx, accountID))
|
||||
}
|
||||
|
||||
// VerifyDeleteCode verifies the account-deletion code against the account's own email and
|
||||
// consumes it on success. It returns ErrNoEmail (no email), the usual confirm-code errors
|
||||
// (ErrNoPendingCode, ErrCodeExpired, ErrTooManyAttempts, ErrCodeMismatch), or nil when the
|
||||
// code is valid — the caller then performs the deletion.
|
||||
func (s *EmailService) VerifyDeleteCode(ctx context.Context, accountID uuid.UUID, code string) error {
|
||||
addr, ok, err := s.store.confirmedEmailOf(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return ErrNoEmail
|
||||
}
|
||||
conf, err := s.verifyPendingCode(ctx, accountID, addr, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.store.consumeConfirmation(ctx, conf.id, s.now())
|
||||
}
|
||||
|
||||
// verifyPendingCode loads and checks the pending confirm-code for (accountID,
|
||||
// addr), counting a wrong attempt. It returns the confirmation on success.
|
||||
func (s *EmailService) verifyPendingCode(ctx context.Context, accountID uuid.UUID, addr, code string) (emailConfirmation, error) {
|
||||
|
||||
@@ -5,6 +5,8 @@ package inttest
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -133,3 +135,47 @@ func TestDropAllRobotGames(t *testing.T) {
|
||||
t.Errorf("the human game should be kept, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteStepUpEmail: an email account's delete code verifies (wrong code rejected, no
|
||||
// deeplink in the mail); a platform-only account has no email and cannot request a code.
|
||||
func TestDeleteStepUpEmail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := account.NewStore(testDB)
|
||||
mailer := &capturingMailer{}
|
||||
svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")
|
||||
|
||||
acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
|
||||
if err != nil {
|
||||
t.Fatalf("provision: %v", err)
|
||||
}
|
||||
if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, "del-"+uuid.NewString()+"@example.com", true); err != nil {
|
||||
t.Fatalf("attach email: %v", err)
|
||||
}
|
||||
if has, err := svc.HasEmail(ctx, acc.ID); err != nil || !has {
|
||||
t.Fatalf("HasEmail = (%v, %v), want true", has, err)
|
||||
}
|
||||
if err := svc.RequestDeleteCode(ctx, acc.ID); err != nil {
|
||||
t.Fatalf("request delete code: %v", err)
|
||||
}
|
||||
if strings.Contains(mailer.lastBody, "/confirm/") {
|
||||
t.Error("a delete email must not carry a one-tap deeplink")
|
||||
}
|
||||
code := sixDigit.FindString(mailer.lastBody)
|
||||
if err := svc.VerifyDeleteCode(ctx, acc.ID, "000000"); err == nil {
|
||||
t.Error("a wrong delete code must be rejected")
|
||||
}
|
||||
if err := svc.VerifyDeleteCode(ctx, acc.ID, code); err != nil {
|
||||
t.Fatalf("verify correct delete code: %v", err)
|
||||
}
|
||||
|
||||
noEmail, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
|
||||
if err != nil {
|
||||
t.Fatalf("provision no-email: %v", err)
|
||||
}
|
||||
if has, _ := svc.HasEmail(ctx, noEmail.ID); has {
|
||||
t.Error("HasEmail must be false for a platform-only account")
|
||||
}
|
||||
if err := svc.RequestDeleteCode(ctx, noEmail.ID); !errors.Is(err, account.ErrNoEmail) {
|
||||
t.Errorf("request delete for no-email account = %v, want ErrNoEmail", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ func (s *Server) registerRoutes() {
|
||||
// refused without disclosure, never merged).
|
||||
u.POST("/link/email/change/request", s.handleChangeEmailRequest)
|
||||
u.POST("/link/email/change/confirm", s.handleChangeEmailConfirm)
|
||||
// Account deletion (legal retention, not erasure): step-up via a mailed code
|
||||
// (email accounts) or a typed phrase (platform-only), then tombstone + free creds.
|
||||
u.POST("/delete/request", s.handleRequestDelete)
|
||||
u.POST("/delete/confirm", s.handleConfirmDelete)
|
||||
}
|
||||
if s.games != nil {
|
||||
u.GET("/games", s.handleListGames)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"scrabble/backend/internal/accountdelete"
|
||||
"scrabble/backend/internal/game"
|
||||
)
|
||||
|
||||
// Account deletion is legal retention, not erasure (docs/ARCHITECTURE.md §9.1): the account
|
||||
// row survives as a tombstone while its credentials are journalled + freed, its live
|
||||
// surfaces anonymised, and its own social/ephemeral data dropped. Messages are kept. The
|
||||
// step-up is a mailed code for an account with a confirmed email, or a typed phrase for a
|
||||
// platform-only account (possession-proof is unattainable there, so the phrase is
|
||||
// anti-impulse only).
|
||||
|
||||
// deletePhrase is the fixed confirmation phrase a no-email account types to delete.
|
||||
// Compared case-insensitively; the client localises only the surrounding instruction.
|
||||
const deletePhrase = "DELETE"
|
||||
|
||||
// deleteRequestResponse tells the client which step-up the account uses.
|
||||
type deleteRequestResponse struct {
|
||||
Method string `json:"method"` // "email" | "phrase"
|
||||
}
|
||||
|
||||
// handleRequestDelete starts account deletion: it mails a delete code to an account with a
|
||||
// confirmed email, or reports the typed-phrase path otherwise.
|
||||
func (s *Server) handleRequestDelete(c *gin.Context) {
|
||||
uid, ok := userID(c)
|
||||
if !ok {
|
||||
abortBadRequest(c, "missing identity")
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
hasEmail, err := s.emails.HasEmail(ctx, uid)
|
||||
if err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
if !hasEmail {
|
||||
c.JSON(http.StatusOK, deleteRequestResponse{Method: "phrase"})
|
||||
return
|
||||
}
|
||||
if err := s.emails.RequestDeleteCode(ctx, uid); err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, deleteRequestResponse{Method: "email"})
|
||||
}
|
||||
|
||||
// deleteConfirmBody carries the step-up proof: a mailed code (email account) or the typed
|
||||
// phrase (platform-only account).
|
||||
type deleteConfirmBody struct {
|
||||
Code string `json:"code"`
|
||||
Phrase string `json:"phrase"`
|
||||
}
|
||||
|
||||
// handleConfirmDelete verifies the step-up and deletes the account.
|
||||
func (s *Server) handleConfirmDelete(c *gin.Context) {
|
||||
uid, ok := userID(c)
|
||||
if !ok {
|
||||
abortBadRequest(c, "missing identity")
|
||||
return
|
||||
}
|
||||
var req deleteConfirmBody
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
abortBadRequest(c, "invalid request body")
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
hasEmail, err := s.emails.HasEmail(ctx, uid)
|
||||
if err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
if hasEmail {
|
||||
if err := s.emails.VerifyDeleteCode(ctx, uid, req.Code); err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
} else if !strings.EqualFold(strings.TrimSpace(req.Phrase), deletePhrase) {
|
||||
abortBadRequest(c, "confirmation phrase does not match")
|
||||
return
|
||||
}
|
||||
if err := s.deleteAccount(ctx, uid); err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, okResponse{OK: true})
|
||||
}
|
||||
|
||||
// deleteAccount runs the deletion orchestration after the step-up passed: it resigns the
|
||||
// account's active games (so opponents are not stranded and robot games end cleanly),
|
||||
// drops its all-robot games, tombstones + anonymises the account (journalling and freeing
|
||||
// its credentials), and revokes its sessions. The tombstone is the point of no return —
|
||||
// its failure aborts; the game cleanup and session revocation around it are best-effort.
|
||||
func (s *Server) deleteAccount(ctx context.Context, uid uuid.UUID) error {
|
||||
deleter := accountdelete.NewDeleter(s.db)
|
||||
if s.games != nil {
|
||||
if games, err := s.games.ListForAccount(ctx, uid); err == nil {
|
||||
for _, g := range games {
|
||||
if g.Status != game.StatusActive {
|
||||
continue
|
||||
}
|
||||
if _, err := s.games.Resign(ctx, g.ID, uid); err != nil {
|
||||
s.log.Warn("delete: resign game failed", zap.String("game", g.ID.String()), zap.Error(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.log.Warn("delete: list games failed", zap.Error(err))
|
||||
}
|
||||
}
|
||||
if _, err := deleter.DropAllRobotGames(ctx, uid); err != nil {
|
||||
s.log.Warn("delete: drop all-robot games failed", zap.Error(err))
|
||||
}
|
||||
if err := deleter.AnonymizeAndTombstone(ctx, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.sessions != nil {
|
||||
if err := s.sessions.RevokeAllForAccount(ctx, uid); err != nil {
|
||||
s.log.Warn("delete: revoke sessions failed", zap.Error(err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -351,6 +351,26 @@ func (c *Client) ChangeEmailConfirm(ctx context.Context, userID, email, code str
|
||||
return out, err
|
||||
}
|
||||
|
||||
// DeleteRequestResp reports which account-deletion step-up the account uses.
|
||||
type DeleteRequestResp struct {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
// DeleteRequest starts account deletion: the backend mails a delete code (email accounts)
|
||||
// or reports the typed-phrase path.
|
||||
func (c *Client) DeleteRequest(ctx context.Context, userID string) (DeleteRequestResp, error) {
|
||||
var out DeleteRequestResp
|
||||
err := c.do(ctx, http.MethodPost, "/api/v1/user/delete/request", userID, "", nil, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// DeleteConfirm verifies the step-up (a mailed code or the typed phrase) and deletes the
|
||||
// account.
|
||||
func (c *Client) DeleteConfirm(ctx context.Context, userID, code, phrase string) error {
|
||||
return c.do(ctx, http.MethodPost, "/api/v1/user/delete/confirm", userID, "",
|
||||
map[string]string{"code": code, "phrase": phrase}, nil)
|
||||
}
|
||||
|
||||
// LinkUnlink detaches a platform identity (kind = "telegram" | "vk") from the caller
|
||||
// and returns the refreshed profile in the result.
|
||||
func (c *Client) LinkUnlink(ctx context.Context, userID, kind string) (LinkResultResp, error) {
|
||||
|
||||
@@ -37,6 +37,17 @@ func encodeAck(ok bool) []byte {
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeDeleteRequestResult builds an AccountDeleteRequestResult payload reporting which
|
||||
// deletion step-up the account uses ("email" | "phrase").
|
||||
func encodeDeleteRequestResult(method string) []byte {
|
||||
b := flatbuffers.NewBuilder(32)
|
||||
m := b.CreateString(method)
|
||||
fb.AccountDeleteRequestResultStart(b)
|
||||
fb.AccountDeleteRequestResultAddMethod(b, m)
|
||||
b.Finish(fb.AccountDeleteRequestResultEnd(b))
|
||||
return b.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeConfirmLinkResult builds an EmailConfirmLinkResult payload, embedding the
|
||||
// minted Session for a login. All strings and the nested Session table are built
|
||||
// before the result table is opened.
|
||||
|
||||
@@ -21,6 +21,8 @@ const (
|
||||
MsgLinkUnlink = "link.unlink"
|
||||
MsgEmailChangeRequest = "link.email.change.request"
|
||||
MsgEmailChangeConfirm = "link.email.change.confirm"
|
||||
MsgAccountDeleteReq = "account.delete.request"
|
||||
MsgAccountDeleteConf = "account.delete.confirm"
|
||||
)
|
||||
|
||||
// registerLinkOps adds the linking & merge operations. The telegram ops need the
|
||||
@@ -32,6 +34,8 @@ func registerLinkOps(r *Registry, backend *backendclient.Client, tg TelegramVali
|
||||
r.ops[MsgLinkUnlink] = Op{Handler: linkUnlinkHandler(backend), Auth: true}
|
||||
r.ops[MsgEmailChangeRequest] = Op{Handler: changeEmailRequestHandler(backend), Auth: true, Email: true}
|
||||
r.ops[MsgEmailChangeConfirm] = Op{Handler: changeEmailConfirmHandler(backend), Auth: true, Email: true}
|
||||
r.ops[MsgAccountDeleteReq] = Op{Handler: deleteRequestHandler(backend), Auth: true, Email: true}
|
||||
r.ops[MsgAccountDeleteConf] = Op{Handler: deleteConfirmHandler(backend), Auth: true}
|
||||
if tg != nil {
|
||||
r.ops[MsgLinkTelegram] = Op{Handler: linkTelegramHandler(backend, tg, false), Auth: true}
|
||||
r.ops[MsgLinkTelegramMerge] = Op{Handler: linkTelegramHandler(backend, tg, true), Auth: true}
|
||||
@@ -94,6 +98,28 @@ func changeEmailConfirmHandler(backend *backendclient.Client) Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// deleteRequestHandler starts account deletion, returning which step-up the account uses.
|
||||
func deleteRequestHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
res, err := backend.DeleteRequest(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeDeleteRequestResult(res.Method), nil
|
||||
}
|
||||
}
|
||||
|
||||
// deleteConfirmHandler verifies the step-up proof and deletes the account.
|
||||
func deleteConfirmHandler(backend *backendclient.Client) Handler {
|
||||
return func(ctx context.Context, req Request) ([]byte, error) {
|
||||
in := fb.GetRootAsAccountDeleteConfirm(req.Payload, 0)
|
||||
if err := backend.DeleteConfirm(ctx, req.UserID, string(in.Code()), string(in.Phrase())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeAck(true), nil
|
||||
}
|
||||
}
|
||||
|
||||
// linkUnlinkHandler detaches a platform identity (telegram|vk) from the caller and
|
||||
// returns the refreshed link result (status "unlinked" + the updated profile).
|
||||
func linkUnlinkHandler(backend *backendclient.Client) Handler {
|
||||
|
||||
@@ -507,6 +507,19 @@ table LinkUnlinkRequest {
|
||||
kind:string;
|
||||
}
|
||||
|
||||
// AccountDeleteConfirm carries the account-deletion step-up proof: a mailed code (email
|
||||
// accounts) or the typed phrase (platform-only accounts).
|
||||
table AccountDeleteConfirm {
|
||||
code:string;
|
||||
phrase:string;
|
||||
}
|
||||
|
||||
// AccountDeleteRequestResult reports which deletion step-up the account uses: "email"
|
||||
// (a code was mailed) or "phrase" (type the confirmation phrase).
|
||||
table AccountDeleteRequestResult {
|
||||
method:string;
|
||||
}
|
||||
|
||||
// LinkResult is the unified result of a confirm or merge step. status is "linked"
|
||||
// (bound to the caller), "merge_required" (the identity belongs to another account —
|
||||
// the secondary_* fields summarise it for the irreversible confirmation), or
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package scrabblefb
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type AccountDeleteConfirm struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsAccountDeleteConfirm(buf []byte, offset flatbuffers.UOffsetT) *AccountDeleteConfirm {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &AccountDeleteConfirm{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishAccountDeleteConfirmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsAccountDeleteConfirm(buf []byte, offset flatbuffers.UOffsetT) *AccountDeleteConfirm {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &AccountDeleteConfirm{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedAccountDeleteConfirmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *AccountDeleteConfirm) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *AccountDeleteConfirm) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *AccountDeleteConfirm) Code() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *AccountDeleteConfirm) Phrase() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AccountDeleteConfirmStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(2)
|
||||
}
|
||||
func AccountDeleteConfirmAddCode(builder *flatbuffers.Builder, code flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(code), 0)
|
||||
}
|
||||
func AccountDeleteConfirmAddPhrase(builder *flatbuffers.Builder, phrase flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(phrase), 0)
|
||||
}
|
||||
func AccountDeleteConfirmEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package scrabblefb
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type AccountDeleteRequestResult struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsAccountDeleteRequestResult(buf []byte, offset flatbuffers.UOffsetT) *AccountDeleteRequestResult {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &AccountDeleteRequestResult{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishAccountDeleteRequestResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsAccountDeleteRequestResult(buf []byte, offset flatbuffers.UOffsetT) *AccountDeleteRequestResult {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &AccountDeleteRequestResult{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedAccountDeleteRequestResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *AccountDeleteRequestResult) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *AccountDeleteRequestResult) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *AccountDeleteRequestResult) Method() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AccountDeleteRequestResultStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(1)
|
||||
}
|
||||
func AccountDeleteRequestResultAddMethod(builder *flatbuffers.Builder, method flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(method), 0)
|
||||
}
|
||||
func AccountDeleteRequestResultEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
export { AccountDeleteConfirm } from './scrabblefb/account-delete-confirm.js';
|
||||
export { AccountDeleteRequestResult } from './scrabblefb/account-delete-request-result.js';
|
||||
export { AccountRef } from './scrabblefb/account-ref.js';
|
||||
export { Ack } from './scrabblefb/ack.js';
|
||||
export { AlphabetEntry } from './scrabblefb/alphabet-entry.js';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
export class AccountDeleteConfirm {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):AccountDeleteConfirm {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsAccountDeleteConfirm(bb:flatbuffers.ByteBuffer, obj?:AccountDeleteConfirm):AccountDeleteConfirm {
|
||||
return (obj || new AccountDeleteConfirm()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsAccountDeleteConfirm(bb:flatbuffers.ByteBuffer, obj?:AccountDeleteConfirm):AccountDeleteConfirm {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new AccountDeleteConfirm()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
code():string|null
|
||||
code(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
code(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
phrase():string|null
|
||||
phrase(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
phrase(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startAccountDeleteConfirm(builder:flatbuffers.Builder) {
|
||||
builder.startObject(2);
|
||||
}
|
||||
|
||||
static addCode(builder:flatbuffers.Builder, codeOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, codeOffset, 0);
|
||||
}
|
||||
|
||||
static addPhrase(builder:flatbuffers.Builder, phraseOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, phraseOffset, 0);
|
||||
}
|
||||
|
||||
static endAccountDeleteConfirm(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createAccountDeleteConfirm(builder:flatbuffers.Builder, codeOffset:flatbuffers.Offset, phraseOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
AccountDeleteConfirm.startAccountDeleteConfirm(builder);
|
||||
AccountDeleteConfirm.addCode(builder, codeOffset);
|
||||
AccountDeleteConfirm.addPhrase(builder, phraseOffset);
|
||||
return AccountDeleteConfirm.endAccountDeleteConfirm(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
export class AccountDeleteRequestResult {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):AccountDeleteRequestResult {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsAccountDeleteRequestResult(bb:flatbuffers.ByteBuffer, obj?:AccountDeleteRequestResult):AccountDeleteRequestResult {
|
||||
return (obj || new AccountDeleteRequestResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsAccountDeleteRequestResult(bb:flatbuffers.ByteBuffer, obj?:AccountDeleteRequestResult):AccountDeleteRequestResult {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new AccountDeleteRequestResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
method():string|null
|
||||
method(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
method(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startAccountDeleteRequestResult(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addMethod(builder:flatbuffers.Builder, methodOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, methodOffset, 0);
|
||||
}
|
||||
|
||||
static endAccountDeleteRequestResult(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createAccountDeleteRequestResult(builder:flatbuffers.Builder, methodOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
AccountDeleteRequestResult.startAccountDeleteRequestResult(builder);
|
||||
AccountDeleteRequestResult.addMethod(builder, methodOffset);
|
||||
return AccountDeleteRequestResult.endAccountDeleteRequestResult(builder);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user