Skip to content

Commit

Permalink
API: extends users module
Browse files Browse the repository at this point in the history
  • Loading branch information
agnlez committed Oct 9, 2024
1 parent 6e42eaa commit eca2169
Show file tree
Hide file tree
Showing 20 changed files with 661 additions and 71 deletions.
4 changes: 3 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
save-prefix=''
save-prefix=''
public-hoist-pattern[]=*nestjs-base-service*
public-hoist-pattern[]=*typeorm*
6 changes: 4 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.2",
"@ts-rest/nest": "^3.51.0",
"@types/multer": "1.4.12",
"bcrypt": "catalog:",
"class-transformer": "catalog:",
"class-validator": "catalog:",
"lodash": "^4.17.21",
"nestjs-base-service": "catalog:",
"nodemailer": "^6.9.15",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
Expand All @@ -38,8 +41,7 @@
"rxjs": "^7.8.1",
"typeorm": "catalog:",
"xlsx": "^0.18.5",
"zod": "catalog:",
"@types/multer": "^1.4.12"
"zod": "catalog:"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
Expand Down
9 changes: 9 additions & 0 deletions api/src/decorators/get-user.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '@shared/entities/users/user.entity';

export const GetUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext): User => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
4 changes: 2 additions & 2 deletions api/src/modules/auth/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { UsersService } from '@api/modules/users/users.service';
import { User } from '@shared/entities/users/user.entity';
import * as bcrypt from 'bcrypt';
import { CommandBus, EventBus } from '@nestjs/cqrs';
import { UserWithAccessToken } from '@shared/dtos/user.dto';
import { UserWithAccessToken } from '@shared/dtos/users/user.dto';
import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema';
import { CreateUserDto } from '@shared/schemas/users/create-user.schema';
import { CreateUserDto } from '@shared/dtos/users/create-user.dto';
import { randomBytes } from 'node:crypto';
import { SendWelcomeEmailCommand } from '@api/modules/notifications/email/commands/send-welcome-email.command';
import { JwtManager } from '@api/modules/auth/services/jwt.manager';
Expand Down
69 changes: 66 additions & 3 deletions api/src/modules/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,67 @@
import { Controller } from '@nestjs/common';
import {
Controller,
ClassSerializerInterceptor,
Body,
Param,
ParseUUIDPipe,
UseInterceptors,
HttpStatus,
UnauthorizedException,
} from '@nestjs/common';

@Controller('users')
export class UsersController {}
import { UsersService } from './users.service';
import { tsRestHandler, TsRestHandler } from '@ts-rest/nest';
import { GetUser } from '@api/decorators/get-user.decorator';
import { User } from '@shared/entities/users/user.entity';
import { usersContract as c } from '@shared/contracts/users.contract';

import { UpdateUserPasswordDto } from '@shared/dtos/users/update-user-password.dto';
import { UpdateUserDto } from '@shared/dtos/users/update-user.dto';

@Controller()
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
constructor(private usersService: UsersService) {}

@TsRestHandler(c.findMe)
async findMe(@GetUser() user: User): Promise<any> {
return tsRestHandler(c.findMe, async ({ query }) => {
const foundUser = await this.usersService.getById(user.id, query);
if (!foundUser) {
throw new UnauthorizedException();
}
return { body: { data: foundUser }, status: HttpStatus.OK };
});
}

@TsRestHandler(c.updatePassword)
async updatePassword(
@Body() dto: UpdateUserPasswordDto['newPassword'],
@GetUser() user: User,
): Promise<any> {
return tsRestHandler(c.updatePassword, async () => {
const updatedUser = await this.usersService.updatePassword(user, dto);
return { body: { data: updatedUser }, status: HttpStatus.OK };
});
}

@TsRestHandler(c.updateUser)
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateUserDto,
): Promise<any> {
return tsRestHandler(c.updateUser, async () => {
const user = await this.usersService.update(id, dto);
//return { body: { data: user }, status: HttpStatus.CREATED };
return { body: { data: user }, status: HttpStatus.CREATED };
});
}

@TsRestHandler(c.deleteMe)
async deleteMe(@GetUser() user: User): Promise<any> {
return tsRestHandler(c.deleteMe, async () => {
await this.usersService.remove(user.id);
return { body: null, status: HttpStatus.OK };
});
}
}
30 changes: 22 additions & 8 deletions api/src/modules/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,53 @@ import { User } from '@shared/entities/users/user.entity';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';

import { AppBaseService } from '@api/utils/app-base.service';
import { CreateUserDto } from '@shared/dtos/users/create-user.dto';
import { UpdateUserDto } from '@shared/dtos/users/update-user.dto';
import { AppInfoDTO } from '@api/utils/info.dto';

@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private repo: Repository<User>) {}
export class UsersService extends AppBaseService<
User,
CreateUserDto,
UpdateUserDto,
AppInfoDTO
> {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
) {
super(userRepository, 'user', 'users');
}

async findOneBy(id: string) {
return this.repo.findOne({
return this.userRepository.findOne({
where: { id },
});
}

async findByEmail(email: string): Promise<User | null> {
return this.repo.findOne({ where: { email } });
return this.userRepository.findOne({ where: { email } });
}

async createUser(newUser: Partial<User>) {
const existingUser = await this.findByEmail(newUser.email);
if (existingUser) {
throw new ConflictException(`Email ${newUser.email} already exists`);
}
return this.repo.save(newUser);
return this.userRepository.save(newUser);
}

async updatePassword(user: User, newPassword: string) {
user.password = await bcrypt.hash(newPassword, 10);
return this.repo.save(user);
return this.userRepository.save(user);
}

async delete(user: User) {
return this.repo.remove(user);
return this.userRepository.remove(user);
}

async isUserActive(id: string) {
const user = await this.repo.findOneBy({ id });
const user = await this.userRepository.findOneBy({ id });
return user.isActive;
}
}
67 changes: 67 additions & 0 deletions api/src/utils/app-base.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
BaseService,
DEFAULT_PAGINATION,
FetchSpecification,
} from 'nestjs-base-service';

import { Repository } from 'typeorm';
import { PaginationMeta } from '@shared/dtos/global/api-response.dto';

export abstract class AppBaseService<
// eslint-disable-next-line @typescript-eslint/ban-types
Entity extends object,
CreateModel,
UpdateModel,
Info,
> extends BaseService<Entity, CreateModel, UpdateModel, Info> {
constructor(
protected readonly repository: Repository<Entity>,
protected alias: string = 'base_entity',
protected pluralAlias: string = 'base_entities',
protected idProperty: string = 'id',
) {
super(repository, alias, { idProperty });
}

async findAllPaginated(
fetchSpecification?: FetchSpecification,
extraOps?: Record<string, any>,
info?: Info,
): Promise<{
data: (Partial<Entity> | undefined)[];
metadata: PaginationMeta | undefined;
}> {
const entitiesAndCount: [Partial<Entity>[], number] = await this.findAll(
{ ...fetchSpecification, ...extraOps },
info,
);
return this._paginate(entitiesAndCount, fetchSpecification);
}

private _paginate(
entitiesAndCount: [Partial<Entity>[], number],
fetchSpecification?: FetchSpecification,
): {
data: (Partial<Entity> | undefined)[];
metadata: PaginationMeta | undefined;
} {
const totalItems: number = entitiesAndCount[1];
const entities: Partial<Entity>[] = entitiesAndCount[0];
const pageSize: number =
fetchSpecification?.pageSize ?? DEFAULT_PAGINATION.pageSize ?? 25;
const page: number =
fetchSpecification?.pageNumber ?? DEFAULT_PAGINATION.pageNumber ?? 1;
const disablePagination: boolean | undefined =
fetchSpecification?.disablePagination;
const meta: PaginationMeta | undefined = disablePagination
? undefined
: new PaginationMeta({
totalPages: Math.ceil(totalItems / pageSize),
totalItems,
size: pageSize,
page,
});

return { data: entities, metadata: meta };
}
}
4 changes: 4 additions & 0 deletions api/src/utils/info.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { InfoDTO } from 'nestjs-base-service';
import { User } from '@shared/entities/users/user.entity';

export type AppInfoDTO = InfoDTO<User>;
Loading

0 comments on commit eca2169

Please sign in to comment.