API: implement user account deletion
This commit is contained in:
parent
a26fc1fbfd
commit
d1c0e43b66
|
@ -0,0 +1,68 @@
|
|||
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, `
|
||||
UPDATE participant
|
||||
SET
|
||||
participant_type = 'email',
|
||||
user_id = null,
|
||||
email = u.email,
|
||||
email_name = '~' || u.username
|
||||
FROM "user" u
|
||||
WHERE u.id = $1 AND participant.user_id = $1
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, 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)
|
||||
}
|
|
@ -991,4 +991,9 @@ type Mutation {
|
|||
|
||||
"Deletes a ticket webhook."
|
||||
deleteTicketWebhook(id: Int!): WebhookSubscription!
|
||||
|
||||
"""
|
||||
Deletes the authenticated user's account. Internal use only.
|
||||
"""
|
||||
deleteUser: Int! @internal
|
||||
}
|
||||
|
|
|
@ -20,6 +20,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/todo.sr.ht/api/account"
|
||||
"git.sr.ht/~sircmpwn/todo.sr.ht/api/graph/api"
|
||||
"git.sr.ht/~sircmpwn/todo.sr.ht/api/graph/model"
|
||||
"git.sr.ht/~sircmpwn/todo.sr.ht/api/imports"
|
||||
|
@ -2404,6 +2405,13 @@ func (r *mutationResolver) DeleteTicketWebhook(ctx context.Context, id int) (mod
|
|||
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) {
|
||||
return &model.Version{
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
work "git.sr.ht/~sircmpwn/dowork"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
|
||||
"git.sr.ht/~sircmpwn/todo.sr.ht/api/account"
|
||||
"git.sr.ht/~sircmpwn/todo.sr.ht/api/graph"
|
||||
"git.sr.ht/~sircmpwn/todo.sr.ht/api/graph/api"
|
||||
"git.sr.ht/~sircmpwn/todo.sr.ht/api/graph/model"
|
||||
|
@ -34,6 +35,7 @@ func main() {
|
|||
scopes[i] = s.String()
|
||||
}
|
||||
|
||||
accountQueue := work.NewQueue("account")
|
||||
importsQueue := work.NewQueue("imports")
|
||||
webhookQueue := webhooks.NewQueue(schema)
|
||||
legacyWebhooks := webhooks.NewLegacyQueue()
|
||||
|
@ -43,10 +45,16 @@ func main() {
|
|||
WithMiddleware(
|
||||
loaders.Middleware,
|
||||
imports.Middleware(importsQueue),
|
||||
account.Middleware(accountQueue),
|
||||
webhooks.Middleware(webhookQueue),
|
||||
webhooks.LegacyMiddleware(legacyWebhooks),
|
||||
).
|
||||
WithSchema(schema, scopes).
|
||||
WithQueues(importsQueue, webhookQueue.Queue, legacyWebhooks.Queue).
|
||||
WithQueues(
|
||||
importsQueue,
|
||||
accountQueue,
|
||||
webhookQueue.Queue,
|
||||
legacyWebhooks.Queue,
|
||||
).
|
||||
Run()
|
||||
}
|
||||
|
|
32
schema.sql
32
schema.sql
|
@ -70,7 +70,7 @@ CREATE TABLE participant (
|
|||
|
||||
CREATE TABLE tracker (
|
||||
id serial PRIMARY KEY,
|
||||
owner_id integer NOT NULL REFERENCES "user"(id),
|
||||
owner_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
created timestamp without time zone NOT NULL,
|
||||
updated timestamp without time zone NOT NULL,
|
||||
name character varying(1024),
|
||||
|
@ -113,7 +113,7 @@ CREATE TABLE ticket (
|
|||
status integer DEFAULT 0 NOT NULL,
|
||||
resolution integer DEFAULT 0 NOT NULL,
|
||||
scoped_id integer NOT NULL,
|
||||
submitter_id integer NOT NULL REFERENCES participant(id),
|
||||
submitter_id integer NOT NULL REFERENCES participant(id) ON DELETE CASCADE,
|
||||
authenticity integer DEFAULT 0 NOT NULL,
|
||||
comment_count integer DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT uq_ticket_scoped_id_tracker_id UNIQUE (scoped_id, tracker_id),
|
||||
|
@ -135,8 +135,8 @@ CREATE TABLE ticket_assignee (
|
|||
created timestamp without time zone NOT NULL,
|
||||
updated timestamp without time zone NOT NULL,
|
||||
ticket_id integer NOT NULL REFERENCES ticket(id) ON DELETE CASCADE,
|
||||
assignee_id integer NOT NULL REFERENCES "user"(id),
|
||||
assigner_id integer NOT NULL REFERENCES "user"(id),
|
||||
assignee_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
assigner_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
CONSTRAINT idx_ticket_assignee_unique UNIQUE (ticket_id, assignee_id)
|
||||
);
|
||||
|
||||
|
@ -162,7 +162,7 @@ CREATE INDEX ticket_comment_ticket_id ON ticket_comment USING btree (ticket_id);
|
|||
CREATE TABLE ticket_label (
|
||||
ticket_id integer NOT NULL REFERENCES ticket(id) ON DELETE CASCADE,
|
||||
label_id integer NOT NULL REFERENCES label(id) ON DELETE CASCADE,
|
||||
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,
|
||||
PRIMARY KEY (ticket_id, label_id)
|
||||
);
|
||||
|
@ -175,7 +175,7 @@ CREATE TABLE ticket_subscription (
|
|||
updated timestamp without time zone NOT NULL,
|
||||
ticket_id integer REFERENCES ticket(id) ON DELETE CASCADE,
|
||||
tracker_id integer REFERENCES tracker(id) ON DELETE CASCADE,
|
||||
participant_id integer REFERENCES participant(id),
|
||||
participant_id integer REFERENCES participant(id) ON DELETE CASCADE,
|
||||
CONSTRAINT subscription_ticket_participant_uq UNIQUE (ticket_id, participant_id),
|
||||
CONSTRAINT subscription_tracker_participant_uq UNIQUE (tracker_id, participant_id)
|
||||
);
|
||||
|
@ -192,8 +192,8 @@ CREATE TABLE event (
|
|||
comment_id integer REFERENCES ticket_comment(id) ON DELETE CASCADE,
|
||||
label_id integer REFERENCES label(id) ON DELETE CASCADE,
|
||||
from_ticket_id integer REFERENCES ticket(id) ON DELETE CASCADE,
|
||||
participant_id integer REFERENCES participant(id),
|
||||
by_participant_id integer REFERENCES participant(id)
|
||||
participant_id integer REFERENCES participant(id) ON DELETE CASCADE,
|
||||
by_participant_id integer REFERENCES participant(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX event_comment_id ON event USING btree (comment_id);
|
||||
|
@ -210,7 +210,7 @@ CREATE TABLE event_notification (
|
|||
id serial PRIMARY KEY,
|
||||
created timestamp without time zone NOT NULL,
|
||||
event_id integer NOT NULL REFERENCES event(id) ON DELETE CASCADE,
|
||||
user_id integer NOT NULL REFERENCES "user"(id)
|
||||
user_id integer NOT NULL REFERENCES "user"(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX event_notification_event_id ON event_notification USING btree (event_id);
|
||||
|
@ -228,7 +228,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'::auth_method]))),
|
||||
CONSTRAINT gql_user_wh_sub_check
|
||||
|
@ -267,7 +267,7 @@ CREATE TABLE gql_tracker_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,
|
||||
tracker_id integer NOT NULL REFERENCES tracker(id) ON DELETE CASCADE,
|
||||
CONSTRAINT gql_tracker_wh_sub_auth_method_check
|
||||
CHECK ((auth_method = ANY (ARRAY['OAUTH2'::auth_method, 'INTERNAL'::auth_method]))),
|
||||
|
@ -307,7 +307,7 @@ CREATE TABLE gql_ticket_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,
|
||||
tracker_id integer NOT NULL REFERENCES tracker(id) ON DELETE CASCADE,
|
||||
ticket_id integer NOT NULL REFERENCES ticket(id) ON DELETE CASCADE,
|
||||
scoped_id integer NOT NULL,
|
||||
|
@ -346,7 +346,7 @@ CREATE TABLE oauthtoken (
|
|||
token_hash character varying(128) NOT NULL,
|
||||
token_partial character varying(8) NOT NULL,
|
||||
scopes character varying(512) NOT NULL,
|
||||
user_id integer REFERENCES "user"(id)
|
||||
user_id integer REFERENCES "user"(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Legacy webhooks (TODO: Remove)
|
||||
|
@ -355,8 +355,8 @@ CREATE TABLE user_webhook_subscription (
|
|||
created timestamp without time zone NOT NULL,
|
||||
url character varying(2048) NOT NULL,
|
||||
events character varying NOT NULL,
|
||||
user_id integer REFERENCES "user"(id),
|
||||
token_id integer REFERENCES oauthtoken(id)
|
||||
user_id integer REFERENCES "user"(id), ON DELETE CASCADE
|
||||
token_id integer REFERENCES oauthtoken(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE user_webhook_delivery (
|
||||
|
@ -370,7 +370,7 @@ CREATE TABLE user_webhook_delivery (
|
|||
response character varying(65536),
|
||||
response_status integer NOT NULL,
|
||||
response_headers character varying(16384),
|
||||
subscription_id integer NOT NULL REFERENCES user_webhook_subscription(id)
|
||||
subscription_id integer NOT NULL REFERENCES user_webhook_subscription(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE tracker_webhook_subscription (
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
"""Update relationship cascades
|
||||
|
||||
Revision ID: 8f822cabf78b
|
||||
Revises: 4c70fa9bc46f
|
||||
Create Date: 2022-11-01 15:54:11.371274
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8f822cabf78b'
|
||||
down_revision = '4c70fa9bc46f'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
cascades = [
|
||||
("tracker", "user", "owner_id", "CASCADE"),
|
||||
("ticket", "participant", "submitter_id", "CASCADE"),
|
||||
("ticket_assignee", "user", "assignee_id", "CASCADE"),
|
||||
("ticket_assignee", "user", "assigner_id", "CASCADE"),
|
||||
("ticket_label", "user", "user_id", "CASCADE"),
|
||||
("ticket_subscription", "participant", "participant_id", "CASCADE"),
|
||||
("event", "participant", "participant_id", "CASCADE"),
|
||||
("event", "participant", "by_participant_id", "CASCADE"),
|
||||
("event_notification", "user", "user_id", "CASCADE"),
|
||||
("gql_user_wh_sub", "user", "user_id", "CASCADE"),
|
||||
("gql_tracker_wh_sub", "user", "user_id", "CASCADE"),
|
||||
("gql_ticket_wh_sub", "user", "user_id", "CASCADE"),
|
||||
("oauthtoken", "user", "user_id", "CASCADE"),
|
||||
("user_webhook_subscription", "user", "user_id", "CASCADE"),
|
||||
("user_webhook_subscription", "oauthtoken", "token_id", "CASCADE"),
|
||||
("user_webhook_delivery", "user_webhook_subscription", "subscription_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);
|
||||
""")
|
Loading…
Reference in New Issue