fix: fixed all issues raised in initial PR review

This commit is contained in:
Balu Babu
2023-01-30 18:55:53 +05:30
parent a8d50223aa
commit 3afc89db6b
21 changed files with 6640 additions and 152 deletions

View File

@@ -8,6 +8,8 @@ POSTMARK_SENDER_EMAIL=************************************************"
# Auth Tokens Config
SIGNED_COOKIE_SECRET='add some secret here'
JWT_SECRET='add some secret here'
TOKEN_SALT_COMPLEXITY=10
MAGIC_LINK_TOKEN_VALIDITY=3
REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days
ACCESS_TOKEN_VALIDITY="120000" # Default validity is 1 day

View File

@@ -112,4 +112,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}

6421
packages/hoppscotch-backend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -85,7 +85,7 @@ model User {
photoURL String?
isAdmin Boolean @default(false)
refreshToken String?
accounts Account[]
providerAccounts Account[]
PasswordlessVerification PasswordlessVerification[]
settings UserSettings?
UserHistory UserHistory[]
@@ -109,7 +109,7 @@ model Account {
@@unique(fields: [provider, providerAccountId], name: "verifyProviderAccount")
}
model PasswordlessVerification {
model VerificationToken {
deviceIdentifier String
token String @unique @default(cuid())
userUid String

View File

@@ -25,20 +25,32 @@ import { AuthGuard } from '@nestjs/passport';
export class AuthController {
constructor(private authService: AuthService) {}
/**
** Route to initiate magic-link auth for a users email
*/
@Post('signin')
async signIn(@Body() authData: signInMagicDto) {
const data = await this.authService.signIn(authData.email);
if (E.isLeft(data)) throwHTTPErr(data.left);
return data.right;
async signInMagicLink(@Body() authData: signInMagicDto) {
const deviceIdToken = await this.authService.signInMagicLink(
authData.email,
);
if (E.isLeft(deviceIdToken)) throwHTTPErr(deviceIdToken.left);
return deviceIdToken.right;
}
/**
** Route to verify and sign in a valid user via magic-link
*/
@Post('verify')
async verify(@Body() data: verifyMagicDto, @Res() res: Response) {
const authTokens = await this.authService.verifyPasswordlessTokens(data);
const authTokens = await this.authService.verifyMagicLinkTokens(data);
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
authCookieHandler(res, authTokens.right, false);
}
/**
** Route to refresh auth tokens with Refresh Token Rotation
* @see https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation
*/
@Get('refresh')
@UseGuards(RTJwtAuthGuard)
async refresh(
@@ -54,10 +66,17 @@ export class AuthController {
authCookieHandler(res, newTokenPair.right, false);
}
/**
** Route to initiate SSO auth via Google
*/
@Get('google')
@UseGuards(AuthGuard('google'))
async googleAuth(@Request() req) {}
/**
** Callback URL for Google SSO
* @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works
*/
@Get('google/callback')
@UseGuards(AuthGuard('google'))
async googleAuthRedirect(@Request() req, @Res() res) {
@@ -66,10 +85,17 @@ export class AuthController {
authCookieHandler(res, authTokens.right, true);
}
/**
** Route to initiate SSO auth via Github
*/
@Get('github')
@UseGuards(AuthGuard('github'))
async githubAuth(@Request() req) {}
/**
** Callback URL for Github SSO
* @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works
*/
@Get('github/callback')
@UseGuards(AuthGuard('github'))
async githubAuthRedirect(@Request() req, @Res() res) {
@@ -78,10 +104,17 @@ export class AuthController {
authCookieHandler(res, authTokens.right, true);
}
/**
** Route to initiate SSO auth via Microsoft
*/
@Get('microsoft')
@UseGuards(AuthGuard('microsoft'))
async microsoftAuth(@Request() req) {}
/**
** Callback URL for Microsoft SSO
* @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works
*/
@Get('microsoft/callback')
@UseGuards(AuthGuard('microsoft'))
async microsoftAuthRedirect(@Request() req, @Res() res) {
@@ -90,6 +123,9 @@ export class AuthController {
authCookieHandler(res, authTokens.right, true);
}
/**
** Log user out by clearing cookies containing auth tokens
*/
@Get('logout')
async logout(@Res() res: Response) {
res.clearCookie('access_token');

View File

@@ -71,9 +71,9 @@ nowPlus30 = new Date(nowPlus30);
const encodedRefreshToken =
'$argon2id$v=19$m=65536,t=3,p=4$JTP8yZ8YXMHdafb5pB9Rfg$tdZrILUxMb9dQbu0uuyeReLgKxsgYnyUNbc5ZxQmy5I';
describe('signIn', () => {
describe('signInMagicLink', () => {
test('should throw error if email is not in valid format', async () => {
const result = await authService.signIn('bbbgmail.com');
const result = await authService.signInMagicLink('bbbgmail.com');
expect(result).toEqualLeft({
message: INVALID_EMAIL,
statusCode: HttpStatus.BAD_REQUEST,
@@ -81,16 +81,18 @@ describe('signIn', () => {
});
test('should successfully create a new user account and return the passwordless details', async () => {
// check to see if user exists, return error
// check to see if user exists, return none
mockUser.findUserByEmail.mockResolvedValue(O.none);
// create new user
mockUser.createUserMagic.mockResolvedValue(user);
mockUser.createUserViaMagicLink.mockResolvedValue(user);
// create new entry in passwordlessVerification table
mockPrisma.passwordlessVerification.create.mockResolvedValueOnce(
passwordlessData,
);
const result = await authService.signIn('dwight@dundermifflin.com');
const result = await authService.signInMagicLink(
'dwight@dundermifflin.com',
);
expect(result).toEqualRight({
deviceIdentifier: passwordlessData.deviceIdentifier,
});
@@ -104,20 +106,22 @@ describe('signIn', () => {
passwordlessData,
);
const result = await authService.signIn('dwight@dundermifflin.com');
const result = await authService.signInMagicLink(
'dwight@dundermifflin.com',
);
expect(result).toEqualRight({
deviceIdentifier: passwordlessData.deviceIdentifier,
});
});
});
describe('verifyPasswordlessTokens', () => {
describe('verifyMagicLinkTokens', () => {
test('should throw INVALID_MAGIC_LINK_DATA if data is invalid', async () => {
mockPrisma.passwordlessVerification.findUniqueOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
const result = await authService.verifyPasswordlessTokens(magicLinkVerify);
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualLeft({
message: INVALID_MAGIC_LINK_DATA,
statusCode: HttpStatus.NOT_FOUND,
@@ -132,7 +136,7 @@ describe('verifyPasswordlessTokens', () => {
// findUserById
mockUser.findUserById.mockResolvedValue(O.none);
const result = await authService.verifyPasswordlessTokens(magicLinkVerify);
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualLeft({
message: USER_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
@@ -160,7 +164,7 @@ describe('verifyPasswordlessTokens', () => {
passwordlessData,
);
const result = await authService.verifyPasswordlessTokens(magicLinkVerify);
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualRight({
access_token: user.refreshToken,
refresh_token: user.refreshToken,
@@ -188,7 +192,7 @@ describe('verifyPasswordlessTokens', () => {
passwordlessData,
);
const result = await authService.verifyPasswordlessTokens(magicLinkVerify);
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualRight({
access_token: user.refreshToken,
refresh_token: user.refreshToken,
@@ -205,7 +209,7 @@ describe('verifyPasswordlessTokens', () => {
// checkIfProviderAccountExists
mockPrisma.account.findUnique.mockResolvedValueOnce(accountDetails);
const result = await authService.verifyPasswordlessTokens(magicLinkVerify);
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualLeft({
message: MAGIC_LINK_EXPIRED,
statusCode: HttpStatus.UNAUTHORIZED,
@@ -229,7 +233,7 @@ describe('verifyPasswordlessTokens', () => {
mockJWT.sign.mockReturnValue(user.refreshToken);
mockPrisma.user.update.mockRejectedValueOnce('RecordNotFound');
const result = await authService.verifyPasswordlessTokens(magicLinkVerify);
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualLeft({
message: USER_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
@@ -257,7 +261,7 @@ describe('verifyPasswordlessTokens', () => {
'RecordNotFound',
);
const result = await authService.verifyPasswordlessTokens(magicLinkVerify);
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualLeft({
message: PASSWORDLESS_DATA_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,

View File

@@ -9,7 +9,6 @@ import * as argon2 from 'argon2';
import * as bcrypt from 'bcrypt';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import * as TE from 'fp-ts/TaskEither';
import { DeviceIdentifierToken } from 'src/types/Passwordless';
import {
INVALID_EMAIL,
@@ -26,9 +25,8 @@ import {
RefreshTokenPayload,
} from 'src/types/AuthTokens';
import { JwtService } from '@nestjs/jwt';
import { AuthErrorHandler } from 'src/types/AuthErrorHandler';
import { AuthError } from 'src/types/AuthError';
import { AuthUser } from 'src/types/AuthUser';
import { isLeafType } from 'graphql';
import { PasswordlessVerification } from '@prisma/client';
@Injectable()
@@ -40,16 +38,20 @@ export class AuthService {
private readonly mailerService: MailerService,
) {}
//
/**
* Generate Id and token for email Magic-Link auth
*
* @param {AuthUser} user User Object
* @returns {Promise<PasswordlessVerification>} Created PasswordlessVerification token
* @param user User Object
* @returns Created PasswordlessVerification token
*/
private async generatePasswordlessTokens(user: AuthUser) {
const salt = await bcrypt.genSalt(10);
const expiresOn = DateTime.now().plus({ hours: 3 }).toISO().toString();
private async generateMagicLinkTokens(user: AuthUser) {
const salt = await bcrypt.genSalt(
parseInt(process.env.TOKEN_SALT_COMPLEXITY),
);
const expiresOn = DateTime.now()
.plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) })
.toISO()
.toString();
const idToken = await this.prismaService.passwordlessVerification.create({
data: {
@@ -63,19 +65,19 @@ export class AuthService {
}
/**
* Find and check passwordlessVerification exists or not
* Check if passwordlessVerification exist or not
*
* @param {verifyMagicDto} data Object containing deviceIdentifier and token
* @returns {Promise<O.None | O.Some<PasswordlessVerification>>} Option of PasswordlessVerification token
* @param magicLinkTokens Object containing deviceIdentifier and token
* @returns Option of PasswordlessVerification token
*/
private async validatePasswordlessTokens(data: verifyMagicDto) {
private async validatePasswordlessTokens(magicLinkTokens: verifyMagicDto) {
try {
const tokens =
await this.prismaService.passwordlessVerification.findUniqueOrThrow({
where: {
passwordless_deviceIdentifier_tokens: {
deviceIdentifier: data.deviceIdentifier,
token: data.token,
deviceIdentifier: magicLinkTokens.deviceIdentifier,
token: magicLinkTokens.token,
},
},
});
@@ -85,35 +87,11 @@ export class AuthService {
}
}
/**
* Update User with new generated hashed refresh token
*
* @param {string} tokenHash Hash of newly generated refresh token
* @param {string} userUid User uid
* @returns {Promise<E.Right<User> | E.Left<"user/not_found">>} Either of User with updated refreshToken
*/
private async UpdateUserRefreshToken(tokenHash: string, userUid: string) {
try {
const user = await this.prismaService.user.update({
where: {
uid: userUid,
},
data: {
refreshToken: tokenHash,
},
});
return E.right(user);
} catch (error) {
return E.left(USER_NOT_FOUND);
}
}
/**
* Generate new refresh token for user
*
* @param {string} userUid User Id
* @returns {Promise<E.Left<AuthErrorHandler> | E.Right<string>>} Generated refreshToken
* @param userUid User Id
* @returns Generated refreshToken
*/
private async generateRefreshToken(userUid: string) {
const refreshTokenPayload: RefreshTokenPayload = {
@@ -128,12 +106,12 @@ export class AuthService {
const refreshTokenHash = await argon2.hash(refreshToken);
const updatedUser = await this.UpdateUserRefreshToken(
const updatedUser = await this.usersService.UpdateUserRefreshToken(
refreshTokenHash,
userUid,
);
if (E.isLeft(updatedUser))
return E.left(<AuthErrorHandler>{
return E.left(<AuthError>{
message: updatedUser.left,
statusCode: HttpStatus.NOT_FOUND,
});
@@ -144,8 +122,8 @@ export class AuthService {
/**
* Generate access and refresh token pair
*
* @param {string} userUid User ID
* @returns {Promise<E.Left<AuthErrorHandler> | E.Right<AuthTokens>>} Either of generated AuthTokens
* @param userUid User ID
* @returns Either of generated AuthTokens
*/
async generateAuthTokens(userUid: string) {
const accessTokenPayload: AccessTokenPayload = {
@@ -155,11 +133,7 @@ export class AuthService {
};
const refreshToken = await this.generateRefreshToken(userUid);
if (E.isLeft(refreshToken))
return E.left(<AuthErrorHandler>{
message: refreshToken.left.message,
statusCode: refreshToken.left.statusCode,
});
if (E.isLeft(refreshToken)) return E.left(refreshToken.left);
return E.right(<AuthTokens>{
access_token: await this.jwtService.sign(accessTokenPayload, {
@@ -172,10 +146,10 @@ export class AuthService {
/**
* Deleted used PasswordlessVerification tokens
*
* @param {PasswordlessVerification} passwordlessTokens
* @returns {Promise<E.Right<PasswordlessVerification> | E.Left<"auth/passwordless_token_data_not_found">>} Either of deleted PasswordlessVerification token
* @param passwordlessTokens PasswordlessVerification entry to delete from DB
* @returns Either of deleted PasswordlessVerification token
*/
private async deletePasswordlessVerificationToken(
private async deleteMagicLinkVerificationTokens(
passwordlessTokens: PasswordlessVerification,
) {
try {
@@ -197,16 +171,16 @@ export class AuthService {
/**
* Verify if Provider account exists for User
*
* @param {User} user User Object
* @param profile Provider Account type (Magic,Google,Github,Microsoft)
* @returns {Promise<O.None | O.Some<Account>>} Either of existing user provider Account
* @param user User Object
* @param SSOUserData User data from SSO providers (Magic,Google,Github,Microsoft)
* @returns Either of existing user provider Account
*/
async checkIfProviderAccountExists(user: AuthUser, profile) {
async checkIfProviderAccountExists(user: AuthUser, SSOUserData) {
const provider = await this.prismaService.account.findUnique({
where: {
verifyProviderAccount: {
provider: profile.provider,
providerAccountId: profile.id,
provider: SSOUserData.provider,
providerAccountId: SSOUserData.id,
},
},
});
@@ -217,12 +191,12 @@ export class AuthService {
}
/**
* Send Magic-Link to provider User email
* Create User (if not already present) and send email to initiate Magic-Link auth
*
* @param {string} email User's email
* @returns {Promise<E.Left<AuthErrorHandler> | E.Right<DeviceIdentifierToken>>} Either containing DeviceIdentifierToken
* @param email User's email
* @returns Either containing DeviceIdentifierToken
*/
async signIn(email: string) {
async signInMagicLink(email: string) {
if (!validateEmail(email))
return E.left({
message: INVALID_EMAIL,
@@ -233,12 +207,12 @@ export class AuthService {
const queriedUser = await this.usersService.findUserByEmail(email);
if (O.isNone(queriedUser)) {
user = await this.usersService.createUserMagic(email);
user = await this.usersService.createUserViaMagicLink(email);
} else {
user = queriedUser.value;
}
const generatedTokens = await this.generatePasswordlessTokens(user);
const generatedTokens = await this.generateMagicLinkTokens(user);
await this.mailerService.sendAuthEmail(email, {
template: 'code-your-own',
@@ -256,13 +230,15 @@ export class AuthService {
/**
* Verify and authenticate user from received data for Magic-Link
*
* @param {verifyMagicDto} data
* @returns {Promise<E.Right<AuthTokens> | E.Left<AuthErrorHandler>>} Either of generated AuthTokens
* @param magicLinkIDTokens magic-link verification tokens from client
* @returns Either of generated AuthTokens
*/
async verifyPasswordlessTokens(
data: verifyMagicDto,
): Promise<E.Right<AuthTokens> | E.Left<AuthErrorHandler>> {
const passwordlessTokens = await this.validatePasswordlessTokens(data);
async verifyMagicLinkTokens(
magicLinkIDTokens: verifyMagicDto,
): Promise<E.Right<AuthTokens> | E.Left<AuthError>> {
const passwordlessTokens = await this.validatePasswordlessTokens(
magicLinkIDTokens,
);
if (O.isNone(passwordlessTokens))
return E.left({
message: INVALID_MAGIC_LINK_DATA,
@@ -317,7 +293,7 @@ export class AuthService {
});
const deletedPasswordlessToken =
await this.deletePasswordlessVerificationToken(passwordlessTokens.value);
await this.deleteMagicLinkVerificationTokens(passwordlessTokens.value);
if (E.isLeft(deletedPasswordlessToken))
return E.left({
message: deletedPasswordlessToken.left,
@@ -330,24 +306,30 @@ export class AuthService {
/**
* Refresh refresh and auth tokens
*
* @param {string} refresh_token Hashed refresh token received from client
* @param {AuthUser} user User Object
* @returns {Promise<E.Left<AuthErrorHandler> | E.Right<AuthTokens>>} Either of generated AuthTokens
* @param hashedRefreshToken Hashed refresh token received from client
* @param user User Object
* @returns Either of generated AuthTokens
*/
async refreshAuthTokens(refresh_token: string, user: AuthUser) {
async refreshAuthTokens(hashedRefreshToken: string, user: AuthUser) {
// Check to see user is valid
if (!user)
return E.left({
message: USER_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
});
const isMatched = await argon2.verify(user.refreshToken, refresh_token);
if (!isMatched)
// Check to see if the hashed refresh_token received from the client is the same as the refresh_token saved in the DB
const isTokenMatched = await argon2.verify(
user.refreshToken,
hashedRefreshToken,
);
if (!isTokenMatched)
return E.left({
message: INVALID_REFRESH_TOKEN,
statusCode: HttpStatus.NOT_FOUND,
});
// if tokens match, generate new pair of auth tokens
const generatedAuthTokens = await this.generateAuthTokens(user.uid);
if (E.isLeft(generatedAuthTokens))
return E.left({

View File

@@ -1,3 +0,0 @@
export class refreshTokensDto {
refresh_token: string;
}

View File

@@ -1,3 +1,4 @@
// Inputs to initiate Magic-Link auth flow
export class signInMagicDto {
email: string;
}

View File

@@ -1,3 +1,4 @@
// Inputs to verify and sign a user in via magic-link
export class verifyMagicDto {
deviceIdentifier: string;
token: string;

View File

@@ -1,6 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
/**
** Decorator to fetch refresh_token from cookie
*/
export const RTCookie = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);

View File

@@ -129,6 +129,12 @@ export const TEAM_REQ_NOT_FOUND = 'team_req/not_found' as const;
export const TEAM_REQ_INVALID_TARGET_COLL_ID =
'team_req/invalid_target_id' as const;
/**
* No Postmark Sender Email defined
* (AuthService)
*/
export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const;
/**
* Tried to perform action on a request when the user is not even member of the team
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
@@ -177,13 +183,15 @@ export const USER_SETTINGS_NOT_FOUND = 'user_settings/not_found' as const;
* User setting already exists for a user
* (UserSettingsService)
*/
export const USER_SETTINGS_ALREADY_EXISTS = 'user_settings/settings_already_exists' as const;
export const USER_SETTINGS_ALREADY_EXISTS =
'user_settings/settings_already_exists' as const;
/**
* User setting invalid (null) settings
* (UserSettingsService)
*/
export const USER_SETTINGS_NULL_SETTINGS = 'user_settings/null_settings' as const;
export const USER_SETTINGS_NULL_SETTINGS =
'user_settings/null_settings' as const;
/*
* Global environment doesnt exists for the user

View File

@@ -8,7 +8,7 @@ export type MailDescription = {
};
export type UserMagicLinkMailDescription = {
template: 'code-your-own'; //Alias of template in Postmark, change this to env variable
template: 'code-your-own';
variables: {
inviteeEmail: string;
magicLink: string;

View File

@@ -7,7 +7,7 @@ import {
import * as postmark from 'postmark';
import { throwErr } from 'src/utils';
import * as TE from 'fp-ts/TaskEither';
import { EMAIL_FAILED } from 'src/errors';
import { EMAIL_FAILED, SENDER_EMAIL_INVALID } from 'src/errors';
@Injectable()
export class MailerService implements OnModuleInit {
@@ -15,8 +15,7 @@ export class MailerService implements OnModuleInit {
onModuleInit() {
this.client = new postmark.ServerClient(
process.env.POSTMARK_SERVER_TOKEN ||
throwErr('No Postmark Server Token defined'),
process.env.POSTMARK_SERVER_TOKEN || throwErr(SENDER_EMAIL_INVALID),
);
}
@@ -29,8 +28,7 @@ export class MailerService implements OnModuleInit {
this.client.sendEmailWithTemplate({
To: to,
From:
process.env.POSTMARK_SENDER_EMAIL ||
throwErr('No Postmark Sender Email defined'),
process.env.POSTMARK_SENDER_EMAIL || throwErr(SENDER_EMAIL_INVALID),
TemplateAlias: mailDesc.template,
TemplateModel: mailDesc.variables,
}),
@@ -40,9 +38,9 @@ export class MailerService implements OnModuleInit {
/**
*
* @param {string} to Receiver's email id
* @param {UserMagicLinkMailDescription} mailDesc Details of email to be sent for Magic-Link auth
* @returns {Promise<postmark.Models.MessageSendingResponse>} Response if email was send successfully or not
* @param to Receiver's email id
* @param mailDesc Details of email to be sent for Magic-Link auth
* @returns Response if email was send successfully or not
*/
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
try {

View File

@@ -0,0 +1,10 @@
import { HttpStatus } from '@nestjs/common';
/**
** Custom interface to handle errors specific to Auth module
** Since its REST we need to return HTTP status code along with error message
*/
export type AuthError = {
message: string;
statusCode: HttpStatus;
};

View File

@@ -1,6 +0,0 @@
import { HttpStatus } from '@nestjs/common';
export interface AuthErrorHandler {
message: string;
statusCode: HttpStatus;
}

View File

@@ -15,7 +15,7 @@ export interface RefreshTokenPayload {
iat?: number;
}
export interface AuthTokens {
export type AuthTokens = {
access_token: string;
refresh_token: string;
}
};

View File

@@ -1,3 +1,3 @@
export interface DeviceIdentifierToken {
export type DeviceIdentifierToken = {
deviceIdentifier: string;
}
};

View File

@@ -74,11 +74,11 @@ describe('findUserById', () => {
});
});
describe('createUserMagic', () => {
describe('createUserViaMagicLink', () => {
test('should successfully create user and account for magic-link given valid inputs', async () => {
mockPrisma.user.create.mockResolvedValueOnce(user);
const result = await userService.createUserMagic(
const result = await userService.createUserViaMagicLink(
'dwight@dundermifflin.com',
);
expect(result).toEqual(user);

View File

@@ -19,8 +19,8 @@ export class UserService {
/**
* Find User with given email id
*
* @param {string} email User's email
* @returns {Promise<O.None | O.Some<User>>} Option of found User
* @param email User's email
* @returns Option of found User
*/
async findUserByEmail(email: string) {
try {
@@ -38,8 +38,8 @@ export class UserService {
/**
* Find User with given ID
*
* @param {string} userUid User ID
* @returns {Promise<O.None | O.Some<User>>} Option of found User
* @param userUid User ID
* @returns Option of found User
*/
async findUserById(userUid: string) {
try {
@@ -54,13 +54,37 @@ export class UserService {
}
}
/**
* Update User with new generated hashed refresh token
*
* @param refreshTokenHash Hash of newly generated refresh token
* @param userUid User uid
* @returns Either of User with updated refreshToken
*/
async UpdateUserRefreshToken(refreshTokenHash: string, userUid: string) {
try {
const user = await this.prisma.user.update({
where: {
uid: userUid,
},
data: {
refreshToken: refreshTokenHash,
},
});
return E.right(user);
} catch (error) {
return E.left(USER_NOT_FOUND);
}
}
/**
* Create a new User when logged in via a Magic Link
*
* @param {string} email User's Email
* @returns {Promise<User>} Created User
* @param email User's Email
* @returns Created User
*/
async createUserMagic(email: string) {
async createUserViaMagicLink(email: string) {
const createdUser = await this.prisma.user.create({
data: {
email: email,
@@ -79,23 +103,30 @@ export class UserService {
/**
* Create a new User when logged in via a SSO provider
*
* @param {string} accessToken User's access token generated by providers
* @param {string} refreshToken User's refresh token generated by providers
* @param {any} profile Data received from SSO provider on the users account
* @returns {Promise<User>} Created User
* @param accessTokenSSO User's access token generated by providers
* @param refreshTokenSSO User's refresh token generated by providers
* @param profile Data received from SSO provider on the users account
* @returns Created User
*/
async createUserSSO(accessToken: string, refreshToken: string, profile) {
async createUserSSO(
accessTokenSSO: string,
refreshTokenSSO: string,
profile,
) {
const userDisplayName = !profile.displayName ? null : profile.displayName;
const userPhotoURL = !profile.photos ? null : profile.photos[0].value;
const createdUser = await this.prisma.user.create({
data: {
displayName: !profile.displayName ? null : profile.displayName,
displayName: userDisplayName,
email: profile.emails[0].value,
photoURL: !profile.photos ? null : profile.photos[0].value,
photoURL: userPhotoURL,
accounts: {
create: {
provider: profile.provider,
providerAccountId: profile.id,
providerRefreshToken: refreshToken,
providerAccessToken: accessToken,
providerRefreshToken: refreshTokenSSO,
providerAccessToken: accessTokenSSO,
},
},
},
@@ -107,11 +138,11 @@ export class UserService {
/**
* Create a new Account for a given User
*
* @param {AuthUser} user User object
* @param {string} accessToken User's access token generated by providers
* @param {string} refreshToken User's refresh token generated by providers
* @param {any} profile Data received from SSO provider on the users account
* @returns {Promise<Account>} Created Account
* @param user User object
* @param accessToken User's access token generated by providers
* @param refreshToken User's refresh token generated by providers
* @param profile Data received from SSO provider on the users account
* @returns Created Account
*/
async createProviderAccount(
user: AuthUser,
@@ -139,9 +170,9 @@ export class UserService {
/**
* Update User displayName and photoURL
*
* @param {AuthUser} user User object
* @param {any} profile Data received from SSO provider on the users account
* @returns {Promise<E.Right<User> | E.Left<"user/not_found">>} Updated user object
* @param user User object
* @param profile Data received from SSO provider on the users account
* @returns Updated user object
*/
async updateUserDetails(user: AuthUser, profile) {
try {

View File

@@ -7,7 +7,7 @@ import * as T from 'fp-ts/Task';
import * as E from 'fp-ts/Either';
import { User } from './user/user.model';
import * as A from 'fp-ts/Array';
import { AuthErrorHandler } from './types/AuthErrorHandler';
import { AuthError } from './types/AuthError';
import { AuthTokens } from './types/AuthTokens';
import { Response } from 'express';
import { DateTime } from 'luxon';
@@ -27,7 +27,7 @@ export function throwErr(errMessage: string): never {
* This function allows throw to be used as an expression
* @param errMessage Message present in the error message
*/
export function throwHTTPErr(errorData: AuthErrorHandler): never {
export function throwHTTPErr(errorData: AuthError): never {
const { message, statusCode } = errorData;
throw new HttpException(message, statusCode);
}