Files
omnivore/packages/api/src/services/create_user.ts
Hongbo Wu 45986184c4 tidy
2024-06-27 17:12:45 +08:00

201 lines
5.7 KiB
TypeScript

import { EntityManager } from 'typeorm'
import { appDataSource } from '../data_source'
import { Filter } from '../entity/filter'
import { GroupMembership } from '../entity/groups/group_membership'
import { Invite } from '../entity/groups/invite'
import { Profile } from '../entity/profile'
import { StatusType, User } from '../entity/user'
import { env } from '../env'
import { SignupErrorCode } from '../generated/graphql'
import { createPubSubClient } from '../pubsub'
import { authTrx, getRepository } from '../repository'
import { userRepository } from '../repository/user'
import { AuthProvider } from '../routers/auth/auth_types'
import { analytics } from '../utils/analytics'
import { IntercomClient } from '../utils/intercom'
import { logger } from '../utils/logger'
import { validateUsername } from '../utils/usernamePolicy'
import { addPopularReadsForNewUser } from './popular_reads'
import { sendNewAccountVerificationEmail } from './send_emails'
export const MAX_RECORDS_LIMIT = 1000
export const createUser = async (input: {
provider: AuthProvider
sourceUserId?: string
email: string
username: string
name: string
pictureUrl?: string
bio?: string
groups?: [string]
inviteCode?: string
password?: string
pendingConfirmation?: boolean
}): Promise<[User, Profile]> => {
const trimmedEmail = input.email.trim()
const existingUser = await userRepository.findByEmail(trimmedEmail)
if (existingUser) {
if (existingUser.profile) {
return Promise.reject({ errorCode: SignupErrorCode.Unknown })
}
// create profile if user exists but profile does not exist
const profile = await getRepository(Profile).save({
username: input.username,
pictureUrl: input.pictureUrl,
bio: input.bio,
user: existingUser,
})
analytics.capture({
distinctId: existingUser.id,
event: 'create_user',
properties: {
env: env.server.apiEnv,
email: existingUser.email,
username: profile.username,
},
})
return [existingUser, profile]
}
if (!validateUsername(input.username)) {
return Promise.reject({ errorCode: SignupErrorCode.InvalidUsername })
}
const [user, profile] = await appDataSource.transaction<[User, Profile]>(
async (t) => {
let hasInvite = false
let invite: Invite | null = null
if (input.inviteCode) {
const inviteCodeRepo = t.getRepository(Invite)
invite = await inviteCodeRepo.findOne({
where: { code: input.inviteCode },
relations: ['group'],
})
if (invite && (await validateInvite(t, invite))) {
hasInvite = true
}
}
const user = await t.getRepository(User).save({
source: input.provider,
name: input.name,
email: trimmedEmail,
sourceUserId: input.sourceUserId,
password: input.password,
status: input.pendingConfirmation
? StatusType.Pending
: StatusType.Active,
})
const profile = await t.getRepository(Profile).save({
username: input.username,
pictureUrl: input.pictureUrl,
bio: input.bio,
user,
})
if (hasInvite && invite) {
await t.getRepository(GroupMembership).save({
user: user,
invite: invite,
group: invite.group,
})
}
await addPopularReadsForNewUser(user.id, t)
await createDefaultFiltersForUser(t)(user.id)
return [user, profile]
}
)
const customAttributes: { source_user_id: string } = {
source_user_id: user.sourceUserId,
}
await IntercomClient?.contacts.createUser({
email: user.email,
externalId: user.id,
name: user.name,
avatar: profile.pictureUrl || undefined,
customAttributes: customAttributes,
signedUpAt: Math.floor(Date.now() / 1000),
})
const pubsubClient = createPubSubClient()
await pubsubClient.userCreated(
user.id,
user.email,
user.name,
profile.username
)
analytics.capture({
distinctId: user.id,
event: 'create_user',
properties: {
env: env.server.apiEnv,
email: user.email,
username: profile.username,
},
})
if (input.pendingConfirmation) {
if (!(await sendNewAccountVerificationEmail(user))) {
return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail })
}
}
return [user, profile]
}
const createDefaultFiltersForUser =
(t: EntityManager) =>
async (userId: string): Promise<Filter[]> => {
const defaultFilters = [
{ name: 'Inbox', filter: 'in:inbox' },
{
name: 'Continue Reading',
filter: 'in:inbox sort:read-desc is:reading',
},
{ name: 'Non-Feed Items', filter: 'no:subscription' },
{ name: 'Highlights', filter: 'in:all has:highlights mode:highlights' },
{ name: 'Unlabeled', filter: 'no:label' },
{ name: 'Oldest First', filter: 'sort:saved-asc' },
{ name: 'Files', filter: 'type:file' },
{ name: 'Archived', filter: 'in:archive' },
].map((it, position) => ({
...it,
user: { id: userId },
position,
defaultFilter: true,
category: 'Search',
}))
return t.getRepository(Filter).save(defaultFilters)
}
// Maybe this should be moved into a service
const validateInvite = async (
entityManager: EntityManager,
invite: Invite
): Promise<boolean> => {
if (invite.expirationTime < new Date()) {
logger.info('rejecting invite, expired', invite)
return false
}
const numMembers = await authTrx(
(t) =>
t.getRepository(GroupMembership).countBy({ invite: { id: invite.id } }),
{
entityManager,
}
)
if (numMembers >= invite.maxMembers) {
logger.info('rejecting invite, too many users', { invite, numMembers })
return false
}
return true
}