From 5f3073011c6a6d509d2267b32872fb6e255f4eb0 Mon Sep 17 00:00:00 2001 From: Darnell Andries Date: Tue, 12 Nov 2024 16:30:50 -0800 Subject: [PATCH] Add account deletion --- controllers/accounts.go | 27 ++++++++++++++- datastore/accounts.go | 9 +++++ datastore/sessions.go | 2 +- docs/docs.go | 50 +++++++++++++++++++++++++++ docs/swagger.json | 50 +++++++++++++++++++++++++++ docs/swagger.yaml | 33 ++++++++++++++++++ main.go | 2 +- migrations/20241021231751_init.up.sql | 6 ++-- misc/test-client-rust/src/main.rs | 6 +++- 9 files changed, 178 insertions(+), 7 deletions(-) diff --git a/controllers/accounts.go b/controllers/accounts.go index 12401fa..850a774 100644 --- a/controllers/accounts.go +++ b/controllers/accounts.go @@ -154,11 +154,12 @@ func NewAccountsController(opaqueService *services.OpaqueService, jwtService *se } } -func (ac *AccountsController) Router(verificationMiddleware func(http.Handler) http.Handler) chi.Router { +func (ac *AccountsController) Router(verificationMiddleware func(http.Handler) http.Handler, authMiddleware func(http.Handler) http.Handler) chi.Router { r := chi.NewRouter() r.With(verificationMiddleware).Post("/password/init", ac.SetupPasswordInit) r.With(verificationMiddleware).Post("/password/finalize", ac.SetupPasswordFinalize) + r.With(authMiddleware).Delete("/", ac.DeleteAccount) return r } @@ -308,3 +309,27 @@ func (ac *AccountsController) SetupPasswordFinalize(w http.ResponseWriter, r *ht AuthToken: authToken, }) } + +// @Summary Delete account +// @Description Deletes the authenticated account and all associated data +// @Tags Accounts +// @Produce json +// @Param Authorization header string true "Bearer + auth token" +// @Param Brave-Key header string false "Brave services key (if one is configured)" +// @Success 204 "No Content" +// @Failure 401 {object} util.ErrorResponse +// @Failure 403 {object} util.ErrorResponse +// @Failure 500 {object} util.ErrorResponse +// @Router /v2/accounts [delete] +func (ac *AccountsController) DeleteAccount(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value(middleware.ContextSession).(*datastore.Session) + + // Delete the account with all associated data + if err := ac.ds.DeleteAccount(session.AccountID); err != nil { + util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) + return + } + + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} diff --git a/datastore/accounts.go b/datastore/accounts.go index 09b03d7..ef9c98b 100644 --- a/datastore/accounts.go +++ b/datastore/accounts.go @@ -110,3 +110,12 @@ func (d *Datastore) UpdateOpaqueRegistration(accountID uuid.UUID, oprfSeedID int return nil } + +func (d *Datastore) DeleteAccount(accountID uuid.UUID) error { + result := d.db.Delete(&Account{}, "id = ?", accountID) + if result.Error != nil { + return fmt.Errorf("error deleting account: %w", result.Error) + } + + return nil +} diff --git a/datastore/sessions.go b/datastore/sessions.go index b92c789..b0241da 100644 --- a/datastore/sessions.go +++ b/datastore/sessions.go @@ -56,7 +56,7 @@ func (d *Datastore) ListSessions(accountID uuid.UUID, minSessionVersion *int) ([ if minSessionVersion == nil { minSessionVersion = &d.minSessionVersion } - if err := d.db.Where("account_id = ? AND version >= ? AND expires_at IS NULL", accountID, *minSessionVersion).Find(&sessions).Error; err != nil { + if err := d.db.Where("account_id = ? AND version >= ?", accountID, *minSessionVersion).Find(&sessions).Error; err != nil { return nil, fmt.Errorf("failed to list sessions: %w", err) } diff --git a/docs/docs.go b/docs/docs.go index 6fca5b4..76a013d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,6 +15,56 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/v2/accounts": { + "delete": { + "description": "Deletes the authenticated account and all associated data", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Delete account", + "parameters": [ + { + "type": "string", + "description": "Bearer + auth token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Brave services key (if one is configured)", + "name": "Brave-Key", + "in": "header" + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/util.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/util.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/util.ErrorResponse" + } + } + } + } + }, "/v2/accounts/password/finalize": { "post": { "description": "Complete the password setup process and return auth token.\nEither ` + "`" + `publicKey` + "`" + `, ` + "`" + `maskingKey` + "`" + ` and ` + "`" + `envelope` + "`" + ` must be provided together,\nor ` + "`" + `serializedRecord` + "`" + ` must be provided.", diff --git a/docs/swagger.json b/docs/swagger.json index 1034bfd..2c1c3d6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5,6 +5,56 @@ "contact": {} }, "paths": { + "/v2/accounts": { + "delete": { + "description": "Deletes the authenticated account and all associated data", + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "summary": "Delete account", + "parameters": [ + { + "type": "string", + "description": "Bearer + auth token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Brave services key (if one is configured)", + "name": "Brave-Key", + "in": "header" + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/util.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/util.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/util.ErrorResponse" + } + } + } + } + }, "/v2/accounts/password/finalize": { "post": { "description": "Complete the password setup process and return auth token.\nEither `publicKey`, `maskingKey` and `envelope` must be provided together,\nor `serializedRecord` must be provided.", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7771701..f6f9002 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -280,6 +280,39 @@ info: contact: {} title: Brave Accounts Service paths: + /v2/accounts: + delete: + description: Deletes the authenticated account and all associated data + parameters: + - description: Bearer + auth token + in: header + name: Authorization + required: true + type: string + - description: Brave services key (if one is configured) + in: header + name: Brave-Key + type: string + produces: + - application/json + responses: + "204": + description: No Content + "401": + description: Unauthorized + schema: + $ref: '#/definitions/util.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/util.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/util.ErrorResponse' + summary: Delete account + tags: + - Accounts /v2/accounts/password/finalize: post: consumes: diff --git a/main.go b/main.go index b301732..d21dff4 100644 --- a/main.go +++ b/main.go @@ -105,7 +105,7 @@ func main() { r.Route("/v2", func(r chi.Router) { r.With(servicesKeyMiddleware).Mount("/auth", authController.Router(authMiddleware)) if passwordAuthEnabled { - r.With(servicesKeyMiddleware).Mount("/accounts", accountsController.Router(verificationMiddleware)) + r.With(servicesKeyMiddleware).Mount("/accounts", accountsController.Router(verificationMiddleware, authMiddleware)) } r.Mount("/verify", verificationController.Router(verificationMiddleware, servicesKeyMiddleware, debugEndpointsEnabled)) r.With(servicesKeyMiddleware).Mount("/sessions", sessionsController.Router(authMiddleware)) diff --git a/migrations/20241021231751_init.up.sql b/migrations/20241021231751_init.up.sql index 1fe76c7..e6227a8 100644 --- a/migrations/20241021231751_init.up.sql +++ b/migrations/20241021231751_init.up.sql @@ -22,7 +22,7 @@ CREATE TABLE accounts ( CREATE TABLE ake_states ( id UUID PRIMARY KEY, - account_id UUID REFERENCES accounts(id), + account_id UUID REFERENCES accounts(id) ON DELETE CASCADE, oprf_seed_id INT REFERENCES oprf_seeds(id), state BYTEA NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP @@ -36,7 +36,7 @@ CREATE TABLE registration_states ( CREATE TABLE sessions ( id UUID PRIMARY KEY, - account_id UUID NOT NULL REFERENCES accounts(id), + account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, user_agent TEXT NOT NULL, version SMALLINT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP @@ -54,7 +54,7 @@ CREATE TABLE verifications ( CREATE INDEX ON verifications (email); CREATE TABLE user_keys ( - account_id UUID NOT NULL REFERENCES accounts(id), + account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, name TEXT NOT NULL, encrypted_key BYTEA NOT NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/misc/test-client-rust/src/main.rs b/misc/test-client-rust/src/main.rs index a1c1053..8c9b1a3 100644 --- a/misc/test-client-rust/src/main.rs +++ b/misc/test-client-rust/src/main.rs @@ -27,7 +27,11 @@ fn post_request( bearer_token: Option<&str>, body: HashMap<&str, Value>, ) -> HashMap { - let client = reqwest::blocking::Client::new(); + // add user agent of some sort. + let client = reqwest::blocking::Client::builder() + .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + .build() + .expect("Failed to create HTTP client"); let mut request_builder = client.post(url).json(&body); // Add authorization header if bearer token is provided