diff --git a/packages/api/src/datalayer/user/model.ts b/packages/api/src/datalayer/user/model.ts index 5e10038e9..945d07753 100644 --- a/packages/api/src/datalayer/user/model.ts +++ b/packages/api/src/datalayer/user/model.ts @@ -44,6 +44,7 @@ export interface UserData { private: boolean } password?: string | null + status?: StatusType } export enum MembershipTier { @@ -72,6 +73,7 @@ export const keys = [ 'sourceUserId', 'createdAt', 'password', + 'status', ] as const export const defaultedKeys = ['id', 'createdAt'] as const diff --git a/packages/api/src/entity/user.ts b/packages/api/src/entity/user.ts index 50ad7e7a9..99a6e1afd 100644 --- a/packages/api/src/entity/user.ts +++ b/packages/api/src/entity/user.ts @@ -59,5 +59,5 @@ export class User { subscriptions?: Subscription[] @Column({ type: 'enum', enum: StatusType }) - status!: string + status!: StatusType } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 142f114b9..2310ca181 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -825,11 +825,6 @@ export enum LoginErrorCode { WrongSource = 'WRONG_SOURCE' } -export type LoginInput = { - email: Scalars['String']; - password: Scalars['String']; -}; - export type LoginResult = LoginError | LoginSuccess; export type LoginSuccess = { @@ -893,7 +888,6 @@ export type Mutation = { googleLogin: LoginResult; googleSignup: GoogleSignupResult; logOut: LogOutResult; - login: LoginResult; mergeHighlight: MergeHighlightResult; reportItem: ReportItemResult; revokeApiKey: RevokeApiKeyResult; @@ -911,7 +905,6 @@ export type Mutation = { setShareHighlight: SetShareHighlightResult; setUserPersonalization: SetUserPersonalizationResult; setWebhook: SetWebhookResult; - signup: SignupResult; subscribe: SubscribeResult; unsubscribe: UnsubscribeResult; updateHighlight: UpdateHighlightResult; @@ -1022,11 +1015,6 @@ export type MutationGoogleSignupArgs = { }; -export type MutationLoginArgs = { - input: LoginInput; -}; - - export type MutationMergeHighlightArgs = { input: MergeHighlightInput; }; @@ -1112,11 +1100,6 @@ export type MutationSetWebhookArgs = { }; -export type MutationSignupArgs = { - input: SignupInput; -}; - - export type MutationSubscribeArgs = { name: Scalars['String']; }; @@ -1825,11 +1808,6 @@ export type SharedArticleSuccess = { article: Article; }; -export type SignupError = { - __typename?: 'SignupError'; - errorCodes: Array>; -}; - export enum SignupErrorCode { AccessDenied = 'ACCESS_DENIED', ExpiredToken = 'EXPIRED_TOKEN', @@ -1841,22 +1819,6 @@ export enum SignupErrorCode { UserExists = 'USER_EXISTS' } -export type SignupInput = { - bio?: InputMaybe; - email: Scalars['String']; - name: Scalars['String']; - password: Scalars['String']; - pictureUrl?: InputMaybe; - username: Scalars['String']; -}; - -export type SignupResult = SignupError | SignupSuccess; - -export type SignupSuccess = { - __typename?: 'SignupSuccess'; - me: User; -}; - export enum SortBy { PublishedAt = 'PUBLISHED_AT', SavedAt = 'SAVED_AT', @@ -2573,7 +2535,6 @@ export type ResolversTypes = { LogOutSuccess: ResolverTypeWrapper; LoginError: ResolverTypeWrapper; LoginErrorCode: LoginErrorCode; - LoginInput: LoginInput; LoginResult: ResolversTypes['LoginError'] | ResolversTypes['LoginSuccess']; LoginSuccess: ResolverTypeWrapper; MergeHighlightError: ResolverTypeWrapper; @@ -2677,11 +2638,7 @@ export type ResolversTypes = { SharedArticleErrorCode: SharedArticleErrorCode; SharedArticleResult: ResolversTypes['SharedArticleError'] | ResolversTypes['SharedArticleSuccess']; SharedArticleSuccess: ResolverTypeWrapper; - SignupError: ResolverTypeWrapper; SignupErrorCode: SignupErrorCode; - SignupInput: SignupInput; - SignupResult: ResolversTypes['SignupError'] | ResolversTypes['SignupSuccess']; - SignupSuccess: ResolverTypeWrapper; SortBy: SortBy; SortOrder: SortOrder; SortParams: SortParams; @@ -2901,7 +2858,6 @@ export type ResolversParentTypes = { LogOutResult: ResolversParentTypes['LogOutError'] | ResolversParentTypes['LogOutSuccess']; LogOutSuccess: LogOutSuccess; LoginError: LoginError; - LoginInput: LoginInput; LoginResult: ResolversParentTypes['LoginError'] | ResolversParentTypes['LoginSuccess']; LoginSuccess: LoginSuccess; MergeHighlightError: MergeHighlightError; @@ -2985,10 +2941,6 @@ export type ResolversParentTypes = { SharedArticleError: SharedArticleError; SharedArticleResult: ResolversParentTypes['SharedArticleError'] | ResolversParentTypes['SharedArticleSuccess']; SharedArticleSuccess: SharedArticleSuccess; - SignupError: SignupError; - SignupInput: SignupInput; - SignupResult: ResolversParentTypes['SignupError'] | ResolversParentTypes['SignupSuccess']; - SignupSuccess: SignupSuccess; SortParams: SortParams; String: Scalars['String']; SubscribeError: SubscribeError; @@ -3715,7 +3667,6 @@ export type MutationResolvers>; googleSignup?: Resolver>; logOut?: Resolver; - login?: Resolver>; mergeHighlight?: Resolver>; reportItem?: Resolver>; revokeApiKey?: Resolver>; @@ -3733,7 +3684,6 @@ export type MutationResolvers>; setUserPersonalization?: Resolver>; setWebhook?: Resolver>; - signup?: Resolver>; subscribe?: Resolver>; unsubscribe?: Resolver>; updateHighlight?: Resolver>; @@ -4125,20 +4075,6 @@ export type SharedArticleSuccessResolvers; }; -export type SignupErrorResolvers = { - errorCodes?: Resolver>, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}; - -export type SignupResultResolvers = { - __resolveType: TypeResolveFn<'SignupError' | 'SignupSuccess', ParentType, ContextType>; -}; - -export type SignupSuccessResolvers = { - me?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; -}; - export type SubscribeErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -4628,9 +4564,6 @@ export type Resolvers = { SharedArticleError?: SharedArticleErrorResolvers; SharedArticleResult?: SharedArticleResultResolvers; SharedArticleSuccess?: SharedArticleSuccessResolvers; - SignupError?: SignupErrorResolvers; - SignupResult?: SignupResultResolvers; - SignupSuccess?: SignupSuccessResolvers; SubscribeError?: SubscribeErrorResolvers; SubscribeResult?: SubscribeResultResolvers; SubscribeSuccess?: SubscribeSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 610450dfd..d3170ed12 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -730,11 +730,6 @@ enum LoginErrorCode { WRONG_SOURCE } -input LoginInput { - email: String! - password: String! -} - union LoginResult = LoginError | LoginSuccess type LoginSuccess { @@ -794,7 +789,6 @@ type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! googleSignup(input: GoogleSignupInput!): GoogleSignupResult! logOut: LogOutResult! - login(input: LoginInput!): LoginResult! mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult! reportItem(input: ReportItemInput!): ReportItemResult! revokeApiKey(id: ID!): RevokeApiKeyResult! @@ -812,7 +806,6 @@ type Mutation { setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult! setUserPersonalization(input: SetUserPersonalizationInput!): SetUserPersonalizationResult! setWebhook(input: SetWebhookInput!): SetWebhookResult! - signup(input: SignupInput!): SignupResult! subscribe(name: String!): SubscribeResult! unsubscribe(name: String!): UnsubscribeResult! updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult! @@ -1347,10 +1340,6 @@ type SharedArticleSuccess { article: Article! } -type SignupError { - errorCodes: [SignupErrorCode]! -} - enum SignupErrorCode { ACCESS_DENIED EXPIRED_TOKEN @@ -1362,21 +1351,6 @@ enum SignupErrorCode { USER_EXISTS } -input SignupInput { - bio: String - email: String! - name: String! - password: String! - pictureUrl: String - username: String! -} - -union SignupResult = SignupError | SignupSuccess - -type SignupSuccess { - me: User! -} - enum SortBy { PUBLISHED_AT SAVED_AT diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 6e5b26cbd..4c29d3b3c 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -35,10 +35,11 @@ import cors from 'cors' import { MembershipTier, RegistrationType, + StatusType, UserData, } from '../../datalayer/user/model' import { comparePassword, hashPassword } from '../../utils/auth' -import { createUser } from '../../services/create_user' +import { createUser, sendConfirmationEmail } from '../../services/create_user' import { isErrorWithCode } from '../../resolvers' import { initModels } from '../../server' @@ -372,6 +373,17 @@ export function authRouter() { ) } + if (user.status === StatusType.Pending && user.email) { + await sendConfirmationEmail({ + id: user.id, + email: user.email, + name: user.name, + }) + return res.redirect( + `${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION` + ) + } + if (!user?.password) { // user has no password, so they need to set one return res.redirect( diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index b97075b48..fc340c870 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -8,7 +8,7 @@ import { validateUsername } from '../utils/usernamePolicy' import { Invite } from '../entity/groups/invite' import { GroupMembership } from '../entity/groups/group_membership' import { AppDataSource } from '../server' -import { getRepository, setClaims } from '../entity/utils' +import { getRepository } from '../entity/utils' import { generateVerificationToken } from '../utils/auth' import { env } from '../env' import { sendEmail } from '../utils/sendEmail' @@ -95,13 +95,6 @@ export const createUser = async (input: { if (input.pendingConfirmation) { if (!(await sendConfirmationEmail(user))) { - // delete user if email failed to send - await AppDataSource.transaction(async (e) => { - await setClaims(e, user.id) - - return e.getRepository(User).delete(user.id) - }) - return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail }) } } @@ -138,7 +131,11 @@ const getUser = async (email: string): Promise => { }) } -export const sendConfirmationEmail = async (user: User): Promise => { +export const sendConfirmationEmail = async (user: { + id: string + name: string + email: string +}): Promise => { // generate confirmation link const confirmationToken = generateVerificationToken(user.id) const confirmationLink = `${env.client.url}/confirm-email/${confirmationToken}` diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index 9ddede738..a3ea6fa59 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -181,6 +181,38 @@ describe('auth router', () => { }) }) + context('when user is not confirmed', async () => { + const pendingUser = await createTestUser( + 'pending_user', + undefined, + correctPassword, + true + ) + + before(async () => { + email = pendingUser.email + password = correctPassword + }) + + after(async () => { + await deleteTestUser(pendingUser.name) + }) + + it('redirects with error code PendingVerification', async () => { + const res = await loginRequest(email, password).expect(302) + expect(res.header.location).to.endWith( + '/email-login?errorCodes=PENDING_VERIFICATION' + ) + }) + + it('sends a verification email', async () => { + const fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true)) + await loginRequest(email, password).expect(302) + sinon.restore() + expect(fake).to.have.been.calledOnce + }) + }) + context('when user not exists', () => { before(() => { email = 'Some email' @@ -194,15 +226,16 @@ describe('auth router', () => { }) }) - context('when user has no password stored in db', () => { - before(async () => { - const anotherUser = await createTestUser('another_user') - email = anotherUser.email + context('when user has no password stored in db', async () => { + const socialAccountUser = await createTestUser('social_account_user') + + before(() => { + email = socialAccountUser.email password = 'Some password' }) after(async () => { - await deleteTestUser('another_user') + await deleteTestUser(socialAccountUser.name) }) it('redirects with error code WrongSource', async () => { diff --git a/packages/api/test/services/create_user.test.ts b/packages/api/test/services/create_user.test.ts index 9615250db..1901e41b6 100644 --- a/packages/api/test/services/create_user.test.ts +++ b/packages/api/test/services/create_user.test.ts @@ -17,8 +17,6 @@ import sinonChai from 'sinon-chai' import sinon from 'sinon' import * as util from '../../src/utils/sendEmail' import { MailDataRequired } from '@sendgrid/helpers/classes/mail' -import { getRepository } from '../../src/entity/utils' -import { User } from '../../src/entity/user' chai.use(sinonChai) @@ -99,10 +97,9 @@ describe('create user', () => { sinon.restore() }) - it('does not create the user', async () => { - await expect(createTestUser(name, undefined, undefined, true)).to.be + it('rejects with error', async () => { + return expect(createTestUser(name, undefined, undefined, true)).to.be .rejected - expect(await getRepository(User).findOneBy({ name })).to.be.null }) }) })