Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions backend/api/src/create-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {APIError, APIHandler} from './helpers/endpoint'
import {getDefaultNotificationPreferences} from 'common/user-notification-preferences'
import {removeUndefinedProps} from 'common/util/object'
import {generateAvatarUrl} from 'shared/helpers/generate-and-update-avatar-urls'
import {getStorage} from 'firebase-admin/storage'
import {ENV_CONFIG, RESERVED_PATHS} from 'common/envs/constants'
import {RESERVED_PATHS} from 'common/envs/constants'
import {getUser, getUserByUsername, log} from 'shared/utils'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {convertPrivateUser, convertUser} from 'common/supabase/users'
import {getBucket} from "shared/firebase-utils";

export const createUser: APIHandler<'create-user'> = async (
props,
Expand Down Expand Up @@ -50,7 +50,7 @@ export const createUser: APIHandler<'create-user'> = async (
const rawName = fbUser.displayName || emailName || 'User' + randomString(4)
const name = cleanDisplayName(rawName)

const bucket = getStorage().bucket(getStorageBucketId())
const bucket = getBucket()
const avatarUrl = fbUser.photoURL
? fbUser.photoURL
: await generateAvatarUrl(auth.uid, name, bucket)
Expand Down Expand Up @@ -135,10 +135,6 @@ export const createUser: APIHandler<'create-user'> = async (
}
}

function getStorageBucketId() {
return ENV_CONFIG.firebaseConfig.storageBucket
}

// Automatically ban users with these device tokens or ip addresses.
const bannedDeviceTokens = [
'fa807d664415',
Expand Down
40 changes: 27 additions & 13 deletions backend/api/src/delete-me.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getUser } from 'shared/utils'
import { APIError, APIHandler } from './helpers/endpoint'
import { updatePrivateUser, updateUser } from 'shared/supabase/users'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { FieldVal } from 'shared/supabase/utils'
import {getUser} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import * as admin from "firebase-admin";
import {deleteUserFiles} from "shared/firebase-utils";

export const deleteMe: APIHandler<'me/delete'> = async (body, auth) => {
const { username } = body
const {username} = body
const user = await getUser(auth.uid)
if (!user) {
throw new APIError(401, 'Your account was not found')
Expand All @@ -16,13 +16,27 @@ export const deleteMe: APIHandler<'me/delete'> = async (body, auth) => {
`Incorrect username. You are logged in as ${user.username}. Are you sure you want to delete this account?`
)
}
const userId = user.id
if (!userId) {
throw new APIError(400, 'Invalid user ID')
}

// Remove user data from Supabase
const pg = createSupabaseDirectClient()
await updateUser(pg, auth.uid, {
userDeleted: true,
isBannedFromPosting: true,
})
await updatePrivateUser(pg, auth.uid, {
email: FieldVal.delete(),
})
await pg.none('DELETE FROM users WHERE id = $1', [userId])
await pg.none('DELETE FROM private_users WHERE id = $1', [userId])
await pg.none('DELETE FROM profiles WHERE user_id = $1', [userId])
// May need to also delete from other tables in the future (such as messages, compatibility responses, etc.)

// Delete user files from Firebase Storage
await deleteUserFiles(user.username)

// Remove user from Firebase Auth
try {
const auth = admin.auth()
await auth.deleteUser(userId)
console.log(`Deleted user ${userId} from Firebase Auth and Supabase`)
} catch (e) {
console.error('Error deleting user from Firebase Auth:', e)
}
}
28 changes: 27 additions & 1 deletion backend/shared/src/firebase-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {readFileSync} from "fs";
import {ENV_CONFIG} from "common/envs/constants";
import {getStorage, Storage} from 'firebase-admin/storage'

import {ENV_CONFIG, getStorageBucketId} from "common/envs/constants";

export const getServiceAccountCredentials = () => {
let keyPath = ENV_CONFIG.googleApplicationCredentials
Expand All @@ -22,4 +24,28 @@ export const getServiceAccountCredentials = () => {
} catch (e) {
throw new Error(`Failed to load service account key from ${keyPath}: ${e}`)
}
}

export function getBucket() {
return getStorage().bucket(getStorageBucketId())
}


export type Bucket = ReturnType<InstanceType<typeof Storage>['bucket']>

export async function deleteUserFiles(username: string) {
const path = `user-images/${username}`

// Delete all files in the directory
const bucket = getBucket()
const [files] = await bucket.getFiles({prefix: path});

if (files.length === 0) {
console.log(`No files found in bucket for user ${username}`);
return;
}

await Promise.all(files.map(file => file.delete()));
console.log(`Deleted ${files.length} files for user ${username}`);

}
8 changes: 3 additions & 5 deletions backend/shared/src/helpers/generate-and-update-avatar-urls.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Storage } from 'firebase-admin/storage'
import { DOMAIN } from 'common/envs/constants'

type Bucket = ReturnType<InstanceType<typeof Storage>['bucket']>
import {DOMAIN} from 'common/envs/constants'
import {Bucket} from "shared/firebase-utils";

export const generateAvatarUrl = async (
userId: string,
Expand Down Expand Up @@ -44,7 +42,7 @@ async function upload(userId: string, buffer: Buffer, bucket: Bucket) {
await file.save(buffer, {
private: false,
public: true,
metadata: { contentType: 'image/png' },
metadata: {contentType: 'image/png'},
})
return `https://storage.googleapis.com/${bucket.cloudStorageURI.hostname}/${filename}`
}
6 changes: 5 additions & 1 deletion common/src/envs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,8 @@ export const RESERVED_PATHS = [
'users',
'web',
'welcome',
]
]

export function getStorageBucketId() {
return ENV_CONFIG.firebaseConfig.storageBucket
}
51 changes: 28 additions & 23 deletions web/components/auth-context.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
'use client'
import { createContext, ReactNode, useEffect, useState } from 'react'
import { pickBy } from 'lodash'
import { onIdTokenChanged, User as FirebaseUser } from 'firebase/auth'
import { auth } from 'web/lib/firebase/users'
import { api } from 'web/lib/api'
import { randomString } from 'common/util/random'
import { useStateCheckEquality } from 'web/hooks/use-state-check-equality'
import { AUTH_COOKIE_NAME, TEN_YEARS_SECS } from 'common/envs/constants'
import { getCookie, setCookie } from 'web/lib/util/cookie'
import {
type PrivateUser,
type User,
type UserAndPrivateUser,
} from 'common/user'
import { safeLocalStorage } from 'web/lib/util/local'
import { updateSupabaseAuth } from 'web/lib/supabase/db'
import { useEffectCheckEquality } from 'web/hooks/use-effect-check-equality'
import { getPrivateUserSafe, getUserSafe } from 'web/lib/supabase/users'
import { useWebsocketPrivateUser, useWebsocketUser } from 'web/hooks/use-user'
import { identifyUser, setUserProperty } from 'web/lib/service/analytics'
import {createContext, ReactNode, useEffect, useState} from 'react'
import {pickBy} from 'lodash'
import {onIdTokenChanged, User as FirebaseUser} from 'firebase/auth'
import {auth} from 'web/lib/firebase/users'
import {api} from 'web/lib/api'
import {randomString} from 'common/util/random'
import {useStateCheckEquality} from 'web/hooks/use-state-check-equality'
import {AUTH_COOKIE_NAME, TEN_YEARS_SECS} from 'common/envs/constants'
import {getCookie, setCookie} from 'web/lib/util/cookie'
import {type PrivateUser, type User, type UserAndPrivateUser,} from 'common/user'
import {safeLocalStorage} from 'web/lib/util/local'
import {useEffectCheckEquality} from 'web/hooks/use-effect-check-equality'
import {getPrivateUserSafe, getUserSafe} from 'web/lib/supabase/users'
import {useWebsocketPrivateUser, useWebsocketUser} from 'web/hooks/use-user'
import {identifyUser, setUserProperty} from 'web/lib/service/analytics'

// Either we haven't looked up the logged-in user yet (undefined), or we know
// the user is not logged in (null), or we know the user is logged in.
Expand Down Expand Up @@ -70,13 +65,23 @@ const setUserCookie = (data: object | undefined) => {
])
}

export const clearUserCookie = () => {
setCookie(AUTH_COOKIE_NAME, '', [
['path', '/'],
['max-age', '0'],
['samesite', 'lax'],
['secure'],
])
}


export const AuthContext = createContext<AuthUser>(undefined)

export function AuthProvider(props: {
children: ReactNode
serverUser?: AuthUser
}) {
const { children, serverUser } = props
const {children, serverUser} = props

const [user, setUser] = useStateCheckEquality<User | undefined | null>(
serverUser ? serverUser.user : serverUser
Expand All @@ -89,8 +94,8 @@ export function AuthProvider(props: {
const authUser = !user
? user
: !privateUser
? privateUser
: { user, privateUser, authLoaded }
? privateUser
: {user, privateUser, authLoaded}

useEffect(() => {
if (serverUser === undefined) {
Expand Down
30 changes: 12 additions & 18 deletions web/components/profile/delete-yourself.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { TrashIcon } from '@heroicons/react/solid'
import {TrashIcon} from '@heroicons/react/solid'
import router from 'next/router'
import { useState } from 'react'
import {useState} from 'react'
import toast from 'react-hot-toast'
import { auth } from 'web/lib/firebase/users'
import { ConfirmationButton } from '../buttons/confirmation-button'
import { Col } from '../layout/col'
import { Input } from '../widgets/input'
import { Title } from '../widgets/title'
import { api } from 'web/lib/api'
import {ConfirmationButton} from '../buttons/confirmation-button'
import {Col} from '../layout/col'
import {Input} from '../widgets/input'
import {Title} from '../widgets/title'
import {deleteAccount} from "web/lib/util/delete";

export function DeleteYourselfButton(props: { username: string }) {
const { username } = props

const deleteAccount = async () => {
await api('me/delete', { username })
await auth.signOut()
}
const {username} = props

const [deleteAccountConfirmation, setDeleteAccountConfirmation] = useState('')

Expand All @@ -24,7 +18,7 @@ export function DeleteYourselfButton(props: { username: string }) {
openModalBtn={{
className: 'p-2',
label: 'Permanently delete this account',
icon: <TrashIcon className="mr-1 h-5 w-5" />,
icon: <TrashIcon className="mr-1 h-5 w-5"/>,
color: 'red',
}}
submitBtn={{
Expand All @@ -35,14 +29,14 @@ export function DeleteYourselfButton(props: { username: string }) {
onSubmitWithSuccess={async () => {
if (deleteAccountConfirmation == 'delete my account') {
toast
.promise(deleteAccount(), {
.promise(deleteAccount(username), {
loading: 'Deleting account...',
success: () => {
router.push('/')
return 'Account deleted'
return 'Your account has been deleted.'
},
error: () => {
return 'Failed to delete account'
return 'Failed to delete account.'
},
})
.then(() => {
Expand Down
25 changes: 21 additions & 4 deletions web/components/profile/profile-header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {DotsHorizontalIcon, EyeIcon, LockClosedIcon, PencilIcon} from '@heroicons/react/outline'
import clsx from 'clsx'
import Router from 'next/router'
import router from 'next/router'
import Link from 'next/link'
import {User} from 'common/user'
import {Button} from 'web/components/buttons/button'
Expand All @@ -17,9 +18,11 @@ import {Profile} from 'common/love/profile'
import {useUser} from 'web/hooks/use-user'
import {linkClass} from 'web/components/widgets/site-link'
import {StarButton} from '../widgets/star-button'
import {api, updateProfile} from 'web/lib/api'
import {updateProfile} from 'web/lib/api'
import React, {useState} from 'react'
import {VisibilityConfirmationModal} from './visibility-confirmation-modal'
import {deleteAccount} from "web/lib/util/delete";
import toast from "react-hot-toast";

export default function ProfileHeader(props: {
user: User
Expand Down Expand Up @@ -110,9 +113,23 @@ export default function ProfileHeader(props: {
'Are you sure you want to delete your profile? This cannot be undone.'
)
if (confirmed) {
track('delete love profile')
await api('me/delete', {username: user.username})
window.location.reload()
toast
.promise(deleteAccount(user.username), {
loading: 'Deleting account...',
success: () => {
router.push('/')
return 'Your account has been deleted.'
},
error: () => {
return 'Failed to delete account.'
},
})
.then(() => {
// return true
})
.catch(() => {
// return false
})
}
},
},
Expand Down
File renamed without changes.
15 changes: 15 additions & 0 deletions web/lib/util/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {track} from "web/lib/service/analytics";
import {api} from "web/lib/api";
import {firebaseLogout} from "web/lib/firebase/users";
import posthog from "posthog-js";
import {clearUserCookie} from "web/components/auth-context";

export async function deleteAccount(username: string) {
track('delete profile')
await api('me/delete', {username})
await firebaseLogout()
clearUserCookie()
localStorage.clear()
sessionStorage.clear()
posthog.reset()
}
Loading