Compare commits

..

1 Commits

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

View File

@ -70,7 +70,7 @@ export const PROD_CONFIG: EnvConfig = {
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
measurementId: 'G-SSFK1Q138D',
},
twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
twitchBotEndpoint: 'https://twitch-bot-nggbo3neva-uc.a.run.app',
cloudRunId: 'nggbo3neva',
cloudRunRegion: 'uc',
adminEmails: [

View File

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

View File

@ -100,7 +100,6 @@ const stringParseExts = [
'[embed]' + node.attrs.src ? `(${node.attrs.src})` : '',
}),
skippableComponent('gridCardsComponent'),
skippableComponent('staticReactEmbedComponent'),
TiptapTweet.extend({ renderText: () => '[tweet]' }),
TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }),
]

View File

@ -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} {

View File

@ -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.

View File

@ -22,7 +22,6 @@ import { linkClass } from './site-link'
import { DisplayMention } from './editor/mention'
import { DisplayContractMention } from './editor/contract-mention'
import GridComponent from './editor/tiptap-grid-cards'
import StaticReactEmbedComponent from './editor/tiptap-static-react-embed'
import Iframe from 'common/util/tiptap-iframe'
import TiptapTweet from './editor/tiptap-tweet'
@ -82,7 +81,6 @@ export const editorExtensions = (simple = false): Extensions => [
DisplayMention,
DisplayContractMention,
GridComponent,
StaticReactEmbedComponent,
Iframe,
TiptapTweet,
TiptapSpoiler.configure({
@ -365,7 +363,6 @@ export function RichContent(props: {
DisplayMention,
DisplayContractMention,
GridComponent,
StaticReactEmbedComponent,
Iframe,
TiptapTweet,
TiptapSpoiler.configure({

View File

@ -1,44 +0,0 @@
import { mergeAttributes, Node } from '@tiptap/core'
import React from 'react'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
import { StaticReactEmbed } from '../static-react-embed'
export default Node.create({
name: 'staticReactEmbedComponent',
group: 'block',
atom: true,
addAttributes() {
return {
embedName: '',
}
},
parseHTML() {
return [
{
tag: 'static-react-embed-component',
},
]
},
renderHTML({ HTMLAttributes }) {
return ['static-react-embed-component', mergeAttributes(HTMLAttributes)]
},
addNodeView() {
return ReactNodeViewRenderer(StaticReactEmbedComponent)
},
})
export function StaticReactEmbedComponent(props: any) {
const embedName = props.node.attrs.embedName
return (
<NodeViewWrapper className="static-react-embed-component">
<StaticReactEmbed embedName={embedName} />
</NodeViewWrapper>
)
}

View File

@ -1,212 +0,0 @@
import { useEffect } from 'react'
import { getContractFromSlug } from 'web/lib/firebase/contracts'
import {
StateElectionMap,
StateElectionMarket,
} from './usa-map/state-election-map'
import { useState } from 'react'
import { LoadingIndicator } from './loading-indicator'
import { CPMMBinaryContract } from 'common/contract'
export function MidtermsMaps(props: { mapType: string }) {
const { mapType } = props
const [contracts, setContracts] = useState<CPMMBinaryContract[] | null>(null)
useEffect(() => {
const getContracts = async () => {
if (props.mapType === 'senate') {
const senateContracts = await Promise.all(
senateMidterms.map((m) =>
getContractFromSlug(m.slug).then((c) => c ?? null)
)
)
setContracts(senateContracts as CPMMBinaryContract[])
} else if (props.mapType === 'governor') {
const governorContracts = await Promise.all(
governorMidterms.map((m) =>
getContractFromSlug(m.slug).then((c) => c ?? null)
)
)
setContracts(governorContracts as CPMMBinaryContract[])
}
}
getContracts()
}, [props.mapType, setContracts])
return contracts ? (
<StateElectionMap
markets={mapType == 'senate' ? senateMidterms : governorMidterms}
contracts={contracts}
/>
) : (
<LoadingIndicator />
)
}
const senateMidterms: StateElectionMarket[] = [
{
state: 'AZ',
creatorUsername: 'BTE',
slug: 'will-blake-masters-win-the-arizona',
isWinRepublican: true,
},
{
state: 'OH',
creatorUsername: 'BTE',
slug: 'will-jd-vance-win-the-ohio-senate-s',
isWinRepublican: true,
},
{
state: 'WI',
creatorUsername: 'BTE',
slug: 'will-ron-johnson-be-reelected-in-th',
isWinRepublican: true,
},
{
state: 'FL',
creatorUsername: 'BTE',
slug: 'will-marco-rubio-be-reelected-to-th',
isWinRepublican: true,
},
{
state: 'PA',
creatorUsername: 'MattP',
slug: 'will-dr-oz-be-elected-to-the-us-sen',
isWinRepublican: true,
},
{
state: 'GA',
creatorUsername: 'NcyRocks',
slug: 'will-a-democrat-win-the-2022-us-sen-3d2432ba6d79',
isWinRepublican: false,
},
{
state: 'NV',
creatorUsername: 'NcyRocks',
slug: 'will-a-democrat-win-the-2022-us-sen',
isWinRepublican: false,
},
{
state: 'NC',
creatorUsername: 'NcyRocks',
slug: 'will-a-democrat-win-the-2022-us-sen-6f1a901e1fcf',
isWinRepublican: false,
},
{
state: 'NH',
creatorUsername: 'NcyRocks',
slug: 'will-a-democrat-win-the-2022-us-sen-23194a72f1b7',
isWinRepublican: false,
},
{
state: 'UT',
creatorUsername: 'SG',
slug: 'will-mike-lee-win-the-2022-utah-sen',
isWinRepublican: true,
},
{
state: 'CO',
creatorUsername: 'SG',
slug: 'will-michael-bennet-win-the-2022-co',
isWinRepublican: false,
},
]
const governorMidterms: StateElectionMarket[] = [
{
state: 'TX',
creatorUsername: 'LarsDoucet',
slug: 'republicans-will-win-the-2022-texas',
isWinRepublican: true,
},
{
state: 'GA',
creatorUsername: 'MattP',
slug: 'will-stacey-abrams-win-the-2022-geo',
isWinRepublican: false,
},
{
state: 'FL',
creatorUsername: 'Tetraspace',
slug: 'if-charlie-crist-is-the-democratic',
isWinRepublican: false,
},
{
state: 'PA',
creatorUsername: 'JonathanMast',
slug: 'will-josh-shapiro-win-the-2022-penn',
isWinRepublican: false,
},
{
state: 'PA',
creatorUsername: 'JonathanMast',
slug: 'will-josh-shapiro-win-the-2022-penn',
isWinRepublican: false,
},
{
state: 'CO',
creatorUsername: 'ScottLawrence',
slug: 'will-jared-polis-be-reelected-as-co',
isWinRepublican: false,
},
{
state: 'OR',
creatorUsername: 'Tetraspace',
slug: 'if-tina-kotek-is-the-2022-democrati',
isWinRepublican: false,
},
{
state: 'MD',
creatorUsername: 'Tetraspace',
slug: 'if-wes-moore-is-the-2022-democratic',
isWinRepublican: false,
},
{
state: 'AK',
creatorUsername: 'SG',
slug: 'will-a-republican-win-the-2022-alas',
isWinRepublican: true,
},
{
state: 'AZ',
creatorUsername: 'SG',
slug: 'will-a-republican-win-the-2022-ariz',
isWinRepublican: true,
},
{
state: 'AZ',
creatorUsername: 'SG',
slug: 'will-a-republican-win-the-2022-ariz',
isWinRepublican: true,
},
{
state: 'WI',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-wiscon',
isWinRepublican: false,
},
{
state: 'NV',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-nevada',
isWinRepublican: false,
},
{
state: 'KS',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-kansas',
isWinRepublican: false,
},
{
state: 'NV',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-new-me',
isWinRepublican: false,
},
{
state: 'ME',
creatorUsername: 'SG',
slug: 'will-a-democrat-win-the-2022-maine',
isWinRepublican: false,
},
]

View File

@ -1,20 +0,0 @@
import { useState, useEffect } from 'react'
import { MidtermsMaps } from './midterms-maps'
export function StaticReactEmbed(props: { embedName: string }) {
const { embedName } = props
const [embed, setEmbed] = useState<JSX.Element | null>(null)
useEffect(() => {
const governorMidtermsMap = <MidtermsMaps mapType="governor" />
const senateMidtermsMap = <MidtermsMaps mapType="senate" />
if (embedName === 'governor-midterms-map') {
setEmbed(governorMidtermsMap)
} else if (embedName === 'senate-midterms-map') {
setEmbed(senateMidtermsMap)
}
}, [embedName, setEmbed])
return <div>{embed}</div>
}

View File

@ -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

View File

@ -33,5 +33,5 @@ export default function UserProfile(props: { user: User | null }) {
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 { 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: {
<div>
<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
type="text"
placeholder="Click refresh to generate key"
@ -240,6 +247,41 @@ export default function ProfilePage(props: {
</ConfirmationButton>
</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>
</Page>