diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 6150cd145..8ed87340f 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -50,7 +50,6 @@ import { googleLoginResolver, googleSignupResolver, labelsResolver, - loginResolver, logOutResolver, mergeHighlightResolver, newsletterEmailsResolver, @@ -153,7 +152,6 @@ export const functionResolvers = { createLabel: createLabelResolver, updateLabel: updateLabelResolver, deleteLabel: deleteLabelResolver, - login: loginResolver, setLabels: setLabelsResolver, generateApiKey: generateApiKeyResolver, unsubscribe: unsubscribeResolver, @@ -570,7 +568,6 @@ export const functionResolvers = { ...resultResolveTypeResolver('Labels'), ...resultResolveTypeResolver('CreateLabel'), ...resultResolveTypeResolver('DeleteLabel'), - ...resultResolveTypeResolver('Login'), ...resultResolveTypeResolver('SetLabels'), ...resultResolveTypeResolver('GenerateApiKey'), ...resultResolveTypeResolver('Search'), diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts index df35f5e2a..d9a6f3828 100644 --- a/packages/api/src/resolvers/user/index.ts +++ b/packages/api/src/resolvers/user/index.ts @@ -10,7 +10,6 @@ import { MutationDeleteAccountArgs, MutationGoogleLoginArgs, MutationGoogleSignupArgs, - MutationLoginArgs, MutationUpdateUserArgs, MutationUpdateUserProfileArgs, QueryUserArgs, @@ -35,7 +34,6 @@ import { env } from '../../env' import { validateUsername } from '../../utils/usernamePolicy' import * as jwt from 'jsonwebtoken' import { createUser } from '../../services/create_user' -import { comparePassword } from '../../utils/auth' import { deletePagesByParam } from '../../elastic/pages' import { setClaims } from '../../entity/utils' import { User as UserEntity } from '../../entity/user' @@ -295,37 +293,6 @@ export function isErrorWithCode(error: unknown): error is ErrorWithCode { ) } -export const loginResolver: ResolverFn< - LoginResult, - unknown, - WithDataSourcesContext, - MutationLoginArgs -> = async (_obj, { input }, { models, setAuth }) => { - const { email, password } = input - - const user = await models.user.getWhere({ - email, - }) - if (!user?.id) { - return { errorCodes: [LoginErrorCode.UserNotFound] } - } - - if (!user?.password) { - // user has no password, so they need to set one - return { errorCodes: [LoginErrorCode.WrongSource] } - } - - // check if password is correct - const validPassword = await comparePassword(password, user.password) - if (!validPassword) { - return { errorCodes: [LoginErrorCode.InvalidCredentials] } - } - - // set auth cookie in response header - await setAuth({ uid: user.id }) - return { me: userDataToUser(user) } -} - export const deleteAccountResolver = authorized< DeleteAccountSuccess, DeleteAccountError, diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 661bf701f..6e5b26cbd 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -37,9 +37,10 @@ import { RegistrationType, UserData, } from '../../datalayer/user/model' -import { hashPassword } from '../../utils/auth' +import { comparePassword, hashPassword } from '../../utils/auth' import { createUser } from '../../services/create_user' import { isErrorWithCode } from '../../resolvers' +import { initModels } from '../../server' const logger = buildLogger('app.dispatch') const signToken = promisify(jwt.sign) @@ -359,56 +360,42 @@ export function authRouter() { cors(corsConfig), async (req: express.Request, res: express.Response) => { const { email, password } = req.body - if (!email || !password) { - res.redirect(`${env.client.url}/email-login?errorCodes=AUTH_FAILED`) - return - } - - const query = ` - mutation login{ - login(input: { - email: "${email}", - password: "${password}" - }) { - __typename - ... on LoginError { errorCodes } - ... on LoginSuccess { - me { - id - name - profile { - username - } - } - } - } - }` try { - const result = await axios.post(env.server.gateway_url + '/graphql', { - query, + const models = initModels(kx, false) + const user = await models.user.getWhere({ + email, }) - const { data } = result.data - - if (data.login.__typename === 'LoginError') { - const errorCodes = data.login.errorCodes.join(',') + if (!user?.id) { return res.redirect( - `${env.client.url}/email-login?errorCodes=${errorCodes}` + `${env.client.url}/email-login?errorCodes=${LoginErrorCode.UserNotFound}` ) } - if (!result.headers['set-cookie']) { + if (!user?.password) { + // user has no password, so they need to set one return res.redirect( - `${env.client.url}/${ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (req.params as any)?.action - }?errorCodes=unknown` + `${env.client.url}/email-login?errorCodes=${LoginErrorCode.WrongSource}` ) } - res.setHeader('set-cookie', result.headers['set-cookie']) + // check if password is correct + const validPassword = await comparePassword(password, user.password) + if (!validPassword) { + return res.redirect( + `${env.client.url}/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}` + ) + } - await handleSuccessfulLogin(req, res, data.login.me, false) + // set auth cookie in response header + const token = await signToken({ uid: user.id }, env.server.jwtSecret) + + res.cookie('auth', token, { + httpOnly: true, + expires: new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000), + }) + + await handleSuccessfulLogin(req, res, user, false) } catch (e) { logger.info('email-login exception:', e) res.redirect(`${env.client.url}/email-login?errorCodes=AUTH_FAILED`) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 3ab828e1a..336056564 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -1410,11 +1410,6 @@ const schema = gql` union UpdateLabelResult = UpdateLabelSuccess | UpdateLabelError - input LoginInput { - password: String! - email: String! - } - input SetLabelsInput { pageId: ID! labelIds: [ID!]! @@ -1825,7 +1820,6 @@ const schema = gql` createLabel(input: CreateLabelInput!): CreateLabelResult! updateLabel(input: UpdateLabelInput!): UpdateLabelResult! deleteLabel(id: ID!): DeleteLabelResult! - login(input: LoginInput!): LoginResult! setLabels(input: SetLabelsInput!): SetLabelsResult! generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult! unsubscribe(name: String!): UnsubscribeResult! diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index aaef52cb1..b97075b48 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -147,6 +147,6 @@ export const sendConfirmationEmail = async (user: User): Promise => { from: `Omnivore <${env.sender.message}>`, to: user.email, subject: 'Confirm your email', - text: `Hey ${user.name},\nPlease confirm your email by clicking the link below:\n\n${confirmationLink}\n\n`, + text: `Hey ${user.name},\n\nPlease confirm your email by clicking the link below:\n\n${confirmationLink}\n\n`, }) } diff --git a/packages/api/test/resolvers/user.test.ts b/packages/api/test/resolvers/user.test.ts index afe6e7f53..58ebd8cb6 100644 --- a/packages/api/test/resolvers/user.test.ts +++ b/packages/api/test/resolvers/user.test.ts @@ -2,7 +2,6 @@ import { createTestUser, deleteTestUser, getProfile, getUser } from '../db' import { graphqlRequest, request } from '../util' import { expect } from 'chai' import { - LoginErrorCode, UpdateUserErrorCode, UpdateUserProfileErrorCode, } from '../../src/generated/graphql' @@ -238,89 +237,4 @@ describe('User API', () => { return graphqlRequest(query, invalidAuthToken).expect(500) }) }) - - describe('login', () => { - let query: string - let email: string - let password: string - - beforeEach(() => { - query = ` - mutation { - login( - input: { - email: "${email}" - password: "${password}" - } - ) { - ... on LoginSuccess { - me { - id - name - profile { - username - } - } - } - ... on LoginError { - errorCodes - } - } - } - ` - }) - - context('when email and password are valid', () => { - before(() => { - email = user.email - password = correctPassword - }) - - it('responds with 200', async () => { - const res = await graphqlRequest(query).expect(200) - expect(res.body.data.login.me.id).to.eql(user.id) - }) - }) - - context('when user not exists', () => { - before(() => { - email = 'Some email' - }) - - it('responds with error code UserNotFound', async () => { - const response = await graphqlRequest(query).expect(200) - expect(response.body.data.login.errorCodes).to.eql([ - LoginErrorCode.UserNotFound, - ]) - }) - }) - - context('when user has no password stored in db', () => { - before(() => { - email = anotherUser.email - password = 'Some password' - }) - - it('responds with error code WrongSource', async () => { - const response = await graphqlRequest(query).expect(200) - expect(response.body.data.login.errorCodes).to.eql([ - LoginErrorCode.WrongSource, - ]) - }) - }) - - context('when password is wrong', () => { - before(() => { - email = user.email - password = 'Some password' - }) - - it('responds with error code UserNotFound', async () => { - const response = await graphqlRequest(query).expect(200) - expect(response.body.data.login.errorCodes).to.eql([ - LoginErrorCode.InvalidCredentials, - ]) - }) - }) - }) }) diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index c35e49e94..bd82ed49d 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -8,6 +8,7 @@ import { MailDataRequired } from '@sendgrid/helpers/classes/mail' import sinon from 'sinon' import * as util from '../../src/utils/sendEmail' import supertest from 'supertest' +import { hashPassword } from '../../src/utils/auth' describe('auth router', () => { const route = '/api/auth' @@ -139,4 +140,85 @@ describe('auth router', () => { }) }) }) + + describe('login', () => { + const loginRequest = (email: string, password: string): supertest.Test => { + return request.post(`${route}/email-login`).send({ + email, + password, + }) + } + const correctPassword = 'correctPassword' + + let user: User + let email: string + let password: string + + before(async () => { + const hashedPassword = await hashPassword(correctPassword) + user = await createTestUser('login_test_user', undefined, hashedPassword) + }) + + after(async () => { + await deleteTestUser(user.name) + }) + + context('when email and password are valid', () => { + before(() => { + email = user.email + password = correctPassword + }) + + it('redirects to waitlist page', async () => { + const res = await loginRequest(email, password).expect(302) + expect(res.header.location).to.endWith('/waitlist') + }) + }) + + context('when user not exists', () => { + before(() => { + email = 'Some email' + }) + + it('redirects with error code UserNotFound', async () => { + const res = await loginRequest(email, password).expect(302) + expect(res.header.location).to.endWith( + '/email-login?errorCodes=USER_NOT_FOUND' + ) + }) + }) + + context('when user has no password stored in db', () => { + before(async () => { + const anotherUser = await createTestUser('another_user') + email = anotherUser.email + password = 'Some password' + }) + + after(async () => { + await deleteTestUser('another_user') + }) + + it('redirects with error code WrongSource', async () => { + const res = await loginRequest(email, password).expect(302) + expect(res.header.location).to.endWith( + '/email-login?errorCodes=WRONG_SOURCE' + ) + }) + }) + + context('when password is wrong', () => { + before(() => { + email = user.email + password = 'Wrong password' + }) + + it('redirects with error code InvalidCredentials', async () => { + const res = await loginRequest(email, password).expect(302) + expect(res.header.location).to.endWith( + '/email-login?errorCodes=INVALID_CREDENTIALS' + ) + }) + }) + }) })