diff --git a/common/user.ts b/common/user.ts index f89223d2..6a7491bd 100644 --- a/common/user.ts +++ b/common/user.ts @@ -63,6 +63,7 @@ export type User = { badges: StreakerBadge[] } } + userDeleted?: boolean } export type PrivateUser = { diff --git a/firestore.rules b/firestore.rules index bfcb7183..d9e004d4 100644 --- a/firestore.rules +++ b/firestore.rules @@ -89,6 +89,7 @@ service cloud.firestore { allow update: if (userId == request.auth.uid || isAdmin()) && request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']); + allow delete: if (userId == request.auth.uid || isAdmin()); } match /private-users/{userId}/views/{viewId} { diff --git a/functions/src/change-user-info.ts b/functions/src/change-user-info.ts index 53908741..07383a2b 100644 --- a/functions/src/change-user-info.ts +++ b/functions/src/change-user-info.ts @@ -18,15 +18,19 @@ const bodySchema = z.object({ username: z.string().optional(), name: z.string().optional(), avatarUrl: z.string().optional(), + userDeleted: z.boolean().optional(), }) export const changeuserinfo = newEndpoint({}, async (req, auth) => { - const { username, name, avatarUrl } = validate(bodySchema, req.body) + const { username, name, avatarUrl, userDeleted } = validate( + bodySchema, + req.body + ) const user = await getUser(auth.uid) if (!user) throw new APIError(400, 'User not found') - await changeUser(user, { username, name, avatarUrl }) + await changeUser(user, { username, name, avatarUrl, userDeleted }) return { message: 'Successfully changed user info.' } }) @@ -36,6 +40,7 @@ export const changeUser = async ( username?: string name?: string avatarUrl?: string + userDeleted?: boolean } ) => { // Update contracts, comments, and answers outside of a transaction to avoid contention. diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 4e29fb1c..5bd01358 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -83,6 +83,10 @@ export async function updatePrivateUser( await updateDoc(doc(privateUsers, userId), { ...update }) } +export async function deletePrivateUser(userId: string) { + await deleteDoc(doc(privateUsers, userId)) +} + export function listenForUser( userId: string, setUser: (user: User | null) => void diff --git a/web/pages/[username]/index.tsx b/web/pages/[username]/index.tsx index 9c8adc39..ad96a20b 100644 --- a/web/pages/[username]/index.tsx +++ b/web/pages/[username]/index.tsx @@ -33,5 +33,5 @@ export default function UserProfile(props: { user: User | null }) { useTracking('view user profile', { username }) - return user ? : + return user && !user.userDeleted ? : } diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index c32fe2fb..6ce775df 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -1,4 +1,5 @@ import { RefreshIcon } from '@heroicons/react/outline' +import { TrashIcon, UserRemoveIcon } from '@heroicons/react/solid' import { PrivateUser, User } from 'common/user' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' import Link from 'next/link' @@ -19,6 +20,7 @@ import { changeUserInfo } from 'web/lib/firebase/api' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { uploadImage } from 'web/lib/firebase/storage' import { + deletePrivateUser, getUserAndPrivateUser, updatePrivateUser, updateUser, @@ -107,6 +109,11 @@ export default function ProfilePage(props: { }) } + const deleteAccount = async () => { + await changeUserInfo({ userDeleted: true }) + await deletePrivateUser(privateUser.id) + } + const fileHandler = async (event: any) => { const file = event.target.files[0] @@ -201,7 +208,7 @@ export default function ProfilePage(props: {
-
+
+
+ +
+ + , + color: 'red', + }} + submitBtn={{ + label: 'Deactivate account', + }} + onSubmitWithSuccess={async () => { + deleteAccount() + return true + }} + > + + + <div> + Deactivating your account means you will no longer be able + to use your account. You will lose access to all of your + data. + </div> + </Col> + </ConfirmationButton> + </div> + </div> </Col> </Col> </Page>