diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index e07f64d1..dd2deede 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -54,8 +54,6 @@ import {createCompatibilityQuestion} from './create-compatibility-question' import {createEvent} from './create-event' import {createPrivateUserMessage} from './create-private-user-message' import {createPrivateUserMessageChannel} from './create-private-user-message-channel' -import {createProfile} from './create-profile' -import {createUser} from './create-user' import {createUserAndProfile} from './create-user-and-profile' import {deleteBookmarkedSearch} from './delete-bookmarked-search' import {deleteCompatibilityAnswer} from './delete-compatibility-answer' @@ -589,8 +587,6 @@ const handlers: {[k in APIPath]: APIHandler} = { 'create-compatibility-question': createCompatibilityQuestion, 'create-private-user-message': createPrivateUserMessage, 'create-private-user-message-channel': createPrivateUserMessageChannel, - 'create-profile': createProfile, - 'create-user': createUser, 'create-user-and-profile': createUserAndProfile, 'create-vote': createVote, 'delete-bookmarked-search': deleteBookmarkedSearch, diff --git a/backend/api/src/create-profile.ts b/backend/api/src/create-profile.ts deleted file mode 100644 index 28ceab84..00000000 --- a/backend/api/src/create-profile.ts +++ /dev/null @@ -1,90 +0,0 @@ -import {APIErrors, APIHandler} from 'api/helpers/endpoint' -import {sendDiscordMessage} from 'common/discord/core' -import {debug} from 'common/logger' -import {jsonToMarkdown} from 'common/md' -import {trimStrings} from 'common/parsing' -import {HOUR_MS, MINUTE_MS, sleep} from 'common/util/time' -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 {updateUserData} from 'shared/supabase/users' -import {insert} from 'shared/supabase/utils' -import {getUser, log} from 'shared/utils' - -export const createProfile: APIHandler<'create-profile'> = async (body, auth) => { - const pg = createSupabaseDirectClient() - - const {data: existingProfile} = await tryCatch( - pg.oneOrNone<{id: string}>('select id from profiles where user_id = $1', [auth.uid]), - ) - if (existingProfile) { - throw APIErrors.badRequest('Profile already exists') - } - - await removePinnedUrlFromPhotoUrls(body) - trimStrings(body) - - const user = await getUser(auth.uid) - 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 - updateUserData(pg, auth.uid, {avatarUrl: body.pinned_url || undefined}) - } - - debug('body', body) - - const {data, error} = await tryCatch(insert(pg, 'profiles', {user_id: auth.uid, ...body})) - - if (error) { - log.error('Error creating user: ' + error.message) - throw APIErrors.internalServerError('Error creating user') - } - - log('Created profile', data) - - const continuation = async () => { - try { - await track(auth.uid, 'create profile', {username: user.username}) - } catch (e) { - console.error('Failed to track create profile', e) - } - try { - // Let the user fill in the optional form with all their info and pictures before notifying discord of their arrival. - // So we can sse their full profile as soon as we get the notif on discord. And that allows OG to pull their pic for the link preview. - // Regardless, you need to wait for at least 5 seconds that the profile is fully in the db—otherwise ISR may cache "profile not created yet" - await sleep(10 * MINUTE_MS) - let message: string = `[**${user.name}**](https://compassmeet.com/${user.username}) just created a profile` - if (body.bio) { - const bioText = jsonToMarkdown(body.bio) - if (bioText) message += `\n${bioText}` - } - await sendDiscordMessage(message, 'members') - } catch (e) { - console.error('Failed to send discord new profile', e) - } - try { - const nProfiles = await pg.one(`SELECT count(*) FROM profiles`, [], (r) => - Number(r.count), - ) - - const isMilestone = (n: number) => { - return ( - [15, 20, 30, 40].includes(n) || // early milestones - n % 50 === 0 - ) - } - debug(nProfiles, isMilestone(nProfiles)) - if (isMilestone(nProfiles)) { - await sendDiscordMessage(`We just reached **${nProfiles}** total profiles! 🎉`, 'general') - } - } catch (e) { - console.error('Failed to send discord user milestone', e) - } - } - - return { - result: data, - continue: continuation, - } -} diff --git a/backend/api/src/create-user.ts b/backend/api/src/create-user.ts deleted file mode 100644 index 34ab3a11..00000000 --- a/backend/api/src/create-user.ts +++ /dev/null @@ -1,173 +0,0 @@ -import {setLastOnlineTimeUser} from 'api/set-last-online-time' -import {defaultLocale} from 'common/constants' -import {RESERVED_PATHS} from 'common/envs/constants' -import {convertPrivateUser, convertUser} from 'common/supabase/users' -import {PrivateUser} from 'common/user' -import {getDefaultNotificationPreferences} from 'common/user-notification-preferences' -import {cleanDisplayName, cleanUsername} from 'common/util/clean-username' -import {removeUndefinedProps} from 'common/util/object' -import {randomString} from 'common/util/random' -import {sendWelcomeEmail} from 'email/functions/helpers' -import * as admin from 'firebase-admin' -import {getIp, track} from 'shared/analytics' -import {getBucket} from 'shared/firebase-utils' -import {generateAvatarUrl} from 'shared/helpers/generate-and-update-avatar-urls' -import {createSupabaseDirectClient} from 'shared/supabase/init' -import {insert} from 'shared/supabase/utils' -import {getUser, getUserByUsername, log} from 'shared/utils' - -import {APIErrors, APIHandler} from './helpers/endpoint' - -/** - * Create User API Handler - * - * Creates a new user account with associated profile and private user data. - * This endpoint is called after Firebase authentication to initialize - * the user's presence in the Compass database. - * - * Process: - * 1. Validates Firebase authentication token - * 2. Creates user record in users table - * 3. Creates private user record in private_users table - * 4. Generates default profile data - * 5. Sends welcome email asynchronously - * 6. Tracks user creation event - * - * @param props - Request parameters including device token and locale - * @param auth - Authenticated user information from Firebase - * @param req - Express request object for accessing headers/IP - * @returns User and private user objects with continuation function for async tasks - * @throws {APIError} 403 if user already exists or username is taken - */ -export const createUser: APIHandler<'create-user'> = async (props, auth, req) => { - const {deviceToken, locale = defaultLocale} = props - - const host = req.get('referer') - log(`Create user from: ${host}, ${props}`) - - const ip = getIp(req) - - const fbUser = await admin.auth().getUser(auth.uid) - const email = fbUser.email - const emailName = email?.replace(/@.*$/, '') - - const rawName = fbUser.displayName || emailName || 'User' + randomString(4) - const name = cleanDisplayName(rawName) - - const bucket = getBucket() - const avatarUrl = fbUser.photoURL ?? (await generateAvatarUrl(auth.uid, name, bucket)) - - const pg = createSupabaseDirectClient() - - let username = cleanUsername(name) - - // Check username case-insensitive - const dupes = await pg.one( - `select count(*) - from users - where username ilike $1`, - [username], - (r) => r.count, - ) - const usernameExists = dupes > 0 - const isReservedName = RESERVED_PATHS.has(username) - if (usernameExists || isReservedName) username += randomString(4) - - const {user, privateUser} = await pg.tx(async (tx) => { - const preexistingUser = await getUser(auth.uid, tx) - if (preexistingUser) - throw APIErrors.forbidden('An account for this user already exists', { - field: 'userId', - context: `User with ID ${auth.uid} already exists`, - }) - - // Check exact username to avoid problems with duplicate requests - const sameNameUser = await getUserByUsername(username, tx) - if (sameNameUser) - throw APIErrors.conflict('Username is already taken', { - field: 'username', - context: `Username "${username}" is already taken`, - }) - - const user = removeUndefinedProps({ - avatarUrl, - is_banned_from_posting: Boolean( - (deviceToken && bannedDeviceTokens.includes(deviceToken)) || - (ip && bannedIpAddresses.includes(ip)), - ), - link: {}, - }) - - const privateUser: PrivateUser = { - id: auth.uid, - email, - locale, - initialIpAddress: ip, - initialDeviceToken: deviceToken, - notificationPreferences: getDefaultNotificationPreferences(), - blockedUserIds: [], - blockedByUserIds: [], - } - - const newUserRow = await insert(tx, 'users', { - id: auth.uid, - name, - username, - data: user, - }) - - const newPrivateUserRow = await insert(tx, 'private_users', { - id: privateUser.id, - data: privateUser, - }) - - return { - user: convertUser(newUserRow), - privateUser: convertPrivateUser(newPrivateUserRow), - } - }) - - log('created user ', {username: user.username, firebaseId: auth.uid}) - - const continuation = async () => { - try { - await track(auth.uid, 'create profile', {username: user.username}) - } catch (e) { - console.error('Failed to track create profile', e) - } - try { - await sendWelcomeEmail(user, privateUser) - } catch (e) { - console.error('Failed to sendWelcomeEmail', e) - } - try { - await setLastOnlineTimeUser(auth.uid) - } catch (e) { - console.error('Failed to set last online time', e) - } - } - - return { - result: { - user, - privateUser, - }, - continue: continuation, - } -} - -// Automatically ban users with these device tokens or ip addresses. -const bannedDeviceTokens = [ - 'fa807d664415', - 'dcf208a11839', - 'bbf18707c15d', - '4c2d15a6cc0c', - '0da6b4ea79d3', -] -const bannedIpAddresses: string[] = [ - '24.176.214.250', - '2607:fb90:bd95:dbcd:ac39:6c97:4e35:3fed', - '2607:fb91:389:ddd0:ac39:8397:4e57:f060', - '2607:fb90:ed9a:4c8f:ac39:cf57:4edd:4027', - '2607:fb90:bd36:517a:ac39:6c91:812c:6328', -] diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 5253c2ec..338daa5e 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -1,7 +1,6 @@ import {QuestionWithStats} from 'common/api/types' // mqp: very unscientific, just balancing our willingness to accept load import { arraybeSchema, - baseProfilesSchema, combinedProfileSchema, contentSchema, dateSchema, @@ -281,42 +280,6 @@ export const API = (_apiTypeCheck = { summary: 'Ban or unban a user', tag: 'Admin', }, - 'create-user': { - method: 'POST', - authed: true, - rateLimited: true, - deprecation: { - deprecated: true, - migrationPath: '/create-user-and-profile', - sunsetDate: 'Sat, 01 Jun 2025 00:00:00 GMT', - }, - returns: {} as {user: User; privateUser: PrivateUser}, - props: z - .object({ - deviceToken: z.string().optional(), - adminToken: z.string().optional(), - locale: z.string().optional(), - }) - .strict(), - summary: - 'Create a new user (admin or onboarding flow) - DEPRECATED: use create-user-and-profile instead', - tag: 'Users', - }, - 'create-profile': { - method: 'POST', - authed: true, - rateLimited: true, - deprecation: { - deprecated: true, - migrationPath: '/create-user-and-profile', - sunsetDate: 'Sat, 01 Jun 2025 00:00:00 GMT', - }, - returns: {} as Row<'profiles'>, - props: baseProfilesSchema, - summary: - 'Create a new profile for the authenticated user - DEPRECATED: use create-user-and-profile instead', - tag: 'Profiles', - }, 'create-user-and-profile': { method: 'POST', authed: true,