mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-11 01:59:14 -04:00
Remove deprecated create-profile and create-user endpoints in favor of create-user-and-profile.
This commit is contained in:
@@ -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<k>} = {
|
||||
'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,
|
||||
|
||||
@@ -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<number>(`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,
|
||||
}
|
||||
}
|
||||
@@ -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<number>(
|
||||
`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',
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user