diff --git a/package-lock.json b/package-lock.json index 5c65ee61..f68903ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,8 +53,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "ora": { @@ -1249,8 +1248,7 @@ }, "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "ora": { @@ -2487,8 +2485,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "chalk": { @@ -2777,6 +2774,11 @@ "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", "dev": true }, + "class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -2832,8 +2834,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "is-fullwidth-code-point": { @@ -6785,9 +6786,33 @@ } }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-int64": { "version": "0.4.0", @@ -9767,8 +9792,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "is-fullwidth-code-point": { @@ -9981,8 +10005,7 @@ "dependencies": { "ansi-regex": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "resolved": "", "dev": true }, "is-fullwidth-code-point": { diff --git a/package.json b/package.json index 0b0bc79c..d675c8c7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@nestjs/swagger": "^4.7.12", "axios": "^0.21.2", "bcrypt": "^5.0.0", + "class-transformer": "^0.5.1", "dot": "^1.1.3", "dotenv": "^8.2.0", "fastify-cookie": "^5.1.0", diff --git a/public/src/interfaces/User.ts b/public/src/interfaces/User.ts index cca77dc9..3e36e9c3 100644 --- a/public/src/interfaces/User.ts +++ b/public/src/interfaces/User.ts @@ -33,5 +33,5 @@ export interface UserData { email: string; lastName: string; firstName: string; - isAdmin: boolean; + isAdmin?: boolean; } diff --git a/public/src/pages/main/Userprofile.tsx b/public/src/pages/main/Userprofile.tsx index 477fcf38..8379c4f8 100644 --- a/public/src/pages/main/Userprofile.tsx +++ b/public/src/pages/main/Userprofile.tsx @@ -8,8 +8,7 @@ import AuthLayout from '../auth/AuthLayout'; const defaultUserData: UserData = { email: '', firstName: '', - lastName: '', - isAdmin: false + lastName: '' }; export const Userprofile = () => { diff --git a/src/app.controller.ts b/src/app.controller.ts index 21341ec2..0165147a 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -38,10 +38,6 @@ import { @Controller('/api') @ApiTags('App controller') export class AppController { - private static readonly XML_ENTITY_INJECTION = ' ]>'.toLowerCase(); - private static readonly XML_ENTITY_INJECTION_RESPONSE = `root:x:0:0:root:/root:/bin/bash - daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin`; - private readonly logger = new Logger(AppController.name); constructor(private readonly configService: ConfigService) {} @@ -89,18 +85,13 @@ export class AppController { }) @Header('content-type', 'text/xml') async xml(@Query('xml') xml: string): Promise { - if (xml?.toLowerCase() === AppController.XML_ENTITY_INJECTION) { - return AppController.XML_ENTITY_INJECTION_RESPONSE; - } - const xmlDoc = parseXml(xml, { dtdload: true, - noent: false, + noent: true, doctype: true, dtdvalid: true, errors: true, }); - this.logger.debug(xmlDoc); this.logger.debug(xmlDoc.getDtd()); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 5ff05813..f8a23ce4 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -16,7 +16,6 @@ import { createHash, randomBytes } from 'crypto'; import { ApiBadRequestResponse, ApiCreatedResponse, - ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiResponse, @@ -256,14 +255,13 @@ export class AuthController { @ApiOkResponse({ type: JwtValidationResponse, }) - @ApiForbiddenResponse({ + @ApiUnauthorizedResponse({ description: 'invalid credentials', schema: { type: 'object', properties: { - statusCode: { type: 'number' }, - message: { type: 'string' }, error: { type: 'string' }, + location: { type: 'string' }, }, }, }) @@ -320,16 +318,15 @@ export class AuthController { @ApiOkResponse({ type: JwtValidationResponse, }) - @ApiForbiddenResponse({ + @ApiUnauthorizedResponse({ + description: 'invalid credentials', schema: { type: 'object', properties: { - statusCode: { type: 'number' }, - message: { type: 'string' }, error: { type: 'string' }, + location: { type: 'string' }, }, }, - description: 'invalid credentials', }) async validateWithWeakKeyJwt(): Promise { return { @@ -381,16 +378,15 @@ export class AuthController { @ApiOkResponse({ type: JwtValidationResponse, }) - @ApiForbiddenResponse({ + @ApiUnauthorizedResponse({ + description: 'invalid credentials', schema: { type: 'object', properties: { - statusCode: { type: 'number' }, - message: { type: 'string' }, error: { type: 'string' }, + location: { type: 'string' }, }, }, - description: 'invalid credentials', }) async validateWithJKUJwt(): Promise { return { @@ -442,16 +438,15 @@ export class AuthController { @ApiOkResponse({ type: JwtValidationResponse, }) - @ApiForbiddenResponse({ + @ApiUnauthorizedResponse({ + description: 'invalid credentials', schema: { type: 'object', properties: { - statusCode: { type: 'number' }, - message: { type: 'string' }, error: { type: 'string' }, + location: { type: 'string' }, }, }, - description: 'invalid credentials', }) async validateWithJWKJwt(): Promise { return { @@ -503,16 +498,15 @@ export class AuthController { @ApiOkResponse({ type: JwtValidationResponse, }) - @ApiForbiddenResponse({ + @ApiUnauthorizedResponse({ + description: 'invalid credentials', schema: { type: 'object', properties: { - statusCode: { type: 'number' }, - message: { type: 'string' }, error: { type: 'string' }, + location: { type: 'string' }, }, }, - description: 'invalid credentials', }) async validateWithX5CJwt(): Promise { return { @@ -564,16 +558,15 @@ export class AuthController { @ApiOkResponse({ type: JwtValidationResponse, }) - @ApiForbiddenResponse({ + @ApiUnauthorizedResponse({ + description: 'invalid credentials', schema: { type: 'object', properties: { - statusCode: { type: 'number' }, - message: { type: 'string' }, error: { type: 'string' }, + location: { type: 'string' }, }, }, - description: 'invalid credentials', }) async validateWithX5UJwt(): Promise { return { diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index 5fafc81b..9a940a98 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -26,15 +26,18 @@ export class AuthGuard implements CanActivate { const request: FastifyRequest = context.switchToHttp().getRequest(); const token = request.headers[AuthGuard.AUTH_HEADER] as string; + if (!token || token.length == 0) { const token = request.cookies[AuthGuard.AUTH_HEADER]; - return token - ? !!(await this.authService.validateToken( - token, - JwtProcessorType.BEARER, - )) - : false; + if (token) { + return !!(await this.authService.validateToken( + token, + JwtProcessorType.BEARER, + )); + } else { + throw new UnauthorizedException(); + } } else if (this.checkIsBearer(token)) { return !!(await this.authService.validateToken( token.substring(7), @@ -45,7 +48,6 @@ export class AuthGuard implements CanActivate { JwTypeMetadataField, context.getHandler(), ); - return !!(await this.authService.validateToken(token, processorType)); } } catch (err) { diff --git a/src/auth/jwt/jwt.token.with.sql.kid.processor.ts b/src/auth/jwt/jwt.token.with.sql.kid.processor.ts index e0141c12..c177adca 100644 --- a/src/auth/jwt/jwt.token.with.sql.kid.processor.ts +++ b/src/auth/jwt/jwt.token.with.sql.kid.processor.ts @@ -22,7 +22,7 @@ export class JwtTokenWithSqlKIDProcessor extends JwtTokenProcessor { this.key, header.kid, ); - this.log.debug(`Executing key fetchign qury: ${query}`); + this.log.debug(`Executing key fetching query: ${query}`); const keyRow: { key: string } = await this.em .getConnection() .execute(query); diff --git a/src/auth/jwt/jwt.token.with.x5c.key.processor.ts b/src/auth/jwt/jwt.token.with.x5c.key.processor.ts index a3f5cdf3..45390a13 100644 --- a/src/auth/jwt/jwt.token.with.x5c.key.processor.ts +++ b/src/auth/jwt/jwt.token.with.x5c.key.processor.ts @@ -14,7 +14,7 @@ export class JwtTokenWithX5CKeyProcessor extends JwtTokenProcessor { return payload; } const keys = header.x5c; - this.log.debug(`Taking keys from from ${JSON.stringify(keys)}`); + this.log.debug(`Taking keys from ${JSON.stringify(keys)}`); return decode(token, keys[0], false, header.alg); } diff --git a/src/users/api/UserDto.ts b/src/users/api/UserDto.ts index c0d11c57..d0093ece 100644 --- a/src/users/api/UserDto.ts +++ b/src/users/api/UserDto.ts @@ -1,4 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; export class UserDto { @ApiProperty() @@ -10,11 +11,25 @@ export class UserDto { @ApiProperty() lastName: string; - @ApiProperty() - isAdmin: boolean; + @Exclude() + @ApiHideProperty() + isAdmin?: boolean; - @ApiProperty() - password: string; + @Exclude() + @ApiHideProperty() + password?: string; + + @Exclude() + id: number; + + @Exclude() + photo: Buffer; + + @Exclude() + updatedAt: Date; + + @Exclude() + createdAt: Date; constructor( params: { diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 73e51e0f..e68202e8 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,6 +1,8 @@ import { Body, + ClassSerializerInterceptor, Controller, + ForbiddenException, Get, Header, HttpException, @@ -23,6 +25,7 @@ import { ApiCreatedResponse, ApiForbiddenResponse, ApiNoContentResponse, + ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, @@ -54,6 +57,7 @@ import { AdminGuard } from './users.guard'; import { PermissionDto } from './api/PermissionDto'; @Controller('/api/users') +@UseInterceptors(ClassSerializerInterceptor) @ApiTags('User controller') export class UsersController { private logger = new Logger(UsersController.name); @@ -79,17 +83,24 @@ export class UsersController { }) @ApiOkResponse({ type: UserDto, - description: 'Returns user object or empty object when user is not found', + description: 'Returns user object if it exists', + }) + @ApiNotFoundResponse({ + description: 'User not founded', + schema: { + type: 'object', + properties: { + statusCode: { type: 'number' }, + message: { type: 'string' }, + }, + }, }) async getUser(@Param('email') email: string): Promise { try { this.logger.debug(`Find a user by email: ${email}`); return new UserDto(await this.usersService.findByEmail(email)); } catch (err) { - throw new InternalServerErrorException({ - error: err.message, - location: __filename, - }); + throw new HttpException(err.message, err.status); } } @@ -205,13 +216,13 @@ export class UsersController { this.logger.debug(`Create a basic user: ${user}`); const userExists = await this.usersService.findByEmail(user.email); - if (userExists) { throw new HttpException('User already exists', 409); } - - return new UserDto(await this.usersService.createUser(user)); } catch (err) { + if (err.status === 404) { + return new UserDto(await this.usersService.createUser(user)); + } throw new HttpException( err.message ?? 'Something went wrong', err.status ?? 500, @@ -275,14 +286,68 @@ export class UsersController { @ApiOkResponse({ description: 'Returns updated user', }) - async changeUserInfo(@Body() body: UserDto, @Param('email') email: string) { + async changeUserInfo( + @Body() newData: UserDto, + @Param('email') email: string, + @Req() req: FastifyRequest, + ): Promise { try { - return await this.usersService.updateUserInfo(email, body); + const user = await this.usersService.findByEmail(email); + if (!user) { + throw new NotFoundException('Could not find user'); + } + if (this.originEmail(req) !== email) { + throw new ForbiddenException(); + } + return new UserDto(await this.usersService.updateUserInfo(user, newData)); } catch (err) { - throw new InternalServerErrorException({ - error: err.message, - location: __filename, - }); + throw new HttpException( + err.message || 'Internal server error', + err.status || 500, + ); + } + } + + @Get('/one/:email/info') + @UseGuards(AuthGuard) + @JwtType(JwtProcessorType.RSA) + @ApiOperation({ + description: SWAGGER_DESC_FIND_USER_BY_EMAIL, + }) + @ApiForbiddenResponse({ + description: 'invalid credentials', + schema: { + type: 'object', + properties: { + statusCode: { type: 'number' }, + message: { type: 'string' }, + error: { type: 'string' }, + }, + }, + }) + @ApiNotFoundResponse() + @ApiOkResponse({ + description: 'Returns user info', + }) + async getUserInfo( + @Param('email') email: string, + @Req() req: FastifyRequest, + ): Promise { + try { + const user = await this.usersService.findByEmail(email); + + if (!user) { + throw new NotFoundException('Could not find user'); + } + if (this.originEmail(req) !== email) { + throw new ForbiddenException(); + } + return new UserDto(user); + } catch (err) { + throw new HttpException( + err.message || 'Internal server error', + err.status || 500, + ); } } @@ -330,4 +395,13 @@ export class UsersController { }); } } + + public originEmail(request: FastifyRequest): string { + return JSON.parse( + Buffer.from( + request.headers.authorization.split('.')[1], + 'base64', + ).toString(), + ).user; + } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 49605d44..91331bfb 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,6 +1,6 @@ import { EntityRepository, NotFoundError, wrap } from '@mikro-orm/core'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { PermissionDto } from './api/PermissionDto'; import { hashPassword } from '../auth/credentials.utils'; import { User } from '../model/user.entity'; @@ -53,22 +53,24 @@ export class UsersService { return user; } - async updateUserInfo(email: string, info: UserDto): Promise { - this.log.debug(`updateUserInfo ${email}`); - const user = await this.findByEmail(email); - if (!user) { - throw new NotFoundError('Could not find user'); - } - wrap(user).assign({ - ...info, + async updateUserInfo(oldUser: User, newData: UserDto): Promise { + this.log.debug(`updateUserInfo ${oldUser.email}`); + const newUser = oldUser; + wrap(newUser).assign({ + ...newData, }); - await this.usersRepository.persistAndFlush(user); - return user; + await this.usersRepository.persistAndFlush(newUser); + return newUser; } async findByEmail(email: string): Promise { this.log.debug(`Called findByEmail ${email}`); - return this.usersRepository.findOne({ email }); + const user = await this.usersRepository.findOne({ email }); + if (user) { + return user; + } else { + throw new NotFoundException('User not found'); + } } async getPermissions(email: string): Promise {