Skip to content

Commit

Permalink
Merge pull request #38 from Honeybrain/add-roles
Browse files Browse the repository at this point in the history
Add roles
  • Loading branch information
valentinbreiz authored Jan 3, 2024
2 parents 5b3d111 + acea27e commit c7d1f55
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 68 deletions.
15 changes: 15 additions & 0 deletions src/_utils/decorators/protect.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { applyDecorators, UseGuards } from '@nestjs/common';
import { RoleEnum } from '../../user/_utils/enums/role.enum';
import { ApiBearerAuth, ApiForbiddenResponse } from '@nestjs/swagger';
import { GrpcAuthGuard } from '../../user/_utils/jwt/grpc-auth.guard';
import { Roles } from './roles.decorator';
import { RolesGuard } from '../../user/_utils/jwt/roles.guard';

export function Protect(...roles: RoleEnum[]) {
return applyDecorators(
ApiBearerAuth(),
Roles(...roles),
UseGuards(GrpcAuthGuard, RolesGuard),
ApiForbiddenResponse({ description: 'Unauthorized' }),
);
}
5 changes: 5 additions & 0 deletions src/_utils/decorators/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { RoleEnum } from '../../user/_utils/enums/role.enum';

export const ROLES_KEY = 'role';
export const Roles = (...roles: RoleEnum[]) => SetMetadata(ROLES_KEY, roles);
10 changes: 5 additions & 5 deletions src/dashboard/dashboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export class DashboardService {
subject$.next(dashboard);
});

const containers$ = this.containersService.streamContainers().subscribe((containerReplyDto) => {
dashboard.containers = containerReplyDto.containers;
subject$.next(dashboard);
});
// const containers$ = this.containersService.streamContainers().subscribe((containerReplyDto) => {
// dashboard.containers = containerReplyDto.containers;
// subject$.next(dashboard);
// });

const blacklist$ = this.blacklistService.getBlackList$(call).subscribe((blacklistReplyDto) => {
dashboard.ips = blacklistReplyDto.ips;
Expand All @@ -36,7 +36,7 @@ export class DashboardService {

call.on('cancelled', () => {
logs$.unsubscribe();
containers$.unsubscribe();
// containers$.unsubscribe();
blacklist$.unsubscribe();
});

Expand Down
7 changes: 4 additions & 3 deletions src/user/_utils/dto/request/change-rights-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { IsBoolean, IsString } from 'class-validator';
import { IsEnum, IsString } from 'class-validator';
import { RoleEnum } from '../../enums/role.enum';

export class ChangeRightsRequestDto {
@IsString()
email: string;

@IsBoolean()
admin: boolean;
@IsEnum(RoleEnum, { each: true })
roles: RoleEnum[];
}
7 changes: 4 additions & 3 deletions src/user/_utils/dto/request/invite-user-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { IsBoolean, IsEmail } from 'class-validator';
import { IsEmail, IsEnum } from 'class-validator';
import { RoleEnum } from '../../enums/role.enum';

export class InviteUserRequestDto {
@IsEmail()
email: string;

@IsBoolean()
admin: boolean;
@IsEnum(RoleEnum, { each: true })
roles: RoleEnum[];
}
9 changes: 9 additions & 0 deletions src/user/_utils/dto/response/get-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { RoleEnum } from '../../enums/role.enum';

export class GetUserDto {
id: string;
email: string;
roles: RoleEnum[];
activated: boolean;
lan: string;
}
6 changes: 2 additions & 4 deletions src/user/_utils/dto/response/get-users-list.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { IsArray, IsString } from 'class-validator';
import { GetUserDto } from './get-user.dto';

export class GetUsersListDto {
@IsArray()
@IsString({ each: true })
users: string[];
users: GetUserDto[];
}
4 changes: 4 additions & 0 deletions src/user/_utils/dto/response/user-response.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { GetUserDto } from './get-user.dto';

export class UserResponseDto {
@ApiProperty()
message: string;

@ApiProperty()
token: string;

@ApiProperty()
user: GetUserDto;
}
9 changes: 9 additions & 0 deletions src/user/_utils/enums/role.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum RoleEnum {
ADMIN,
CAN_INVITE,
CAN_MANAGE_IP,
CAN_READ_IP,
CAN_READ_LOGS,
CAN_MANAGE_CONFIGURATION,
CAN_READ_SERVICES,
}
2 changes: 1 addition & 1 deletion src/user/_utils/jwt/grpc-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class GrpcAuthGuard implements CanActivate {
const metadata = context.getArgByIndex(1);
if (!metadata) throw new RpcException({ code: Status.UNAUTHENTICATED, message: 'UNAUTHENTICATED' });

const token = metadata.get('Authorization')[0].split(' ')[1];
const token = metadata.get('Authorization')[0]?.split(' ')?.[1];
if (!token) throw new RpcException({ code: Status.UNAUTHENTICATED, message: 'UNAUTHENTICATED' });

const payload: JwtPayload = await this.jwtService.verifyAsync(token).catch(() => {
Expand Down
24 changes: 24 additions & 0 deletions src/user/_utils/jwt/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { ROLES_KEY } from '../../../_utils/decorators/roles.decorator';
import { RoleEnum } from '../enums/role.enum';
import { User } from '../../user.schema';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<RoleEnum[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
const { user }: { user: User } = context.switchToRpc().getContext();

if (!requiredRoles.length) return true;
if (user.roles.includes(RoleEnum.ADMIN)) return true;

return requiredRoles.some((role) => user.roles.includes(role));
}
}
31 changes: 26 additions & 5 deletions src/user/_utils/user.proto
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ message SignInSignUpRequest {
message UserResponse {
string message = 1;
string token = 2;
UserDto user = 3;
}

message EmptyRequest {}
Expand All @@ -27,18 +28,36 @@ message EmailRequest {
string email = 1;
}

enum RoleEnum {
ADMIN = 0;
CAN_INVITE = 1;
CAN_MANAGE_IP = 2;
CAN_READ_IP = 3;
CAN_READ_LOGS = 4;
CAN_MANAGE_CONFIGURATION = 5;
CAN_READ_SERVICES = 6;
}

message InviteUserRequest {
string email = 1;
bool admin = 2;
repeated RoleEnum roles = 2;
}

message ChangeRightsRequest {
string email = 1;
bool admin = 2;
repeated RoleEnum roles = 2;
}

message UserDto {
string id = 1;
string email = 2;
repeated RoleEnum roles = 3;
bool activated = 4;
string language = 5;
}

message GetUsersResponse {
repeated string users = 1;
repeated UserDto users = 1;
}

message ActivateUserRequest {
Expand All @@ -48,6 +67,7 @@ message ActivateUserRequest {

message ActivateUserResponse {
string token = 1;
UserDto user = 2;
}

message ChangeLanguageRequest {
Expand All @@ -64,11 +84,12 @@ message UserRequest {}
service User {
rpc SignIn(SignInSignUpRequest) returns (UserResponse);
rpc SignUp(SignInSignUpRequest) returns (UserResponse);
rpc GetMe(EmptyRequest) returns (UserDto);
rpc ResetPassword(PasswordRequest) returns (EmptyResponse);
rpc ChangeEmail(EmailRequest) returns (EmptyResponse);
rpc InviteUser(InviteUserRequest) returns (EmptyResponse);
rpc InviteUser(InviteUserRequest) returns (UserDto);
rpc ActivateUser(ActivateUserRequest) returns (ActivateUserResponse);
rpc ChangeRights(ChangeRightsRequest) returns (EmptyResponse);
rpc ChangeRights(ChangeRightsRequest) returns (UserDto);
rpc GetUsers(EmptyRequest) returns (GetUsersResponse);
rpc DeleteUser(EmailRequest) returns (EmptyResponse);
rpc ChangeLanguage(ChangeLanguageRequest) returns (EmptyResponse);
Expand Down
22 changes: 17 additions & 5 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { GrpcAuthGuard } from './_utils/jwt/grpc-auth.guard';
import { MetadataWithUser } from './_utils/interface/metadata-with-user.interface';
import { GetUsersListDto } from './_utils/dto/response/get-users-list.dto';
import { ActivateResponseDto } from './_utils/dto/response/activate-response.dto';
import { Protect } from '../_utils/decorators/protect.decorator';
import { RoleEnum } from './_utils/enums/role.enum';
import { GetUserDto } from './_utils/dto/response/get-user.dto';
import { UserRequestDto } from './_utils/dto/request/user-request.dto';
import { UserLanguageResponseDto } from './_utils/dto/response/user-language-response.dto';

Expand All @@ -32,30 +35,38 @@ export class UserController {
return this.userService.signIn(signInSignUpDto);
}

@UseGuards(GrpcAuthGuard)
@Protect()
@GrpcMethod('User', 'GetMe')
getMe(_: unknown, meta: MetadataWithUser) {
return this.userService.getMe(meta.user);
}

@Protect()
@GrpcMethod('User', 'ChangeEmail')
changeEmail(data: EmailRequestDto, meta: MetadataWithUser): Promise<GetEmptyDto> {
return this.userService.changeEmail(data.email, meta.user);
}

@UseGuards(GrpcAuthGuard)
@Protect()
@GrpcMethod('User', 'ResetPassword')
resetPassword(data: PasswordRequestDto, meta: MetadataWithUser): Promise<GetEmptyDto> {
return this.userService.resetPassword(data.password, meta.user);
}

@Protect(RoleEnum.CAN_INVITE)
@GrpcMethod('User', 'InviteUser')
inviteUser(data: InviteUserRequestDto): Promise<GetEmptyDto> {
return this.userService.inviteUser(data.email, data.admin);
inviteUser(data: InviteUserRequestDto): Promise<GetUserDto> {
return this.userService.inviteUser(data.email, data.roles);
}

@GrpcMethod('User', 'ActivateUser')
activateUser(data: ActivateUserRequestDto): Promise<ActivateResponseDto> {
return this.userService.activateUser(data);
}

@Protect()
@GrpcMethod('User', 'ChangeRights')
changeRights(data: ChangeRightsRequestDto): Promise<GetEmptyDto> {
changeRights(data: ChangeRightsRequestDto): Promise<GetUserDto> {
return this.userService.changeRights(data);
}

Expand All @@ -64,6 +75,7 @@ export class UserController {
return this.userService.findAllUsers();
}

@Protect(RoleEnum.ADMIN)
@GrpcMethod('User', 'DeleteUser')
deleteUser(emailRequestDto: EmailRequestDto): Promise<GetEmptyDto> {
return this.userService.deleteUser(emailRequestDto);
Expand Down
17 changes: 17 additions & 0 deletions src/user/user.mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { UserDocument } from './user.schema';
import { GetUsersListDto } from './_utils/dto/response/get-users-list.dto';
import { GetUserDto } from './_utils/dto/response/get-user.dto';

@Injectable()
export class UserMapper {
toGetUser = (user: UserDocument): GetUserDto => ({
id: user._id.toString(),
email: user.email,
roles: user.roles,
activated: user.activated,
lan: user.lan,
});

toGetUsers = (users: UserDocument[]): GetUsersListDto => ({ users: users.map((user) => this.toGetUser(user)) });
}
3 changes: 2 additions & 1 deletion src/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { UserController } from './user.controller';
import { GrpcAuthGuard } from './_utils/jwt/grpc-auth.guard';
import { InvitationModule } from 'src/invitation/invitation.module';
import { MailsModule } from '../mails/mails.module';
import { UserMapper } from './user.mapper';

@Module({
imports: [InvitationModule, MailsModule],
controllers: [UserController],
providers: [UserService, GrpcAuthGuard],
providers: [UserService, GrpcAuthGuard, UserMapper],
})
export class UserModule {}
16 changes: 6 additions & 10 deletions src/user/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { User, UserDocument } from './user.schema';
import { hashSync } from 'bcrypt';
import { RpcException } from '@nestjs/microservices';
import { status } from '@grpc/grpc-js';
import { RoleEnum } from './_utils/enums/role.enum';

@Injectable()
export class UserRepository {
Expand Down Expand Up @@ -47,9 +48,9 @@ export class UserRepository {
this.model.create({
email: user.email,
password: user.password && hashSync(user.password, 10),
admin: user.admin,
roles: user.roles,
activated: user.activated,
lan: "en",
lan: 'en',
});

updateEmailByUserId = (userId: Types.ObjectId, newEmail: string): Promise<UserDocument> =>
Expand All @@ -70,16 +71,11 @@ export class UserRepository {
.orFail(new RpcException({ code: status.NOT_FOUND, message: 'USER_NOT_FOUND' }))
.exec();

updateRightByUserEmail = (email: string, admin?: boolean): Promise<UserDocument> => {
if (admin === undefined) {
admin = false;
}

return this.model
.findOneAndUpdate({ email }, { admin }, { new: true })
updateRightByUserEmail = (email: string, roles: RoleEnum[]): Promise<UserDocument> =>
this.model
.findOneAndUpdate({ email }, { roles }, { new: true })
.orFail(new RpcException({ code: status.NOT_FOUND, message: 'USER_NOT_FOUND' }))
.exec();
};

updateDeleteByUserEmail = async (email: string) => {
const user = await this.findByEmail(email);
Expand Down
9 changes: 5 additions & 4 deletions src/user/user.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
import { RoleEnum } from './_utils/enums/role.enum';

export type UserDocument = HydratedDocument<User>;

Expand All @@ -11,14 +12,14 @@ export class User {
@Prop({ default: null, type: String })
password: string | null;

@Prop({ required: true })
admin: boolean;
@Prop({ default: [], type: [{ type: Number, enum: RoleEnum }] })
roles: RoleEnum[];

@Prop({ required: true })
activated: boolean;

@Prop({required: true, default: "en"})
lan: string | "en";
@Prop({ required: true, default: 'en' })
lan: string | 'en';
}

export const UserSchema = SchemaFactory.createForClass(User);
Loading

0 comments on commit c7d1f55

Please sign in to comment.