From 6d32d05d986a66b57fc58769f0dcc7f17e5b5a10 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 20 Mar 2023 15:04:32 +0100 Subject: [PATCH 1/3] feat(backend): add force delete endpoint --- backend/db/user.go | 13 ++++++ backend/routes/auth/routes.go | 3 ++ backend/routes/auth/undelete.go | 80 ++++++++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/backend/db/user.go b/backend/db/user.go index 6e288c7..dddffa8 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -403,3 +403,16 @@ func (db *DB) UndoDeleteUser(ctx context.Context, id xid.ID) error { } return nil } + +func (db *DB) ForceDeleteUser(ctx context.Context, id xid.ID) error { + sql, args, err := sq.Delete("users").Where("id = ?", id).ToSql() + if err != nil { + return errors.Wrap(err, "building sql") + } + + _, err = db.Exec(ctx, sql, args...) + if err != nil { + return errors.Wrap(err, "executing query") + } + return nil +} diff --git a/backend/routes/auth/routes.go b/backend/routes/auth/routes.go index 79366b9..fd833a3 100644 --- a/backend/routes/auth/routes.go +++ b/backend/routes/auth/routes.go @@ -103,6 +103,9 @@ func Mount(srv *server.Server, r chi.Router) { // cancel user delete // uses a special token, so handled in the function itself r.Get("/cancel-delete", server.WrapHandler(s.cancelDelete)) + // force user delete + // uses a special token (same as above) + r.Get("/force-delete", server.WrapHandler(s.forceDelete)) }) } diff --git a/backend/routes/auth/undelete.go b/backend/routes/auth/undelete.go index c9db8f1..8768b2f 100644 --- a/backend/routes/auth/undelete.go +++ b/backend/routes/auth/undelete.go @@ -6,9 +6,11 @@ import ( "encoding/base64" "net/http" + "codeberg.org/u1f320/pronouns.cc/backend/db" "codeberg.org/u1f320/pronouns.cc/backend/log" "codeberg.org/u1f320/pronouns.cc/backend/server" "emperror.dev/errors" + "github.com/georgysavva/scany/pgxscan" "github.com/go-chi/render" "github.com/mediocregopher/radix/v4" "github.com/rs/xid" @@ -57,7 +59,7 @@ func (s *Server) saveUndeleteToken(ctx context.Context, userID xid.ID, token str func (s *Server) getUndeleteToken(ctx context.Context, token string) (userID xid.ID, err error) { var idString string - err = s.DB.Redis.Do(ctx, radix.Cmd(&idString, "GET", "undelete:"+token)) + err = s.DB.Redis.Do(ctx, radix.Cmd(&idString, "GETDEL", "undelete:"+token)) if err != nil { return userID, errors.Wrap(err, "getting undelete key") } @@ -68,3 +70,79 @@ func (s *Server) getUndeleteToken(ctx context.Context, token string) (userID xid } return userID, nil } + +func (s *Server) forceDelete(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + token := r.Header.Get("X-Delete-Token") + if token == "" { + return server.APIError{Code: server.ErrForbidden} + } + + id, err := s.getUndeleteToken(ctx, token) + if err != nil { + log.Errorf("getting delete token: %v", err) + return server.APIError{Code: server.ErrNotFound} // assume invalid token + } + + u, err := s.DB.User(ctx, id) + if err != nil { + log.Errorf("getting user: %v", err) + return errors.Wrap(err, "getting user") + } + + if u.Avatar != nil { + err = s.DB.DeleteUserAvatar(ctx, u.ID, *u.Avatar) + if err != nil { + log.Errorf("deleting avatars for user %v: %v", u.ID, err) + return errors.Wrap(err, "deleting user avatar") + } + } + + var exports []db.DataExport + err = pgxscan.Select(ctx, s.DB, &exports, "SELECT * FROM data_exports WHERE user_id = $1", u.ID) + if err != nil { + log.Errorf("getting to-be-deleted export files: %v", err) + return errors.Wrap(err, "getting export iles") + } + + for _, de := range exports { + err = s.DB.DeleteExport(ctx, de) + if err != nil { + log.Errorf("deleting export %v: %v", de.ID, err) + continue + } + + log.Debugf("deleted export %v", de.ID) + } + + members, err := s.DB.UserMembers(ctx, u.ID) + if err != nil { + log.Errorf("getting members for user %v: %v", u.ID, err) + return errors.Wrap(err, "getting members") + } + + for _, m := range members { + if m.Avatar == nil { + continue + } + + log.Debugf("deleting avatars for member %v", m.ID) + + err = s.DB.DeleteMemberAvatar(ctx, m.ID, *m.Avatar) + if err != nil { + log.Errorf("deleting avatars for member %v: %v", m.ID, err) + continue + } + + log.Debugf("deleted avatars for member %v", m.ID) + } + + err = s.DB.ForceDeleteUser(ctx, u.ID) + if err != nil { + log.Errorf("force deleting user: %v", err) + return errors.Wrap(err, "deleting user") + } + + render.JSON(w, r, map[string]any{"success": true}) + return nil +} From 1b949a013d227a96373e9dbc8e7141ed15a325f0 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 20 Mar 2023 15:57:16 +0100 Subject: [PATCH 2/3] refactor: use shared component for login callback pages --- frontend/src/lib/api/responses.ts | 23 +++ frontend/src/routes/+layout.server.ts | 9 +- .../src/routes/auth/login/+page.server.ts | 5 +- .../src/routes/auth/login/CallbackPage.svelte | 193 ++++++++++++++++++ .../routes/auth/login/discord/+page.svelte | 130 ++---------- .../login/mastodon/[instance]/+page.svelte | 130 ++---------- frontend/src/routes/settings/auth/+page.ts | 5 +- frontend/src/routes/settings/export/+page.ts | 6 +- 8 files changed, 253 insertions(+), 248 deletions(-) create mode 100644 frontend/src/lib/api/responses.ts create mode 100644 frontend/src/routes/auth/login/CallbackPage.svelte diff --git a/frontend/src/lib/api/responses.ts b/frontend/src/lib/api/responses.ts new file mode 100644 index 0000000..b7ba0c5 --- /dev/null +++ b/frontend/src/lib/api/responses.ts @@ -0,0 +1,23 @@ +import type { MeUser } from "./entities"; + +export interface SignupResponse { + user: MeUser; + token: string; +} + +export interface MetaResponse { + git_repository: string; + git_commit: string; + users: number; + members: number; + require_invite: boolean; +} + +export interface UrlsResponse { + discord: string; +} + +export interface ExportResponse { + path: string; + created_at: string; +} diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts index d522a21..f8d44d8 100644 --- a/frontend/src/routes/+layout.server.ts +++ b/frontend/src/routes/+layout.server.ts @@ -2,6 +2,7 @@ import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; import type { APIError } from "$lib/api/entities"; import { apiFetch } from "$lib/api/fetch"; +import type { MetaResponse } from "$lib/api/responses"; export const load = (async (event) => { try { @@ -10,11 +11,3 @@ export const load = (async (event) => { throw error(500, (e as APIError).message); } }) satisfies LayoutServerLoad; - -interface MetaResponse { - git_repository: string; - git_commit: string; - users: number; - members: number; - require_invite: boolean; -} diff --git a/frontend/src/routes/auth/login/+page.server.ts b/frontend/src/routes/auth/login/+page.server.ts index 7595151..e9e880a 100644 --- a/frontend/src/routes/auth/login/+page.server.ts +++ b/frontend/src/routes/auth/login/+page.server.ts @@ -1,5 +1,6 @@ import { apiFetch } from "$lib/api/fetch"; import { PUBLIC_BASE_URL } from "$env/static/public"; +import type { UrlsResponse } from "$lib/api/responses"; export const load = async () => { const resp = await apiFetch("/auth/urls", { @@ -11,7 +12,3 @@ export const load = async () => { return resp; }; - -interface UrlsResponse { - discord: string; -} diff --git a/frontend/src/routes/auth/login/CallbackPage.svelte b/frontend/src/routes/auth/login/CallbackPage.svelte new file mode 100644 index 0000000..b20f111 --- /dev/null +++ b/frontend/src/routes/auth/login/CallbackPage.svelte @@ -0,0 +1,193 @@ + + + + Log in with the {authType} - pronouns.cc + + +

Log in with the {authType}

+ +{#if error} + +{/if} +{#if ticket && $userStore} +
+ + + +
+
+ + + +
+
+ + +
+{:else if ticket} +
signupForm(username, inviteCode)}> +
+ + + +
+
+ + + +
+ {#if requireInvite} +
+ + + +
+ You currently need an invite code to sign up. You can get + one from an existing user. +
+
+ {/if} +
+ By signing up, you agree to the terms of service and the + privacy policy. +
+ +
+{:else if isDeleted && token} +

Your account is pending deletion since {deletedAt}.

+

If you wish to cancel deletion, press the button below.

+

+ +

+

+ Alternatively, if you want your data wiped immediately, press the force delete link below. This is irreversible. +

+

+ +

+ + +

+ If you want to delete your account, type your username below: +
+ + This is irreversible! Your account cannot be recovered after you press "Force delete + account". + +

+

+ +

+ {#if deleteError} + + {/if} +
+ + + + +
+ {#if deleteCancelled} + + Account deletion cancelled! You can now log in again. + + {/if} + {#if deleteError} + + {/if} +{:else} + Loading... +{/if} diff --git a/frontend/src/routes/auth/login/discord/+page.svelte b/frontend/src/routes/auth/login/discord/+page.svelte index c26f486..5cd3926 100644 --- a/frontend/src/routes/auth/login/discord/+page.svelte +++ b/frontend/src/routes/auth/login/discord/+page.svelte @@ -1,36 +1,16 @@ - - Log in with Discord - pronouns.cc - - -

Log in with Discord

- -{#if data.error} - -{/if} -{#if data.ticket && $userStore} -
- - - -
-
- - - -
-
- - -
-{:else if data.ticket} -
-
- - - -
-
- - - -
- {#if data.require_invite} -
- - - -
- You currently need an invite code to sign up. You can get - one from an existing user. -
-
- {/if} -
- By signing up, you agree to the terms of service and the - privacy policy. -
- -
-{:else if data.is_deleted && data.token} -

Your account is pending deletion since {data.deleted_at}.

-

If you wish to cancel deletion, press the button below.

-

- -

- {#if deleteCancelled} - - Account deletion cancelled! You can now log in again. - - {/if} - {#if deleteError} - - {/if} -{:else} - Loading... -{/if} + diff --git a/frontend/src/routes/auth/login/mastodon/[instance]/+page.svelte b/frontend/src/routes/auth/login/mastodon/[instance]/+page.svelte index ac3c7e8..596f727 100644 --- a/frontend/src/routes/auth/login/mastodon/[instance]/+page.svelte +++ b/frontend/src/routes/auth/login/mastodon/[instance]/+page.svelte @@ -1,35 +1,16 @@ - - Log in with the Fediverse - pronouns.cc - - -

Log in with the Fediverse

- -{#if data.error} - -{/if} -{#if data.ticket && $userStore} -
- - - -
-
- - - -
-
- - -
-{:else if data.ticket} -
-
- - - -
-
- - - -
- {#if data.require_invite} -
- - - -
- You currently need an invite code to sign up. You can get - one from an existing user. -
-
- {/if} -
- By signing up, you agree to the terms of service and the - privacy policy. -
- -
-{:else if data.is_deleted && data.token} -

Your account is pending deletion since {data.deleted_at}.

-

If you wish to cancel deletion, press the button below.

-

- -

- {#if deleteCancelled} - - Account deletion cancelled! You can now log in again. - - {/if} - {#if deleteError} - - {/if} -{:else} - Loading... -{/if} + diff --git a/frontend/src/routes/settings/auth/+page.ts b/frontend/src/routes/settings/auth/+page.ts index d4ba145..b044327 100644 --- a/frontend/src/routes/settings/auth/+page.ts +++ b/frontend/src/routes/settings/auth/+page.ts @@ -1,5 +1,6 @@ import { PUBLIC_BASE_URL } from "$env/static/public"; import { apiFetch } from "$lib/api/fetch"; +import type { UrlsResponse } from "$lib/api/responses"; export const load = async () => { const resp = await apiFetch("/auth/urls", { @@ -11,7 +12,3 @@ export const load = async () => { return { urls: resp }; }; - -interface UrlsResponse { - discord: string; -} diff --git a/frontend/src/routes/settings/export/+page.ts b/frontend/src/routes/settings/export/+page.ts index 9ba40c1..4596125 100644 --- a/frontend/src/routes/settings/export/+page.ts +++ b/frontend/src/routes/settings/export/+page.ts @@ -1,5 +1,6 @@ import { ErrorCode, type APIError } from "$lib/api/entities"; import { apiFetchClient } from "$lib/api/fetch"; +import type { ExportResponse } from "$lib/api/responses"; import { error } from "@sveltejs/kit"; export const load = async () => { @@ -12,8 +13,3 @@ export const load = async () => { throw error((e as APIError).code, (e as APIError).message); } }; - -interface ExportResponse { - path: string; - created_at: string; -} From 938005cd9fbf7981454b7ed62db0caaee08c5dd8 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 20 Mar 2023 16:40:36 +0100 Subject: [PATCH 3/3] chore: merge gitignore files --- .gitignore | 11 +++++++---- frontend/.gitignore | 10 ---------- 2 files changed, 7 insertions(+), 14 deletions(-) delete mode 100644 frontend/.gitignore diff --git a/.gitignore b/.gitignore index ae27ec4..9169dd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ .vscode node_modules *.log* -.nuxt -.nitro -.cache -.output .env +.env.* +!.env.example dist dump.rdb +build +.svelte-kit +package +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 6635cf5..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example -vite.config.js.timestamp-* -vite.config.ts.timestamp-*