@boarteam/boar-pack-users-backend 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +60 -0
- package/src/auth/auth-manage.controller.ts +36 -0
- package/src/auth/auth-strategies.constants.ts +4 -0
- package/src/auth/auth.constants.ts +1 -0
- package/src/auth/auth.controller.ts +83 -0
- package/src/auth/auth.module.ts +98 -0
- package/src/auth/auth.service.ts +55 -0
- package/src/auth/google-auth.config.ts +38 -0
- package/src/auth/google-auth.filter.ts +15 -0
- package/src/auth/google-auth.guard.ts +6 -0
- package/src/auth/google-auth.strategy.ts +59 -0
- package/src/auth/index.ts +15 -0
- package/src/auth/local-auth.dto.ts +8 -0
- package/src/auth/local-auth.guard.ts +6 -0
- package/src/auth/local-auth.strategy.ts +21 -0
- package/src/auth/ms-auth.config.ts +45 -0
- package/src/auth/ms-auth.guard.ts +8 -0
- package/src/auth/ms-auth.strategy.ts +63 -0
- package/src/casl/action.enum.ts +7 -0
- package/src/casl/casl-ability.factory.ts +122 -0
- package/src/casl/casl.module.ts +31 -0
- package/src/casl/index.ts +5 -0
- package/src/casl/policies/manage-all.policy.ts +9 -0
- package/src/casl/policies.guard.ts +80 -0
- package/src/event-logs/dto/event-log-create.dto.ts +47 -0
- package/src/event-logs/dto/event-log-timeline-query.dto.ts +13 -0
- package/src/event-logs/dto/event-log-timeline.dto.ts +9 -0
- package/src/event-logs/dto/event-log-update.dto.ts +47 -0
- package/src/event-logs/entities/event-log.entity.ts +139 -0
- package/src/event-logs/event-logs.constants.ts +2 -0
- package/src/event-logs/event-logs.controller.ts +67 -0
- package/src/event-logs/event-logs.interceptor.ts +75 -0
- package/src/event-logs/event-logs.logger.ts +48 -0
- package/src/event-logs/event-logs.middleware.ts +60 -0
- package/src/event-logs/event-logs.module.ts +129 -0
- package/src/event-logs/event-logs.permissions.ts +4 -0
- package/src/event-logs/event-logs.service.ts +187 -0
- package/src/event-logs/event-logs.types.ts +4 -0
- package/src/event-logs/index.ts +10 -0
- package/src/event-logs/policies/manage-event-logs.policy.ts +8 -0
- package/src/event-logs/policies/view-event-logs.policy.ts +8 -0
- package/src/generateTypes.ts +72 -0
- package/src/index.ts +6 -0
- package/src/jwt-auth/index.ts +5 -0
- package/src/jwt-auth/jwt-auth.config.ts +24 -0
- package/src/jwt-auth/jwt-auth.guard.ts +26 -0
- package/src/jwt-auth/jwt-auth.module.ts +58 -0
- package/src/jwt-auth/jwt-auth.service.ts +19 -0
- package/src/jwt-auth/jwt-auth.srtategy.ts +54 -0
- package/src/users/bcrypt.service.ts +20 -0
- package/src/users/dto/permission.dto.ts +5 -0
- package/src/users/dto/user-create.dto.ts +37 -0
- package/src/users/dto/user-update.dto.ts +37 -0
- package/src/users/entities/permissions.ts +23 -0
- package/src/users/entities/user.entity.ts +53 -0
- package/src/users/hash-password.interceptor.ts +22 -0
- package/src/users/index.ts +15 -0
- package/src/users/me.controller.ts +57 -0
- package/src/users/policies/view-users.policy.ts +10 -0
- package/src/users/users-editing.guard.ts +60 -0
- package/src/users/users.config.ts +24 -0
- package/src/users/users.constants.ts +1 -0
- package/src/users/users.controller.ts +73 -0
- package/src/users/users.module.ts +75 -0
- package/src/users/users.service.ts +23 -0
- package/src/ws-auth/index.ts +3 -0
- package/src/ws-auth/ws-auth.d2 +14 -0
- package/src/ws-auth/ws-auth.gateway.ts +25 -0
- package/src/ws-auth/ws-auth.guard.ts +28 -0
- package/src/ws-auth/ws-auth.module.ts +18 -0
- package/src/ws-auth/ws-auth.service.ts +93 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
2
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
3
|
+
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
4
|
+
import { JWTAuthConfigService } from './jwt-auth.config';
|
|
5
|
+
import { Request } from 'express';
|
|
6
|
+
import { JWT_AUTH, tokenName } from '../auth';
|
|
7
|
+
import { UsersService } from '../users';
|
|
8
|
+
|
|
9
|
+
export type TJWTPayload = {
|
|
10
|
+
email: string;
|
|
11
|
+
sub: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
@Injectable()
|
|
15
|
+
export class JwtAuthStrategy extends PassportStrategy(Strategy, JWT_AUTH) {
|
|
16
|
+
constructor(
|
|
17
|
+
private usersService: UsersService,
|
|
18
|
+
private jwtAuthConfigService: JWTAuthConfigService,
|
|
19
|
+
) {
|
|
20
|
+
super({
|
|
21
|
+
jwtFromRequest: ExtractJwt.fromExtractors([
|
|
22
|
+
(req: Request) => {
|
|
23
|
+
const cookies = req.headers.cookie?.split('; ');
|
|
24
|
+
if (!cookies) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cookie = cookies.find(c => c.startsWith(`${tokenName}=`));
|
|
29
|
+
if (!cookie) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return cookie.split('=')[1];
|
|
34
|
+
},
|
|
35
|
+
]),
|
|
36
|
+
ignoreExpiration: false,
|
|
37
|
+
secretOrKey: jwtAuthConfigService.config.jwtSecret,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async validate(payload: TJWTPayload) {
|
|
42
|
+
const userId = payload.sub;
|
|
43
|
+
const user = await this.usersService.findOne({
|
|
44
|
+
select: ['id', 'email', 'role', 'permissions'],
|
|
45
|
+
where: { id: userId },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!user) {
|
|
49
|
+
throw new UnauthorizedException();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return user;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common";
|
|
2
|
+
import { TUsersConfig, UsersConfigService } from "./users.config";
|
|
3
|
+
import bcrypt from "bcrypt";
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class BcryptService {
|
|
7
|
+
private config: TUsersConfig;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private usersConfig: UsersConfigService,
|
|
11
|
+
) {
|
|
12
|
+
this.config = this.usersConfig.config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
hashPassword(password: string): Promise<string> {
|
|
16
|
+
return bcrypt.hash(password, this.config.saltRounds);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default BcryptService;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Joi from "joi";
|
|
2
|
+
import { JoiSchema } from 'nestjs-joi';
|
|
3
|
+
import { Roles } from '../entities/user.entity';
|
|
4
|
+
import { Permission, Permissions } from "../entities/permissions";
|
|
5
|
+
|
|
6
|
+
export class UserCreateDto {
|
|
7
|
+
@JoiSchema(Joi.string().required())
|
|
8
|
+
name: string;
|
|
9
|
+
|
|
10
|
+
@JoiSchema(Joi.string().trim().lowercase().email().required())
|
|
11
|
+
email: string;
|
|
12
|
+
|
|
13
|
+
@JoiSchema(
|
|
14
|
+
Joi.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.valid(...Object.values(Roles)),
|
|
17
|
+
)
|
|
18
|
+
role?: Roles;
|
|
19
|
+
|
|
20
|
+
@JoiSchema(Joi.string().optional().allow(null))
|
|
21
|
+
pass?: string | null;
|
|
22
|
+
|
|
23
|
+
@JoiSchema(
|
|
24
|
+
Joi.array().items(
|
|
25
|
+
Joi.string()
|
|
26
|
+
).optional().default([]).custom((value: string[], helpers) => {
|
|
27
|
+
value.forEach((permission) => {
|
|
28
|
+
if (!Permissions.isValidPermission(permission as Permission)) {
|
|
29
|
+
helpers.error('any.invalid');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
permissions?: Permission[];
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Joi from "joi";
|
|
2
|
+
import { JoiSchema } from 'nestjs-joi';
|
|
3
|
+
import { Roles } from '../entities/user.entity';
|
|
4
|
+
import { Permission, Permissions } from "../entities/permissions";
|
|
5
|
+
|
|
6
|
+
export class UserUpdateDto {
|
|
7
|
+
@JoiSchema(Joi.string().optional())
|
|
8
|
+
name?: string;
|
|
9
|
+
|
|
10
|
+
@JoiSchema(Joi.string().trim().lowercase().email().optional())
|
|
11
|
+
email?: string;
|
|
12
|
+
|
|
13
|
+
@JoiSchema(
|
|
14
|
+
Joi.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.valid(...Object.values(Roles)),
|
|
17
|
+
)
|
|
18
|
+
role?: Roles;
|
|
19
|
+
|
|
20
|
+
@JoiSchema(Joi.string().optional().allow(null))
|
|
21
|
+
pass?: string | null;
|
|
22
|
+
|
|
23
|
+
@JoiSchema(
|
|
24
|
+
Joi.array().items(
|
|
25
|
+
Joi.string()
|
|
26
|
+
).optional().custom((value: string[], helpers) => {
|
|
27
|
+
value.forEach((permission) => {
|
|
28
|
+
if (!Permissions.isValidPermission(permission as Permission)) {
|
|
29
|
+
helpers.error('any.invalid');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
permissions?: Permission[];
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { VIEW_USERS } from "../users.constants";
|
|
2
|
+
|
|
3
|
+
export interface IPermissions {
|
|
4
|
+
VIEW_USERS: typeof VIEW_USERS;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type Permission = string; // todo: fix and use IPermissions[keyof IPermissions];
|
|
8
|
+
|
|
9
|
+
export class Permissions {
|
|
10
|
+
private static values: Set<Permission> = new Set;
|
|
11
|
+
|
|
12
|
+
public static isValidPermission(permission: string): boolean {
|
|
13
|
+
return this.values.has(permission as Permission);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static addPermission(permission: Permission): void {
|
|
17
|
+
this.values.add(permission);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public static toArray(): Permission[] {
|
|
21
|
+
return Array.from(this.values);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Unique, UpdateDateColumn, } from 'typeorm';
|
|
2
|
+
import { SubjectRawRule } from '@casl/ability';
|
|
3
|
+
import { PackRule } from '@casl/ability/extra';
|
|
4
|
+
import { Action, AppAbility, TSubjectsNames } from '../../casl';
|
|
5
|
+
import { Permission } from './permissions';
|
|
6
|
+
|
|
7
|
+
export enum Roles {
|
|
8
|
+
ADMIN = 'admin',
|
|
9
|
+
USER = 'user',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const EMAIL_UNIQUE_CONSTRAINT = 'UQ_users_email';
|
|
13
|
+
|
|
14
|
+
export type TUser = Omit<User, 'pass'> & {
|
|
15
|
+
ability?: AppAbility;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
@Entity('users')
|
|
19
|
+
@Unique(EMAIL_UNIQUE_CONSTRAINT, ['email'])
|
|
20
|
+
export class User {
|
|
21
|
+
@PrimaryGeneratedColumn('uuid')
|
|
22
|
+
id: string;
|
|
23
|
+
|
|
24
|
+
@Column()
|
|
25
|
+
name: string;
|
|
26
|
+
|
|
27
|
+
@Column()
|
|
28
|
+
email: string;
|
|
29
|
+
|
|
30
|
+
@Column({
|
|
31
|
+
enum: Roles,
|
|
32
|
+
default: Roles.USER,
|
|
33
|
+
})
|
|
34
|
+
role: Roles;
|
|
35
|
+
|
|
36
|
+
@Column({ type: 'varchar', nullable: true })
|
|
37
|
+
pass: string | null;
|
|
38
|
+
|
|
39
|
+
@Column({
|
|
40
|
+
type: 'varchar',
|
|
41
|
+
array: true,
|
|
42
|
+
default: [],
|
|
43
|
+
})
|
|
44
|
+
permissions: Permission[];
|
|
45
|
+
|
|
46
|
+
@CreateDateColumn({ name: 'created_at' })
|
|
47
|
+
createdAt: Date;
|
|
48
|
+
|
|
49
|
+
@UpdateDateColumn({ name: 'updated_at' })
|
|
50
|
+
updatedAt: Date;
|
|
51
|
+
|
|
52
|
+
policies: PackRule<SubjectRawRule<Action, TSubjectsNames, unknown>>[];
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
|
2
|
+
import BcryptService from "./bcrypt.service";
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class HashPasswordInterceptor implements NestInterceptor {
|
|
6
|
+
constructor(
|
|
7
|
+
private bcryptService: BcryptService,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
async intercept(
|
|
11
|
+
context: ExecutionContext,
|
|
12
|
+
next: CallHandler,
|
|
13
|
+
) {
|
|
14
|
+
const request = context.switchToHttp().getRequest();
|
|
15
|
+
|
|
16
|
+
if (request.body?.pass) {
|
|
17
|
+
request.body.pass = await this.bcryptService.hashPassword(request.body.pass);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return next.handle();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './dto/permission.dto';
|
|
2
|
+
export * from './dto/user-create.dto';
|
|
3
|
+
export * from './dto/user-update.dto';
|
|
4
|
+
export * from './entities/permissions';
|
|
5
|
+
export * from './entities/user.entity';
|
|
6
|
+
export * from './policies/view-users.policy';
|
|
7
|
+
export * from './bcrypt.service';
|
|
8
|
+
export * from './hash-password.interceptor';
|
|
9
|
+
export * from './me.controller';
|
|
10
|
+
export * from './users.config';
|
|
11
|
+
export * from './users.constants';
|
|
12
|
+
export * from './users.controller';
|
|
13
|
+
export * from './users.module';
|
|
14
|
+
export * from './users.service';
|
|
15
|
+
export * from './users-editing.guard';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Crud, CrudAuth, CrudController, type CrudRequest, Override, ParsedRequest, } from '@nestjsx/crud';
|
|
2
|
+
import { TUser, User } from './entities/user.entity';
|
|
3
|
+
import { Controller, Req } from '@nestjs/common';
|
|
4
|
+
import type { Request } from 'express';
|
|
5
|
+
import { UsersService } from './users.service';
|
|
6
|
+
import { CaslAbilityFactory } from '../casl/casl-ability.factory';
|
|
7
|
+
import { SkipPoliciesGuard } from '../casl/policies.guard';
|
|
8
|
+
import { ApiTags } from '@nestjs/swagger';
|
|
9
|
+
|
|
10
|
+
@Crud({
|
|
11
|
+
model: {
|
|
12
|
+
type: User,
|
|
13
|
+
},
|
|
14
|
+
routes: {
|
|
15
|
+
only: ['getOneBase'],
|
|
16
|
+
},
|
|
17
|
+
params: {
|
|
18
|
+
id: {
|
|
19
|
+
primary: true,
|
|
20
|
+
disabled: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
query: {
|
|
24
|
+
exclude: ['pass'],
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
@CrudAuth({
|
|
28
|
+
property: 'user',
|
|
29
|
+
filter: (user: TUser) => ({
|
|
30
|
+
id: user.id,
|
|
31
|
+
}),
|
|
32
|
+
})
|
|
33
|
+
@SkipPoliciesGuard()
|
|
34
|
+
@ApiTags('Users')
|
|
35
|
+
@Controller('me')
|
|
36
|
+
export class MeController implements CrudController<User> {
|
|
37
|
+
constructor(
|
|
38
|
+
public service: UsersService,
|
|
39
|
+
private caslAbilityFactory: CaslAbilityFactory,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
get base(): CrudController<User> {
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Override()
|
|
47
|
+
async getOne(
|
|
48
|
+
@ParsedRequest() req: CrudRequest,
|
|
49
|
+
@Req() originReq: Request,
|
|
50
|
+
): Promise<User> {
|
|
51
|
+
const user = await this.base.getOneBase!(req);
|
|
52
|
+
const ability = await this.caslAbilityFactory.createForUser(user);
|
|
53
|
+
user.policies = this.caslAbilityFactory.packAbility(ability);
|
|
54
|
+
|
|
55
|
+
return user;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IPolicyHandler } from "../../casl/policies.guard";
|
|
2
|
+
import { AppAbility } from "../../casl/casl-ability.factory";
|
|
3
|
+
import { Action } from "../../casl/action.enum";
|
|
4
|
+
import { User } from "../entities/user.entity";
|
|
5
|
+
|
|
6
|
+
export class ViewUsersPolicy implements IPolicyHandler {
|
|
7
|
+
handle(ability: AppAbility) {
|
|
8
|
+
return ability.can(Action.Read, User);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
ForbiddenException,
|
|
5
|
+
Injectable,
|
|
6
|
+
InternalServerErrorException,
|
|
7
|
+
Logger,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
import { Request } from 'express';
|
|
10
|
+
import { getAction } from "@nestjsx/crud";
|
|
11
|
+
import { isEqual } from 'lodash';
|
|
12
|
+
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class UsersEditingGuard implements CanActivate {
|
|
15
|
+
private readonly logger = new Logger(UsersEditingGuard.name);
|
|
16
|
+
|
|
17
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
18
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
19
|
+
const user = request.user;
|
|
20
|
+
|
|
21
|
+
if (!user) {
|
|
22
|
+
this.logger.warn(`User not found in users editing guard`);
|
|
23
|
+
throw new InternalServerErrorException(`User not found in users editing guard`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const editingUserId = request.params['id'];
|
|
27
|
+
switch (getAction(context.getHandler())) {
|
|
28
|
+
case 'Update-One':
|
|
29
|
+
if (editingUserId === user.id) {
|
|
30
|
+
const newRole = request.body['role'];
|
|
31
|
+
if (newRole !== user.role && newRole !== undefined) {
|
|
32
|
+
this.logger.warn(`User can't change his role`);
|
|
33
|
+
throw new ForbiddenException(`User can't change his role`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const newEmail = request.body['email'];
|
|
37
|
+
if (newEmail !== user.email && newEmail !== undefined) {
|
|
38
|
+
this.logger.warn(`User can't change his email`);
|
|
39
|
+
throw new ForbiddenException(`User can't change his email`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const newPermissions = request.body['permissions'];
|
|
43
|
+
if (!isEqual(newPermissions, user.permissions) && newPermissions !== undefined) {
|
|
44
|
+
this.logger.warn(`User can't change his permissions`);
|
|
45
|
+
throw new ForbiddenException(`User can't change his permissions`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
case 'Delete-One':
|
|
51
|
+
if (editingUserId === user.id) {
|
|
52
|
+
this.logger.warn(`User can't delete himself`);
|
|
53
|
+
throw new ForbiddenException(`User can't delete himself`);
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
|
|
4
|
+
export type TUsersConfig = {
|
|
5
|
+
saltRounds: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class UsersConfigService {
|
|
10
|
+
constructor(private configService: ConfigService) {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get config(): TUsersConfig {
|
|
14
|
+
const saltRounds = Number.parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS', ''), 10);
|
|
15
|
+
|
|
16
|
+
if (!Number.isInteger(saltRounds)) {
|
|
17
|
+
throw new Error('BCRYPT_SALT_ROUNDS is not defined, set it as integer');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
saltRounds,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VIEW_USERS = 'view_users';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Controller, UseFilters, UseGuards } from '@nestjs/common';
|
|
2
|
+
import { UsersService } from './users.service';
|
|
3
|
+
import { Crud } from '@nestjsx/crud';
|
|
4
|
+
import { User } from './entities/user.entity';
|
|
5
|
+
import { CheckPolicies, ManageAllPolicy } from '../casl';
|
|
6
|
+
import { UserCreateDto } from './dto/user-create.dto';
|
|
7
|
+
import { ApiExtraModels, ApiTags } from '@nestjs/swagger';
|
|
8
|
+
import { UserUpdateDto } from "./dto/user-update.dto";
|
|
9
|
+
import { HashPasswordInterceptor } from "./hash-password.interceptor";
|
|
10
|
+
import { PermissionDto } from "./dto/permission.dto";
|
|
11
|
+
import { UsersEditingGuard } from "./users-editing.guard";
|
|
12
|
+
import { ViewUsersPolicy } from "./policies/view-users.policy";
|
|
13
|
+
import { Tools } from "@boarteam/boar-pack-common-backend";
|
|
14
|
+
|
|
15
|
+
@Crud({
|
|
16
|
+
model: {
|
|
17
|
+
type: User,
|
|
18
|
+
},
|
|
19
|
+
params: {
|
|
20
|
+
id: {
|
|
21
|
+
field: 'id',
|
|
22
|
+
type: 'uuid',
|
|
23
|
+
primary: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
query: {
|
|
27
|
+
alwaysPaginate: true,
|
|
28
|
+
exclude: ['pass'],
|
|
29
|
+
},
|
|
30
|
+
routes: {
|
|
31
|
+
only: ['getManyBase', 'getOneBase', 'createOneBase', 'updateOneBase', 'deleteOneBase'],
|
|
32
|
+
getManyBase: {
|
|
33
|
+
decorators: [
|
|
34
|
+
CheckPolicies(new ViewUsersPolicy()),
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
getOneBase: {
|
|
38
|
+
decorators: [
|
|
39
|
+
CheckPolicies(new ViewUsersPolicy()),
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
createOneBase: {
|
|
43
|
+
interceptors: [
|
|
44
|
+
HashPasswordInterceptor,
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
updateOneBase: {
|
|
48
|
+
interceptors: [
|
|
49
|
+
HashPasswordInterceptor,
|
|
50
|
+
],
|
|
51
|
+
decorators: [
|
|
52
|
+
UseGuards(UsersEditingGuard),
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
deleteOneBase: {
|
|
56
|
+
decorators: [
|
|
57
|
+
UseGuards(UsersEditingGuard),
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
dto: {
|
|
62
|
+
create: UserCreateDto,
|
|
63
|
+
update: UserUpdateDto,
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
@CheckPolicies(new ManageAllPolicy())
|
|
67
|
+
@UseFilters(Tools.TypeOrmExceptionFilter)
|
|
68
|
+
@ApiTags('Users')
|
|
69
|
+
@ApiExtraModels(PermissionDto)
|
|
70
|
+
@Controller('users')
|
|
71
|
+
export class UsersController {
|
|
72
|
+
constructor(private readonly service: UsersService) {}
|
|
73
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { DynamicModule, Logger, Module, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { UsersService } from './users.service';
|
|
3
|
+
import { UsersController } from './users.controller';
|
|
4
|
+
import { getDataSourceToken, TypeOrmModule } from '@nestjs/typeorm';
|
|
5
|
+
import { EMAIL_UNIQUE_CONSTRAINT, Roles, User } from './entities/user.entity';
|
|
6
|
+
import { MeController } from './me.controller';
|
|
7
|
+
import { Action, CaslAbilityFactory, CaslModule } from '../casl';
|
|
8
|
+
import { UsersConfigService } from "./users.config";
|
|
9
|
+
import BcryptService from "./bcrypt.service";
|
|
10
|
+
import { ConfigModule } from "@nestjs/config";
|
|
11
|
+
import { Tools } from "@boarteam/boar-pack-common-backend";
|
|
12
|
+
import { VIEW_USERS } from "./users.constants";
|
|
13
|
+
import { DataSource } from "typeorm";
|
|
14
|
+
|
|
15
|
+
@Module({})
|
|
16
|
+
export class UsersModule implements OnModuleInit {
|
|
17
|
+
static register(config: {
|
|
18
|
+
withControllers?: boolean;
|
|
19
|
+
dataSourceName?: string;
|
|
20
|
+
} = { withControllers: true }): DynamicModule {
|
|
21
|
+
const dynamicModule: DynamicModule = {
|
|
22
|
+
module: UsersModule,
|
|
23
|
+
imports: [
|
|
24
|
+
ConfigModule,
|
|
25
|
+
TypeOrmModule.forFeature([User], config.dataSourceName),
|
|
26
|
+
CaslModule.forFeature(),
|
|
27
|
+
],
|
|
28
|
+
providers: [
|
|
29
|
+
{
|
|
30
|
+
provide: UsersService,
|
|
31
|
+
inject: [getDataSourceToken(config.dataSourceName)],
|
|
32
|
+
useFactory: (dataSource: DataSource) => {
|
|
33
|
+
return new UsersService(dataSource.getRepository(User));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
UsersConfigService,
|
|
37
|
+
BcryptService,
|
|
38
|
+
],
|
|
39
|
+
exports: [UsersService, BcryptService],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (config.withControllers) {
|
|
43
|
+
dynamicModule.controllers = [UsersController, MeController];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return dynamicModule;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private readonly logger = new Logger(UsersModule.name);
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly usersService: UsersService,
|
|
53
|
+
private readonly bcryptService: BcryptService,
|
|
54
|
+
) {
|
|
55
|
+
Tools.TypeOrmExceptionFilter.setUniqueConstraintMessage(EMAIL_UNIQUE_CONSTRAINT, 'User with this email already exists');
|
|
56
|
+
CaslAbilityFactory.addPermissionToAction({
|
|
57
|
+
permission: VIEW_USERS,
|
|
58
|
+
action: Action.Read,
|
|
59
|
+
subject: User,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async onModuleInit() {
|
|
64
|
+
const usersCount = await this.usersService.count();
|
|
65
|
+
if (!usersCount) {
|
|
66
|
+
this.logger.log('Creating default admin user');
|
|
67
|
+
await this.usersService.create({
|
|
68
|
+
name: 'Admin',
|
|
69
|
+
email: 'admin@admirals.com',
|
|
70
|
+
role: Roles.ADMIN,
|
|
71
|
+
pass: await this.bcryptService.hashPassword('pass'),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
|
|
3
|
+
import { User } from './entities/user.entity';
|
|
4
|
+
import { Repository } from 'typeorm';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class UsersService extends TypeOrmCrudService<User> {
|
|
8
|
+
constructor(
|
|
9
|
+
readonly repo: Repository<User>,
|
|
10
|
+
) {
|
|
11
|
+
super(repo);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
findByEmail(email: string): Promise<User | null> {
|
|
15
|
+
return this.repo.findOne({ where: { email: email.toLowerCase() } });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async create(data: Partial<User>): Promise<User> {
|
|
19
|
+
const user = this.repo.create(data);
|
|
20
|
+
await this.repo.save(user);
|
|
21
|
+
return user;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
shape: sequence_diagram
|
|
2
|
+
|
|
3
|
+
client: Client
|
|
4
|
+
server: Server
|
|
5
|
+
|
|
6
|
+
client -> server: Connect
|
|
7
|
+
server -> wsAuthService: Handle Connection (Sync)
|
|
8
|
+
wsAuthService -> usersService: Check user from DB (Async)
|
|
9
|
+
client -> server: Subscribe
|
|
10
|
+
server.WS Auth Guard
|
|
11
|
+
usersService -> wsAuthService: OK
|
|
12
|
+
wsAuthService -> server: OK
|
|
13
|
+
server -> client: message 1
|
|
14
|
+
server -> client: message 2
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
import { IncomingMessage } from "http";
|
|
4
|
+
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway } from "@nestjs/websockets";
|
|
5
|
+
import { AuthSocket, WsAuthService } from "./ws-auth.service";
|
|
6
|
+
|
|
7
|
+
@WebSocketGateway({
|
|
8
|
+
path: '/ws',
|
|
9
|
+
})
|
|
10
|
+
export class WsAuthGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|
11
|
+
private logger = new Logger(WsAuthGateway.name);
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly wsAuthService: WsAuthService,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
public handleConnection(socket: AuthSocket, req: IncomingMessage) {
|
|
18
|
+
this.logger.debug(`Client connected`);
|
|
19
|
+
this.wsAuthService.handleConnection(socket, req);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public handleDisconnect(client: WebSocket) {
|
|
23
|
+
this.wsAuthService.handleDisconnect(client);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {CanActivate, ExecutionContext, Injectable, Logger, UnauthorizedException} from '@nestjs/common';
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
import { WsAuthService } from "./ws-auth.service";
|
|
4
|
+
import {
|
|
5
|
+
WsErrorCodes
|
|
6
|
+
} from "@boarteam/boar-pack-common-backend/src/modules/websockets/websockets.clients";
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class WsAuthGuard implements CanActivate {
|
|
10
|
+
private readonly logger = new Logger(WsAuthGuard.name);
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly wsAuthService: WsAuthService,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
17
|
+
const client = context.switchToWs().getClient<WebSocket>();
|
|
18
|
+
const user = await this.wsAuthService.finishInitialization(client);
|
|
19
|
+
|
|
20
|
+
if (!user) {
|
|
21
|
+
this.logger.warn(`Unauthorized connection by websocket`);
|
|
22
|
+
client.close(WsErrorCodes.Unauthorized, 'You have been logged out, please login again');
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return !!user;
|
|
27
|
+
}
|
|
28
|
+
}
|