diff --git a/backend/internal/account/email.go b/backend/internal/account/email.go index 1b1420f..d65015d 100644 --- a/backend/internal/account/email.go +++ b/backend/internal/account/email.go @@ -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 { diff --git a/backend/internal/account/emailtemplate.go b/backend/internal/account/emailtemplate.go index 9222c2c..7bff757 100644 --- a/backend/internal/account/emailtemplate.go +++ b/backend/internal/account/emailtemplate.go @@ -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. diff --git a/backend/internal/account/link.go b/backend/internal/account/link.go index fc9db93..c897e15 100644 --- a/backend/internal/account/link.go +++ b/backend/internal/account/link.go @@ -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) { diff --git a/backend/internal/inttest/delete_test.go b/backend/internal/inttest/delete_test.go index 04697fa..7dcabcd 100644 --- a/backend/internal/inttest/delete_test.go +++ b/backend/internal/inttest/delete_test.go @@ -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) + } +} diff --git a/backend/internal/server/handlers.go b/backend/internal/server/handlers.go index dc442a6..6d55e1e 100644 --- a/backend/internal/server/handlers.go +++ b/backend/internal/server/handlers.go @@ -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) diff --git a/backend/internal/server/handlers_delete.go b/backend/internal/server/handlers_delete.go new file mode 100644 index 0000000..9bd9da8 --- /dev/null +++ b/backend/internal/server/handlers_delete.go @@ -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 +} diff --git a/gateway/internal/backendclient/api_social.go b/gateway/internal/backendclient/api_social.go index 164edf0..7a79288 100644 --- a/gateway/internal/backendclient/api_social.go +++ b/gateway/internal/backendclient/api_social.go @@ -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) { diff --git a/gateway/internal/transcode/encode.go b/gateway/internal/transcode/encode.go index 8cb2562..afc5434 100644 --- a/gateway/internal/transcode/encode.go +++ b/gateway/internal/transcode/encode.go @@ -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. diff --git a/gateway/internal/transcode/transcode_link.go b/gateway/internal/transcode/transcode_link.go index 5d6d3af..72f2c70 100644 --- a/gateway/internal/transcode/transcode_link.go +++ b/gateway/internal/transcode/transcode_link.go @@ -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 { diff --git a/pkg/fbs/scrabble.fbs b/pkg/fbs/scrabble.fbs index 758a212..c078538 100644 --- a/pkg/fbs/scrabble.fbs +++ b/pkg/fbs/scrabble.fbs @@ -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 diff --git a/pkg/fbs/scrabblefb/AccountDeleteConfirm.go b/pkg/fbs/scrabblefb/AccountDeleteConfirm.go new file mode 100644 index 0000000..e28fa49 --- /dev/null +++ b/pkg/fbs/scrabblefb/AccountDeleteConfirm.go @@ -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() +} diff --git a/pkg/fbs/scrabblefb/AccountDeleteRequestResult.go b/pkg/fbs/scrabblefb/AccountDeleteRequestResult.go new file mode 100644 index 0000000..40837b4 --- /dev/null +++ b/pkg/fbs/scrabblefb/AccountDeleteRequestResult.go @@ -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() +} diff --git a/ui/src/gen/fbs/scrabblefb.ts b/ui/src/gen/fbs/scrabblefb.ts index ce02ba1..73aa84a 100644 --- a/ui/src/gen/fbs/scrabblefb.ts +++ b/ui/src/gen/fbs/scrabblefb.ts @@ -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'; diff --git a/ui/src/gen/fbs/scrabblefb/account-delete-confirm.ts b/ui/src/gen/fbs/scrabblefb/account-delete-confirm.ts new file mode 100644 index 0000000..f0f120b --- /dev/null +++ b/ui/src/gen/fbs/scrabblefb/account-delete-confirm.ts @@ -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); +} +} diff --git a/ui/src/gen/fbs/scrabblefb/account-delete-request-result.ts b/ui/src/gen/fbs/scrabblefb/account-delete-request-result.ts new file mode 100644 index 0000000..1c92dae --- /dev/null +++ b/ui/src/gen/fbs/scrabblefb/account-delete-request-result.ts @@ -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); +} +}