API: implement user account deletion
This commit is contained in:
parent
2eac1686b7
commit
d05425f4aa
|
@ -0,0 +1,54 @@
|
|||
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)
|
||||
}
|
|
@ -4,6 +4,7 @@ go 1.16
|
|||
|
||||
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/gocelery/gocelery v0.0.0-20201111034804-825d89059344
|
||||
|
|
|
@ -14,6 +14,12 @@ access token, and are not available to clients using OAuth 2.0 access tokens.
|
|||
"""
|
||||
directive @private on FIELD_DEFINITION
|
||||
|
||||
"""
|
||||
This is 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")
|
||||
JOBS @scopehelp(details: "build jobs")
|
||||
|
@ -488,4 +494,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
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/builds.sr.ht/api/account"
|
||||
"git.sr.ht/~sircmpwn/builds.sr.ht/api/graph/api"
|
||||
"git.sr.ht/~sircmpwn/builds.sr.ht/api/graph/model"
|
||||
"git.sr.ht/~sircmpwn/builds.sr.ht/api/loaders"
|
||||
|
@ -693,6 +694,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
|
||||
}
|
||||
|
||||
// PrivateKey is the resolver for the privateKey field.
|
||||
func (r *pGPKeyResolver) PrivateKey(ctx context.Context, obj *model.PGPKey) (string, error) {
|
||||
// TODO: This is simple to implement, but I'm not going to rig it up until
|
||||
|
|
|
@ -7,8 +7,10 @@ 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"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
|
||||
"git.sr.ht/~sircmpwn/builds.sr.ht/api/account"
|
||||
"git.sr.ht/~sircmpwn/builds.sr.ht/api/graph"
|
||||
"git.sr.ht/~sircmpwn/builds.sr.ht/api/graph/api"
|
||||
"git.sr.ht/~sircmpwn/builds.sr.ht/api/graph/model"
|
||||
|
@ -20,6 +22,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) {
|
||||
|
@ -36,12 +39,20 @@ func main() {
|
|||
scopes[i] = s.String()
|
||||
}
|
||||
|
||||
accountQueue := work.NewQueue("account")
|
||||
webhookQueue := webhooks.NewQueue(schema)
|
||||
|
||||
server.NewServer("builds.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,
|
||||
).
|
||||
Run()
|
||||
}
|
||||
|
|
|
@ -3,4 +3,3 @@ import buildsrht.alembic
|
|||
import srht.alembic
|
||||
from srht.database import alembic
|
||||
alembic("builds.sr.ht", buildsrht.alembic)
|
||||
alembic("builds.sr.ht", srht.alembic)
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
"""Configure cascades on relationships
|
||||
|
||||
Revision ID: 7e863c9389ef
|
||||
Revises: 6e5389a7ff68
|
||||
Create Date: 2022-11-01 15:31:46.416272
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7e863c9389ef'
|
||||
down_revision = '6e5389a7ff68'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
cascades = [
|
||||
("secret", "user", "user_id", "CASCADE"),
|
||||
("job_group", "user", "owner_id", "CASCADE"),
|
||||
("job", "user", "owner_id", "CASCADE"),
|
||||
("job", "job_group", "job_group_id", "SET NULL"),
|
||||
("artifact", "job", "job_id", "CASCADE"),
|
||||
("task", "job", "job_id", "CASCADE"),
|
||||
("trigger", "job", "job_id", "CASCADE"),
|
||||
("trigger", "job_group", "job_group_id", "CASCADE"),
|
||||
("gql_user_wh_sub", "user", "user_id", "CASCADE"),
|
||||
("oauthtoken", "user", "user_id", "CASCADE"),
|
||||
]
|
||||
|
||||
def upgrade():
|
||||
for (table, relation, col, do) in cascades:
|
||||
op.execute(f"""
|
||||
ALTER TABLE {table} DROP CONSTRAINT IF EXISTS {table}_{col}_fkey;
|
||||
ALTER TABLE {table} ADD CONSTRAINT {table}_{col}_fkey
|
||||
FOREIGN KEY ({col})
|
||||
REFERENCES "{relation}"(id) ON DELETE {do};
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
for (table, relation, col, do) in tables:
|
||||
op.execute(f"""
|
||||
ALTER TABLE {table} DROP CONSTRAINT IF EXISTS {table}_{col}_fkey;
|
||||
ALTER TABLE {table} ADD CONSTRAINT {table}_{col}_fkey FOREIGN KEY ({col}) REFERENCES "{relation}"(id);
|
||||
""")
|
20
schema.sql
20
schema.sql
|
@ -29,7 +29,7 @@ CREATE TABLE "user" (
|
|||
|
||||
CREATE TABLE secret (
|
||||
id serial PRIMARY KEY,
|
||||
user_id integer NOT NULL REFERENCES "user"(id),
|
||||
user_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created timestamp without time zone NOT NULL,
|
||||
updated timestamp without time zone NOT NULL,
|
||||
uuid uuid NOT NULL,
|
||||
|
@ -48,7 +48,7 @@ CREATE TABLE job_group (
|
|||
id serial PRIMARY KEY,
|
||||
created timestamp without time zone NOT NULL,
|
||||
updated timestamp without time zone NOT NULL,
|
||||
owner_id integer NOT NULL REFERENCES "user"(id),
|
||||
owner_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
note character varying(4096)
|
||||
);
|
||||
|
||||
|
@ -57,8 +57,8 @@ CREATE TABLE job (
|
|||
created timestamp without time zone NOT NULL,
|
||||
updated timestamp without time zone NOT NULL,
|
||||
manifest character varying(16384) NOT NULL,
|
||||
owner_id integer NOT NULL REFERENCES "user"(id),
|
||||
job_group_id integer REFERENCES job_group(id),
|
||||
owner_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
job_group_id integer REFERENCES job_group(id) ON DELETE SET NULL,
|
||||
note character varying(4096),
|
||||
tags character varying,
|
||||
runner character varying,
|
||||
|
@ -72,7 +72,7 @@ CREATE INDEX ix_job_owner_id ON job USING btree (owner_id);
|
|||
CREATE TABLE artifact (
|
||||
id serial PRIMARY KEY,
|
||||
created timestamp without time zone NOT NULL,
|
||||
job_id integer NOT NULL REFERENCES job(id),
|
||||
job_id integer NOT NULL REFERENCES job(id) ON DELETE CASCADE,
|
||||
name character varying NOT NULL,
|
||||
path character varying NOT NULL,
|
||||
url character varying NOT NULL,
|
||||
|
@ -85,7 +85,7 @@ CREATE TABLE task (
|
|||
updated timestamp without time zone NOT NULL,
|
||||
name character varying(256) NOT NULL,
|
||||
status character varying NOT NULL,
|
||||
job_id integer NOT NULL REFERENCES job(id)
|
||||
job_id integer NOT NULL REFERENCES job(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX ix_task_job_id ON task USING btree (job_id);
|
||||
|
@ -97,8 +97,8 @@ CREATE TABLE trigger (
|
|||
details character varying(4096) NOT NULL,
|
||||
condition character varying NOT NULL,
|
||||
trigger_type character varying NOT NULL,
|
||||
job_id integer REFERENCES job(id),
|
||||
job_group_id integer REFERENCES job_group(id)
|
||||
job_id integer REFERENCES job(id) ON DELETE CASCADE,
|
||||
job_group_id integer REFERENCES job_group(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- GraphQL webhooks
|
||||
|
@ -114,7 +114,7 @@ CREATE TABLE gql_user_wh_sub (
|
|||
client_id uuid,
|
||||
expires timestamp without time zone,
|
||||
node_id character varying,
|
||||
user_id integer NOT NULL REFERENCES "user"(id),
|
||||
user_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
CONSTRAINT gql_user_wh_sub_auth_method_check
|
||||
CHECK ((auth_method = ANY (ARRAY['OAUTH2'::auth_method, 'INTERNAL'::public.auth_method]))),
|
||||
CONSTRAINT gql_user_wh_sub_check
|
||||
|
@ -150,7 +150,7 @@ CREATE TABLE oauthtoken (
|
|||
created timestamp without time zone NOT NULL,
|
||||
updated timestamp without time zone NOT NULL,
|
||||
expires timestamp without time zone NOT NULL,
|
||||
user_id integer REFERENCES "user"(id),
|
||||
user_id integer REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
token_hash character varying(128) NOT NULL,
|
||||
token_partial character varying(8) NOT NULL,
|
||||
scopes character varying(512) NOT NULL
|
||||
|
|
Loading…
Reference in New Issue