From 06552663667f7a51acf277e4ade2a9b039abcba1 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Fri, 6 Mar 2026 23:51:49 +0100 Subject: [PATCH] Move avatar URL and is-banned to separate columns and social links to profiles table --- android/app/build.gradle | 2 +- backend/api/package.json | 2 +- backend/api/src/app.ts | 4 +- backend/api/src/ban-user.ts | 6 +- backend/api/src/create-profile.ts | 4 +- backend/api/src/create-user-and-profile.ts | 19 +- backend/api/src/create-user.ts | 6 +- backend/api/src/get-events.ts | 2 +- backend/api/src/get-likes-and-ships.ts | 8 +- backend/api/src/get-profiles.ts | 5 +- backend/api/src/update-me.ts | 65 +- backend/api/src/update-profile.ts | 11 +- backend/api/tests/unit/ban-user.unit.test.ts | 10 +- .../tests/unit/create-comment.unit.test.ts | 12 +- ...-private-user-message-channel.unit.test.ts | 10 +- .../create-private-user-message.unit.test.ts | 4 +- .../tests/unit/create-profile.unit.test.ts | 370 --------- .../unit/create-user-and-profile.unit.test.ts | 14 +- .../api/tests/unit/create-user.unit.test.ts | 719 ------------------ backend/api/tests/unit/update-me.unit.test.ts | 43 +- .../tests/unit/update-profile.unit.test.ts | 20 +- backend/email/emails/functions/mock.ts | 7 - ...-03-08-migrate-social-links-to-profiles.ts | 68 ++ backend/scripts/restore_data_column.ts | 42 + backend/shared/src/profiles/supabase.ts | 8 +- backend/shared/src/supabase/users.ts | 47 +- backend/supabase/profiles.sql | 3 +- backend/supabase/users.sql | 37 +- common/src/api/schema.ts | 2 - common/src/api/user-types.ts | 4 +- common/src/api/zod-types.ts | 2 +- common/src/supabase/schema.ts | 23 +- common/src/supabase/users.ts | 42 +- common/src/supabase/utils.ts | 3 +- common/src/user.ts | 2 - tests/e2e/utils/seedDatabase.ts | 8 +- web/components/optional-profile-form.tsx | 17 +- web/components/profile-about.tsx | 3 +- web/components/widgets/user-link.tsx | 4 +- web/lib/supabase/users.ts | 5 +- web/pages/profile.tsx | 1 - web/pages/signup.tsx | 3 - 42 files changed, 319 insertions(+), 1348 deletions(-) delete mode 100644 backend/api/tests/unit/create-profile.unit.test.ts delete mode 100644 backend/api/tests/unit/create-user.unit.test.ts create mode 100644 backend/scripts/2026-03-08-migrate-social-links-to-profiles.ts create mode 100644 backend/scripts/restore_data_column.ts diff --git a/android/app/build.gradle b/android/app/build.gradle index 640c052e..b5c2a8ba 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "com.compassconnections.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 56 + versionCode 57 versionName "1.11.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { diff --git a/backend/api/package.json b/backend/api/package.json index 9a32a305..3aea242a 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -1,6 +1,6 @@ { "name": "@compass/api", - "version": "1.22.2", + "version": "1.23.0", "private": true, "description": "Backend API endpoints", "main": "src/serve.ts", diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index bc64a0fa..371efb97 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -92,7 +92,7 @@ import {updateEvent} from './update-event' import {updateMe} from './update-me' import {updateNotifSettings} from './update-notif-setting' import {updatePrivateUserMessageChannel} from './update-private-user-message-channel' -import {updateProfile} from './update-profile' +import {updateProfileEndpoint} from './update-profile' import {updateUserLocale} from './update-user-locale' import {validateUsernameEndpoint} from './validate-username' @@ -618,7 +618,7 @@ const handlers: {[k in APIPath]: APIHandler} = { 'update-options': updateOptions, 'update-user-locale': updateUserLocale, 'update-private-user-message-channel': updatePrivateUserMessageChannel, - 'update-profile': updateProfile, + 'update-profile': updateProfileEndpoint, 'get-connection-interests': getConnectionInterestsEndpoint, 'update-connection-interest': updateConnectionInterests, 'user/by-id/:id': getUser, diff --git a/backend/api/src/ban-user.ts b/backend/api/src/ban-user.ts index f008e7c7..aaa7bf13 100644 --- a/backend/api/src/ban-user.ts +++ b/backend/api/src/ban-user.ts @@ -2,20 +2,16 @@ import {APIErrors, APIHandler} from 'api/helpers/endpoint' import {isAdminId} from 'common/envs/constants' import {trackPublicEvent} from 'shared/analytics' import {throwErrorIfNotMod} from 'shared/helpers/auth' -import {createSupabaseDirectClient} from 'shared/supabase/init' import {updateUser} from 'shared/supabase/users' import {log} from 'shared/utils' export const banUser: APIHandler<'ban-user'> = async (body, auth) => { const {userId, unban} = body - const db = createSupabaseDirectClient() await throwErrorIfNotMod(auth.uid) if (isAdminId(userId)) throw APIErrors.forbidden('Cannot ban admin') await trackPublicEvent(auth.uid, 'ban user', { userId, }) - await updateUser(db, userId, { - isBannedFromPosting: !unban, - }) + await updateUser(userId, {isBannedFromPosting: !unban}) log('updated user') } diff --git a/backend/api/src/create-profile.ts b/backend/api/src/create-profile.ts index 8f1b05b9..c066706d 100644 --- a/backend/api/src/create-profile.ts +++ b/backend/api/src/create-profile.ts @@ -8,7 +8,7 @@ import {tryCatch} from 'common/util/try-catch' import {track} from 'shared/analytics' import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' import {createSupabaseDirectClient} from 'shared/supabase/init' -import {updateUser} from 'shared/supabase/users' +import {updateUserData} from 'shared/supabase/users' import {insert} from 'shared/supabase/utils' import {getUser, log} from 'shared/utils' @@ -29,7 +29,7 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) => if (!user) throw APIErrors.unauthorized('Your account was not found') if (user.createdTime > Date.now() - HOUR_MS) { // If they just signed up, set their avatar to be their pinned photo - updateUser(pg, auth.uid, {avatarUrl: body.pinned_url || undefined}) + updateUserData(pg, auth.uid, {avatarUrl: body.pinned_url || undefined}) } debug('body', body) diff --git a/backend/api/src/create-user-and-profile.ts b/backend/api/src/create-user-and-profile.ts index 73a652eb..1ad0bb23 100644 --- a/backend/api/src/create-user-and-profile.ts +++ b/backend/api/src/create-user-and-profile.ts @@ -35,7 +35,6 @@ export const createUserAndProfile: APIHandler<'create-user-and-profile'> = async locale = defaultLocale, username, name, - link, profile, interests, causes, @@ -55,7 +54,7 @@ export const createUserAndProfile: APIHandler<'create-user-and-profile'> = async const email = fbUser.email const bucket = getBucket() - const avatarUrl = await generateAvatarUrl(auth.uid, cleanName, bucket) + const avatarUrl = profile.pinned_url ?? (await generateAvatarUrl(auth.uid, cleanName, bucket)) let finalUsername = username const validation = await validateUsername(username) @@ -88,15 +87,6 @@ export const createUserAndProfile: APIHandler<'create-user-and-profile'> = async }) } - const userData = removeUndefinedProps({ - avatarUrl, - isBannedFromPosting: Boolean( - (deviceToken && bannedDeviceTokens.includes(deviceToken)) || - (ip && bannedIpAddresses.includes(ip)), - ), - link: link, - }) - const privateUserData: PrivateUser = { id: auth.uid, email, @@ -112,7 +102,12 @@ export const createUserAndProfile: APIHandler<'create-user-and-profile'> = async id: auth.uid, name: cleanName, username: finalUsername, - data: userData, + avatar_url: avatarUrl, + is_banned_from_posting: Boolean( + (deviceToken && bannedDeviceTokens.includes(deviceToken)) || + (ip && bannedIpAddresses.includes(ip)), + ), + data: {}, }) const newPrivateUserRow = await insert(tx, 'private_users', { diff --git a/backend/api/src/create-user.ts b/backend/api/src/create-user.ts index 136d706a..34ab3a11 100644 --- a/backend/api/src/create-user.ts +++ b/backend/api/src/create-user.ts @@ -76,7 +76,7 @@ export const createUser: APIHandler<'create-user'> = async (props, auth, req) => const {user, privateUser} = await pg.tx(async (tx) => { const preexistingUser = await getUser(auth.uid, tx) if (preexistingUser) - throw APIErrors.forbidden('User already exists', { + throw APIErrors.forbidden('An account for this user already exists', { field: 'userId', context: `User with ID ${auth.uid} already exists`, }) @@ -84,14 +84,14 @@ export const createUser: APIHandler<'create-user'> = async (props, auth, req) => // Check exact username to avoid problems with duplicate requests const sameNameUser = await getUserByUsername(username, tx) if (sameNameUser) - throw APIErrors.conflict('Username already taken', { + throw APIErrors.conflict('Username is already taken', { field: 'username', context: `Username "${username}" is already taken`, }) const user = removeUndefinedProps({ avatarUrl, - isBannedFromPosting: Boolean( + is_banned_from_posting: Boolean( (deviceToken && bannedDeviceTokens.includes(deviceToken)) || (ip && bannedIpAddresses.includes(ip)), ), diff --git a/backend/api/src/get-events.ts b/backend/api/src/get-events.ts index 326f1c6a..bdc66929 100644 --- a/backend/api/src/get-events.ts +++ b/backend/api/src/get-events.ts @@ -52,7 +52,7 @@ export const getEvents: APIHandler<'get-events'> = async () => { username: string avatar_url: string | null }>( - `SELECT id, name, username, data ->> 'avatarUrl' as avatar_url + `SELECT id, name, username, avatar_url FROM users WHERE id = ANY ($1)`, [creatorIds], diff --git a/backend/api/src/get-likes-and-ships.ts b/backend/api/src/get-likes-and-ships.ts index 55cd8aeb..0f0f0d0a 100644 --- a/backend/api/src/get-likes-and-ships.ts +++ b/backend/api/src/get-likes-and-ships.ts @@ -25,7 +25,7 @@ export const getLikesAndShipsMain = async (userId: string) => { where creator_id = $1 and looking_for_matches and profiles.pinned_url is not null - and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null) + and not is_banned_from_posting order by created_time desc `, [userId], @@ -47,7 +47,7 @@ export const getLikesAndShipsMain = async (userId: string) => { where target_id = $1 and looking_for_matches and profiles.pinned_url is not null - and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null) + and not is_banned_from_posting order by created_time desc `, [userId], @@ -74,7 +74,7 @@ export const getLikesAndShipsMain = async (userId: string) => { where target2_id = $1 and profiles.looking_for_matches and profiles.pinned_url is not null - and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null) + and not is_banned_from_posting union all @@ -87,7 +87,7 @@ export const getLikesAndShipsMain = async (userId: string) => { where target1_id = $1 and profiles.looking_for_matches and profiles.pinned_url is not null - and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null) + and not is_banned_from_posting `, [userId], (r) => ({ diff --git a/backend/api/src/get-profiles.ts b/backend/api/src/get-profiles.ts index 455b1b06..1e4b7de8 100644 --- a/backend/api/src/get-profiles.ts +++ b/backend/api/src/get-profiles.ts @@ -239,9 +239,8 @@ export const loadProfiles = async (props: profileQueryType) => { const filters = [ where('looking_for_matches = true'), where(`profiles.disabled != true`), - // where(`pinned_url is not null and pinned_url != ''`), - where(`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`), - where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`), + where(`not users.is_banned_from_posting`), + // where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`), ...keywords.map((word) => where( diff --git a/backend/api/src/update-me.ts b/backend/api/src/update-me.ts index 27fb0e34..104072f5 100644 --- a/backend/api/src/update-me.ts +++ b/backend/api/src/update-me.ts @@ -1,12 +1,11 @@ import {toUserAPIResponse} from 'common/api/user-types' import {RESERVED_PATHS} from 'common/envs/constants' import {debug} from 'common/logger' -import {strip} from 'common/socials' import {cleanDisplayName, cleanUsername} from 'common/util/clean-username' import {removeUndefinedProps} from 'common/util/object' -import {cloneDeep, mapValues} from 'lodash' +import {cloneDeep} from 'lodash' import {createSupabaseDirectClient} from 'shared/supabase/init' -import {updateUser} from 'shared/supabase/users' +import {updateUser, updateUserData} from 'shared/supabase/users' import {getUser, getUserByUsername} from 'shared/utils' import {broadcastUpdatedUser} from 'shared/websockets/helpers' @@ -29,7 +28,7 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => { if (reservedName) throw APIErrors.forbidden('This username is reserved') const otherUserExists = await getUserByUsername(cleanedUsername) if (otherUserExists && otherUserExists.id !== auth.uid) - throw APIErrors.conflict('Username already taken') + throw APIErrors.conflict('Username is already taken') update.username = cleanedUsername } @@ -37,69 +36,23 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => { debug({update}) - const {name, username, avatarUrl, link = {}, ...rest} = update - await updateUser(pg, auth.uid, removeUndefinedProps(rest)) - - const stripped = mapValues(link, (value, site) => value && strip(site as any, value)) - - const adds = {} as {[key: string]: string} - const removes = [] - for (const [key, value] of Object.entries(stripped)) { - if (value === null || value === '') { - removes.push(key) - } else if (value) { - adds[key] = value - } - } - - let newLinks: any = null - if (Object.keys(adds).length > 0 || removes.length > 0) { - const data = await pg.oneOrNone( - `update users - set data = jsonb_set( - data, '{link}', - (data -> 'link' || $(adds)) - $(removes) - ) - where id = $(id) - returning data -> 'link' as link`, - {adds, removes, id: auth.uid}, - ) - newLinks = data?.link - } - - if (name) { - await pg.none( - `update users - set name = $1 - where id = $2`, - [name, auth.uid], - ) - } - if (username) { - await pg.none( - `update users - set username = $1 - where id = $2`, - [username, auth.uid], - ) - } - if (avatarUrl) { - await updateUser(pg, auth.uid, {avatarUrl}) - } + const {name, username, avatarUrl, ...rest} = update + await updateUserData(pg, auth.uid, removeUndefinedProps(rest)) // Ensure clients listening on `user/{id}` (e.g. AuthContext via useWebsocketUser) // get notified about link-only changes as well. - if (name || username || avatarUrl || newLinks != null) { + if (name || username || avatarUrl) { + await updateUser(auth.uid, {name, username, avatarUrl}) + broadcastUpdatedUser( removeUndefinedProps({ id: auth.uid, name, username, avatarUrl, - link: newLinks ?? undefined, }), ) } - return toUserAPIResponse({...user, ...update, link: newLinks}) + return toUserAPIResponse({...user, ...update}) } diff --git a/backend/api/src/update-profile.ts b/backend/api/src/update-profile.ts index 95f1bb05..09b51fdc 100644 --- a/backend/api/src/update-profile.ts +++ b/backend/api/src/update-profile.ts @@ -4,8 +4,7 @@ import {type Row} from 'common/supabase/utils' import {tryCatch} from 'common/util/try-catch' import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' import {createSupabaseDirectClient} from 'shared/supabase/init' -import {updateUser} from 'shared/supabase/users' -import {update} from 'shared/supabase/utils' +import {updateProfile, updateUser} from 'shared/supabase/users' import {log} from 'shared/utils' /** @@ -25,7 +24,7 @@ import {log} from 'shared/utils' * @returns Updated profile data * @throws {APIError} 404 if profile doesn't exist, 500 for database errors */ -export const updateProfile: APIHandler<'update-profile'> = async (parsedBody, auth) => { +export const updateProfileEndpoint: APIHandler<'update-profile'> = async (parsedBody, auth) => { trimStrings(parsedBody) log('Updating profile', parsedBody) const pg = createSupabaseDirectClient() @@ -43,12 +42,10 @@ export const updateProfile: APIHandler<'update-profile'> = async (parsedBody, au await removePinnedUrlFromPhotoUrls(parsedBody) if (parsedBody.pinned_url) { - await updateUser(pg, auth.uid, {avatarUrl: parsedBody.pinned_url}) + await updateUser(auth.uid, {avatarUrl: parsedBody.pinned_url}) } - const {data, error} = await tryCatch( - update(pg, 'profiles', 'user_id', {user_id: auth.uid, ...parsedBody}), - ) + const {data, error} = await tryCatch(updateProfile(auth.uid, parsedBody)) if (error) { log('Error updating profile', error) diff --git a/backend/api/tests/unit/ban-user.unit.test.ts b/backend/api/tests/unit/ban-user.unit.test.ts index 320747fc..58759866 100644 --- a/backend/api/tests/unit/ban-user.unit.test.ts +++ b/backend/api/tests/unit/ban-user.unit.test.ts @@ -45,9 +45,9 @@ describe('banUser', () => { expect(sharedAnalytics.trackPublicEvent).toBeCalledWith(mockAuth.uid, 'ban user', { userId: mockUser.userId, }) - expect(supabaseUsers.updateUser).toBeCalledTimes(1) - expect(supabaseUsers.updateUser).toBeCalledWith(mockPg, mockUser.userId, { - isBannedFromPosting: true, + expect(supabaseUsers.updateUserData).toBeCalledTimes(1) + expect(supabaseUsers.updateUserData).toBeCalledWith(mockPg, mockUser.userId, { + is_banned_from_posting: true, }) }) @@ -68,8 +68,8 @@ describe('banUser', () => { expect(sharedAnalytics.trackPublicEvent).toBeCalledWith(mockAuth.uid, 'ban user', { userId: mockUser.userId, }) - expect(supabaseUsers.updateUser).toBeCalledWith(mockPg, mockUser.userId, { - isBannedFromPosting: false, + expect(supabaseUsers.updateUserData).toBeCalledWith(mockPg, mockUser.userId, { + is_banned_from_posting: false, }) }) }) diff --git a/backend/api/tests/unit/create-comment.unit.test.ts b/backend/api/tests/unit/create-comment.unit.test.ts index bace89ef..3a9aa186 100644 --- a/backend/api/tests/unit/create-comment.unit.test.ts +++ b/backend/api/tests/unit/create-comment.unit.test.ts @@ -44,7 +44,7 @@ describe('createComment', () => { name: 'Mock Creator', username: 'mock.creator.username', avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false, + is_banned_from_posting: false, } const mockContent = { content: { @@ -143,7 +143,7 @@ describe('createComment', () => { name: 'Mock Creator', username: 'mock.creator.username', avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false, + is_banned_from_posting: false, } const mockContent = { content: { @@ -227,7 +227,7 @@ describe('createComment', () => { name: 'Mock Creator', username: 'mock.creator.username', avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: true, + is_banned_from_posting: true, } const mockContent = { content: { @@ -270,7 +270,7 @@ describe('createComment', () => { name: 'Mock Creator', username: 'mock.creator.username', avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false, + is_banned_from_posting: false, } const mockContent = { content: { @@ -316,7 +316,7 @@ describe('createComment', () => { name: 'Mock Creator', username: 'mock.creator.username', avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false, + is_banned_from_posting: false, } const mockContent = { content: { @@ -362,7 +362,7 @@ describe('createComment', () => { name: 'Mock Creator', username: 'mock.creator.username', avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false, + is_banned_from_posting: false, } const mockContent = { content: { diff --git a/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts b/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts index a5190153..eea0f80c 100644 --- a/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts +++ b/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts @@ -58,7 +58,7 @@ describe('createPrivateUserMessageChannel', () => { const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any const mockCreator = { - isBannedFromPosting: false, + is_banned_from_posting: false, } ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) @@ -104,7 +104,7 @@ describe('createPrivateUserMessageChannel', () => { const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any const mockCreator = { - isBannedFromPosting: false, + is_banned_from_posting: false, } ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) @@ -183,7 +183,7 @@ describe('createPrivateUserMessageChannel', () => { const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any const mockCreator = { - isBannedFromPosting: true, + is_banned_from_posting: true, } ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) @@ -207,7 +207,7 @@ describe('createPrivateUserMessageChannel', () => { const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any const mockCreator = { - isBannedFromPosting: false, + is_banned_from_posting: false, } ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) @@ -237,7 +237,7 @@ describe('createPrivateUserMessageChannel', () => { const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any const mockCreator = { - isBannedFromPosting: false, + is_banned_from_posting: false, } ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) diff --git a/backend/api/tests/unit/create-private-user-message.unit.test.ts b/backend/api/tests/unit/create-private-user-message.unit.test.ts index d5231f54..c181c863 100644 --- a/backend/api/tests/unit/create-private-user-message.unit.test.ts +++ b/backend/api/tests/unit/create-private-user-message.unit.test.ts @@ -31,7 +31,7 @@ describe('createPrivateUserMessage', () => { const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any const mockCreator = { - isBannedFromPosting: false, + is_banned_from_posting: false, } ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) @@ -87,7 +87,7 @@ describe('createPrivateUserMessage', () => { const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any const mockCreator = { - isBannedFromPosting: true, + is_banned_from_posting: true, } ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) diff --git a/backend/api/tests/unit/create-profile.unit.test.ts b/backend/api/tests/unit/create-profile.unit.test.ts deleted file mode 100644 index 95a09835..00000000 --- a/backend/api/tests/unit/create-profile.unit.test.ts +++ /dev/null @@ -1,370 +0,0 @@ -jest.mock('shared/supabase/init') -jest.mock('shared/utils') -jest.mock('shared/profiles/parse-photos') -jest.mock('shared/supabase/users') -jest.mock('shared/supabase/utils') -jest.mock('common/util/try-catch') -jest.mock('shared/analytics') -jest.mock('common/discord/core') -jest.mock('common/util/time') - -import {createProfile} from 'api/create-profile' -import {AuthedUser} from 'api/helpers/endpoint' -import {sendDiscordMessage} from 'common/discord/core' -import {sqlMatch} from 'common/test-utils' -import {tryCatch} from 'common/util/try-catch' -import * as sharedAnalytics from 'shared/analytics' -import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' -import * as supabaseInit from 'shared/supabase/init' -import * as supabaseUsers from 'shared/supabase/users' -import * as supabaseUtils from 'shared/supabase/utils' -import * as sharedUtils from 'shared/utils' - -describe('createProfile', () => { - let mockPg = {} as any - beforeEach(() => { - jest.resetAllMocks() - mockPg = { - oneOrNone: jest.fn(), - one: jest.fn(), - } - ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) - }) - afterEach(() => { - jest.restoreAllMocks() - }) - - describe('when given valid input', () => { - it('should successfully create a profile', async () => { - const mockBody = { - city: 'mockCity', - gender: 'mockGender', - looking_for_matches: true, - photo_urls: ['mockPhotoUrl1'], - pinned_url: 'mockPinnedUrl', - pref_gender: ['mockPrefGender'], - pref_relation_styles: ['mockPrefRelationStyles'], - visibility: 'public' as 'public' | 'member', - wants_kids_strength: 2, - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReq = {} as any - const mockNProfiles = 10 - const mockData = { - age: 30, - city: 'mockCity', - } - const mockUser = { - createdTime: Date.now(), - name: 'mockName', - username: 'mockUserName', - } - - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null}) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}) - - const results: any = await createProfile(mockBody, mockAuth, mockReq) - - expect(results.result).toEqual(mockData) - expect(tryCatch).toBeCalledTimes(2) - expect(mockPg.oneOrNone).toBeCalledTimes(1) - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('select id from profiles where user_id = $1'), - [mockAuth.uid], - ) - expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1) - expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockBody) - expect(sharedUtils.getUser).toBeCalledTimes(1) - expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid) - expect(supabaseUsers.updateUser).toBeCalledTimes(1) - expect(supabaseUsers.updateUser).toBeCalledWith(expect.any(Object), mockAuth.uid, { - avatarUrl: mockBody.pinned_url, - }) - expect(supabaseUtils.insert).toBeCalledTimes(1) - expect(supabaseUtils.insert).toBeCalledWith( - expect.any(Object), - 'profiles', - expect.objectContaining({user_id: mockAuth.uid}), - ) - ;(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles) - - await results.continue() - - expect(sharedAnalytics.track).toBeCalledTimes(1) - expect(sharedAnalytics.track).toBeCalledWith(mockAuth.uid, 'create profile', { - username: mockUser.username, - }) - expect(sendDiscordMessage).toBeCalledTimes(1) - expect(sendDiscordMessage).toBeCalledWith( - expect.stringContaining(mockUser.name && mockUser.username), - 'members', - ) - }) - - it('should successfully create milestone profile', async () => { - const mockBody = { - city: 'mockCity', - gender: 'mockGender', - looking_for_matches: true, - photo_urls: ['mockPhotoUrl1'], - pinned_url: 'mockPinnedUrl', - pref_gender: ['mockPrefGender'], - pref_relation_styles: ['mockPrefRelationStyles'], - visibility: 'public' as 'public' | 'member', - wants_kids_strength: 2, - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReq = {} as any - const mockNProfiles = 15 - const mockData = { - age: 30, - city: 'mockCity', - } - const mockUser = { - createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago - name: 'mockName', - username: 'mockUserName', - } - - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null}) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}) - - const results: any = await createProfile(mockBody, mockAuth, mockReq) - - expect(results.result).toEqual(mockData) - ;(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles) - - await results.continue() - - expect(mockPg.one).toBeCalledTimes(1) - expect(mockPg.one).toBeCalledWith( - sqlMatch('SELECT count(*) FROM profiles'), - [], - expect.any(Function), - ) - expect(sendDiscordMessage).toBeCalledTimes(2) - expect(sendDiscordMessage).toHaveBeenNthCalledWith( - 2, - expect.stringContaining(String(mockNProfiles)), - 'general', - ) - }) - }) - describe('when an error occurs', () => { - it('should throw if it failed to track create profile', async () => { - const mockBody = { - city: 'mockCity', - gender: 'mockGender', - looking_for_matches: true, - photo_urls: ['mockPhotoUrl1'], - pinned_url: 'mockPinnedUrl', - pref_gender: ['mockPrefGender'], - pref_relation_styles: ['mockPrefRelationStyles'], - visibility: 'public' as 'public' | 'member', - wants_kids_strength: 2, - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReq = {} as any - const mockData = { - age: 30, - city: 'mockCity', - } - const mockUser = { - createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago - name: 'mockName', - username: 'mockUserName', - } - - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null}) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}) - - const results: any = await createProfile(mockBody, mockAuth, mockReq) - - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - - ;(sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Track error')) - - await results.continue() - - expect(errorSpy).toBeCalledWith( - 'Failed to track create profile', - expect.objectContaining({name: 'Error'}), - ) - }) - - it('should throw if it failed to send discord new profile', async () => { - const mockBody = { - city: 'mockCity', - gender: 'mockGender', - looking_for_matches: true, - photo_urls: ['mockPhotoUrl1'], - pinned_url: 'mockPinnedUrl', - pref_gender: ['mockPrefGender'], - pref_relation_styles: ['mockPrefRelationStyles'], - visibility: 'public' as 'public' | 'member', - wants_kids_strength: 2, - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReq = {} as any - const mockData = { - age: 30, - city: 'mockCity', - } - const mockUser = { - createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago - name: 'mockName', - username: 'mockUserName', - } - - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}) - - const results: any = await createProfile(mockBody, mockAuth, mockReq) - - expect(results.result).toEqual(mockData) - - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - - ;(sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Sending error')) - - await results.continue() - - expect(errorSpy).toBeCalledWith( - 'Failed to send discord new profile', - expect.objectContaining({name: 'Error'}), - ) - }) - - it('should throw if it failed to send discord user milestone', async () => { - const mockBody = { - city: 'mockCity', - gender: 'mockGender', - looking_for_matches: true, - photo_urls: ['mockPhotoUrl1'], - pinned_url: 'mockPinnedUrl', - pref_gender: ['mockPrefGender'], - pref_relation_styles: ['mockPrefRelationStyles'], - visibility: 'public' as 'public' | 'member', - wants_kids_strength: 2, - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReq = {} as any - const mockNProfiles = 15 - const mockData = { - age: 30, - city: 'mockCity', - } - const mockUser = { - createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago - name: 'mockName', - username: 'mockUserName', - } - - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null}) - - const results: any = await createProfile(mockBody, mockAuth, mockReq) - - expect(results.result).toEqual(mockData) - - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - - ;(sendDiscordMessage as jest.Mock) - .mockResolvedValueOnce(null) - .mockRejectedValueOnce(new Error('Discord error')) - ;(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles) - - await results.continue() - - expect(sendDiscordMessage).toBeCalledTimes(2) - expect(sendDiscordMessage).toHaveBeenNthCalledWith( - 2, - expect.stringContaining(String(mockNProfiles)), - 'general', - ) - expect(errorSpy).toBeCalledWith( - 'Failed to send discord user milestone', - expect.objectContaining({name: 'Error'}), - ) - }) - - it('should throw if the profile already exists', async () => { - const mockBody = { - city: 'mockCity', - gender: 'mockGender', - looking_for_matches: true, - photo_urls: ['mockPhotoUrl1'], - pinned_url: 'mockPinnedUrl', - pref_gender: ['mockPrefGender'], - pref_relation_styles: ['mockPrefRelationStyles'], - visibility: 'public' as 'public' | 'member', - wants_kids_strength: 2, - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReq = {} as any - - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: true, error: null}) - - await expect(createProfile(mockBody, mockAuth, mockReq)).rejects.toThrowError( - 'Profile already exists', - ) - }) - - it('should throw if unable to find the account', async () => { - const mockBody = { - city: 'mockCity', - gender: 'mockGender', - looking_for_matches: true, - photo_urls: ['mockPhotoUrl1'], - pinned_url: 'mockPinnedUrl', - pref_gender: ['mockPrefGender'], - pref_relation_styles: ['mockPrefRelationStyles'], - visibility: 'public' as 'public' | 'member', - wants_kids_strength: 2, - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReq = {} as any - - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - - await expect(createProfile(mockBody, mockAuth, mockReq)).rejects.toThrowError( - 'Your account was not found', - ) - }) - - it('should throw if anything unexpected happens when creating the user', async () => { - const mockBody = { - city: 'mockCity', - gender: 'mockGender', - looking_for_matches: true, - photo_urls: ['mockPhotoUrl1'], - pinned_url: 'mockPinnedUrl', - pref_gender: ['mockPrefGender'], - pref_relation_styles: ['mockPrefRelationStyles'], - visibility: 'public' as 'public' | 'member', - wants_kids_strength: 2, - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReq = {} as any - const mockUser = { - createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago - name: 'mockName', - username: 'mockUserName', - } - - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) - ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: Error}) - - await expect(createProfile(mockBody, mockAuth, mockReq)).rejects.toThrowError( - 'Error creating user', - ) - }) - }) -}) diff --git a/backend/api/tests/unit/create-user-and-profile.unit.test.ts b/backend/api/tests/unit/create-user-and-profile.unit.test.ts index e56a5891..4d3110d2 100644 --- a/backend/api/tests/unit/create-user-and-profile.unit.test.ts +++ b/backend/api/tests/unit/create-user-and-profile.unit.test.ts @@ -161,7 +161,7 @@ describe('createUserAndProfile', () => { expect(usernameUtils.cleanDisplayName).toBeCalledTimes(1) expect(usernameUtils.cleanDisplayName).toHaveBeenCalledWith(mockProps.name) expect(firebaseUtils.getBucket).toBeCalledTimes(1) - expect(avatarHelpers.generateAvatarUrl).toBeCalledTimes(1) + expect(avatarHelpers.generateAvatarUrl).toBeCalledTimes(0) expect(validateUsernameModule.validateUsername).toBeCalledTimes(1) expect(validateUsernameModule.validateUsername).toHaveBeenCalledWith(mockProps.username) expect(parsePhotos.removePinnedUrlFromPhotoUrls).toBeCalledTimes(1) @@ -318,11 +318,11 @@ describe('createUserAndProfile', () => { const mockAvatarUrl = 'mockGeneratedAvatarUrl' const mockNewUserRow = { created_time: 'mockCreatedTime', - data: {isBannedFromPosting: true}, id: 'mockNewUserId', name: 'mockName', name_username_vector: 'mockNameUsernameVector', username: 'mockUsername', + is_banned_from_posting: true, } const mockPrivateUserRow = { data: {}, @@ -357,9 +357,11 @@ describe('createUserAndProfile', () => { await createUserAndProfile(mockProps, mockAuth, mockReq) - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith( + expect(supabaseUtils.insert).toHaveBeenCalledWith( + expect.any(Object), + 'users', expect.objectContaining({ - isBannedFromPosting: true, + is_banned_from_posting: true, }), ) }) @@ -405,7 +407,7 @@ describe('createUserAndProfile', () => { }) await expect(createUserAndProfile(mockProps, mockAuth, mockReq)).rejects.toThrowError( - 'User already exists', + 'An account for this user already exists', ) }) @@ -450,7 +452,7 @@ describe('createUserAndProfile', () => { }) await expect(createUserAndProfile(mockProps, mockAuth, mockReq)).rejects.toThrowError( - 'Username already taken', + 'Username is already taken', ) }) diff --git a/backend/api/tests/unit/create-user.unit.test.ts b/backend/api/tests/unit/create-user.unit.test.ts deleted file mode 100644 index e1f22e0a..00000000 --- a/backend/api/tests/unit/create-user.unit.test.ts +++ /dev/null @@ -1,719 +0,0 @@ -jest.mock('shared/supabase/init') -jest.mock('shared/supabase/utils') -jest.mock('common/supabase/users') -jest.mock('email/functions/helpers') -jest.mock('api/set-last-online-time') -jest.mock('firebase-admin', () => ({ - auth: jest.fn(), -})) -jest.mock('shared/utils') -jest.mock('shared/analytics') -jest.mock('shared/firebase-utils') -jest.mock('shared/helpers/generate-and-update-avatar-urls') -jest.mock('common/util/object') -jest.mock('common/user-notification-preferences') -jest.mock('common/util/clean-username') -jest.mock('shared/monitoring/log') -jest.mock('common/hosting/constants') - -import {createUser} from 'api/create-user' -import {AuthedUser} from 'api/helpers/endpoint' -import * as apiSetLastTimeOnline from 'api/set-last-online-time' -import * as hostingConstants from 'common/hosting/constants' -import * as supabaseUsers from 'common/supabase/users' -import * as userNotificationPref from 'common/user-notification-preferences' -import * as usernameUtils from 'common/util/clean-username' -import * as objectUtils from 'common/util/object' -import * as emailHelpers from 'email/functions/helpers' -import * as firebaseAdmin from 'firebase-admin' -import * as sharedAnalytics from 'shared/analytics' -import * as firebaseUtils from 'shared/firebase-utils' -import * as avatarHelpers from 'shared/helpers/generate-and-update-avatar-urls' -import * as supabaseInit from 'shared/supabase/init' -import * as supabaseUtils from 'shared/supabase/utils' -import * as sharedUtils from 'shared/utils' - -describe('createUser', () => { - const originalIsLocal = (hostingConstants as any).IS_LOCAL - let mockPg = {} as any - - beforeEach(() => { - jest.resetAllMocks() - mockPg = { - one: jest.fn(), - tx: jest.fn(async (cb) => { - const mockTx = {} as any - return cb(mockTx) - }), - } - ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) - }) - - afterEach(() => { - jest.restoreAllMocks() - Object.defineProperty(hostingConstants, 'IS_LOCAL', { - value: originalIsLocal, - writable: true, - }) - }) - - describe('when given valid input', () => { - it('should successfully create a user', async () => { - Object.defineProperty(hostingConstants, 'IS_LOCAL', { - value: false, - writable: true, - }) - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - photoURL: 'mockPhotoUrl', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - const mockNewUserRow = { - created_time: 'mockCreatedTime', - data: {mockNewUserJson: 'mockNewUserJsonData'}, - id: 'mockNewUserId', - name: 'mockName', - name_username_vector: 'mockNameUsernameVector', - username: 'mockUsername', - } - const mockPrivateUserRow = { - data: {mockPrivateUserJson: 'mockPrivateUserJsonData'}, - id: 'mockPrivateUserId', - } - - const mockGetUser = jest.fn().mockResolvedValueOnce(mockFbUser) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(mockPg.one as jest.Mock).mockResolvedValue(0) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - ;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false) - ;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null) - ;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) - ;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow) - ;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow) - - const results: any = await createUser(mockProps, mockAuth, mockReq) - - expect(results.result.user).toEqual(mockNewUserRow) - expect(results.result.privateUser).toEqual(mockPrivateUserRow) - expect(mockGetUser).toBeCalledTimes(1) - expect(mockGetUser).toHaveBeenNthCalledWith(1, mockAuth.uid) - expect(usernameUtils.cleanDisplayName).toBeCalledTimes(1) - expect(usernameUtils.cleanDisplayName).toHaveBeenCalledWith(mockFbUser.displayName) - expect(usernameUtils.cleanUsername).toBeCalledTimes(1) - expect(usernameUtils.cleanUsername).toBeCalledWith(mockFbUser.displayName) - expect(mockPg.one).toBeCalledTimes(1) - expect(mockPg.tx).toBeCalledTimes(1) - expect(sharedUtils.getUser).toBeCalledTimes(1) - expect(sharedUtils.getUser).toHaveBeenCalledWith(mockAuth.uid, expect.any(Object)) - expect(userNotificationPref.getDefaultNotificationPreferences).toBeCalledTimes(1) - expect(supabaseUtils.insert).toBeCalledTimes(2) - expect(supabaseUtils.insert).toHaveBeenNthCalledWith( - 1, - expect.any(Object), - 'users', - expect.objectContaining({ - id: mockAuth.uid, - name: mockFbUser.displayName, - username: mockFbUser.displayName, - }), - ) - expect(supabaseUtils.insert).toHaveBeenNthCalledWith( - 2, - expect.any(Object), - 'private_users', - expect.objectContaining({ - id: mockAuth.uid, - }), - ) - ;(sharedAnalytics.track as jest.Mock).mockResolvedValue(null) - ;(emailHelpers.sendWelcomeEmail as jest.Mock).mockResolvedValue(null) - ;(apiSetLastTimeOnline.setLastOnlineTimeUser as jest.Mock).mockResolvedValue(null) - - await results.continue() - - expect(sharedAnalytics.track).toBeCalledTimes(1) - expect(sharedAnalytics.track).toBeCalledWith(mockAuth.uid, 'create profile', { - username: mockNewUserRow.username, - }) - expect(emailHelpers.sendWelcomeEmail).toBeCalledTimes(1) - expect(emailHelpers.sendWelcomeEmail).toBeCalledWith(mockNewUserRow, mockPrivateUserRow) - expect(apiSetLastTimeOnline.setLastOnlineTimeUser).toBeCalledTimes(1) - expect(apiSetLastTimeOnline.setLastOnlineTimeUser).toBeCalledWith(mockAuth.uid) - }) - - it('should generate a avatar Url when creating a user', async () => { - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFirebaseUser = { - providerData: [ - { - providerId: 'password', - }, - ], - } - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - const mockAvatarUrl = 'mockGeneratedAvatarUrl' - const mockNewUserRow = { - created_time: 'mockCreatedTime', - data: {mockNewUserJson: 'mockNewUserJsonData'}, - id: 'mockNewUserId', - name: 'mockName', - name_username_vector: 'mockNameUsernameVector', - username: 'mockUsername', - } - const mockPrivateUserRow = { - data: {mockPrivateUserJson: 'mockPrivateUserJsonData'}, - id: 'mockPrivateUserId', - } - - const mockGetUser = jest - .fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser) - - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(avatarHelpers.generateAvatarUrl as jest.Mock).mockResolvedValue(mockAvatarUrl) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(mockPg.one as jest.Mock).mockResolvedValue(0) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - ;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false) - ;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null) - ;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) - ;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow) - ;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow) - - await createUser(mockProps, mockAuth, mockReq) - - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1) - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith({ - avatarUrl: mockAvatarUrl, - isBannedFromPosting: false, - link: expect.any(Object), - }) - }) - - it('should not allow a username that already exists when creating a user', async () => { - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFirebaseUser = { - providerData: [ - { - providerId: 'passwords', - }, - ], - } - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - photoURL: 'mockPhotoUrl', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - const mockNewUserRow = { - created_time: 'mockCreatedTime', - data: {mockNewUserJson: 'mockNewUserJsonData'}, - id: 'mockNewUserId', - name: 'mockName', - name_username_vector: 'mockNameUsernameVector', - username: 'mockUsername', - } - const mockPrivateUserRow = { - data: {mockPrivateUserJson: 'mockPrivateUserJsonData'}, - id: 'mockPrivateUserId', - } - - const mockGetUser = jest - .fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser) - - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(mockPg.one as jest.Mock).mockResolvedValue(1) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - ;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false) - ;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null) - ;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) - ;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow) - ;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow) - - await createUser(mockProps, mockAuth, mockReq) - - expect(mockPg.one).toBeCalledTimes(1) - expect(supabaseUtils.insert).toBeCalledTimes(2) - expect(supabaseUtils.insert).not.toHaveBeenNthCalledWith( - 1, - expect.any(Object), - 'users', - expect.objectContaining({ - id: mockAuth.uid, - name: mockFbUser.displayName, - username: mockFbUser.displayName, - }), - ) - }) - - it('should successfully create a user who is banned from posting if there ip/device token is banned', async () => { - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFirebaseUser = { - providerData: [ - { - providerId: 'passwords', - }, - ], - } - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - photoURL: 'mockPhotoUrl', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - const mockNewUserRow = { - created_time: 'mockCreatedTime', - data: {mockNewUserJson: 'mockNewUserJsonData'}, - id: 'mockNewUserId', - name: 'mockName', - name_username_vector: 'mockNameUsernameVector', - username: 'mockUsername', - } - const mockPrivateUserRow = { - data: {mockPrivateUserJson: 'mockPrivateUserJsonData'}, - id: 'mockPrivateUserId', - } - - const mockGetUser = jest - .fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser) - - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(avatarHelpers.generateAvatarUrl as jest.Mock).mockResolvedValue(mockFbUser.photoURL) - ;(mockPg.one as jest.Mock).mockResolvedValue(0) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - ;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false) - jest.spyOn(Array.prototype, 'includes').mockReturnValue(true) - ;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null) - ;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) - ;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow) - ;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow) - - await createUser(mockProps, mockAuth, mockReq) - - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1) - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith({ - avatarUrl: mockFbUser.photoURL, - isBannedFromPosting: true, - link: {}, - }) - }) - }) - - describe('when an error occurs', () => { - it('should throw if the user already exists', async () => { - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFirebaseUser = { - providerData: [ - { - providerId: 'passwords', - }, - ], - } - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - photoURL: 'mockPhotoUrl', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - - const mockGetUser = jest - .fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser) - - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(mockPg.one as jest.Mock).mockResolvedValue(0) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(true) - - expect(createUser(mockProps, mockAuth, mockReq)).rejects.toThrowError('User already exists') - }) - - it('should throw if the username is already taken', async () => { - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFirebaseUser = { - providerData: [ - { - providerId: 'passwords', - }, - ], - } - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - photoURL: 'mockPhotoUrl', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - - const mockGetUser = jest - .fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser) - - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(mockPg.one as jest.Mock).mockResolvedValue(0) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - ;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(true) - - expect(createUser(mockProps, mockAuth, mockReq)).rejects.toThrowError( - 'Username already taken', - ) - }) - - it('should throw if failed to track create profile', async () => { - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFirebaseUser = { - providerData: [ - { - providerId: 'passwords', - }, - ], - } - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - photoURL: 'mockPhotoUrl', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - const mockNewUserRow = { - created_time: 'mockCreatedTime', - data: {mockNewUserJson: 'mockNewUserJsonData'}, - id: 'mockNewUserId', - name: 'mockName', - name_username_vector: 'mockNameUsernameVector', - username: 'mockUsername', - } - const mockPrivateUserRow = { - data: {mockPrivateUserJson: 'mockPrivateUserJsonData'}, - id: 'mockPrivateUserId', - } - - const mockGetUser = jest - .fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser) - - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(mockPg.one as jest.Mock).mockResolvedValue(0) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - ;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false) - ;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null) - ;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) - ;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow) - ;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow) - - const results: any = await createUser(mockProps, mockAuth, mockReq) - - ;(sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Tracking failed')) - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - - await results.continue() - - expect(errorSpy).toHaveBeenCalledWith('Failed to track create profile', expect.any(Error)) - }) - - it('should throw if failed to send a welcome email', async () => { - Object.defineProperty(hostingConstants, 'IS_LOCAL', { - value: false, - writable: true, - }) - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFirebaseUser = { - providerData: [ - { - providerId: 'passwords', - }, - ], - } - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - photoURL: 'mockPhotoUrl', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - const mockNewUserRow = { - created_time: 'mockCreatedTime', - data: {mockNewUserJson: 'mockNewUserJsonData'}, - id: 'mockNewUserId', - name: 'mockName', - name_username_vector: 'mockNameUsernameVector', - username: 'mockUsername', - } - const mockPrivateUserRow = { - data: {mockPrivateUserJson: 'mockPrivateUserJsonData'}, - id: 'mockPrivateUserId', - } - - const mockGetUser = jest - .fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser) - - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(mockPg.one as jest.Mock).mockResolvedValue(0) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - ;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false) - ;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null) - ;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) - ;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow) - ;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow) - - const results: any = await createUser(mockProps, mockAuth, mockReq) - - ;(sharedAnalytics.track as jest.Mock).mockResolvedValue(null) - ;(emailHelpers.sendWelcomeEmail as jest.Mock).mockRejectedValue( - new Error('Welcome email failed'), - ) - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - - await results.continue() - - expect(errorSpy).toBeCalledWith('Failed to sendWelcomeEmail', expect.any(Error)) - }) - - it('should throw if failed to set last time online', async () => { - const mockProps = { - deviceToken: 'mockDeviceToken', - adminToken: 'mockAdminToken', - } - const mockAuth = {uid: '321'} as AuthedUser - const mockReferer = { - headers: { - referer: 'mockReferer', - }, - } - const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any - const mockFirebaseUser = { - providerData: [ - { - providerId: 'passwords', - }, - ], - } - const mockFbUser = { - email: 'mockEmail@mockServer.com', - displayName: 'mockDisplayName', - photoURL: 'mockPhotoUrl', - } - const mockIp = 'mockIP' - const mockBucket = {} as any - const mockNewUserRow = { - created_time: 'mockCreatedTime', - data: {mockNewUserJson: 'mockNewUserJsonData'}, - id: 'mockNewUserId', - name: 'mockName', - name_username_vector: 'mockNameUsernameVector', - username: 'mockUsername', - } - const mockPrivateUserRow = { - data: {mockPrivateUserJson: 'mockPrivateUserJsonData'}, - id: 'mockPrivateUserId', - } - - const mockGetUser = jest - .fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser) - - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp) - ;(firebaseAdmin.auth as jest.Mock).mockReturnValue({ - getUser: mockGetUser, - }) - ;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket) - ;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName) - ;(mockPg.one as jest.Mock).mockResolvedValue(0) - ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - ;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false) - ;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null) - ;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) - ;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow) - ;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow) - - const results: any = await createUser(mockProps, mockAuth, mockReq) - - ;(sharedAnalytics.track as jest.Mock).mockResolvedValue(null) - ;(emailHelpers.sendWelcomeEmail as jest.Mock).mockResolvedValue(null) - ;(apiSetLastTimeOnline.setLastOnlineTimeUser as jest.Mock).mockRejectedValue( - new Error('Failed to set last online time'), - ) - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - - await results.continue() - - expect(errorSpy).toHaveBeenCalledWith('Failed to set last online time', expect.any(Error)) - }) - }) -}) diff --git a/backend/api/tests/unit/update-me.unit.test.ts b/backend/api/tests/unit/update-me.unit.test.ts index 7d2828e6..7533986d 100644 --- a/backend/api/tests/unit/update-me.unit.test.ts +++ b/backend/api/tests/unit/update-me.unit.test.ts @@ -5,7 +5,6 @@ jest.mock('shared/websockets/helpers') import {AuthedUser} from 'api/helpers/endpoint' import {updateMe} from 'api/update-me' -import {sqlMatch} from 'common/test-utils' import * as supabaseInit from 'shared/supabase/init' import * as supabaseUsers from 'shared/supabase/users' import * as sharedUtils from 'shared/utils' @@ -31,18 +30,16 @@ describe('updateMe', () => { name: 'mockName', username: 'mockUsername', avatarUrl: 'mockAvatarUrl', - link: {mockLink: 'mockLinkValue'}, } as any const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any - const mockData = {link: mockProps.link} ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(true) ;(sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(false) - ;(supabaseUsers.updateUser as jest.Mock) + ;(supabaseUsers.updateUserData as jest.Mock) .mockResolvedValueOnce(null) .mockResolvedValueOnce(null) - ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockData) + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue({}) ;(mockPg.none as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) ;(websocketHelperModules.broadcastUpdatedUser as jest.Mock).mockReturnValue(null) @@ -52,40 +49,14 @@ describe('updateMe', () => { expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid) expect(sharedUtils.getUserByUsername).toBeCalledTimes(1) expect(sharedUtils.getUserByUsername).toBeCalledWith(mockProps.username) - expect(supabaseUsers.updateUser).toBeCalledTimes(2) - expect(supabaseUsers.updateUser).toHaveBeenNthCalledWith( - 1, + expect(supabaseUsers.updateUserData).toBeCalledTimes(1) + expect(supabaseUsers.updateUserData).toHaveBeenCalledWith( expect.any(Object), mockAuth.uid, {}, ) - expect(supabaseUsers.updateUser).toHaveBeenNthCalledWith( - 2, - expect.any(Object), - mockAuth.uid, - {avatarUrl: mockProps.avatarUrl}, - ) - expect(mockPg.oneOrNone).toBeCalledTimes(1) - expect(mockPg.oneOrNone).toBeCalledWith(sqlMatch('update users'), { - adds: expect.any(Object), - removes: expect.any(Array), - id: mockAuth.uid, - }) - expect(mockPg.none).toBeCalledTimes(2) - expect(mockPg.none).toHaveBeenNthCalledWith( - 1, - sqlMatch(`update users - set name = $1 - where id = $2`), - [mockProps.name, mockAuth.uid], - ) - expect(mockPg.none).toHaveBeenNthCalledWith( - 2, - sqlMatch(`update users - set username = $1 - where id = $2`), - [mockProps.username, mockAuth.uid], - ) + expect(supabaseUsers.updateUser).toBeCalledTimes(1) + expect(supabaseUsers.updateUser).toHaveBeenCalledWith(mockAuth.uid, mockProps) expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledTimes(1) expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledWith({ ...mockProps, @@ -149,7 +120,7 @@ describe('updateMe', () => { arraySpy.mockReturnValue(false) ;(sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(true) - expect(updateMe(mockProps, mockAuth, mockReq)).rejects.toThrow('Username already taken') + expect(updateMe(mockProps, mockAuth, mockReq)).rejects.toThrow('Username is already taken') }) }) }) diff --git a/backend/api/tests/unit/update-profile.unit.test.ts b/backend/api/tests/unit/update-profile.unit.test.ts index 8de010ed..3fd77a39 100644 --- a/backend/api/tests/unit/update-profile.unit.test.ts +++ b/backend/api/tests/unit/update-profile.unit.test.ts @@ -5,7 +5,7 @@ jest.mock('shared/profiles/parse-photos') jest.mock('shared/supabase/users') import {AuthedUser} from 'api/helpers/endpoint' -import {updateProfile} from 'api/update-profile' +import {updateProfileEndpoint} from 'api/update-profile' import {sqlMatch} from 'common/test-utils' import {tryCatch} from 'common/util/try-catch' import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' @@ -39,7 +39,7 @@ describe('updateProfiles', () => { .mockResolvedValueOnce({data: true}) .mockResolvedValueOnce({data: mockData, error: null}) - const result = await updateProfile(mockProps, mockAuth, mockReq) + const result = await updateProfileEndpoint(mockProps, mockAuth, mockReq) expect(result).toBe(mockData) expect(mockPg.oneOrNone).toBeCalledTimes(1) @@ -49,8 +49,8 @@ describe('updateProfiles', () => { ) expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1) expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockProps) - expect(supabaseUsers.updateUser).toBeCalledTimes(1) - expect(supabaseUsers.updateUser).toBeCalledWith(expect.any(Object), mockAuth.uid, { + expect(supabaseUsers.updateUserData).toBeCalledTimes(1) + expect(supabaseUsers.updateUserData).toBeCalledWith(expect.any(Object), mockAuth.uid, { avatarUrl: mockProps.pinned_url, }) expect(supabaseUtils.update).toBeCalledTimes(1) @@ -66,19 +66,21 @@ describe('updateProfiles', () => { describe('when an error occurs', () => { it('should throw if the profile does not exist', async () => { const mockProps = { - avatar_url: 'mockAvatarUrl', + age: 28, } const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any ;(tryCatch as jest.Mock).mockResolvedValue({data: false}) - expect(updateProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Profile not found') + expect(updateProfileEndpoint(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'Profile not found', + ) }) it('should throw if unable to update the profile', async () => { const mockProps = { - avatar_url: 'mockAvatarUrl', + age: 28, } const mockAuth = {uid: '321'} as AuthedUser const mockReq = {} as any @@ -87,7 +89,9 @@ describe('updateProfiles', () => { .mockResolvedValueOnce({data: true}) .mockResolvedValueOnce({data: null, error: Error}) - expect(updateProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Error updating profile') + expect(updateProfileEndpoint(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'Error updating profile', + ) }) }) }) diff --git a/backend/email/emails/functions/mock.ts b/backend/email/emails/functions/mock.ts index f8e2b430..234e33b8 100644 --- a/backend/email/emails/functions/mock.ts +++ b/backend/email/emails/functions/mock.ts @@ -23,10 +23,6 @@ export const mockUser: User = { id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2', username: 'Martin', name: 'Martin', - link: { - site: 'martinbraquet.com', - x: 'martin', - }, } export const jamesUser: User = { @@ -35,9 +31,6 @@ export const jamesUser: User = { id: '5LZ4LgYuySdL1huCWe7bti02ghx2', username: 'James', name: 'James', - link: { - x: 'james', - }, } export const jamesProfile: PartialProfile = { diff --git a/backend/scripts/2026-03-08-migrate-social-links-to-profiles.ts b/backend/scripts/2026-03-08-migrate-social-links-to-profiles.ts new file mode 100644 index 00000000..c77ac83d --- /dev/null +++ b/backend/scripts/2026-03-08-migrate-social-links-to-profiles.ts @@ -0,0 +1,68 @@ +import {runScript} from './run-script' +import {log} from 'shared/monitoring/log' +import {bulkUpdate} from 'shared/supabase/utils' +import {chunk} from 'lodash' +import {removeUndefinedProps} from 'common/util/object' + +runScript(async ({pg}) => { + const users = await pg.manyOrNone(` + select u.data, u.id, p.id as pid + from users u + left join profiles p on p.user_id = u.id + `) + + log('Found', users.length, 'users to migrate') + + const userUpdates: {id: string; avatar_url: string; is_banned_from_posting: boolean}[] = [] + const profileUpdates: {id: string; links: Record}[] = [] + + for (const {id, pid, data} of users) { + const {link: links, avatarUrl: avatar_url, isBannedFromPosting: is_banned_from_posting} = data + + if (avatar_url || is_banned_from_posting) { + userUpdates.push( + removeUndefinedProps({ + id, + avatar_url: avatar_url ?? null, + is_banned_from_posting, + }), + ) + } + + if (links && pid) { + profileUpdates.push({id: pid, links}) + } else if (links && !pid) { + log('Warning: user has links but no profile', id) + } + } + + log(`Migrating ${userUpdates.length} users, ${profileUpdates.length} profiles`) + + let userCount = 0 + for (const batch of chunk(userUpdates, 100)) { + await bulkUpdate(pg, 'users', ['id'], batch) + log('Updated users', (userCount += batch.length)) + } + + let profileCount = 0 + for (const batch of chunk(profileUpdates, 100)) { + // If not, write the profiles update directly: + await pg.none(` + UPDATE profiles SET links = v.links::jsonb + FROM (VALUES ${batch.map((r) => `(${r.id}, '${JSON.stringify(r.links)}')`).join(',')}) + AS v(id, links) + WHERE profiles.id = v.id::bigint + `) + log('Updated profiles', (profileCount += batch.length)) + } + + // Strip migrated fields from the JSON blob + await pg.none(` + update users + set data = data - '{link,avatarUrl,isBannedFromPosting}'::text[] + `) + + log('Migration complete') + + process.exit(0) +}) diff --git a/backend/scripts/restore_data_column.ts b/backend/scripts/restore_data_column.ts new file mode 100644 index 00000000..a63aec8a --- /dev/null +++ b/backend/scripts/restore_data_column.ts @@ -0,0 +1,42 @@ +import {runScript} from './run-script' +import {log} from 'shared/monitoring/log' +import {chunk} from 'lodash' +import {Client} from 'pg' + +runScript(async ({pg}) => { + // Connect to local Supabase emulator + const localClient = new Client({ + host: '127.0.0.1', + port: 54322, + user: 'postgres', + password: 'postgres', + database: 'postgres', + }) + await localClient.connect() + log('Connected to local emulator') + + // Read all user data from the backup + let {rows} = await localClient.query<{id: string; data: object}>(`SELECT id, data FROM users`) + await localClient.end() + + // rows = rows.slice(0, 5) + // log(rows) + + log(`Found ${rows.length} users in backup`) + + // Update remote in batches + let count = 0 + for (const batch of chunk(rows, 100)) { + await pg.none( + `UPDATE users AS target + SET data = v.data::jsonb + FROM (VALUES ${batch.map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`).join(', ')}) + AS v(id, data) + WHERE target.id = v.id`, + batch.flatMap((r) => [r.id, JSON.stringify(r.data)]), + ) + log(`Restored data for ${(count += batch.length)} / ${rows.length} users`) + } + + log('Done restoring data column from backup') +}) diff --git a/backend/shared/src/profiles/supabase.ts b/backend/shared/src/profiles/supabase.ts index a72d8676..04b0e03d 100644 --- a/backend/shared/src/profiles/supabase.ts +++ b/backend/shared/src/profiles/supabase.ts @@ -66,8 +66,8 @@ export const getGenderCompatibleProfiles = async (profile: ProfileRow) => { users on users.id = profiles.user_id where user_id != $(user_id) and looking_for_matches - and (data ->> 'isBannedFromPosting' != 'true' or data ->> 'isBannedFromPosting' is null) - and (data ->> 'userDeleted' != 'true' or data ->> 'userDeleted' is null) + and not is_banned_from_posting +-- and (data ->> 'userDeleted' != 'true' or data ->> 'userDeleted' is null) and profiles.pinned_url is not null `, {...profile}, @@ -86,8 +86,8 @@ export const getCompatibleProfiles = async (profile: ProfileRow, radiusKm: numbe users on users.id = profiles.user_id where user_id != $(user_id) and looking_for_matches - and (data ->> 'isBannedFromPosting' != 'true' or data ->> 'isBannedFromPosting' is null) - and (data ->> 'userDeleted' != 'true' or data ->> 'userDeleted' is null) + and not is_banned_from_posting +-- and (data ->> 'userDeleted' != 'true' or data ->> 'userDeleted' is null) -- Gender and (profiles.gender = any ($(pref_gender)) or profiles.gender = 'non-binary') diff --git a/backend/shared/src/supabase/users.ts b/backend/shared/src/supabase/users.ts index 0a694e27..f4d7b186 100644 --- a/backend/shared/src/supabase/users.ts +++ b/backend/shared/src/supabase/users.ts @@ -1,21 +1,52 @@ +import {ProfileRow} from 'common/profiles/profile' +import {convertUserToDb} from 'common/supabase/users' import {User} from 'common/user' -import {SupabaseDirectClient} from 'shared/supabase/init' +import {removeUndefinedProps} from 'common/util/object' +import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init' import {broadcastUpdatedPrivateUser, broadcastUpdatedUser} from 'shared/websockets/helpers' -import {DataUpdate, updateData} from './utils' +import {DataUpdate, update, updateData} from './utils' + +export const updateProfile = async (user_id: string, updated: Partial) => { + updated = removeUndefinedProps(updated) + // if (!updated) return + const fullUpdate = {user_id, ...updated} + const pg = createSupabaseDirectClient() + return await update(pg, 'profiles', 'user_id', fullUpdate) +} + +export const updateUser = async (id: string, updated: Partial) => { + updated = removeUndefinedProps(updated) + if (!updated) return + const fullUpdate = {id, ...updated} + const pg = createSupabaseDirectClient() + const result = await update(pg, 'users', 'id', convertUserToDb(fullUpdate)) + broadcastUpdatedUser(fullUpdate) + return result +} /** only updates data column. do not use for name, username */ -export const updateUser = async (db: SupabaseDirectClient, id: string, update: Partial) => { - const fullUpdate = {id, ...update} - await updateData(db, 'users', 'id', fullUpdate) - broadcastUpdatedUser(fullUpdate) +export const updateUserData = async ( + db: SupabaseDirectClient, + id: string, + updated: DataUpdate<'users'>, +) => { + updated = removeUndefinedProps(updated) + if (!updated) return + const fullUpdate = {id, ...updated} + const result = await updateData(db, 'users', 'id', fullUpdate) + broadcastUpdatedUser(fullUpdate as any) // maybe fix + return result } export const updatePrivateUser = async ( db: SupabaseDirectClient, id: string, - update: DataUpdate<'private_users'>, + updated: DataUpdate<'private_users'>, ) => { - await updateData(db, 'private_users', 'id', {id, ...update}) + updated = removeUndefinedProps(updated) + if (!updated) return + const result = await updateData(db, 'private_users', 'id', {id, ...updated}) broadcastUpdatedPrivateUser(id) + return result } diff --git a/backend/supabase/profiles.sql b/backend/supabase/profiles.sql index e2b1d396..8e1e93c0 100644 --- a/backend/supabase/profiles.sql +++ b/backend/supabase/profiles.sql @@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS profiles image_descriptions jsonb, is_smoker BOOLEAN, last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL, + links JSONB default '{}'::jsonb not null, looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL, allow_direct_messaging BOOLEAN DEFAULT TRUE NOT NULL, allow_interest_indicating BOOLEAN DEFAULT TRUE NOT NULL, @@ -61,12 +62,10 @@ CREATE TABLE IF NOT EXISTS profiles religion TEXT[], religious_belief_strength INTEGER, religious_beliefs TEXT, - twitter TEXT, university TEXT, user_id TEXT NOT NULL, visibility profile_visibility DEFAULT 'member'::profile_visibility NOT NULL, wants_kids_strength INTEGER DEFAULT 0, - website TEXT, CONSTRAINT profiles_pkey PRIMARY KEY (id) ); diff --git a/backend/supabase/users.sql b/backend/supabase/users.sql index ae5aa839..8df3704a 100644 --- a/backend/supabase/users.sql +++ b/backend/supabase/users.sql @@ -1,26 +1,31 @@ -CREATE TABLE IF NOT EXISTS users ( - created_time TIMESTAMPTZ DEFAULT now() NOT NULL, - data JSONB NOT NULL, - id TEXT DEFAULT random_alphanumeric(12) NOT NULL, - name TEXT NOT NULL, - name_username_vector tsvector GENERATED ALWAYS AS ( +CREATE TABLE IF NOT EXISTS users +( + created_time TIMESTAMPTZ DEFAULT now() NOT NULL, + data JSONB NOT NULL, + id TEXT DEFAULT random_alphanumeric(12) NOT NULL, + name TEXT NOT NULL, + name_username_vector tsvector GENERATED ALWAYS AS ( to_tsvector( - 'english'::regconfig, - (name || ' '::text) || username + 'english'::regconfig, + (name || ' '::text) || username ) - ) STORED, - username TEXT NOT NULL, + ) STORED, + username TEXT NOT NULL, + avatar_url TEXT, + is_banned_from_posting BOOLEAN DEFAULT FALSE NOT NULL, + CONSTRAINT users_pkey PRIMARY KEY (id) - ); +); -- Row Level Security -ALTER TABLE users ENABLE ROW LEVEL SECURITY; +ALTER TABLE users + ENABLE ROW LEVEL SECURITY; -- Policies DROP POLICY IF EXISTS "public read" ON users; CREATE POLICY "public read" ON users -FOR SELECT - USING (true); + FOR SELECT + USING (true); -- Indexes CREATE INDEX IF NOT EXISTS user_username_idx ON public.users USING btree (username); @@ -28,7 +33,3 @@ CREATE INDEX IF NOT EXISTS user_username_idx ON public.users USING btree (userna CREATE INDEX IF NOT EXISTS users_created_time ON public.users USING btree (created_time DESC); CREATE INDEX IF NOT EXISTS users_name_idx ON public.users USING btree (name); - --- Remove these if you trust PRIMARY KEY auto-index: --- DROP INDEX IF EXISTS users_pkey; --- CREATE UNIQUE INDEX users_pkey ON public.users USING btree (id); diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 60249b5e..87a80a2d 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -309,7 +309,6 @@ export const API = (_apiTypeCheck = { locale: z.string().optional(), username: z.string().min(1), name: z.string().min(1), - link: z.record(z.string().nullable()).optional(), profile: combinedProfileSchema, interests: arraybeSchema.optional(), causes: arraybeSchema.optional(), @@ -378,7 +377,6 @@ export const API = (_apiTypeCheck = { name: z.string().trim().min(1).optional(), username: z.string().trim().min(1).optional(), avatarUrl: z.string().optional(), - link: z.record(z.string().nullable()).optional(), }), returns: {} as FullUser, summary: 'Update authenticated user profile and settings', diff --git a/common/src/api/user-types.ts b/common/src/api/user-types.ts index 2ad3aa30..4e736840 100644 --- a/common/src/api/user-types.ts +++ b/common/src/api/user-types.ts @@ -1,4 +1,4 @@ -import {ENV_CONFIG, MOD_USERNAMES} from 'common/envs/constants' +import {ENV_CONFIG, MOD_USERNAMES, WEB_URL} from 'common/envs/constants' import {User} from 'common/user' import {removeUndefinedProps} from 'common/util/object' @@ -20,7 +20,7 @@ export type FullUser = User & { export function toUserAPIResponse(user: User): FullUser { return removeUndefinedProps({ ...user, - url: `https://${ENV_CONFIG.domain}/${user.username}`, + url: `${WEB_URL}/${user.username}`, isAdmin: ENV_CONFIG.adminIds.includes(user.id), isTrustworthy: MOD_USERNAMES.includes(user.username), }) diff --git a/common/src/api/zod-types.ts b/common/src/api/zod-types.ts index 960fa1f7..a7ef232b 100644 --- a/common/src/api/zod-types.ts +++ b/common/src/api/zod-types.ts @@ -70,7 +70,6 @@ export const baseProfilesSchema = z.object({ }) const optionalProfilesSchema = z.object({ - avatar_url: z.string().optional().nullable(), bio: contentSchema.optional().nullable(), big5_openness: z.number().min(0).max(100).optional().nullable(), big5_conscientiousness: z.number().min(0).max(100).optional().nullable(), @@ -96,6 +95,7 @@ const optionalProfilesSchema = z.object({ image_descriptions: z.any().optional().nullable(), interests: z.array(z.string()).optional().nullable(), is_smoker: zBoolean.optional().nullable(), + links: z.record(z.string().nullable()).optional(), mbti: z.string().optional().nullable(), occupation: z.string().optional().nullable(), occupation_title: z.string().optional().nullable(), diff --git a/common/src/supabase/schema.ts b/common/src/supabase/schema.ts index 82822e61..f31e2af6 100644 --- a/common/src/supabase/schema.ts +++ b/common/src/supabase/schema.ts @@ -1105,12 +1105,13 @@ export type Database = { has_kids: number | null headline: string | null height_in_inches: number | null - keywords: string[] | null id: number image_descriptions: Json | null is_smoker: boolean | null + keywords: string[] | null languages: string[] | null last_modification_time: string + links: Json looking_for_matches: boolean mbti: string | null messaging_status: string @@ -1140,12 +1141,10 @@ export type Database = { religious_beliefs: string | null search_text: string | null search_tsv: unknown - twitter: string | null university: string | null user_id: string visibility: Database['public']['Enums']['lover_visibility'] wants_kids_strength: number | null - website: string | null } Insert: { age?: number | null @@ -1180,10 +1179,11 @@ export type Database = { height_in_inches?: number | null id?: number image_descriptions?: Json | null - keywords?: string[] | null is_smoker?: boolean | null + keywords?: string[] | null languages?: string[] | null last_modification_time?: string + links?: Json looking_for_matches?: boolean mbti?: string | null messaging_status?: string @@ -1213,12 +1213,10 @@ export type Database = { religious_beliefs?: string | null search_text?: string | null search_tsv?: unknown - twitter?: string | null university?: string | null user_id: string visibility?: Database['public']['Enums']['lover_visibility'] wants_kids_strength?: number | null - website?: string | null } Update: { age?: number | null @@ -1253,10 +1251,11 @@ export type Database = { height_in_inches?: number | null id?: number image_descriptions?: Json | null - keywords?: string[] | null is_smoker?: boolean | null + keywords?: string[] | null languages?: string[] | null last_modification_time?: string + links?: Json looking_for_matches?: boolean mbti?: string | null messaging_status?: string @@ -1286,12 +1285,10 @@ export type Database = { religious_beliefs?: string | null search_text?: string | null search_tsv?: unknown - twitter?: string | null university?: string | null user_id?: string visibility?: Database['public']['Enums']['lover_visibility'] wants_kids_strength?: number | null - website?: string | null } Relationships: [ { @@ -1538,25 +1535,31 @@ export type Database = { } users: { Row: { + avatar_url: string | null created_time: string data: Json id: string + is_banned_from_posting: boolean | null name: string name_username_vector: unknown username: string } Insert: { + avatar_url?: string | null created_time?: string data: Json id?: string + is_banned_from_posting?: boolean | null name: string name_username_vector?: unknown username: string } Update: { + avatar_url?: string | null created_time?: string data?: Json id?: string + is_banned_from_posting?: boolean | null name?: string name_username_vector?: unknown username?: string @@ -1741,7 +1744,7 @@ export type Database = { is_admin: {Args: never; Returns: boolean} | {Args: {user_id: string}; Returns: boolean} millis_interval: { Args: {end_millis: number; start_millis: number} - Returns: unknown + Returns: string } millis_to_ts: {Args: {millis: number}; Returns: string} random_alphanumeric: {Args: {length: number}; Returns: string} diff --git a/common/src/supabase/users.ts b/common/src/supabase/users.ts index 2c79c66c..2830b9a1 100644 --- a/common/src/supabase/users.ts +++ b/common/src/supabase/users.ts @@ -1,23 +1,49 @@ import {PrivateUser, User} from 'common/user' +import {removeUndefinedProps} from 'common/util/object' -import {Row, run, SupabaseClient, tsToMillis} from './utils' +import {millisToTs, Row, run, SupabaseClient, tsToMillis} from './utils' export async function getUserForStaticProps(db: SupabaseClient, username: string) { const {data} = await run(db.from('users').select().ilike('username', username)) return convertUser(data[0] ?? null) } -export function convertUser(row: Row<'users'>): User -export function convertUser(row: Row<'users'> | null): User | null { - if (!row) return null - +function toDb(row: any): any { return { ...(row.data as any), id: row.id, username: row.username, name: row.name, - createdTime: tsToMillis(row.created_time), - } as User + avatarUrl: row.avatar_url, + isBannedFromPosting: row.is_banned_from_posting, + createdTime: row.created_time ? tsToMillis(row.created_time) : undefined, + } +} + +// From DB to typescript +export function convertUser(row: Row<'users'>): User +export function convertUser(row: Row<'users'> | null): User | null { + if (!row) return null + return toDb(row) +} + +export function convertPartialUser(row: Partial>): Partial { + return removeUndefinedProps(toDb(row)) +} + +// Reciprocal of convertUser, from typescript to DB +export function convertUserToDb(user: Partial): Partial> +export function convertUserToDb(user: Partial | null): Partial> | null { + if (!user) return null + + return removeUndefinedProps({ + id: user.id, + username: user.username, + name: user.name, + avatar_url: user.avatarUrl, + is_banned_from_posting: user.isBannedFromPosting, + created_time: millisToTs(user.createdTime), + }) } export function convertPrivateUser(row: Row<'private_users'>): PrivateUser @@ -26,4 +52,4 @@ export function convertPrivateUser(row: Row<'private_users'> | null): PrivateUse return row.data as PrivateUser } -export const displayUserColumns = `id,name,username,data->>'avatarUrl' as "avatarUrl",data->'isBannedFromPosting' as "isBannedFromPosting"` +export const displayUserColumns = `id,name,username, avatar_url as "avatarUrl",is_banned_from_posting as "isBannedFromPosting"` diff --git a/common/src/supabase/utils.ts b/common/src/supabase/utils.ts index 7afa79a7..d5035730 100644 --- a/common/src/supabase/utils.ts +++ b/common/src/supabase/utils.ts @@ -92,7 +92,8 @@ export function selectFrom< return db.from(table).select(query) } -export function millisToTs(millis: number) { +export function millisToTs(millis: number | undefined) { + if (!millis) return return new Date(millis).toISOString() } diff --git a/common/src/user.ts b/common/src/user.ts index 1ca3edc2..7a9ff1ca 100644 --- a/common/src/user.ts +++ b/common/src/user.ts @@ -1,11 +1,9 @@ -import {Socials} from './socials' import {notification_preferences} from './user-notification-preferences' export type BaseUser = { id: string name: string username: string - link: Socials // Social links } export type User = BaseUser & { diff --git a/tests/e2e/utils/seedDatabase.ts b/tests/e2e/utils/seedDatabase.ts index 0d931936..a7f6543b 100644 --- a/tests/e2e/utils/seedDatabase.ts +++ b/tests/e2e/utils/seedDatabase.ts @@ -67,12 +67,6 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?: const profileData = profileType === 'basic' ? basicProfile : profileType === 'medium' ? mediumProfile : fullProfile - const user = { - // avatarUrl, - isBannedFromPosting: false, - link: {}, - } - const privateUser: PrivateUser = { id: userId, email: userInfo.email, @@ -91,7 +85,7 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?: id: userId, name: userInfo.name, username: cleanUsername(userInfo.name), - data: user, + data: {}, }) await insert(tx, 'private_users', { diff --git a/web/components/optional-profile-form.tsx b/web/components/optional-profile-form.tsx index 46bac6a5..9145f5c6 100644 --- a/web/components/optional-profile-form.tsx +++ b/web/components/optional-profile-form.tsx @@ -17,7 +17,7 @@ import { import {debug} from 'common/logger' import {MultipleChoiceOptions} from 'common/profiles/multiple-choice' import {Profile, ProfileWithoutUser} from 'common/profiles/profile' -import {PLATFORM_LABELS, type Site, SITE_ORDER} from 'common/socials' +import {PLATFORM_LABELS, type Site, SITE_ORDER, Socials} from 'common/socials' import {BaseUser} from 'common/user' import {range} from 'lodash' import {Fragment, useEffect, useRef, useState} from 'react' @@ -51,20 +51,11 @@ export const OptionalProfileUserForm = (props: { profile: ProfileWithoutUser setProfile: (key: K, value: ProfileWithoutUser[K]) => void user: BaseUser - setUser: (key: K, value: BaseUser[K]) => void buttonLabel?: string bottomNavBarVisible?: boolean onSubmit: () => Promise }) => { - const { - profile, - user, - buttonLabel, - setProfile, - setUser, - onSubmit, - bottomNavBarVisible = true, - } = props + const {profile, user, buttonLabel, setProfile, onSubmit, bottomNavBarVisible = true} = props const [isSubmitting, setIsSubmitting] = useState(false) const [lookingRelationship, setLookingRelationship] = useState( @@ -125,7 +116,7 @@ export const OptionalProfileUserForm = (props: { } const updateUserLink = (platform: string, value: string | null) => { - setUser('link', {...user.link, [platform]: value}) + setProfile('links', {...((profile.links as Socials) ?? {}), [platform]: value}) } const addNewLink = () => { @@ -913,7 +904,7 @@ export const OptionalProfileUserForm = (props: { {/**/}
- {Object.entries(user.link) + {Object.entries(profile.links as Socials) .filter(([_, value]) => value != null) .map(([platform, value]) => ( diff --git a/web/components/profile-about.tsx b/web/components/profile-about.tsx index 330b0a43..7f26a06e 100644 --- a/web/components/profile-about.tsx +++ b/web/components/profile-about.tsx @@ -14,6 +14,7 @@ import {convertGenderPlural, Gender} from 'common/gender' import {getLocationText} from 'common/geodb' import {formatHeight, MeasurementSystem} from 'common/measurement-utils' import {Profile} from 'common/profiles/profile' +import {Socials} from 'common/socials' import {UserActivity} from 'common/user' import {Home} from 'lucide-react' import React, {ReactNode} from 'react' @@ -171,7 +172,7 @@ export default function ProfileAbout(props: { {!isCurrentUser && } - + ) } diff --git a/web/components/widgets/user-link.tsx b/web/components/widgets/user-link.tsx index a54022b6..a00e4fd2 100644 --- a/web/components/widgets/user-link.tsx +++ b/web/components/widgets/user-link.tsx @@ -149,7 +149,7 @@ function FreshBadge() { // name: string // username: string // createdTime: number -// isBannedFromPosting?: boolean +// is_banned_from_posting?: boolean // } // followsYou?: boolean // className?: string @@ -167,7 +167,7 @@ function FreshBadge() { // fresh={isFresh(user.createdTime)} // /> // } -// {user.isBannedFromPosting && } +// {user.is_banned_from_posting && } //
// // diff --git a/web/lib/supabase/users.ts b/web/lib/supabase/users.ts index d2450464..ecaacd00 100644 --- a/web/lib/supabase/users.ts +++ b/web/lib/supabase/users.ts @@ -1,5 +1,6 @@ import type {DisplayUser} from 'common/api/user-types' import {APIError} from 'common/api/utils' +import {convertPartialUser} from 'common/supabase/users' import {run, TableName} from 'common/supabase/utils' import {MONTH_MS} from 'common/util/time' import {api} from 'web/lib/api' @@ -51,11 +52,11 @@ export async function getDisplayUsers(userIds: string[]) { const {data} = await run( db .from('users') - .select(`id, name, username, data->avatarUrl, data->isBannedFromPosting`) + .select(`id, name, username, avatar_url, is_banned_from_posting`) .in('id', userIds), ) - return data as unknown as DisplayUser[] + return data.map(convertPartialUser) as unknown as DisplayUser[] } export async function getProfilesCreations() { diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index c5979de1..9631071b 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -114,7 +114,6 @@ function ProfilePageInner(props: {user: User; profile: Profile}) { profile={profile} setProfile={setProfileState} user={baseUser} - setUser={setBaseUserState} buttonLabel={t('profile.save', 'Save')} onSubmit={async () => await submitForm()} /> diff --git a/web/pages/signup.tsx b/web/pages/signup.tsx index 30fd6e18..a6a00cdd 100644 --- a/web/pages/signup.tsx +++ b/web/pages/signup.tsx @@ -92,7 +92,6 @@ export default function SignupPage() { name, locale, deviceToken, - link: baseUser.link, profile: otherProfileProps, interests, causes, @@ -142,7 +141,6 @@ export default function SignupPage() { profile={profileForm} setProfile={setProfileState} user={baseUser} - setUser={setBaseUserState} bottomNavBarVisible={false} onSubmit={handleFinalSubmit} /> @@ -168,7 +166,6 @@ function getInitialBaseUser() { id: auth.currentUser?.uid ?? '', username: cleanUsername(name), name: name, - link: {}, } return initialState }