Skip to content

Commit

Permalink
add user module
Browse files Browse the repository at this point in the history
  • Loading branch information
ariefgp committed Apr 3, 2024
1 parent bf67547 commit 216f382
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 2 deletions.
3 changes: 2 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';
import { CommonModule } from './common/common.module';
import { UserModule } from './user/user.module';

@Module({
imports: [CommonModule],
imports: [CommonModule, UserModule],
controllers: [],
providers: [],
})
Expand Down
17 changes: 17 additions & 0 deletions src/common/auth.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
createParamDecorator,
ExecutionContext,
HttpException,
} from '@nestjs/common';

export const Auth = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (user) {
return user;
} else {
throw new HttpException('Unauthorized', 401);
}
},
);
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule);

const logger = app.get(WINSTON_MODULE_NEST_PROVIDER);
app.use(logger);
app.useLogger(logger);

await app.listen(3000);
}
Expand Down
27 changes: 27 additions & 0 deletions src/model/user.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export class RegisterUserRequest {
username: string;
password: string;
first_name: string;
last_name?: string;
email?: string;
phone?: string;
}

export class UserResponse {
username: string;
first_name: string;
last_name?: string;
email?: string;
phone?: string;
token?: string;
}

export class LoginUserRequest {
username: string;
password: string;
}

export class UpdateUserRequest {
first_name?: string;
password?: string;
}
11 changes: 11 additions & 0 deletions src/model/web.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class WebResponse<T> {
data?: T;
errors?: string;
paging?: Paging;
}

export class Paging {
size: number;
total_page: number;
current_page: number;
}
76 changes: 76 additions & 0 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Patch,
Post,
} from '@nestjs/common';
import { UserService } from './user.service';
import { WebResponse } from '../model/web.model';
import {
LoginUserRequest,
RegisterUserRequest,
UpdateUserRequest,
UserResponse,
} from '../model/user.model';
import { Auth } from '../common/auth.decorator';
import { User } from '@prisma/client';

@Controller('/api/users')
export class UserController {
constructor(private userService: UserService) {}

@Post()
@HttpCode(200)
async register(
@Body() request: RegisterUserRequest,
): Promise<WebResponse<UserResponse>> {
const result = await this.userService.register(request);
return {
data: result,
};
}

@Post('/login')
@HttpCode(200)
async login(
@Body() request: LoginUserRequest,
): Promise<WebResponse<UserResponse>> {
const result = await this.userService.login(request);
return {
data: result,
};
}

@Get('/current')
@HttpCode(200)
async get(@Auth() user: User): Promise<WebResponse<UserResponse>> {
const result = await this.userService.get(user);
return {
data: result,
};
}

@Patch('/current')
@HttpCode(200)
async update(
@Auth() user: User,
@Body() request: UpdateUserRequest,
): Promise<WebResponse<UserResponse>> {
const result = await this.userService.update(user, request);
return {
data: result,
};
}

@Delete('/current')
@HttpCode(200)
async logout(@Auth() user: User): Promise<WebResponse<boolean>> {
await this.userService.logout(user);
return {
data: true,
};
}
}
9 changes: 9 additions & 0 deletions src/user/user.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
providers: [UserService],
controllers: [UserController],
})
export class UserModule {}
147 changes: 147 additions & 0 deletions src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { HttpException, Inject, Injectable } from '@nestjs/common';
import {
RegisterUserRequest,
UserResponse,
LoginUserRequest,
UpdateUserRequest,
} from 'src/model/user.model';
import { ValidationService } from 'src/common/validation.service';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { PrismaService } from 'src/common/prisma.service';
import { UserValidation } from './user.validation';
import * as bcrypt from 'bcrypt';
import { v4 as uuid } from 'uuid';
import { User } from '@prisma/client';

@Injectable()
export class UserService {
constructor(
private validationService: ValidationService,
@Inject(WINSTON_MODULE_PROVIDER) private logger: Logger,
private prismaService: PrismaService,
) {}

async register(request: RegisterUserRequest): Promise<UserResponse> {
this.logger.info(`Register new yser ${JSON.stringify(request)}`);
const registerRequest: RegisterUserRequest =
this.validationService.validate(UserValidation.REGISTER, request);

const totalUserWithSameUsername = await this.prismaService.user.count({
where: {
username: registerRequest.username,
},
});

if (totalUserWithSameUsername != 0) {
throw new HttpException('Username already exists', 400);
}

registerRequest.password = await bcrypt.hash(registerRequest.password, 10);

const user = await this.prismaService.user.create({
data: registerRequest,
});

return {
username: user.username,
first_name: user.first_name,
};
}

async login(request: LoginUserRequest): Promise<UserResponse> {
this.logger.debug(`UserService.login(${JSON.stringify(request)})`);
const loginRequest: LoginUserRequest = this.validationService.validate(
UserValidation.LOGIN,
request,
);

let user = await this.prismaService.user.findUnique({
where: {
username: loginRequest.username,
},
});

if (!user) {
throw new HttpException('Username or password is invalid', 401);
}

const isPasswordValid = await bcrypt.compare(
loginRequest.password,
user.password,
);

if (!isPasswordValid) {
throw new HttpException('Username or password is invalid', 401);
}

user = await this.prismaService.user.update({
where: {
username: loginRequest.username,
},
data: {
token: uuid(),
},
});

return {
username: user.username,
first_name: user.first_name,
token: user.token,
};
}

async get(user: User): Promise<UserResponse> {
return {
username: user.username,
first_name: user.first_name,
};
}

async update(user: User, request: UpdateUserRequest): Promise<UserResponse> {
this.logger.debug(
`UserService.update( ${JSON.stringify(user)} , ${JSON.stringify(request)} )`,
);

const updateRequest: UpdateUserRequest = this.validationService.validate(
UserValidation.UPDATE,
request,
);

if (updateRequest.first_name) {
user.first_name = updateRequest.first_name;
}

if (updateRequest.password) {
user.password = await bcrypt.hash(updateRequest.password, 10);
}

const result = await this.prismaService.user.update({
where: {
username: user.username,
},
data: user,
});

return {
first_name: result.first_name,
username: result.username,
};
}

async logout(user: User): Promise<UserResponse> {
const result = await this.prismaService.user.update({
where: {
username: user.username,
},
data: {
token: null,
},
});

return {
username: result.username,
first_name: result.first_name,
};
}
}
26 changes: 26 additions & 0 deletions src/user/user.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z, ZodType } from 'zod';

export class UserValidation {
static readonly REGISTER: ZodType = z.object({
username: z.string().min(1).max(100),
password: z.string().min(1).max(100),
first_name: z.string().min(1).max(100),
last_name: z.string().min(1).max(100).optional(),
email: z.string().min(1).max(100).optional(),
phone: z.string().min(1).max(100).optional(),
});

static readonly LOGIN: ZodType = z.object({
username: z.string().min(1).max(100),
password: z.string().min(1).max(100),
});

static readonly UPDATE: ZodType = z.object({
first_name: z.string().min(1).max(100).optional(),
last_name: z.string().min(1).max(100).optional(),
email: z.string().min(1).max(100).optional(),
phone: z.string().min(1).max(100).optional(),
name: z.string().min(1).max(100).optional(),
password: z.string().min(1).max(100).optional(),
});
}

0 comments on commit 216f382

Please sign in to comment.