Compare commits
1 Commits
main
...
delete-acc
Author | SHA1 | Date | |
---|---|---|---|
|
fd794d5879 |
|
@ -63,6 +63,7 @@ export type User = {
|
||||||
badges: StreakerBadge[]
|
badges: StreakerBadge[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
userDeleted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PrivateUser = {
|
export type PrivateUser = {
|
||||||
|
|
|
@ -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} {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 />
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user