From 0fb8803a609620b5187ca6372e13846b8721c9a3 Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Tue, 1 Nov 2022 11:32:57 -0400 Subject: [PATCH] API: implement user account deletion --- api/account/middleware.go | 53 +++++++++++++++++++++++++++++++++++ api/go.mod | 2 +- api/graph/schema.graphqls | 11 ++++++++ api/graph/schema.resolvers.go | 8 ++++++ api/server.go | 12 ++++++-- 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 api/account/middleware.go diff --git a/api/account/middleware.go b/api/account/middleware.go new file mode 100644 index 0000000..2ce5805 --- /dev/null +++ b/api/account/middleware.go @@ -0,0 +1,53 @@ +package account + +import ( + "context" + "database/sql" + "log" + "net/http" + + "git.sr.ht/~sircmpwn/core-go/database" + work "git.sr.ht/~sircmpwn/dowork" +) + +type contextKey struct { + name string +} + +var ctxKey = &contextKey{"account"} + +func Middleware(queue *work.Queue) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), ctxKey, queue) + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + }) + } +} + +// Schedules a user account deletion. +func Delete(ctx context.Context, userID int, username string) { + queue, ok := ctx.Value(ctxKey).(*work.Queue) + if !ok { + panic("No account worker for this context") + } + + task := work.NewTask(func(ctx context.Context) error { + log.Printf("Processing deletion of user account %d %s", userID, username) + + if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error { + _, err := tx.ExecContext(ctx, ` + DELETE FROM "user" WHERE id = $1; + `, userID) + return err + }); err != nil { + return err + } + + log.Printf("Deletion of user account %d %s complete", userID, username) + return nil + }) + queue.Enqueue(task) + log.Printf("Enqueued deletion of user account %d %s", userID, username) +} diff --git a/api/go.mod b/api/go.mod index e70c4f1..7b48940 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( git.sr.ht/~sircmpwn/core-go v0.0.0-20221025082458-3e69641ef307 + git.sr.ht/~sircmpwn/dowork v0.0.0-20210820133136-d3970e97def3 github.com/99designs/gqlgen v0.17.20 github.com/Masterminds/squirrel v1.5.0 github.com/go-chi/chi v4.1.2+incompatible @@ -14,7 +15,6 @@ require ( ) require ( - git.sr.ht/~sircmpwn/dowork v0.0.0-20210820133136-d3970e97def3 // indirect git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 // indirect git.sr.ht/~sircmpwn/go-bare v0.0.0-20210227202403-5dae5c48f917 // indirect github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index fd5c8c8..068683c 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -21,6 +21,12 @@ directive @private on FIELD_DEFINITION "Used to provide a human-friendly description of an access scope." directive @scopehelp(details: String!) on ENUM_VALUE +""" +This used to decorate fields which are for internal use, and are not +available to normal API users. +""" +directive @internal on FIELD_DEFINITION + enum AccessScope { PROFILE @scopehelp(details: "profile information") PASTES @scopehelp(details: "pastes") @@ -288,4 +294,9 @@ type Mutation { unexpected behavior with the third-party integration. """ deleteUserWebhook(id: Int!): WebhookSubscription! + + """ + Deletes the authenticated user's account. Internal use only. + """ + deleteUser: Int! @internal } diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index e12f651..ea2e80e 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -23,6 +23,7 @@ import ( "git.sr.ht/~sircmpwn/core-go/server" "git.sr.ht/~sircmpwn/core-go/valid" corewebhooks "git.sr.ht/~sircmpwn/core-go/webhooks" + "git.sr.ht/~sircmpwn/paste.sr.ht/api/account" "git.sr.ht/~sircmpwn/paste.sr.ht/api/graph/api" "git.sr.ht/~sircmpwn/paste.sr.ht/api/graph/model" "git.sr.ht/~sircmpwn/paste.sr.ht/api/loaders" @@ -350,6 +351,13 @@ func (r *mutationResolver) DeleteUserWebhook(ctx context.Context, id int) (model return &sub, nil } +// DeleteUser is the resolver for the deleteUser field. +func (r *mutationResolver) DeleteUser(ctx context.Context) (int, error) { + user := auth.ForContext(ctx) + account.Delete(ctx, user.UserID, user.Username) + return user.UserID, nil +} + // Files is the resolver for the files field. func (r *pasteResolver) Files(ctx context.Context, obj *model.Paste) ([]*model.File, error) { var files []*model.File diff --git a/api/server.go b/api/server.go index 0fff069..43696ea 100644 --- a/api/server.go +++ b/api/server.go @@ -9,9 +9,11 @@ import ( "git.sr.ht/~sircmpwn/core-go/database" "git.sr.ht/~sircmpwn/core-go/server" "git.sr.ht/~sircmpwn/core-go/webhooks" + work "git.sr.ht/~sircmpwn/dowork" "github.com/99designs/gqlgen/graphql" "github.com/go-chi/chi" + "git.sr.ht/~sircmpwn/paste.sr.ht/api/account" "git.sr.ht/~sircmpwn/paste.sr.ht/api/graph" "git.sr.ht/~sircmpwn/paste.sr.ht/api/graph/api" "git.sr.ht/~sircmpwn/paste.sr.ht/api/graph/model" @@ -23,6 +25,7 @@ func main() { gqlConfig := api.Config{Resolvers: &graph.Resolver{}} gqlConfig.Directives.Private = server.Private + gqlConfig.Directives.Internal = server.Internal gqlConfig.Directives.Access = func(ctx context.Context, obj interface{}, next graphql.Resolver, scope model.AccessScope, kind model.AccessKind) (interface{}, error) { @@ -35,13 +38,18 @@ func main() { scopes[i] = s.String() } + accountQueue := work.NewQueue("account") webhookQueue := webhooks.NewQueue(schema) gsrv := server.NewServer("paste.sr.ht", appConfig). WithDefaultMiddleware(). - WithMiddleware(loaders.Middleware, webhooks.Middleware(webhookQueue)). + WithMiddleware( + loaders.Middleware, + account.Middleware(accountQueue), + webhooks.Middleware(webhookQueue), + ). WithSchema(schema, scopes). - WithQueues(webhookQueue.Queue) + WithQueues(accountQueue, webhookQueue.Queue) // Bulk transfer endpoints gsrv.Router().Get("/query/blob/{id}", func(w http.ResponseWriter, r *http.Request) {