From f342995e89ffe180e82cda1e8ad6646798402050 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Fri, 13 Jan 2023 21:42:21 +0200 Subject: [PATCH] lots more item apis --- src/app-storage/app-storage.controller.ts | 97 +++++-- src/app-storage/app-storage.service.ts | 239 +++++++++++++++++- .../dto/storage-add-item-request.dto.ts | 20 +- .../dto/storage-item-response.dto.ts | 18 ++ src/app-storage/dto/storage-response.dto.ts | 7 + src/app-user/app-user.controller.ts | 16 +- src/app-user/app-user.service.ts | 20 +- src/app-user/dto/user-login-response.dto.ts | 12 +- src/app-user/dto/user-login.dto.ts | 12 - src/main.ts | 5 + src/objects/storage/storage.service.ts | 68 ++++- src/objects/user/user.entity.ts | 2 + src/shared/guards/login.guard.ts | 45 ++++ 13 files changed, 492 insertions(+), 69 deletions(-) delete mode 100644 src/app-user/dto/user-login.dto.ts create mode 100644 src/shared/guards/login.guard.ts diff --git a/src/app-storage/app-storage.controller.ts b/src/app-storage/app-storage.controller.ts index ae1557a..402626f 100644 --- a/src/app-storage/app-storage.controller.ts +++ b/src/app-storage/app-storage.controller.ts @@ -37,11 +37,16 @@ import { AppStorageService } from './app-storage.service'; import { StorageAddExistingItemRequestDto, StorageAddItemRequestDto, + StorageItemUpdateRequestDto, + StorageStoredItemTransactionRequestDto, + StorageStoredItemUpdateRequestDto, } from './dto/storage-add-item-request.dto'; import { StorageItemRequestQueryDto } from './dto/storage-item-request.dto'; import { + StorageItemResponseDto, StorageItemSearchResponseDto, StorageStoredItemResponseDto, + StorageTransactionResponseDto, } from './dto/storage-item-response.dto'; import { StorageCreateRequestDto, @@ -64,6 +69,28 @@ import { StorageSetResponseDto } from './dto/storage-set-response.dto'; export class AppStorageController { constructor(private readonly service: AppStorageService) {} + @Get('storages/:storageId') + @ApiParam({ name: 'storageId', description: 'Storage ID' }) + @ApiOperation({ summary: 'Get storage by ID' }) + @ApiOkResponse({ type: StorageResponseDto }) + async getStorage( + @CurrentStorage() storage: Storage, + ): Promise { + return this.service.getStorageWithItems(storage); + } + + @Patch('storages/:storageId') + @ApiParam({ name: 'storageId', description: 'Storage ID' }) + @ApiBody({ type: StorageUpdateRequestDto }) + @ApiOperation({ summary: 'Update storage by ID' }) + @ApiOkResponse({ type: StorageResponseDto }) + async updateStorage( + @CurrentStorage() storage: Storage, + @Body() body: StorageUpdateRequestDto, + ): Promise { + return this.service.updateStorage(storage, body); + } + @UseGuards(StorageSetGuard) @Get('set/:storageSetId') @ApiParam({ name: 'storageSetId', description: 'Storage set ID' }) @@ -174,7 +201,7 @@ export class AppStorageController { @Body() body: StorageAddItemRequestDto, @CurrentStorage() storage: Storage, ) { - return; + return this.service.createNewItem(user, storage, body); } @Post('item/:storageId/:itemId') @@ -189,28 +216,62 @@ export class AppStorageController { @Body() body: StorageAddExistingItemRequestDto, @CurrentStorage() storage: Storage, ) { - return; + return this.service.addExistingItemToStorage(user, storage, itemId, body); } - @Get(':storageId') - @ApiParam({ name: 'storageId', description: 'Storage ID' }) - @ApiOperation({ summary: 'Get storage by ID' }) - @ApiOkResponse({ type: StorageResponseDto }) - async getStorage( + @Patch('item/:storageId/:storedItemId') + @ApiParam({ name: 'storedItemId', description: 'Stored Item ID' }) + @ApiOperation({ summary: 'Update a stored items details' }) + @ApiBody({ type: StorageStoredItemUpdateRequestDto }) + @ApiOkResponse({ type: StorageStoredItemResponseDto }) + async updateStoredItem( + @Param('storedItemId', ParseIntPipe) storedItemId: number, + @Body() body: StorageStoredItemUpdateRequestDto, @CurrentStorage() storage: Storage, - ): Promise { - return this.service.formatStorageNoItems(storage); + ) { + return this.service.updateStoredItemDetails(storage, storedItemId, body); } - @Patch(':storageId') - @ApiParam({ name: 'storageId', description: 'Storage ID' }) - @ApiBody({ type: StorageUpdateRequestDto }) - @ApiOperation({ summary: 'Update storage by ID' }) - @ApiOkResponse({ type: StorageResponseDto }) - async updateStorage( + @Post('item/:storageId/:storedItemId/transaction') + @ApiParam({ name: 'storedItemId', description: 'Stored Item ID' }) + @ApiOperation({ summary: 'Create a new stored item transaction' }) + @ApiBody({ type: StorageStoredItemTransactionRequestDto }) + @ApiOkResponse({ type: StorageTransactionResponseDto }) + async createStoredItemTransaction( + @LoggedInUser() user: User, + @Param('storedItemId', ParseIntPipe) storedItemId: number, + @Body() body: StorageStoredItemTransactionRequestDto, @CurrentStorage() storage: Storage, - @Body() body: StorageUpdateRequestDto, - ): Promise { - return this.service.updateStorage(storage, body); + ) { + return this.service.createStoredItemTransaction( + user, + storage, + storedItemId, + body, + ); + } + + @Get('item/:storageId/:storedItemId') + @ApiParam({ name: 'storedItemId', description: 'Stored Item ID' }) + @ApiOperation({ summary: 'Get a stored items details' }) + @ApiOkResponse({ type: StorageStoredItemResponseDto }) + async getStoredItem( + @Param('storedItemId', ParseIntPipe) storedItemId: number, + @CurrentStorage() storage: Storage, + ) { + return this.service.getStoredItemDetails(storage, storedItemId); + } + + @Patch('item/:itemId') + @ApiParam({ name: 'itemId', description: 'Item ID' }) + @ApiBody({ type: StorageItemUpdateRequestDto }) + @ApiOperation({ summary: 'Update an item owned by the current user' }) + @ApiOkResponse({ type: StorageItemResponseDto }) + async updateItem( + @Param('itemId', ParseIntPipe) itemId: number, + @LoggedInUser() user: User, + @Body() body: StorageItemUpdateRequestDto, + ) { + return this.service.updateOwnedItem(user, itemId, body); } } diff --git a/src/app-storage/app-storage.service.ts b/src/app-storage/app-storage.service.ts index 1038b21..91d7030 100644 --- a/src/app-storage/app-storage.service.ts +++ b/src/app-storage/app-storage.service.ts @@ -1,14 +1,31 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import omit from 'lodash.omit'; import pick from 'lodash.pick'; import { BuildingService } from 'src/objects/building/building.service'; import { Room } from 'src/objects/building/entities/room.entity'; +import { StoredItemTransaction } from 'src/objects/storage/entities/item-transaction.entity'; +import { Item } from 'src/objects/storage/entities/item.entity'; import { StorageSet } from 'src/objects/storage/entities/storage-set.entity'; import { Storage } from 'src/objects/storage/entities/storage.entity'; +import { StoredItem } from 'src/objects/storage/entities/stored-item.entity'; +import { TransactionType } from 'src/objects/storage/enums/transaction-type.enum'; import { StorageService } from 'src/objects/storage/storage.service'; import { User } from 'src/objects/user/user.entity'; +import { + StorageAddExistingItemRequestDto, + StorageAddItemRequestDto, + StorageItemUpdateRequestDto, + StorageStoredItemRequestDto, + StorageStoredItemTransactionRequestDto, + StorageStoredItemUpdateRequestDto, +} from './dto/storage-add-item-request.dto'; import { StorageItemRequestQueryDto } from './dto/storage-item-request.dto'; -import { StorageItemSearchResponseDto } from './dto/storage-item-response.dto'; +import { + StorageItemResponseDto, + StorageItemSearchResponseDto, + StorageStoredItemResponseDto, + StorageTransactionResponseDto, +} from './dto/storage-item-response.dto'; import { StorageCreateRequestDto, StorageUpdateRequestDto, @@ -116,6 +133,188 @@ export class AppStorageService { return responses; } + async createStoredItem( + user: User, + item: Item, + storage: Storage, + transactionInfo: StorageStoredItemTransactionRequestDto, + additionalInfo?: StorageStoredItemRequestDto, + ) { + // Create stored item + let storedItem = new StoredItem(); + storedItem.addedBy = user; + storedItem.item = item; + storedItem.storage = storage; + additionalInfo && Object.assign(storedItem, additionalInfo); + + storedItem = await this.storageService.saveStoredItem(storedItem); + + // Create transaction + let transaction = new StoredItemTransaction(); + transaction.actor = user; + transaction.storedItem = storedItem; + Object.assign(transaction, transactionInfo); + + transaction = await this.storageService.saveStoredItemTransaction( + transaction, + ); + + storedItem.transactions = [transaction]; + + return new StorageStoredItemResponseDto(this.formatStoredItem(storedItem)); + } + + async createNewItem( + user: User, + storage: Storage, + body: StorageAddItemRequestDto, + ) { + // Create item + let item = new Item(); + item.addedBy = user; + Object.assign( + item, + pick(body, [ + 'displayName', + 'type', + 'barcode', + 'consumable', + 'image', + 'weight', + 'url', + 'notes', + 'public', + ]), + ); + + item = await this.storageService.saveItem(item); + + return this.createStoredItem( + user, + item, + storage, + body.transactionInfo, + body.additionalInfo, + ); + } + + async addExistingItemToStorage( + user: User, + storage: Storage, + itemId: number, + body: StorageAddExistingItemRequestDto, + ) { + const item = await this.storageService.getItemByIdBySub(itemId, user.sub); + if (!item) { + throw new NotFoundException('Item not found'); + } + + return this.createStoredItem( + user, + item, + storage, + body.transactionInfo, + body.additionalInfo, + ); + } + + async updateOwnedItem( + user: User, + itemId: number, + body: StorageItemUpdateRequestDto, + ) { + const item = await this.storageService.getItemByIdOwnedBySub( + itemId, + user.sub, + ); + if (!item) { + throw new NotFoundException('Item not found'); + } + + Object.assign(item, body); + + await this.storageService.saveItem(item); + + return this.formatItem(item); + } + + async updateStoredItemDetails( + storage: Storage, + storedItemId: number, + body: StorageStoredItemUpdateRequestDto, + ) { + const storedItem = await this.storageService.getStoredItemByStorageAndId( + storage, + storedItemId, + ); + if (!storedItem) { + throw new NotFoundException('Stored item not found'); + } + + Object.assign(storedItem, body); + + await this.storageService.saveStoredItem(storedItem); + + return this.formatStoredItem(storedItem); + } + + async createStoredItemTransaction( + user: User, + storage: Storage, + storedItemId: number, + body: StorageStoredItemTransactionRequestDto, + ) { + const storedItem = await this.storageService.getStoredItemByStorageAndId( + storage, + storedItemId, + ); + if (!storedItem) { + throw new NotFoundException('Stored item not found'); + } + + const transaction = await this.storageService.saveStoredItemTransaction({ + ...body, + actor: user, + storedItem, + }); + + if ( + [ + TransactionType.SOLD, + TransactionType.DESTROYED, + TransactionType.BINNED, + ].includes(body.type) + ) { + storedItem.consumedAt = new Date(); + await this.storageService.saveStoredItem(storedItem); + } + + return this.formatTransactionNoDetails(transaction); + } + + async getStoredItemDetails(storage: Storage, storedItemId: number) { + const storedItem = await this.storageService.getStoredItemByStorageAndId( + storage, + storedItemId, + ['transactions', 'item', 'addedBy'], + ); + if (!storedItem) { + throw new NotFoundException('Stored item not found'); + } + + return this.formatStoredItem(storedItem); + } + + async getStorageWithItems(storage: Storage) { + storage = await this.storageService.getStorageById(storage.id, [ + 'items', + 'items.addedBy', + 'items.item', + ]); + + return this.formatStorageWithItems(storage); + } + formatActor(input: User): StorageActorResponse { return pick(input, ['name', 'sub', 'color']); } @@ -127,6 +326,42 @@ export class AppStorageService { }; } + formatStorageWithItems(storage: Storage): StorageResponseDto { + return { + ...omit(storage, ['room', 'set']), + items: !!storage.items?.length + ? storage.items.map((item) => this.formatStoredItem(item)) + : null, + addedBy: storage.addedBy && this.formatActor(storage.addedBy), + }; + } + + formatItem(item: Item): StorageItemResponseDto { + return { + ...omit(item, ['instances', 'addedBy']), + addedBy: item.addedBy && this.formatActor(item.addedBy), + }; + } + + formatTransactionNoDetails( + transaction: StoredItemTransaction, + ): StorageTransactionResponseDto { + return omit(transaction, ['storedItem', 'actor']); + } + + formatStoredItem(storedItem: StoredItem): StorageStoredItemResponseDto { + return { + ...omit(storedItem, ['storage']), + transactions: !!storedItem.transactions?.length + ? storedItem.transactions.map((transaction) => + this.formatTransactionNoDetails(transaction), + ) + : null, + item: storedItem.item ? this.formatItem(storedItem.item) : null, + addedBy: storedItem.addedBy ? this.formatActor(storedItem.addedBy) : null, + }; + } + formatStorageSetNoItems(set: StorageSet): StorageSetResponseDto { return { ...omit(set, ['room']), diff --git a/src/app-storage/dto/storage-add-item-request.dto.ts b/src/app-storage/dto/storage-add-item-request.dto.ts index 2411140..6851ef8 100644 --- a/src/app-storage/dto/storage-add-item-request.dto.ts +++ b/src/app-storage/dto/storage-add-item-request.dto.ts @@ -1,8 +1,8 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsBoolean, - IsDate, + IsDateString, IsEnum, IsNumber, IsObject, @@ -62,6 +62,10 @@ export class StorageItemRequestDto { public?: boolean; } +export class StorageItemUpdateRequestDto extends PartialType( + StorageItemRequestDto, +) {} + export class StorageStoredItemRequestDto { @ApiProperty() @IsString() @@ -69,21 +73,25 @@ export class StorageStoredItemRequestDto { notes?: string; @ApiPropertyOptional() - @IsDate() + @IsDateString() @IsOptional() expiresAt?: Date; @ApiPropertyOptional() - @IsDate() + @IsDateString() @IsOptional() acquiredAt?: Date; @ApiPropertyOptional() - @IsDate() + @IsDateString() @IsOptional() consumedAt?: Date; } +export class StorageStoredItemUpdateRequestDto extends PartialType( + StorageStoredItemRequestDto, +) {} + export class StorageStoredItemTransactionRequestDto { @ApiProperty({ type: String, enum: TransactionType }) @IsEnum(TransactionType) @@ -107,7 +115,7 @@ export class StorageStoredItemTransactionRequestDto { notes?: string; @ApiPropertyOptional() - @IsDate() + @IsDateString() @IsOptional() actionAt?: Date; } diff --git a/src/app-storage/dto/storage-item-response.dto.ts b/src/app-storage/dto/storage-item-response.dto.ts index 8cf4d3e..cee17fc 100644 --- a/src/app-storage/dto/storage-item-response.dto.ts +++ b/src/app-storage/dto/storage-item-response.dto.ts @@ -22,14 +22,32 @@ export class StorageItemSearchResponseDto extends PickType(Item, [ 'createdAt', ]) {} +export class StorageTransactionResponseDto extends OmitType( + StoredItemTransaction, + ['storedItem', 'actor'], +) {} + export class StorageStoredItemResponseDto extends OmitType(StoredItem, [ + 'addedBy', 'transactions', 'storage', 'item', ]) { + @ApiProperty({ type: StorageActorResponse }) + addedBy: StorageActorResponse; + @ApiProperty({ type: StorageItemResponseDto }) @Type(() => StorageItemResponseDto) item: StorageItemResponseDto; + + @ApiProperty({ type: StorageTransactionResponseDto, isArray: true }) + @Type(() => StorageTransactionResponseDto) + transactions: StorageTransactionResponseDto[]; + + constructor(obj: Partial) { + super(obj); + Object.assign(this, obj); + } } export class StorageStoredItemTransactionDto extends OmitType( diff --git a/src/app-storage/dto/storage-response.dto.ts b/src/app-storage/dto/storage-response.dto.ts index f3b7185..905aeaf 100644 --- a/src/app-storage/dto/storage-response.dto.ts +++ b/src/app-storage/dto/storage-response.dto.ts @@ -1,6 +1,7 @@ import { ApiPropertyOptional, OmitType, PickType } from '@nestjs/swagger'; import { Storage } from 'src/objects/storage/entities/storage.entity'; import { User } from 'src/objects/user/user.entity'; +import { StorageStoredItemResponseDto } from './storage-item-response.dto'; export class StorageActorResponse extends PickType(User, [ 'sub', @@ -16,4 +17,10 @@ export class StorageResponseDto extends OmitType(Storage, [ ]) { @ApiPropertyOptional({ type: StorageActorResponse }) addedBy: StorageActorResponse; + + @ApiPropertyOptional({ + type: () => StorageStoredItemResponseDto, + isArray: true, + }) + items?: StorageStoredItemResponseDto[]; } diff --git a/src/app-user/app-user.controller.ts b/src/app-user/app-user.controller.ts index 4d2a604..9f8c206 100644 --- a/src/app-user/app-user.controller.ts +++ b/src/app-user/app-user.controller.ts @@ -1,5 +1,4 @@ import { - Body, ClassSerializerInterceptor, Controller, Get, @@ -8,19 +7,19 @@ import { UseInterceptors, } from '@nestjs/common'; import { - ApiBadRequestResponse, + ApiBasicAuth, ApiBearerAuth, - ApiBody, ApiOkResponse, ApiOperation, ApiTags, + ApiUnauthorizedResponse, } from '@nestjs/swagger'; import { User } from 'src/objects/user/user.entity'; import { LoggedInUser } from 'src/shared/decorators/user.decorator'; import { AuthGuard } from 'src/shared/guards/auth.guard'; +import { LoginGuard } from 'src/shared/guards/login.guard'; import { AppUserService } from './app-user.service'; import { UserLoginResponseDto } from './dto/user-login-response.dto'; -import { UserLoginDto } from './dto/user-login.dto'; @ApiTags('users') @UseInterceptors(ClassSerializerInterceptor) @@ -31,12 +30,13 @@ export class AppUserController { constructor(private readonly service: AppUserService) {} @Post('login') - @ApiBody({ type: UserLoginDto }) + @UseGuards(LoginGuard) + @ApiBasicAuth('Email and Password') @ApiOperation({ summary: 'Log in using email and password' }) - @ApiBadRequestResponse() + @ApiUnauthorizedResponse({ description: 'Invalid email or password' }) @ApiOkResponse({ type: UserLoginResponseDto }) - async userDoLogin(@Body() body: UserLoginDto) { - return this.service.login(body); + async userDoLogin(@LoggedInUser() loginRequest: User) { + return this.service.login(loginRequest); } @Get() diff --git a/src/app-user/app-user.service.ts b/src/app-user/app-user.service.ts index f8638a7..03e396d 100644 --- a/src/app-user/app-user.service.ts +++ b/src/app-user/app-user.service.ts @@ -1,9 +1,9 @@ import { UserService } from 'src/objects/user/user.service'; -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { UserLoginDto } from './dto/user-login.dto'; import { UserLoginResponseDto } from './dto/user-login-response.dto'; import { AuthService } from 'src/shared/auth/auth.service'; +import { User } from 'src/objects/user/user.entity'; @Injectable() export class AppUserService { @@ -13,21 +13,13 @@ export class AppUserService { private readonly auth: AuthService, ) {} - async login({ email, password }: UserLoginDto) { - const user = await this.userService.getUserByEmail(email); - if (!user) { - throw new BadRequestException('Invalid username or password'); - } - - if (!(await this.auth.comparePassword(user, password))) { - throw new BadRequestException('Invalid username or password'); - } - + async login(user: User) { const token = await this.auth.issueJWT(user); return new UserLoginResponseDto({ - accessToken: token, - expiresIn: this.auth.expiry, + access_token: token, + expires_in: this.auth.expiry, + token_type: 'Bearer', }); } } diff --git a/src/app-user/dto/user-login-response.dto.ts b/src/app-user/dto/user-login-response.dto.ts index fd6cd68..2e9d79f 100644 --- a/src/app-user/dto/user-login-response.dto.ts +++ b/src/app-user/dto/user-login-response.dto.ts @@ -1,10 +1,16 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ConstructableDto } from 'src/shared/dto/constructable.dto'; export class UserLoginResponseDto extends ConstructableDto { @ApiProperty() - accessToken: string; + access_token: string; + + @ApiPropertyOptional() + refresh_token: string; @ApiProperty() - expiresIn: number; + expires_in: number; + + @ApiProperty() + token_type: string; } diff --git a/src/app-user/dto/user-login.dto.ts b/src/app-user/dto/user-login.dto.ts deleted file mode 100644 index ae736b3..0000000 --- a/src/app-user/dto/user-login.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEmail, IsString } from 'class-validator'; - -export class UserLoginDto { - @ApiProperty() - @IsEmail() - email: string; - - @ApiProperty() - @IsString() - password: string; -} diff --git a/src/main.ts b/src/main.ts index e183b67..d421a49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,11 @@ async function bootstrap() { name: 'Bearer token', type: 'apiKey', }) + .addBasicAuth({ + name: 'Email and Password', + description: 'For acquiring a Bearer token', + type: 'http', + }) .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); diff --git a/src/objects/storage/storage.service.ts b/src/objects/storage/storage.service.ts index 91550d0..06f8d51 100644 --- a/src/objects/storage/storage.service.ts +++ b/src/objects/storage/storage.service.ts @@ -1,9 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ILike, Repository } from 'typeorm'; -import { BuildingService } from '../building/building.service'; -import { GroupService } from '../group/group.service'; -import { UserService } from '../user/user.service'; import { StoredItemTransaction } from './entities/item-transaction.entity'; import { Item } from './entities/item.entity'; import { StorageSet } from './entities/storage-set.entity'; @@ -23,11 +20,17 @@ export class StorageService { private readonly storedItemRepository: Repository, @InjectRepository(StoredItemTransaction) private readonly transactionRepository: Repository, - private readonly groupService: GroupService, - private readonly userService: UserService, - private readonly buildingService: BuildingService, ) {} + async getStorageById(id: number, relations = []) { + return this.storageRepository.findOne({ + where: { + id, + }, + relations, + }); + } + async getStorageByIdAndSub(id: number, sub: string, relations = []) { return this.storageRepository.findOne({ where: { @@ -216,6 +219,59 @@ export class StorageService { }); } + async getItemByIdBySub(id: number, sub: string) { + return this.itemRepository.findOne({ + where: [ + { + id, + addedBy: { + sub, + }, + }, + { + id, + public: true, + }, + { + id, + addedBy: { + groups: { + members: { + sub, + }, + }, + }, + }, + ], + }); + } + + async getItemByIdOwnedBySub(id: number, sub: string, relations = []) { + return this.itemRepository.findOne({ + where: { + id, + addedBy: { + sub, + }, + }, + relations, + }); + } + + async getStoredItemByStorageAndId( + storage: Storage, + storedItemId: number, + relations = [], + ) { + return this.storedItemRepository.findOne({ + where: { + id: storedItemId, + storage: { id: storage.id }, + }, + relations, + }); + } + async saveStorage(data: Partial) { const newStorage = new Storage(); Object.assign(newStorage, data); diff --git a/src/objects/user/user.entity.ts b/src/objects/user/user.entity.ts index e055d70..11087ad 100644 --- a/src/objects/user/user.entity.ts +++ b/src/objects/user/user.entity.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; import { Column, CreateDateColumn, @@ -34,6 +35,7 @@ export class User { emailVerified: boolean; @Column() + @Exclude() password: string; @ApiProperty() diff --git a/src/shared/guards/login.guard.ts b/src/shared/guards/login.guard.ts new file mode 100644 index 0000000..0b7bef8 --- /dev/null +++ b/src/shared/guards/login.guard.ts @@ -0,0 +1,45 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { UserService } from 'src/objects/user/user.service'; +import { AuthService } from '../auth/auth.service'; + +@Injectable() +export class LoginGuard implements CanActivate { + constructor( + private readonly authService: AuthService, + private readonly userService: UserService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const http = context.switchToHttp(); + const request = http.getRequest(); + const response = http.getResponse(); + + const authHeader = request.header('authorization'); + if (!authHeader) return false; + + const [method, token] = authHeader.split(' '); + if (!token || method !== 'Basic') return false; + + const [email, password] = Buffer.from(token, 'base64') + .toString() + .split(':'); + + const user = await this.userService.getUserByEmail(email); + if (!user) { + throw new UnauthorizedException('Invalid username or password'); + } + + if (!(await this.authService.comparePassword(user, password))) { + throw new UnauthorizedException('Invalid username or password'); + } + + response.locals.user = user; + + return true; + } +}