import { AuthenticationError } from 'apollo-server'; import { merge } from 'lodash'; import ms from 'ms'; import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql'; import { EntityManager, Transaction, TransactionManager } from 'typeorm'; import { ContextType, UserStatus } from '@/types'; import logger from '@/logger'; import { PasswordAuthenticationStrategy } from '@/entities'; import { setRefreshTokenCookie } from '@/config/auth'; import { ChangePasswordInput, ChangePasswordPayload, ChangePasswordWithResetPasswordKeyInput, ChangePasswordWithResetPasswordKeyPayload, CHANGE_PASSWORD_DEFAULTS, CHANGE_PASSWORD_WITH_RESET_PASSWORD_KEY_DEFAULTS, EmailAvailableInput, EmailAvailablePayload, LogoutResponse, RequestOneTimePasswordPayload, RequestResetPasswordInput, RequestResetPasswordPayload, REQUEST_RESET_PASSWORD_DEFAULTS, SignInInput, SignInResponse, SignUpAnonymouslyInput, SignUpAnonymouslyPayload, SignUpInput, SignUpResponse, SIGN_UP_DEFAULTS, VerifyOneTimePasswordInput, VerifyOneTimePasswordPayload, VERIFY_ONE_TIME_PASSWORD_DEFAULTS, } from './types'; @Resolver() export class AuthResolver { @Mutation(() => SignUpAnonymouslyPayload) async signUpAnonymously( @Arg('input', { nullable: true }) input: SignUpAnonymouslyInput, @Ctx() context: ContextType ): Promise { const { user: contextUser, dataSources, res } = context; if (contextUser) { throw new Error('Cannot sign in if already signed in'); } const { user, accessToken, refreshToken } = await dataSources.authProvider.anonymousStrategy.signUp(); context.user = user; setRefreshTokenCookie(refreshToken, res, { maxAge: ms('1y'), }); return { user, accessToken, }; } @Mutation(() => SignInResponse) async signIn( @Arg('input') { username, password }: SignInInput, @Ctx() context: ContextType ): Promise { const { user: contextUser, dataSources, res } = context; if (contextUser) { throw new Error('Cannot sign in if already signed in'); } try { const { user, accessToken, refreshToken } = await dataSources.authProvider.passwordStrategy.signInOrFail( username, password ); context.user = user; setRefreshTokenCookie(refreshToken, res); return { user, accessToken, }; } catch (e) { throw new AuthenticationError('Invalid Username/Password'); } } @Mutation(() => SignUpResponse) @Transaction() async signUp( @Arg('input') inputWithoutDefaults: SignUpInput, @Ctx() context: ContextType, @TransactionManager() manager: EntityManager ): Promise { const { username, password, email } = merge( {}, SIGN_UP_DEFAULTS, inputWithoutDefaults ); const { user: contextUser, dataSources, res } = context; if (contextUser) { throw new Error('Cannot sign up if already signed in'); } const { user, accessToken, refreshToken, verificationRequested } = await dataSources.authProvider.passwordStrategy.signUpWithEmailAndPasswordOrFail( { username, email, password, profile: {}, }, manager ); context.user = user; setRefreshTokenCookie(refreshToken, res); return { user, accessToken, verificationRequested, }; } @Mutation(() => VerifyOneTimePasswordPayload) @Transaction() async verifyOneTimePassword( @Arg('input') inputWithoutDefaults: VerifyOneTimePasswordInput, @Ctx() context: ContextType, @TransactionManager() manager: EntityManager ): Promise { const { otpAttempt } = merge( {}, VERIFY_ONE_TIME_PASSWORD_DEFAULTS, inputWithoutDefaults ); const { user: contextUser, dataSources } = context; if (!contextUser) { throw new Error('Cannot verify one time password if not signed in'); } // just throw error if the user is already verified... if (await contextUser.isVerified(context)) { throw new Error('No need to verify if already verified.'); } const success = !!(await dataSources.authProvider.passwordStrategy.verifyUserWithOneTimePasswordOrFail( { user: contextUser, otpAttempt }, manager )); return { success, }; } @Mutation(() => RequestOneTimePasswordPayload) @Transaction() async requestOneTimePassword( @Ctx() context: ContextType, @TransactionManager() manager: EntityManager ): Promise { const { user: contextUser, dataSources } = context; if (!contextUser) { throw new Error('Cannot request a one time password if not signed in.'); } // just throw error if the user is already verified... if (await contextUser.isVerified(context)) { throw new Error('No need to verify if already verified.'); } const success = !!(await dataSources.authProvider.passwordStrategy.sendTOTPToUser( contextUser, manager )); return { success, }; } @Mutation(() => ChangePasswordPayload) @Authorized(UserStatus.UNVERIFIED) @Transaction() async changePassword( @Arg('input') inputWithoutDefaults: ChangePasswordInput, @Ctx() context: ContextType, @TransactionManager() manager: EntityManager ): Promise { const { oldPassword, newPassword } = merge( {}, CHANGE_PASSWORD_DEFAULTS, inputWithoutDefaults ); const { user: contextUser, dataSources } = context; if (!contextUser) { throw new Error('Cannot change password if not signed in.'); } await dataSources.authProvider.passwordStrategy.changePasswordOrFail( { oldPassword, newPassword, userId: contextUser.id }, manager ); return { success: true, }; } @Mutation(() => RequestResetPasswordPayload) @Transaction() async requestResetPassword( @Arg('input') inputWithoutDefaults: RequestResetPasswordInput, @Ctx() context: ContextType, @TransactionManager() manager: EntityManager ): Promise { const { username } = merge( {}, REQUEST_RESET_PASSWORD_DEFAULTS, inputWithoutDefaults ); const { user: contextUser, dataSources } = context; if (contextUser) { throw new Error('Cannot request reset password if already signed in'); } try { await dataSources.authProvider.passwordStrategy.requestResetPasswordOrFail( username, manager ); } catch (error) { logger.error(error); } return { success: true, }; } @Mutation(() => ChangePasswordWithResetPasswordKeyPayload) @Transaction() async changePasswordWithResetPasswordKey( @Arg('input') inputWithoutDefaults: ChangePasswordWithResetPasswordKeyInput, @Ctx() context: ContextType, @TransactionManager() manager: EntityManager ): Promise { const { resetPasswordKey, newPassword, username } = merge( {}, CHANGE_PASSWORD_WITH_RESET_PASSWORD_KEY_DEFAULTS, inputWithoutDefaults ); const { user: contextUser, dataSources } = context; if (contextUser) { throw new Error( 'Cannot change password with reset password key if signed in.' ); } await dataSources.authProvider.passwordStrategy.changePasswordWithResetPasswordKeyOrFail( { resetPasswordKey, newPassword, usernameOrEmail: username }, manager ); return { success: true, }; } @Query(() => EmailAvailablePayload) async emailAvailable( @Arg('input') inputWithoutDefaults: EmailAvailableInput ): Promise { const count = await PasswordAuthenticationStrategy.count({ where: { email: inputWithoutDefaults.email }, take: 1, }); const available = count === 0; return { available, }; } @Mutation(() => LogoutResponse) async logout(@Ctx() context: ContextType): Promise { const { user: contextUser, res } = context; if (!contextUser) { throw new Error('Cannot sign out if not signed in'); } context.user = undefined; setRefreshTokenCookie('', res); return { success: true, }; } }