A => api/account/middleware.go +64 -0
@@ 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)
+}
M api/go.mod +1 -0
@@ 4,6 4,7 @@ go 1.14
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
M api/graph/schema.graphqls +11 -0
@@ 11,6 11,12 @@ access token, and are not available to c
"""
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 @@ type Mutation {
unexpected behavior with the third-party integration.
"""
deleteWebhook(id: Int!): WebhookSubscription!
+
+ """
+ Deletes the authenticated user's account. Internal use only.
+ """
+ deleteUser: Int!
}
M api/graph/schema.resolvers.go +8 -0
@@ 24,6 24,7 @@ import (
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 @@ func (r *mutationResolver) DeleteWebhook
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)
M api/server.go +10 -1
@@ 10,7 10,9 @@ import (
"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 @@ 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) {
@@ 55,6 58,7 @@ func main() {
scopes[i] = s.String()
}
+ accountQueue := work.NewQueue("account")
webhooksQueue := webhooks.NewQueue(schema)
legacyWebhooks := webhooks.NewLegacyQueue()
@@ 64,11 68,16 @@ func main() {
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()
}