# HG changeset patch # User Drew DeVault # Date 1667306349 -3600 # Tue Nov 01 13:39:09 2022 +0100 # Node ID 1a3eeaf60da57f821b6264d3e6da21651bacf2e5 # Parent 01c9b16b71d10d2e43e9237e93eb05a40195d35c API: implement user account deletion diff --git a/api/account/middleware.go b/api/account/middleware.go new file mode 100644 --- /dev/null +++ b/api/account/middleware.go @@ -0,0 +1,64 @@ +package account + +import ( + "context" + "database/sql" + "log" + "net/http" + "os" + "path" + + "git.sr.ht/~sircmpwn/core-go/config" + "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") + } + + conf := config.ForContext(ctx) + repoStore, ok := conf.Get("hg.sr.ht", "repos") + + task := work.NewTask(func(ctx context.Context) error { + log.Printf("Processing deletion of user account %d %s", userID, username) + + userPath := path.Join(repoStore, "~"+username) + if err := os.RemoveAll(userPath); err != nil { + log.Printf("Failed to remove %s: %s", userPath, err.Error()) + } + + 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 --- a/api/go.mod +++ b/api/go.mod @@ -4,6 +4,7 @@ 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.4.0 github.com/google/uuid v1.0.0 diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -11,6 +11,12 @@ """ directive @private on FIELD_DEFINITION +""" +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") REPOSITORIES @scopehelp(details: "repository metadata") @@ -433,4 +439,9 @@ unexpected behavior with the third-party integration. """ deleteWebhook(id: Int!): WebhookSubscription! + + """ + Deletes the authenticated user's account. Internal use only. + """ + deleteUser: Int! } diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -24,6 +24,7 @@ corewebhooks "git.sr.ht/~sircmpwn/core-go/webhooks" sq "github.com/Masterminds/squirrel" "github.com/lib/pq" + "hg.sr.ht/~sircmpwn/hg.sr.ht/api/account" "hg.sr.ht/~sircmpwn/hg.sr.ht/api/graph/api" "hg.sr.ht/~sircmpwn/hg.sr.ht/api/graph/model" "hg.sr.ht/~sircmpwn/hg.sr.ht/api/hgpool" @@ -503,6 +504,13 @@ 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 +} + // Version is the resolver for the version field. func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) { conf := config.ForContext(ctx) diff --git a/api/server.go b/api/server.go --- a/api/server.go +++ b/api/server.go @@ -10,7 +10,9 @@ "git.sr.ht/~sircmpwn/core-go/config" "git.sr.ht/~sircmpwn/core-go/server" "git.sr.ht/~sircmpwn/core-go/webhooks" + work "git.sr.ht/~sircmpwn/dowork" + "hg.sr.ht/~sircmpwn/hg.sr.ht/api/account" "hg.sr.ht/~sircmpwn/hg.sr.ht/api/graph" "hg.sr.ht/~sircmpwn/hg.sr.ht/api/graph/api" "hg.sr.ht/~sircmpwn/hg.sr.ht/api/graph/model" @@ -43,6 +45,7 @@ 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) { @@ -55,6 +58,7 @@ scopes[i] = s.String() } + accountQueue := work.NewQueue("account") webhooksQueue := webhooks.NewQueue(schema) legacyWebhooks := webhooks.NewLegacyQueue() @@ -64,11 +68,16 @@ WithDefaultMiddleware(). WithMiddleware( loaders.Middleware, + account.Middleware(accountQueue), webhooks.Middleware(webhooksQueue), webhooks.LegacyMiddleware(legacyWebhooks), hgpool.Middleware(globalPool), ). WithSchema(schema, scopes). - WithQueues(webhooksQueue.Queue, legacyWebhooks.Queue). + WithQueues( + accountQueue, + webhooksQueue.Queue, + legacyWebhooks.Queue, + ). Run() }