Skip to content

Commit

Permalink
Merge pull request #497 from chaynHQ/develop
Browse files Browse the repository at this point in the history
Merge Develop onto Main
  • Loading branch information
eleanorreem authored Jul 1, 2024
2 parents 2546082 + f1031b9 commit 33c0f5d
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 61 deletions.
38 changes: 38 additions & 0 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,44 @@ export class AuthService {
}
}

public async updateFirebaseUserEmail(firebaseUid: string, newEmail: string) {
try {
const firebaseUser = await this.firebase.admin.auth().updateUser(firebaseUid, {
email: newEmail,
});
return firebaseUser;
} catch (err) {
const errorCode = err.code;

if (errorCode === 'auth/invalid-email') {
this.logger.warn({
error: FIREBASE_ERRORS.UPDATE_USER_INVALID_EMAIL,
status: HttpStatus.BAD_REQUEST,
});
throw new HttpException(FIREBASE_ERRORS.UPDATE_USER_INVALID_EMAIL, HttpStatus.BAD_REQUEST);
} else if (
errorCode === 'auth/email-already-in-use' ||
errorCode === 'auth/email-already-exists'
) {
this.logger.warn({
error: FIREBASE_ERRORS.UPDATE_USER_ALREADY_EXISTS,
status: HttpStatus.BAD_REQUEST,
});
throw new HttpException(FIREBASE_ERRORS.UPDATE_USER_ALREADY_EXISTS, HttpStatus.BAD_REQUEST);
} else {
this.logger.warn({
error: FIREBASE_ERRORS.UPDATE_USER_FIREBASE_ERROR,
errorMessage: errorCode,
status: HttpStatus.INTERNAL_SERVER_ERROR,
});
throw new HttpException(
FIREBASE_ERRORS.CREATE_USER_FIREBASE_ERROR,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

public async getFirebaseUser(email: string) {
const firebaseUser = await this.firebase.admin.auth().getUserByEmail(email);
return firebaseUser;
Expand Down
25 changes: 24 additions & 1 deletion src/firebase/firebase-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import {
HttpException,
HttpStatus,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { FIREBASE_ERRORS } from 'src/utils/errors';

import { AUTH_GUARD_ERRORS, FIREBASE_ERRORS } from 'src/utils/errors';
import { AuthService } from '../auth/auth.service';
import { UserService } from '../user/user.service';
import { IFirebaseUser } from './firebase-user.interface';

@Injectable()
export class FirebaseAuthGuard implements CanActivate {
private readonly logger = new Logger('FirebaseAuthGuard');

constructor(
private authService: AuthService,
private userService: UserService,
Expand All @@ -25,6 +29,10 @@ export class FirebaseAuthGuard implements CanActivate {
const { authorization } = request.headers;

if (!authorization) {
this.logger.warn({
error: AUTH_GUARD_ERRORS.MISSING_HEADER,
errorMessage: `FirebaseAuthGuard: Authorisation failed for ${request.originalUrl}`,
});
throw new UnauthorizedException('Unauthorized: missing required Authorization token');
}

Expand All @@ -33,8 +41,18 @@ export class FirebaseAuthGuard implements CanActivate {
user = await this.authService.parseAuth(authorization);
} catch (error) {
if (error.code === 'auth/id-token-expired') {
this.logger.warn({
error: AUTH_GUARD_ERRORS.TOKEN_EXPIRED,
errorMessage: `FireabaseAuthGuard: Authorisation failed for ${request.originalUrl}`,
status: HttpStatus.UNAUTHORIZED,
});
throw new HttpException(FIREBASE_ERRORS.ID_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
}
this.logger.warn({
error: AUTH_GUARD_ERRORS.PARSING_ERROR,
errorMessage: `FirebaseAuthGuard: Authorisation failed for ${request.originalUrl}`,
status: HttpStatus.INTERNAL_SERVER_ERROR,
});

throw new HttpException(
`FirebaseAuthGuard - Error parsing firebase user: ${error}`,
Expand All @@ -50,6 +68,11 @@ export class FirebaseAuthGuard implements CanActivate {
request['userEntity'] = userEntity;
} catch (error) {
if (error.message === 'USER NOT FOUND') {
this.logger.warn({
error: AUTH_GUARD_ERRORS.USER_NOT_FOUND,
errorMessage: `FirebaseAuthGuard: Authorisation failed for ${request.originalUrl}`,
status: HttpStatus.INTERNAL_SERVER_ERROR,
});
throw new HttpException(
`FirebaseAuthGuard - Firebase user exists but user no record in bloom database for ${user.email}: ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
Expand Down
6 changes: 4 additions & 2 deletions src/logger/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ConsoleLogger } from '@nestjs/common';
import Rollbar from 'rollbar';
import { FIREBASE_ERRORS } from 'src/utils/errors';
import { isProduction, rollbarEnv, rollbarToken } from '../utils/constants';
import { ErrorLog } from './utils';

export class Logger extends ConsoleLogger {
private rollbar?: Rollbar;
Expand All @@ -12,12 +13,13 @@ export class Logger extends ConsoleLogger {
this.initialiseRollbar();
}

error(message: string, trace?: string): void {
error(message: string | ErrorLog, trace?: string): void {
if (this.rollbar) {
this.rollbar.error(message);
}
const formattedMessage = typeof message === 'string' ? message : JSON.stringify(message);

const taggedMessage = `[error] ${message}`;
const taggedMessage = `[error] ${formattedMessage}`;
super.error(taggedMessage, trace);
}

Expand Down
5 changes: 4 additions & 1 deletion src/logger/logging.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<void> {
const req = context.switchToHttp().getRequest<Request>();

const commonMessage = `${req.method} "${req.originalUrl}" for ${req.ip}`;
//@ts-expect-error: userEntity is modified in authGuard
const userId = req?.userEntity?.id;

const commonMessage = `${req.method} "${req.originalUrl}" (IP address: ${req.ip}, requestUserId: ${userId})`;

this.logger.log(`Started ${commonMessage}`);

Expand Down
16 changes: 16 additions & 0 deletions src/logger/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HttpStatus } from '@nestjs/common';

export type ErrorLog = {
error: string;
status: HttpStatus;
errorMessage?: string;
userId?: string;
requestUserId?: string;
};

export type EventLog = {
event: string;
fields?: string[];
userId?: string;
requestUserId?: string;
};
21 changes: 19 additions & 2 deletions src/partner-admin/partner-admin-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import {
HttpException,
HttpStatus,
Injectable,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Request } from 'express';
import { UserEntity } from 'src/entities/user.entity';
import { FIREBASE_ERRORS } from 'src/utils/errors';
import { AUTH_GUARD_ERRORS } from 'src/utils/errors';
import { Repository } from 'typeorm';
import { AuthService } from '../auth/auth.service';

@Injectable()
export class PartnerAdminAuthGuard implements CanActivate {
private readonly logger = new Logger('SuperAdminAuthGuard');

constructor(
private authService: AuthService,
@InjectRepository(UserEntity) private userRepository: Repository<UserEntity>,
Expand All @@ -23,6 +26,10 @@ export class PartnerAdminAuthGuard implements CanActivate {
const request = context.switchToHttp().getRequest<Request>();
const { authorization } = request.headers;
if (!authorization) {
this.logger.warn({
error: AUTH_GUARD_ERRORS.MISSING_HEADER,
errorMessage: `PartnerAdminAuthGuard: Authorisation failed for ${request.originalUrl}`,
});
return false;
}
let userUid;
Expand All @@ -32,9 +39,18 @@ export class PartnerAdminAuthGuard implements CanActivate {
userUid = uid;
} catch (error) {
if (error.code === 'auth/id-token-expired') {
throw new HttpException(FIREBASE_ERRORS.ID_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
this.logger.warn({
error: AUTH_GUARD_ERRORS.TOKEN_EXPIRED,
errorMessage: `PartnerAdminAuthGuard: Authorisation failed for ${request.originalUrl}`,
});
throw new HttpException(AUTH_GUARD_ERRORS.TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
}

this.logger.warn({
error: AUTH_GUARD_ERRORS.PARSING_ERROR,
errorMessage: `PartnerAdminAuthGuard: Authorisation failed for ${request.originalUrl}`,
});

throw new HttpException(
`PartnerAdminAuthGuard - Error parsing firebase user: ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
Expand All @@ -56,6 +72,7 @@ export class PartnerAdminAuthGuard implements CanActivate {

request['partnerId'] = user.partnerAdmin.partner.id; // TODO is this the best way to be handling user details
request['partnerAdminId'] = user.partnerAdmin.id;
request['userEntity'] = user;

if (user.partnerAdmin.id) return true;
return false;
Expand Down
18 changes: 15 additions & 3 deletions src/partner-admin/super-admin-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import {
HttpException,
HttpStatus,
Injectable,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Request } from 'express';
import { UserEntity } from 'src/entities/user.entity';
import { FIREBASE_ERRORS } from 'src/utils/errors';
import { AUTH_GUARD_ERRORS, FIREBASE_ERRORS } from 'src/utils/errors';
import { Repository } from 'typeorm';
import { AuthService } from '../auth/auth.service';

@Injectable()
export class SuperAdminAuthGuard implements CanActivate {
private readonly logger = new Logger('SuperAdminAuthGuard');

constructor(
private authService: AuthService,
@InjectRepository(UserEntity) private userRepository: Repository<UserEntity>,
Expand All @@ -36,17 +39,26 @@ export class SuperAdminAuthGuard implements CanActivate {
userUid = uid;
} catch (error) {
if (error.code === 'auth/id-token-expired') {
this.logger.warn({
error: AUTH_GUARD_ERRORS.TOKEN_EXPIRED,
errorMessage: `Authorisation failed for ${request.originalUrl}`,
status: HttpStatus.UNAUTHORIZED,
});
throw new HttpException(FIREBASE_ERRORS.ID_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED);
}

this.logger.warn({
error: AUTH_GUARD_ERRORS.PARSING_ERROR,
errorMessage: `Authorisation failed for ${request.originalUrl}`,
status: HttpStatus.INTERNAL_SERVER_ERROR,
});
throw new HttpException(
`SuperAdminAuthGuard - Error parsing firebase user: ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
const user = await this.userRepository.findOneBy({ firebaseUid: userUid });

request['userEntity'] = user;
return !!user.isSuperAdmin && user.email.indexOf('@chayn.co') !== -1;
} catch (error) {
throw new HttpException(
Expand Down
6 changes: 4 additions & 2 deletions src/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ config();
const configService = new ConfigService();

const isProduction = configService.get('NODE_ENV') === 'production';
const isStaging = configService.get('NODE_ENV') === 'staging';

const { host, port, user, password, database } = PostgressConnectionStringParser.parse(
configService.get('DATABASE_URL'),
);
Expand Down Expand Up @@ -114,9 +116,9 @@ export const dataSourceOptions = {
BloomBackend1718728423454,
],
subscribers: [],
ssl: isProduction,
ssl: isProduction || isStaging,
extra: {
ssl: isProduction ? { rejectUnauthorized: false } : null,
ssl: isProduction || isStaging ? { rejectUnauthorized: false } : null,
},
};

Expand Down
7 changes: 6 additions & 1 deletion src/user/dtos/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsDate, IsOptional, IsString } from 'class-validator';
import { IsBoolean, IsDate, IsEmail, IsOptional, IsString } from 'class-validator';
import { EMAIL_REMINDERS_FREQUENCY } from '../../utils/constants';

export class UpdateUserDto {
Expand Down Expand Up @@ -32,4 +32,9 @@ export class UpdateUserDto {
@IsOptional()
@ApiProperty({ type: 'date' })
lastActiveAt: Date;

@IsEmail({})
@IsOptional()
@ApiProperty({ type: 'email' })
email: string;
}
15 changes: 12 additions & 3 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs
import { Request } from 'express';
import { UserEntity } from 'src/entities/user.entity';
import { SuperAdminAuthGuard } from 'src/partner-admin/super-admin-auth.guard';
import { formatUserObject } from 'src/utils/serialize';
import { FirebaseAuthGuard } from '../firebase/firebase-auth.guard';
import { ControllerDecorator } from '../utils/controller.decorator';
import { CreateUserDto } from './dtos/create-user.dto';
Expand Down Expand Up @@ -45,7 +46,7 @@ export class UserController {
@UseGuards(FirebaseAuthGuard)
async getUserByFirebaseId(@Req() req: Request): Promise<GetUserDto> {
const user = req['user'];
this.userService.updateUser({ lastActiveAt: new Date() }, user);
this.userService.updateUser({ lastActiveAt: new Date() }, user.id);
return user;
}

Expand Down Expand Up @@ -99,7 +100,14 @@ export class UserController {
@Patch()
@UseGuards(FirebaseAuthGuard)
async updateUser(@Body() updateUserDto: UpdateUserDto, @Req() req: Request) {
return await this.userService.updateUser(updateUserDto, req['user'] as GetUserDto);
return await this.userService.updateUser(updateUserDto, req['user'].user.id);
}

@ApiBearerAuth()
@Patch('/admin/:id')
@UseGuards(SuperAdminAuthGuard)
async adminUpdateUser(@Param() { id }, @Body() updateUserDto: UpdateUserDto) {
return await this.userService.updateUser(updateUserDto, id);
}

@ApiBearerAuth()
Expand All @@ -109,7 +117,8 @@ export class UserController {
const { include, fields, limit, ...userQuery } = query.searchCriteria
? JSON.parse(query.searchCriteria)
: { include: [], fields: [], limit: undefined };
return await this.userService.getUsers(userQuery, include, fields, limit);
const users = await this.userService.getUsers(userQuery, include || [], fields, limit);
return users.map((u) => formatUserObject(u));
}

// Use only if users have not been added to mailchimp due to e.g. an ongoing bug
Expand Down
Loading

0 comments on commit 33c0f5d

Please sign in to comment.