diff --git a/generators/server/files.js b/generators/server/files.js index 9067195c..1c114131 100644 --- a/generators/server/files.js +++ b/generators/server/files.js @@ -31,6 +31,7 @@ const serverFiles = { 'src/security/guards/roles.guard.ts', 'src/security/guards/auth.guard.ts', 'src/security/role-type.ts', + 'src/security/password-util.ts', 'src/security/decorators/auth-user.decorator.ts', 'src/security/decorators/roles.decorator.ts', 'src/security/index.ts', diff --git a/generators/server/templates/server/e2e/account.e2e-spec.ts.ejs b/generators/server/templates/server/e2e/account.e2e-spec.ts.ejs index 1f00db8f..90516dde 100644 --- a/generators/server/templates/server/e2e/account.e2e-spec.ts.ejs +++ b/generators/server/templates/server/e2e/account.e2e-spec.ts.ejs @@ -59,7 +59,7 @@ describe('Account', () => { await app.init(); service = moduleFixture.get(UserService); authService = moduleFixture.get(AuthService); - userAuthenticated = await service.save(testUserAuthenticated); + userAuthenticated = await service.save(testUserAuthenticated, true); }); it('/POST register new user', async () => { diff --git a/generators/server/templates/server/e2e/user.e2e-spec.ts.ejs b/generators/server/templates/server/e2e/user.e2e-spec.ts.ejs index e7980011..0bab1b0c 100644 --- a/generators/server/templates/server/e2e/user.e2e-spec.ts.ejs +++ b/generators/server/templates/server/e2e/user.e2e-spec.ts.ejs @@ -64,10 +64,12 @@ describe('User', () => { it('/PUT update user', async () => { testUserDTO.login = 'TestUserUpdate'; - const savedUser: UserDTO = await service.save(testUserDTO); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password: savedPassword, ...savedUser } = await service.save(testUserDTO); savedUser.firstName = 'Updated Name'; - const updatedUser: UserDTO = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password: updatedPassword, ...updatedUser } = ( await request(app.getHttpServer()) .put('/api/admin/users') .send(savedUser) @@ -80,7 +82,7 @@ describe('User', () => { expect(updatedUser.firstName).toEqual(savedUser.firstName); <%_ } _%> - await service.delete(savedUser); + await service.delete(savedUser as UserDTO); }); it('/GET user with a login name', async () => { diff --git a/generators/server/templates/server/package.json.ejs b/generators/server/templates/server/package.json.ejs index 0de8df8c..fa918534 100644 --- a/generators/server/templates/server/package.json.ejs +++ b/generators/server/templates/server/package.json.ejs @@ -37,6 +37,9 @@ <%_ } _%> "@nestjs/swagger": "3.1.0", "@nestjs/typeorm": "7.1.4", + <%_ if (authenticationType === 'jwt') { _%> + "bcrypt": "5.0.1", + <%_ } _%> "class-transformer": "0.3.1", "class-validator": "0.13.1", "cloud-config-client": "1.4.2", @@ -79,6 +82,7 @@ "typeorm-encrypted": "0.5.6" }, "devDependencies": { + "@types/bcrypt": "5.0.0", "@nestjs/testing": "7.5.1", "@types/express": "4.17.1", "@types/express-serve-static-core": "4.17.3", @@ -123,6 +127,7 @@ "^.+\\.(t|j)s$": "ts-jest" }, "coverageDirectory": "coverage", - "testEnvironment": "node" + "testEnvironment": "node", + "setupFiles": ["./e2e/setup.test.js"] } } diff --git a/generators/server/templates/server/src/config.ts.ejs b/generators/server/templates/server/src/config.ts.ejs index edd4656d..6e29d6d9 100644 --- a/generators/server/templates/server/src/config.ts.ejs +++ b/generators/server/templates/server/src/config.ts.ejs @@ -14,6 +14,7 @@ export class Config { 'jhipster.security.authentication.jwt.base64-secret' = ''; 'jhipster.security.authentication.jwt.token-validity-in-seconds' = 86400; 'jhipster.security.authentication.jwt.token-validity-in-seconds-for-remember-me' = 2592000; + 'jhipster.security.authentication.jwt.hash-salt-or-rounds' = 10; <%_ } else if (authenticationType === 'oauth2') { _%> 'jhipster.security.session.secret' = ''; 'jhipster.security.oauth2.client.provider.oidc.issuer-uri' = ''; @@ -49,9 +50,7 @@ export class Config { 'cloud.config.uri' = 'http://admin:${jhipster.registry.password}@localhost:8761/config'; 'cloud.config.name' = '<%= baseName %>'; 'cloud.config.profile' = 'prod'; - 'loud.config.label' = 'master'; - 'crypto.key' = '3772c1cdbd27c225735d116d1e4c5421a3aec26c919cc7ab457f21a4d16a1821'; - 'crypto.iv' = '54f3ad979d9262d3a2dd4489531daf34'; + 'cloud.config.label' = 'master'; constructor(properties) { this.addAll(properties); diff --git a/generators/server/templates/server/src/domain/user.entity.ts.ejs b/generators/server/templates/server/src/domain/user.entity.ts.ejs index fb72dfa8..53d0b309 100644 --- a/generators/server/templates/server/src/domain/user.entity.ts.ejs +++ b/generators/server/templates/server/src/domain/user.entity.ts.ejs @@ -1,8 +1,6 @@ import { Authority } from './authority.entity'; -import { Entity, Column <%_ if (databaseType !== 'mongodb') { _%>, ManyToMany, JoinTable <%_ } _%> } from 'typeorm'; +import { Entity, Column<%_ if (databaseType !== 'mongodb') { _%>, ManyToMany, JoinTable <%_ } _%> } from 'typeorm'; import { BaseEntity } from './base/base.entity'; -import { config } from '../config'; -import { EncryptionTransformer } from "typeorm-encrypted"; @Entity('nhi_user') export class User extends BaseEntity { @@ -29,14 +27,7 @@ export class User extends BaseEntity { <%_ } _%> @Column({ - type: "varchar", - transformer: new EncryptionTransformer({ - key: config.get('crypto.key'), - algorithm: 'aes-256-cbc', - ivLength: 16, - iv: config.get('crypto.iv') - }), - select: false + type: "varchar" }) password: string; @Column({ nullable: true }) diff --git a/generators/server/templates/server/src/migrations/1570200490072-SeedUsersRoles.ts.ejs b/generators/server/templates/server/src/migrations/1570200490072-SeedUsersRoles.ts.ejs index 2ec1eab9..c74dce0b 100644 --- a/generators/server/templates/server/src/migrations/1570200490072-SeedUsersRoles.ts.ejs +++ b/generators/server/templates/server/src/migrations/1570200490072-SeedUsersRoles.ts.ejs @@ -1,6 +1,7 @@ import { MigrationInterface, QueryRunner, getRepository } from 'typeorm'; <%_ if (authenticationType !== 'oauth2') { _%> import { User } from '../domain/user.entity'; +import { transformPassword } from '../security'; <%_ } _%> import { Authority } from '../domain/authority.entity'; @@ -83,8 +84,9 @@ export class SeedUsersRoles1570200490072 implements MigrationInterface { this.user3.authorities= [adminRole, userRole]; this.user4.authorities= [userRole]; - await userRepository.save([this.user1, this.user2, this.user3, this.user4]); + await Promise.all([this.user1, this.user2, this.user3, this.user4].map(u => transformPassword(u))); + await userRepository.save([this.user1, this.user2, this.user3, this.user4]); <%_ } _%> } diff --git a/generators/server/templates/server/src/security/index.ts.ejs b/generators/server/templates/server/src/security/index.ts.ejs index 8be849eb..b701c66b 100644 --- a/generators/server/templates/server/src/security/index.ts.ejs +++ b/generators/server/templates/server/src/security/index.ts.ejs @@ -3,3 +3,4 @@ export * from './guards/roles.guard'; export * from './decorators/roles.decorator'; export * from './decorators/auth-user.decorator'; export * from './role-type'; +export * from './password-util'; diff --git a/generators/server/templates/server/src/security/password-util.ts.ejs b/generators/server/templates/server/src/security/password-util.ts.ejs new file mode 100644 index 00000000..31e44261 --- /dev/null +++ b/generators/server/templates/server/src/security/password-util.ts.ejs @@ -0,0 +1,16 @@ +<%_ if (authenticationType === 'jwt') { _%> +import * as bcrypt from 'bcrypt'; +import { config } from '../config'; +<%_ } _%> + +export async function transformPassword(user: { password?: string }): Promise { + <%_ if (authenticationType === 'jwt') { _%> + if (user.password) { + user.password = await bcrypt.hash( + user.password, + config.get('jhipster.security.authentication.jwt.hash-salt-or-rounds'), + ); + } + <%_ } _%> + return Promise.resolve(); +} diff --git a/generators/server/templates/server/src/service/auth.service.ts.ejs b/generators/server/templates/server/src/service/auth.service.ts.ejs index 6ca5edc4..3024a949 100644 --- a/generators/server/templates/server/src/service/auth.service.ts.ejs +++ b/generators/server/templates/server/src/service/auth.service.ts.ejs @@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { JwtService } from '@nestjs/jwt'; import { UserLoginDTO } from '../service/dto/user-login.dto'; import { Payload } from '../security/payload.interface'; +import * as bcrypt from 'bcrypt'; <%_ } _%> import { AuthorityRepository } from '../repository/authority.repository'; import { UserService } from '../service/user.service'; @@ -31,8 +32,9 @@ export class AuthService { const loginUserName = userLogin.username; const loginPassword = userLogin.password; - const userFind = await this.userService.findByfields({ where: { login: loginUserName, password: loginPassword } }); - if (!userFind) { + const userFind = await this.userService.findByfields({ where: { login: loginUserName } }); + const validPassword = !!userFind && await bcrypt.compare(loginPassword, userFind.password); + if (!userFind || !validPassword) { throw new HttpException('Invalid login name or password!', HttpStatus.BAD_REQUEST); } @@ -73,15 +75,16 @@ export class AuthService { } async changePassword(userLogin: string, currentClearTextPassword: string, newPassword: string): Promise { - const userFind: UserDTO = await this.userService.findByfields({ where: { login: userLogin }, select: [ 'id', 'password' ] }); + const userFind: UserDTO = await this.userService.findByfields({ where: { login: userLogin } }); if (!userFind) { throw new HttpException('Invalid login name!', HttpStatus.BAD_REQUEST); } - if (userFind.password !== currentClearTextPassword ) { + + if (!(await bcrypt.compare(currentClearTextPassword, userFind.password))) { throw new HttpException('Invalid password!', HttpStatus.BAD_REQUEST); } userFind.password = newPassword; - await this.userService.save(userFind); + await this.userService.save(userFind, true); return; } @@ -95,7 +98,7 @@ export class AuthService { throw new HttpException('Email is already in use!', HttpStatus.BAD_REQUEST); } newUser.authorities = ['ROLE_USER']; - const user: UserDTO = await this.userService.save(newUser); + const user: UserDTO = await this.userService.save(newUser, true); return user; } diff --git a/generators/server/templates/server/src/service/dto/base.dto.ts.ejs b/generators/server/templates/server/src/service/dto/base.dto.ts.ejs index 6ba1bafb..208f6880 100644 --- a/generators/server/templates/server/src/service/dto/base.dto.ts.ejs +++ b/generators/server/templates/server/src/service/dto/base.dto.ts.ejs @@ -1,9 +1,13 @@ +<%_ if (databaseType === 'mongodb') { _%> +import { Transform } from 'class-transformer'; +<%_ } _%> /** * A DTO base object. */ export class BaseDTO { + <%_ if (databaseType === 'mongodb') { _%>@Transform(id => id?.toHexString ? id?.toHexString() : id, {toPlainOnly: true})<%_ } _%> id?:<%_ if (getPkType(databaseType) === 'Long') { _%>number<%_ } else { _%>string<%_ } _%>; createdBy?: string; diff --git a/generators/server/templates/server/src/service/dto/user.dto.ts.ejs b/generators/server/templates/server/src/service/dto/user.dto.ts.ejs index e1dcf267..bf6db0fc 100644 --- a/generators/server/templates/server/src/service/dto/user.dto.ts.ejs +++ b/generators/server/templates/server/src/service/dto/user.dto.ts.ejs @@ -1,6 +1,8 @@ import { ApiModelProperty } from '@nestjs/swagger'; import { IsString, IsEmail } from 'class-validator'; import { BaseDTO } from './base.dto'; +import { Exclude } from 'class-transformer'; + /** * An User DTO object. */ @@ -33,6 +35,7 @@ export class UserDTO extends BaseDTO { }) authorities?: any[]; + @Exclude() @ApiModelProperty({ example: 'myuser', description: 'User password' }) password: string; diff --git a/generators/server/templates/server/src/service/user.service.ts.ejs b/generators/server/templates/server/src/service/user.service.ts.ejs index 680b2eb7..667e3648 100644 --- a/generators/server/templates/server/src/service/user.service.ts.ejs +++ b/generators/server/templates/server/src/service/user.service.ts.ejs @@ -5,6 +5,7 @@ import { UserDTO } from './dto/user.dto'; import { UserMapper } from './mapper/user.mapper'; import { UserRepository } from '../repository/user.repository'; import { FindManyOptions, FindOneOptions } from 'typeorm'; +import { transformPassword } from '../security'; @Injectable() export class UserService { @@ -41,8 +42,11 @@ export class UserService { return resultList; } - async save(userDTO: UserDTO): Promise { + async save(userDTO: UserDTO, updatePassword = false): Promise { const user = this.convertInAuthorities(UserMapper.fromDTOtoEntity(userDTO)); + if (updatePassword) { + await transformPassword(user); + } const result = await this.userRepository.save(user); return UserMapper.fromEntityToDTO(this.flatAuthorities(result)); } diff --git a/generators/server/templates/server/src/web/rest/account.controller.ts.ejs b/generators/server/templates/server/src/web/rest/account.controller.ts.ejs index d54d5840..bdbbcc5d 100644 --- a/generators/server/templates/server/src/web/rest/account.controller.ts.ejs +++ b/generators/server/templates/server/src/web/rest/account.controller.ts.ejs @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { <%_ if (authenticationType === 'jwt') { _%> Body, Param, Post, Res, UseGuards, <%_ } _%> Controller, Get, Logger, Req, UseInterceptors } from '@nestjs/common'; +import { <%_ if (authenticationType === 'jwt') { _%> Body, Param, Post, Res, UseGuards, <%_ } _%> Controller, Get, Logger, Req, UseInterceptors, ClassSerializerInterceptor, InternalServerErrorException } from '@nestjs/common'; <%_ if (authenticationType === 'jwt') { _%> import { Response, Request } from 'express'; import { AuthGuard, Roles, RoleType, RolesGuard } from '../../security'; @@ -11,7 +11,7 @@ import {<%_ if (authenticationType === 'jwt') { _%> ApiBearerAuth, <%_ } _%> Api import { AuthService } from '../../service/auth.service'; @Controller('api') -@UseInterceptors(LoggingInterceptor) +@UseInterceptors(LoggingInterceptor, ClassSerializerInterceptor) @ApiUseTags('account-resource') export class AccountController { logger = new Logger('AccountController'); @@ -26,7 +26,7 @@ export class AccountController { description: 'Registered user', type: UserDTO, }) - async registerAccount(@Req() req: Request, @Body() userDTO: UserDTO): Promise { + async registerAccount(@Req() req: Request, @Body() userDTO: UserDTO & { password: string }): Promise { return await this.authService.registerNewUser(userDTO); } @@ -40,7 +40,7 @@ export class AccountController { description: 'activated', }) activateAccount(@Param() key: string, @Res() res: Response): any{ - return res.sendStatus(500); + throw new InternalServerErrorException(); } @Get('/authenticate') @@ -116,7 +116,7 @@ export class AccountController { type: 'string', }) requestPasswordReset(@Req() req: Request, @Body() email: string, @Res() res: Response): any { - return res.sendStatus(500); + throw new InternalServerErrorException(); } @Post('/account/reset-password/finish') @@ -129,7 +129,7 @@ export class AccountController { type: 'string', }) finishPasswordReset(@Req() req: Request, @Body() keyAndPassword: string, @Res() res: Response): any { - return res.sendStatus(500); + throw new InternalServerErrorException(); } <%_ } else if (authenticationType === 'oauth2') { _%> diff --git a/generators/server/templates/server/src/web/rest/user.controller.ts.ejs b/generators/server/templates/server/src/web/rest/user.controller.ts.ejs index 14e0fe4d..3ae37c58 100644 --- a/generators/server/templates/server/src/web/rest/user.controller.ts.ejs +++ b/generators/server/templates/server/src/web/rest/user.controller.ts.ejs @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Logger, Param, Post, Put, UseGuards, Req, UseInterceptors } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Logger, Param, Post, Put, UseGuards, Req, UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common'; import { Request } from 'express'; import { AuthGuard, Roles, RolesGuard, RoleType } from '../../security'; import { PageRequest, Page } from '../../domain/base/pagination.entity'; @@ -10,7 +10,7 @@ import { UserService } from '../../service/user.service'; @Controller('api/admin/users') @UseGuards(AuthGuard, RolesGuard) -@UseInterceptors(LoggingInterceptor) +@UseInterceptors(LoggingInterceptor, ClassSerializerInterceptor) <%_ if (authenticationType === 'jwt') { _%> @ApiBearerAuth() <%_ } else if (authenticationType === 'oauth2') { _%> diff --git a/test-integration/07-run-generated-app-sample.sh b/test-integration/07-run-generated-app-sample.sh index 2991fef1..22c3574e 100644 --- a/test-integration/07-run-generated-app-sample.sh +++ b/test-integration/07-run-generated-app-sample.sh @@ -41,7 +41,7 @@ launchCurlOrProtractor() { do result=0 echo "***${GREEN}run protractor e2e test in client for : "$1 - node node_modules/webdriver-manager/bin/webdriver-manager update --gecko false && JHI_E2E_HEADLESS=true npm run e2e + node node_modules/webdriver-manager/bin/webdriver-manager update --gecko false --versions.chrome 90.0.4430.24 && JHI_E2E_HEADLESS=true npm run e2e result=$? [ $result -eq 0 ] && break retryCount=$((retryCount+1))