send confirmation email to pending user when login
This commit is contained in:
committed by
Jackson Harper
parent
bd2327a3ae
commit
9ea9bd9ea1
@ -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
|
||||
|
||||
@ -59,5 +59,5 @@ export class User {
|
||||
subscriptions?: Subscription[]
|
||||
|
||||
@Column({ type: 'enum', enum: StatusType })
|
||||
status!: string
|
||||
status!: StatusType
|
||||
}
|
||||
|
||||
@ -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<Maybe<SignupErrorCode>>;
|
||||
};
|
||||
|
||||
export enum SignupErrorCode {
|
||||
AccessDenied = 'ACCESS_DENIED',
|
||||
ExpiredToken = 'EXPIRED_TOKEN',
|
||||
@ -1841,22 +1819,6 @@ export enum SignupErrorCode {
|
||||
UserExists = 'USER_EXISTS'
|
||||
}
|
||||
|
||||
export type SignupInput = {
|
||||
bio?: InputMaybe<Scalars['String']>;
|
||||
email: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
pictureUrl?: InputMaybe<Scalars['String']>;
|
||||
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<LogOutSuccess>;
|
||||
LoginError: ResolverTypeWrapper<LoginError>;
|
||||
LoginErrorCode: LoginErrorCode;
|
||||
LoginInput: LoginInput;
|
||||
LoginResult: ResolversTypes['LoginError'] | ResolversTypes['LoginSuccess'];
|
||||
LoginSuccess: ResolverTypeWrapper<LoginSuccess>;
|
||||
MergeHighlightError: ResolverTypeWrapper<MergeHighlightError>;
|
||||
@ -2677,11 +2638,7 @@ export type ResolversTypes = {
|
||||
SharedArticleErrorCode: SharedArticleErrorCode;
|
||||
SharedArticleResult: ResolversTypes['SharedArticleError'] | ResolversTypes['SharedArticleSuccess'];
|
||||
SharedArticleSuccess: ResolverTypeWrapper<SharedArticleSuccess>;
|
||||
SignupError: ResolverTypeWrapper<SignupError>;
|
||||
SignupErrorCode: SignupErrorCode;
|
||||
SignupInput: SignupInput;
|
||||
SignupResult: ResolversTypes['SignupError'] | ResolversTypes['SignupSuccess'];
|
||||
SignupSuccess: ResolverTypeWrapper<SignupSuccess>;
|
||||
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<ContextType = ResolverContext, ParentType extends
|
||||
googleLogin?: Resolver<ResolversTypes['LoginResult'], ParentType, ContextType, RequireFields<MutationGoogleLoginArgs, 'input'>>;
|
||||
googleSignup?: Resolver<ResolversTypes['GoogleSignupResult'], ParentType, ContextType, RequireFields<MutationGoogleSignupArgs, 'input'>>;
|
||||
logOut?: Resolver<ResolversTypes['LogOutResult'], ParentType, ContextType>;
|
||||
login?: Resolver<ResolversTypes['LoginResult'], ParentType, ContextType, RequireFields<MutationLoginArgs, 'input'>>;
|
||||
mergeHighlight?: Resolver<ResolversTypes['MergeHighlightResult'], ParentType, ContextType, RequireFields<MutationMergeHighlightArgs, 'input'>>;
|
||||
reportItem?: Resolver<ResolversTypes['ReportItemResult'], ParentType, ContextType, RequireFields<MutationReportItemArgs, 'input'>>;
|
||||
revokeApiKey?: Resolver<ResolversTypes['RevokeApiKeyResult'], ParentType, ContextType, RequireFields<MutationRevokeApiKeyArgs, 'id'>>;
|
||||
@ -3733,7 +3684,6 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
||||
setShareHighlight?: Resolver<ResolversTypes['SetShareHighlightResult'], ParentType, ContextType, RequireFields<MutationSetShareHighlightArgs, 'input'>>;
|
||||
setUserPersonalization?: Resolver<ResolversTypes['SetUserPersonalizationResult'], ParentType, ContextType, RequireFields<MutationSetUserPersonalizationArgs, 'input'>>;
|
||||
setWebhook?: Resolver<ResolversTypes['SetWebhookResult'], ParentType, ContextType, RequireFields<MutationSetWebhookArgs, 'input'>>;
|
||||
signup?: Resolver<ResolversTypes['SignupResult'], ParentType, ContextType, RequireFields<MutationSignupArgs, 'input'>>;
|
||||
subscribe?: Resolver<ResolversTypes['SubscribeResult'], ParentType, ContextType, RequireFields<MutationSubscribeArgs, 'name'>>;
|
||||
unsubscribe?: Resolver<ResolversTypes['UnsubscribeResult'], ParentType, ContextType, RequireFields<MutationUnsubscribeArgs, 'name'>>;
|
||||
updateHighlight?: Resolver<ResolversTypes['UpdateHighlightResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightArgs, 'input'>>;
|
||||
@ -4125,20 +4075,6 @@ export type SharedArticleSuccessResolvers<ContextType = ResolverContext, ParentT
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SignupErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SignupError'] = ResolversParentTypes['SignupError']> = {
|
||||
errorCodes?: Resolver<Array<Maybe<ResolversTypes['SignupErrorCode']>>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SignupResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SignupResult'] = ResolversParentTypes['SignupResult']> = {
|
||||
__resolveType: TypeResolveFn<'SignupError' | 'SignupSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SignupSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SignupSuccess'] = ResolversParentTypes['SignupSuccess']> = {
|
||||
me?: Resolver<ResolversTypes['User'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SubscribeErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SubscribeError'] = ResolversParentTypes['SubscribeError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['SubscribeErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@ -4628,9 +4564,6 @@ export type Resolvers<ContextType = ResolverContext> = {
|
||||
SharedArticleError?: SharedArticleErrorResolvers<ContextType>;
|
||||
SharedArticleResult?: SharedArticleResultResolvers<ContextType>;
|
||||
SharedArticleSuccess?: SharedArticleSuccessResolvers<ContextType>;
|
||||
SignupError?: SignupErrorResolvers<ContextType>;
|
||||
SignupResult?: SignupResultResolvers<ContextType>;
|
||||
SignupSuccess?: SignupSuccessResolvers<ContextType>;
|
||||
SubscribeError?: SubscribeErrorResolvers<ContextType>;
|
||||
SubscribeResult?: SubscribeResultResolvers<ContextType>;
|
||||
SubscribeSuccess?: SubscribeSuccessResolvers<ContextType>;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<User | null> => {
|
||||
})
|
||||
}
|
||||
|
||||
export const sendConfirmationEmail = async (user: User): Promise<boolean> => {
|
||||
export const sendConfirmationEmail = async (user: {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}): Promise<boolean> => {
|
||||
// generate confirmation link
|
||||
const confirmationToken = generateVerificationToken(user.id)
|
||||
const confirmationLink = `${env.client.url}/confirm-email/${confirmationToken}`
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user