Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
✨ Add support for password change, refactor auth
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed Oct 24, 2020
1 parent b7cf9f3 commit 21a7cb4
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 49 deletions.
37 changes: 21 additions & 16 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,10 @@ export class AuthService {
const ignorePwnedPassword = !!data.ignorePwnedPassword;
delete data.email;
delete data.ignorePwnedPassword;

if (data.password) {
if (!ignorePwnedPassword) await this.ensureSafePassword(data.password);
data.password = await hash(
data.password,
this.configService.get<number>('security.saltRounds'),
);
}
data.password = await this.hashAndValidatePassword(
data.password,
ignorePwnedPassword,
);

const users = await this.users.users({
take: 1,
Expand Down Expand Up @@ -162,14 +158,23 @@ export class AuthService {
});
}

async ensureSafePassword(password: string): Promise<void> {
if (!this.configService.get<boolean>('security.passwordPwnedCheck')) return;
const isSafe = this.pwnedService.isPasswordSafe(password);
if (!isSafe)
throw new HttpException(
'This password has been compromised in a data breach.',
HttpStatus.BAD_REQUEST,
);
async hashAndValidatePassword(
password: string,
ignorePwnedPassword: boolean,
): Promise<string> {
if (!ignorePwnedPassword) {
if (!this.configService.get<boolean>('security.passwordPwnedCheck'))
return;
if (!(await this.pwnedService.isPasswordSafe(password)))
throw new HttpException(
'This password has been compromised in a data breach.',
HttpStatus.BAD_REQUEST,
);
}
return await hash(
password,
this.configService.get<number>('security.saltRounds'),
);
}

async getScopes(userId: number): Promise<string[]> {
Expand Down
8 changes: 4 additions & 4 deletions src/modules/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ export class UserController {
@Query('where', WherePipe) where?: Record<string, number | string>,
@Query('orderBy', OrderByPipe) orderBy?: Record<string, 'asc' | 'desc'>,
): Promise<Expose<users>[]> {
return this.usersService.users({ skip, take, orderBy, cursor, where });
return this.usersService.getUsers({ skip, take, orderBy, cursor, where });
}

@Get(':id')
@Scopes('user-{id}:read-info')
async get(@Param('id', ParseIntPipe) id: number): Promise<Expose<users>> {
return this.usersService.user({ id: Number(id) });
return this.usersService.getUser(Number(id));
}

@Patch(':id')
Expand All @@ -46,12 +46,12 @@ export class UserController {
@Param('id', ParseIntPipe) id: number,
@Body() data: UpdateUserDto,
): Promise<Expose<users>> {
return this.usersService.updateUser({ where: { id: Number(id) }, data });
return this.usersService.updateUser(Number(id), data);
}

@Delete(':id')
@Scopes('user-{id}:delete')
async remove(@Param('id', ParseIntPipe) id: number): Promise<Expose<users>> {
return this.usersService.deleteUser({ id: Number(id) });
return this.usersService.deleteUser(Number(id));
}
}
34 changes: 21 additions & 13 deletions src/modules/user/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,58 +13,66 @@ import {
export class UpdateUserDto {
@IsBoolean()
@IsOptional()
checkLocationOnLogin: boolean;
checkLocationOnLogin?: boolean;

@IsString()
@Length(2, 2)
@IsOptional()
countryCode: string;
countryCode?: string;

@IsString()
@IsIn(['MALE', 'FEMALE', 'NONBINARY', 'UNKNOWN'])
@IsOptional()
gender: 'MALE' | 'FEMALE' | 'NONBINARY' | 'UNKNOWN';
gender?: 'MALE' | 'FEMALE' | 'NONBINARY' | 'UNKNOWN';

@IsString()
@MinLength(3)
@IsOptional()
name: string;
name?: string;

@IsIn(['ACCOUNT', 'UPDATES', 'PROMOTIONS'])
@IsOptional()
notificationEmails: 'ACCOUNT' | 'UPDATES' | 'PROMOTIONS';
notificationEmails?: 'ACCOUNT' | 'UPDATES' | 'PROMOTIONS';

@IsString()
@IsOptional()
password: string | null;
newPassword?: string | null;

@IsString()
@IsOptional()
currentPassword?: string | null;

@IsBoolean()
@IsOptional()
ignorePwnedPassword?: boolean;

@IsLocale()
@IsOptional()
prefersLanguage: string;
prefersLanguage?: string;

@IsString()
@IsIn(['NO_PREFERENCE', 'LIGHT', 'DARK'])
@IsOptional()
prefersColorScheme: 'NO_PREFERENCE' | 'LIGHT' | 'DARK';
prefersColorScheme?: 'NO_PREFERENCE' | 'LIGHT' | 'DARK';

@IsString()
@IsIn(['NO_PREFERENCE', 'REDUCE'])
@IsOptional()
prefersReducedMotion: 'NO_PREFERENCE' | 'REDUCE';
prefersReducedMotion?: 'NO_PREFERENCE' | 'REDUCE';

@IsUrl()
@IsOptional()
profilePictureUrl: string;
profilePictureUrl?: string;

@IsString()
@IsOptional()
timezone: string;
timezone?: string;

@IsBoolean()
@IsOptional()
twoFactorEnabled: boolean;
twoFactorEnabled?: boolean;

@IsObject()
@IsOptional()
attributes: Record<string, any>;
attributes?: Record<string, any>;
}
5 changes: 5 additions & 0 deletions src/modules/user/user.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface PasswordUpdateInput {
currentPassword?: string;
newPassword?: string;
ignorePwnedPassword?: boolean;
}
59 changes: 43 additions & 16 deletions src/modules/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
usersUpdateInput,
Expand All @@ -9,22 +14,23 @@ import {
usersOrderByInput,
} from '@prisma/client';
import { Expose } from 'src/modules/prisma/prisma.interface';
import { AuthService } from '../auth/auth.service';
import { compare } from 'bcrypt';
import { PasswordUpdateInput } from './user.interface';

@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
constructor(private prisma: PrismaService, private auth: AuthService) {}

async user(
userWhereUniqueInput: usersWhereUniqueInput,
): Promise<Expose<users> | null> {
async getUser(id: number): Promise<Expose<users> | null> {
const user = await this.prisma.users.findOne({
where: userWhereUniqueInput,
where: { id },
});
if (!user) throw new HttpException('User not found', HttpStatus.NOT_FOUND);
return this.prisma.expose<users>(user);
}

async users(params: {
async getUsers(params: {
skip?: number;
take?: number;
cursor?: usersWhereUniqueInput;
Expand All @@ -48,21 +54,42 @@ export class UsersService {
});
}

async updateUser(params: {
where: usersWhereUniqueInput;
data: usersUpdateInput;
}): Promise<Expose<users>> {
const { where, data } = params;
async updateUser(
id: number,
data: Omit<usersUpdateInput, 'password'> & PasswordUpdateInput,
): Promise<Expose<users>> {
const transformed: usersUpdateInput & PasswordUpdateInput = data;
if (data.newPassword) {
if (!data.currentPassword)
throw new BadRequestException('Current password is required');
const previousPassword = (
await this.prisma.users.findOne({
where: { id },
select: { password: true },
})
)?.password;
if (previousPassword)
if (!(await compare(data.currentPassword, previousPassword)))
throw new BadRequestException('Current password is incorrect');
transformed.password = await this.auth.hashAndValidatePassword(
data.newPassword,
!!data.ignorePwnedPassword,
);
}
delete transformed.currentPassword;
delete transformed.newPassword;
delete transformed.ignorePwnedPassword;
const updateData: usersUpdateInput = transformed;
const user = await this.prisma.users.update({
data,
where,
data: updateData,
where: { id },
});
return this.prisma.expose<users>(user);
}

async deleteUser(where: usersWhereUniqueInput): Promise<Expose<users>> {
async deleteUser(id: number): Promise<Expose<users>> {
const user = await this.prisma.users.delete({
where,
where: { id },
});
return this.prisma.expose<users>(user);
}
Expand Down

0 comments on commit 21a7cb4

Please sign in to comment.