Compare commits

...

1 Commits

Author SHA1 Message Date
Pico2x
fd794d5879 Adds option to delete your account. 2022-10-14 14:04:04 +01:00
6 changed files with 57 additions and 4 deletions

View File

@ -63,6 +63,7 @@ export type User = {
badges: StreakerBadge[] badges: StreakerBadge[]
} }
} }
userDeleted?: boolean
} }
export type PrivateUser = { export type PrivateUser = {

View File

@ -89,6 +89,7 @@ service cloud.firestore {
allow update: if (userId == request.auth.uid || isAdmin()) allow update: if (userId == request.auth.uid || isAdmin())
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']); .hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']);
allow delete: if (userId == request.auth.uid || isAdmin());
} }
match /private-users/{userId}/views/{viewId} { match /private-users/{userId}/views/{viewId} {

View File

@ -18,15 +18,19 @@ const bodySchema = z.object({
username: z.string().optional(), username: z.string().optional(),
name: z.string().optional(), name: z.string().optional(),
avatarUrl: z.string().optional(), avatarUrl: z.string().optional(),
userDeleted: z.boolean().optional(),
}) })
export const changeuserinfo = newEndpoint({}, async (req, auth) => { 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) const user = await getUser(auth.uid)
if (!user) throw new APIError(400, 'User not found') 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.' } return { message: 'Successfully changed user info.' }
}) })
@ -36,6 +40,7 @@ export const changeUser = async (
username?: string username?: string
name?: string name?: string
avatarUrl?: string avatarUrl?: string
userDeleted?: boolean
} }
) => { ) => {
// Update contracts, comments, and answers outside of a transaction to avoid contention. // Update contracts, comments, and answers outside of a transaction to avoid contention.

View File

@ -83,6 +83,10 @@ export async function updatePrivateUser(
await updateDoc(doc(privateUsers, userId), { ...update }) await updateDoc(doc(privateUsers, userId), { ...update })
} }
export async function deletePrivateUser(userId: string) {
await deleteDoc(doc(privateUsers, userId))
}
export function listenForUser( export function listenForUser(
userId: string, userId: string,
setUser: (user: User | null) => void setUser: (user: User | null) => void

View File

@ -33,5 +33,5 @@ export default function UserProfile(props: { user: User | null }) {
useTracking('view user profile', { username }) useTracking('view user profile', { username })
return user ? <UserPage user={user} /> : <Custom404 /> return user && !user.userDeleted ? <UserPage user={user} /> : <Custom404 />
} }

View File

@ -1,4 +1,5 @@
import { RefreshIcon } from '@heroicons/react/outline' import { RefreshIcon } from '@heroicons/react/outline'
import { TrashIcon, UserRemoveIcon } from '@heroicons/react/solid'
import { PrivateUser, User } from 'common/user' import { PrivateUser, User } from 'common/user'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username' import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import Link from 'next/link' 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 { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
import { uploadImage } from 'web/lib/firebase/storage' import { uploadImage } from 'web/lib/firebase/storage'
import { import {
deletePrivateUser,
getUserAndPrivateUser, getUserAndPrivateUser,
updatePrivateUser, updatePrivateUser,
updateUser, 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 fileHandler = async (event: any) => {
const file = event.target.files[0] const file = event.target.files[0]
@ -201,7 +208,7 @@ export default function ProfilePage(props: {
<div> <div>
<label className="px-1 py-2">API key</label> <label className="px-1 py-2">API key</label>
<div className="flex w-full items-stretch"> <div className="flex w-full items-stretch space-x-1">
<Input <Input
type="text" type="text"
placeholder="Click refresh to generate key" placeholder="Click refresh to generate key"
@ -240,6 +247,41 @@ export default function ProfilePage(props: {
</ConfirmationButton> </ConfirmationButton>
</div> </div>
</div> </div>
<div>
<label className="px-1 py-2">Deactivate Account</label>
<div className="flex w-full items-stretch space-x-1">
<Input
type="text"
placeholder="Click to permanently deactivate this account"
readOnly
className="w-full"
/>
<ConfirmationButton
openModalBtn={{
className: 'p-2',
label: '',
icon: <TrashIcon className="h-5 w-5" />,
color: 'red',
}}
submitBtn={{
label: 'Deactivate account',
}}
onSubmitWithSuccess={async () => {
deleteAccount()
return true
}}
>
<Col>
<Title text={'Are you sure?'} />
<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>
</Col> </Col>
</Page> </Page>