forked from Shared/esg
initial commit
This commit is contained in:
92
src/users/dto/create-user.dto.ts
Normal file
92
src/users/dto/create-user.dto.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEmail,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { ClientType } from '../../common/enums/client-type.enum';
|
||||
import { Role } from '../../common/enums/role.enum';
|
||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
fullName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mobile?: string;
|
||||
|
||||
@ApiProperty({ minLength: 8 })
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password!: string;
|
||||
|
||||
@ApiProperty({ enum: Role, default: Role.USER })
|
||||
@IsEnum(Role)
|
||||
role!: Role;
|
||||
|
||||
@ApiProperty({ enum: ClientType, default: ClientType.EXTERNAL })
|
||||
@IsEnum(ClientType)
|
||||
clientType!: ClientType;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
appName?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
clientName?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
example: [InquiryType.PERSON, InquiryType.POSTAL_CODE],
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsString({ each: true })
|
||||
allowedInquiries?: string[];
|
||||
|
||||
@ApiPropertyOptional({ default: 60 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
requestLimitPerMinute?: number;
|
||||
|
||||
@ApiPropertyOptional({ default: 10000 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
requestLimitPerDay?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
10
src/users/dto/reset-password.dto.ts
Normal file
10
src/users/dto/reset-password.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class ResetPasswordDto {
|
||||
@ApiProperty({ minLength: 8 })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(8)
|
||||
newPassword!: string;
|
||||
}
|
||||
79
src/users/dto/update-user.dto.ts
Normal file
79
src/users/dto/update-user.dto.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEmail,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { ClientType } from '../../common/enums/client-type.enum';
|
||||
import { Role } from '../../common/enums/role.enum';
|
||||
|
||||
export class UpdateUserDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
fullName?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mobile?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: Role })
|
||||
@IsOptional()
|
||||
@IsEnum(Role)
|
||||
role?: Role;
|
||||
|
||||
@ApiPropertyOptional({ enum: ClientType })
|
||||
@IsOptional()
|
||||
@IsEnum(ClientType)
|
||||
clientType?: ClientType;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
appName?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
clientName?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsString({ each: true })
|
||||
allowedInquiries?: string[];
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
requestLimitPerMinute?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
requestLimitPerDay?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
62
src/users/dto/user-response.dto.ts
Normal file
62
src/users/dto/user-response.dto.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ClientType } from '../../common/enums/client-type.enum';
|
||||
import { Role } from '../../common/enums/role.enum';
|
||||
|
||||
export class UserResponseDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
fullName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
username!: string;
|
||||
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
mobile?: string;
|
||||
|
||||
@ApiProperty({ enum: Role })
|
||||
role!: Role;
|
||||
|
||||
@ApiProperty({ enum: ClientType })
|
||||
clientType!: ClientType;
|
||||
|
||||
@ApiProperty()
|
||||
appName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
clientName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
isActive!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
isBlocked!: boolean;
|
||||
|
||||
@ApiProperty({ type: [String] })
|
||||
allowedInquiries!: string[];
|
||||
|
||||
@ApiProperty()
|
||||
requestLimitPerMinute!: number;
|
||||
|
||||
@ApiProperty()
|
||||
requestLimitPerDay!: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalRequests!: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
lastLoginAt?: Date;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
passwordChangedAt?: Date;
|
||||
|
||||
@ApiProperty()
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
34
src/users/mappers/user.mapper.ts
Normal file
34
src/users/mappers/user.mapper.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { UserDocument } from '../schemas/user.schema';
|
||||
import { ProfileDto } from '../../auth/dto/profile.dto';
|
||||
import { UserResponseDto } from '../dto/user-response.dto';
|
||||
|
||||
/** Maps Mongoose user documents to API DTOs (never exposes password/hash fields). */
|
||||
export class UserMapper {
|
||||
static toProfileDto(user: UserDocument): ProfileDto {
|
||||
return {
|
||||
id: user._id.toString(),
|
||||
fullName: user.fullName,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
mobile: user.mobile,
|
||||
role: user.role,
|
||||
clientType: user.clientType,
|
||||
appName: user.appName,
|
||||
clientName: user.clientName,
|
||||
isActive: user.isActive,
|
||||
isBlocked: user.isBlocked,
|
||||
allowedInquiries: user.allowedInquiries,
|
||||
requestLimitPerMinute: user.requestLimitPerMinute,
|
||||
requestLimitPerDay: user.requestLimitPerDay,
|
||||
totalRequests: user.totalRequests,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
passwordChangedAt: user.passwordChangedAt,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
static toResponseDto(user: UserDocument): UserResponseDto {
|
||||
return UserMapper.toProfileDto(user);
|
||||
}
|
||||
}
|
||||
85
src/users/schemas/user.schema.ts
Normal file
85
src/users/schemas/user.schema.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { HydratedDocument, Types } from 'mongoose';
|
||||
import { ClientType } from '../../common/enums/client-type.enum';
|
||||
import { Role } from '../../common/enums/role.enum';
|
||||
|
||||
export type UserDocument = HydratedDocument<User>;
|
||||
|
||||
/**
|
||||
* Unified user model — internal admins and external API clients share one collection.
|
||||
* Designed for future extensions: API keys, HMAC, IP whitelist, permissions, multi-tenant.
|
||||
*/
|
||||
@Schema({ timestamps: true, collection: 'users' })
|
||||
export class User {
|
||||
@Prop({ required: true, trim: true })
|
||||
fullName!: string;
|
||||
|
||||
@Prop({ required: true, unique: true, lowercase: true, trim: true, index: true })
|
||||
username!: string;
|
||||
|
||||
@Prop({ required: true, unique: true, lowercase: true, trim: true, index: true })
|
||||
email!: string;
|
||||
|
||||
@Prop({ trim: true })
|
||||
mobile?: string;
|
||||
|
||||
@Prop({ required: true, select: false })
|
||||
password!: string;
|
||||
|
||||
@Prop({ required: true, enum: Role, default: Role.USER, index: true })
|
||||
role!: Role;
|
||||
|
||||
@Prop({ required: true, enum: ClientType, default: ClientType.EXTERNAL, index: true })
|
||||
clientType!: ClientType;
|
||||
|
||||
@Prop({ trim: true, default: '' })
|
||||
appName!: string;
|
||||
|
||||
@Prop({ trim: true, default: '' })
|
||||
clientName!: string;
|
||||
|
||||
@Prop({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@Prop({ default: false })
|
||||
isBlocked!: boolean;
|
||||
|
||||
/** Future: machine-to-machine auth without JWT. */
|
||||
@Prop({ select: false })
|
||||
apiKey?: string;
|
||||
|
||||
@Prop({ type: [String], default: [] })
|
||||
allowedInquiries!: string[];
|
||||
|
||||
@Prop({ default: 60 })
|
||||
requestLimitPerMinute!: number;
|
||||
|
||||
@Prop({ default: 10000 })
|
||||
requestLimitPerDay!: number;
|
||||
|
||||
@Prop({ default: 0 })
|
||||
totalRequests!: number;
|
||||
|
||||
@Prop()
|
||||
lastLoginAt?: Date;
|
||||
|
||||
@Prop()
|
||||
passwordChangedAt?: Date;
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: 'User' })
|
||||
createdBy?: Types.ObjectId;
|
||||
|
||||
@Prop({ select: false })
|
||||
refreshToken?: string;
|
||||
|
||||
@Prop({ type: Object, default: {} })
|
||||
metadata!: Record<string, unknown>;
|
||||
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
||||
export const UserSchema = SchemaFactory.createForClass(User);
|
||||
|
||||
UserSchema.index({ role: 1, isActive: 1 });
|
||||
UserSchema.index({ clientType: 1, appName: 1 });
|
||||
128
src/users/users.controller.ts
Normal file
128
src/users/users.controller.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { ADMIN_ROLES, Role } from '../common/enums/role.enum';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UserResponseDto } from './dto/user-response.dto';
|
||||
import { UsersService } from './users.service';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { AuthenticatedUser } from '../auth/interfaces/authenticated-user.interface';
|
||||
import { UserMapper } from './mappers/user.mapper';
|
||||
|
||||
@ApiTags('Users')
|
||||
@ApiBearerAuth()
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(...ADMIN_ROLES)
|
||||
@ApiOperation({ summary: 'Create a new user (ADMIN / SUPER_ADMIN only)' })
|
||||
@ApiResponse({ status: 201, type: UserResponseDto })
|
||||
async create(
|
||||
@Body() dto: CreateUserDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
@Req() req: Request,
|
||||
): Promise<UserResponseDto> {
|
||||
return this.usersService.create(dto, actor, {
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent'],
|
||||
});
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles(...ADMIN_ROLES)
|
||||
@ApiOperation({ summary: 'List all users' })
|
||||
@ApiResponse({ status: 200, type: [UserResponseDto] })
|
||||
async findAll(): Promise<UserResponseDto[]> {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles(...ADMIN_ROLES)
|
||||
@ApiOperation({ summary: 'Get user by ID' })
|
||||
@ApiResponse({ status: 200, type: UserResponseDto })
|
||||
async findOne(@Param('id') id: string): Promise<UserResponseDto> {
|
||||
const user = await this.usersService.findById(id);
|
||||
return UserMapper.toResponseDto(user);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles(...ADMIN_ROLES)
|
||||
@ApiOperation({ summary: 'Update user' })
|
||||
@ApiResponse({ status: 200, type: UserResponseDto })
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateUserDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
): Promise<UserResponseDto> {
|
||||
return this.usersService.update(id, dto, actor);
|
||||
}
|
||||
|
||||
@Patch(':id/block')
|
||||
@Roles(...ADMIN_ROLES)
|
||||
@ApiOperation({ summary: 'Block user' })
|
||||
@ApiResponse({ status: 200, type: UserResponseDto })
|
||||
async block(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
@Req() req: Request,
|
||||
): Promise<UserResponseDto> {
|
||||
return this.usersService.block(id, actor, {
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent'],
|
||||
});
|
||||
}
|
||||
|
||||
@Patch(':id/unblock')
|
||||
@Roles(...ADMIN_ROLES)
|
||||
@ApiOperation({ summary: 'Unblock user' })
|
||||
@ApiResponse({ status: 200, type: UserResponseDto })
|
||||
async unblock(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
@Req() req: Request,
|
||||
): Promise<UserResponseDto> {
|
||||
return this.usersService.unblock(id, actor, {
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent'],
|
||||
});
|
||||
}
|
||||
|
||||
@Patch(':id/reset-password')
|
||||
@Roles(...ADMIN_ROLES)
|
||||
@ApiOperation({ summary: 'Reset user password' })
|
||||
@ApiResponse({ status: 200, description: 'Password reset successfully' })
|
||||
async resetPassword(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: ResetPasswordDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
@Req() req: Request,
|
||||
): Promise<{ message: string }> {
|
||||
await this.usersService.resetPassword(id, dto, actor, {
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent'],
|
||||
});
|
||||
return { message: 'Password reset successfully' };
|
||||
}
|
||||
}
|
||||
17
src/users/users.module.ts
Normal file
17
src/users/users.module.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { User, UserSchema } from './schemas/user.schema';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
|
||||
forwardRef(() => AuthModule),
|
||||
],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService, MongooseModule],
|
||||
})
|
||||
export class UsersModule {}
|
||||
251
src/users/users.service.ts
Normal file
251
src/users/users.service.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model, Types } from 'mongoose';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { AuditEvent } from '../common/enums/audit-event.enum';
|
||||
import { ADMIN_ROLES, Role } from '../common/enums/role.enum';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { PasswordService } from '../auth/services/password.service';
|
||||
import { AuthenticatedUser } from '../auth/interfaces/authenticated-user.interface';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UserMapper } from './mappers/user.mapper';
|
||||
import { User, UserDocument } from './schemas/user.schema';
|
||||
import { UserResponseDto } from './dto/user-response.dto';
|
||||
|
||||
export interface RequestContext {
|
||||
ip?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectModel(User.name) private readonly userModel: Model<UserDocument>,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<UserDocument> {
|
||||
const user = await this.userModel.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async findByUsername(username: string, includePassword = false): Promise<UserDocument | null> {
|
||||
const query = this.userModel.findOne({ username: username.toLowerCase().trim() });
|
||||
if (includePassword) {
|
||||
query.select('+password +refreshToken');
|
||||
}
|
||||
return query.exec();
|
||||
}
|
||||
|
||||
async findAll(): Promise<UserResponseDto[]> {
|
||||
const users = await this.userModel.find().sort({ createdAt: -1 });
|
||||
return users.map((u) => UserMapper.toResponseDto(u));
|
||||
}
|
||||
|
||||
async create(
|
||||
dto: CreateUserDto,
|
||||
actor: AuthenticatedUser,
|
||||
context: RequestContext = {},
|
||||
): Promise<UserResponseDto> {
|
||||
this.assertCanAssignRole(actor.role, dto.role);
|
||||
|
||||
const username = dto.username.toLowerCase().trim();
|
||||
const email = dto.email.toLowerCase().trim();
|
||||
|
||||
const existing = await this.userModel.findOne({
|
||||
$or: [{ username }, { email }],
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException('Username or email already exists');
|
||||
}
|
||||
|
||||
const passwordHash = await this.passwordService.hash(dto.password);
|
||||
const apiKey = this.generateApiKey();
|
||||
|
||||
const user = await this.userModel.create({
|
||||
fullName: dto.fullName,
|
||||
username,
|
||||
email,
|
||||
mobile: dto.mobile,
|
||||
password: passwordHash,
|
||||
role: dto.role,
|
||||
clientType: dto.clientType,
|
||||
appName: dto.appName ?? '',
|
||||
clientName: dto.clientName ?? '',
|
||||
isActive: dto.isActive ?? true,
|
||||
allowedInquiries: dto.allowedInquiries ?? [],
|
||||
requestLimitPerMinute: dto.requestLimitPerMinute ?? 60,
|
||||
requestLimitPerDay: dto.requestLimitPerDay ?? 10000,
|
||||
apiKey,
|
||||
passwordChangedAt: new Date(),
|
||||
createdBy: new Types.ObjectId(actor.id),
|
||||
metadata: dto.metadata ?? {},
|
||||
});
|
||||
|
||||
await this.auditService.log(AuditEvent.USER_CREATED, {
|
||||
actorId: actor.id,
|
||||
userId: user._id.toString(),
|
||||
username: user.username,
|
||||
ip: context.ip,
|
||||
userAgent: context.userAgent,
|
||||
metadata: { role: user.role, clientType: user.clientType },
|
||||
});
|
||||
|
||||
return UserMapper.toResponseDto(user);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateUserDto, actor: AuthenticatedUser): Promise<UserResponseDto> {
|
||||
const user = await this.findById(id);
|
||||
|
||||
if (dto.role !== undefined) {
|
||||
this.assertCanAssignRole(actor.role, dto.role);
|
||||
}
|
||||
|
||||
if (dto.email) {
|
||||
const email = dto.email.toLowerCase().trim();
|
||||
const conflict = await this.userModel.findOne({ email, _id: { $ne: id } });
|
||||
if (conflict) {
|
||||
throw new ConflictException('Email already in use');
|
||||
}
|
||||
user.email = email;
|
||||
}
|
||||
|
||||
Object.assign(user, {
|
||||
...(dto.fullName !== undefined && { fullName: dto.fullName }),
|
||||
...(dto.mobile !== undefined && { mobile: dto.mobile }),
|
||||
...(dto.role !== undefined && { role: dto.role }),
|
||||
...(dto.clientType !== undefined && { clientType: dto.clientType }),
|
||||
...(dto.appName !== undefined && { appName: dto.appName }),
|
||||
...(dto.clientName !== undefined && { clientName: dto.clientName }),
|
||||
...(dto.isActive !== undefined && { isActive: dto.isActive }),
|
||||
...(dto.allowedInquiries !== undefined && { allowedInquiries: dto.allowedInquiries }),
|
||||
...(dto.requestLimitPerMinute !== undefined && {
|
||||
requestLimitPerMinute: dto.requestLimitPerMinute,
|
||||
}),
|
||||
...(dto.requestLimitPerDay !== undefined && { requestLimitPerDay: dto.requestLimitPerDay }),
|
||||
...(dto.metadata !== undefined && { metadata: dto.metadata }),
|
||||
});
|
||||
|
||||
await user.save();
|
||||
return UserMapper.toResponseDto(user);
|
||||
}
|
||||
|
||||
async block(
|
||||
id: string,
|
||||
actor: AuthenticatedUser,
|
||||
context: RequestContext = {},
|
||||
): Promise<UserResponseDto> {
|
||||
const user = await this.findById(id);
|
||||
if (user._id.toString() === actor.id) {
|
||||
throw new BadRequestException('Cannot block your own account');
|
||||
}
|
||||
user.isBlocked = true;
|
||||
user.refreshToken = undefined;
|
||||
await user.save();
|
||||
|
||||
await this.auditService.log(AuditEvent.USER_BLOCKED, {
|
||||
actorId: actor.id,
|
||||
userId: id,
|
||||
username: user.username,
|
||||
ip: context.ip,
|
||||
userAgent: context.userAgent,
|
||||
});
|
||||
|
||||
return UserMapper.toResponseDto(user);
|
||||
}
|
||||
|
||||
async unblock(
|
||||
id: string,
|
||||
actor: AuthenticatedUser,
|
||||
context: RequestContext = {},
|
||||
): Promise<UserResponseDto> {
|
||||
const user = await this.findById(id);
|
||||
user.isBlocked = false;
|
||||
await user.save();
|
||||
|
||||
await this.auditService.log(AuditEvent.USER_UNBLOCKED, {
|
||||
actorId: actor.id,
|
||||
userId: id,
|
||||
username: user.username,
|
||||
ip: context.ip,
|
||||
userAgent: context.userAgent,
|
||||
});
|
||||
|
||||
return UserMapper.toResponseDto(user);
|
||||
}
|
||||
|
||||
async resetPassword(
|
||||
id: string,
|
||||
dto: ResetPasswordDto,
|
||||
actor: AuthenticatedUser,
|
||||
context: RequestContext = {},
|
||||
): Promise<void> {
|
||||
const user = await this.userModel.findById(id).select('+password +refreshToken');
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
user.password = await this.passwordService.hash(dto.newPassword);
|
||||
user.passwordChangedAt = new Date();
|
||||
user.refreshToken = undefined;
|
||||
await user.save();
|
||||
|
||||
await this.auditService.log(AuditEvent.PASSWORD_RESET, {
|
||||
actorId: actor.id,
|
||||
userId: id,
|
||||
username: user.username,
|
||||
ip: context.ip,
|
||||
userAgent: context.userAgent,
|
||||
});
|
||||
}
|
||||
|
||||
async saveRefreshToken(userId: string, refreshToken: string): Promise<void> {
|
||||
const hash = await this.passwordService.hash(refreshToken);
|
||||
await this.userModel.findByIdAndUpdate(userId, { refreshToken: hash });
|
||||
}
|
||||
|
||||
async clearRefreshToken(userId: string): Promise<void> {
|
||||
await this.userModel.findByIdAndUpdate(userId, { $unset: { refreshToken: 1 } });
|
||||
}
|
||||
|
||||
async validateRefreshToken(userId: string, refreshToken: string): Promise<boolean> {
|
||||
const user = await this.userModel.findById(userId).select('+refreshToken');
|
||||
if (!user?.refreshToken) {
|
||||
return false;
|
||||
}
|
||||
return this.passwordService.compare(refreshToken, user.refreshToken);
|
||||
}
|
||||
|
||||
async recordLogin(userId: string): Promise<void> {
|
||||
await this.userModel.findByIdAndUpdate(userId, { lastLoginAt: new Date() });
|
||||
}
|
||||
|
||||
async incrementTotalRequests(userId: string): Promise<void> {
|
||||
await this.userModel.findByIdAndUpdate(userId, { $inc: { totalRequests: 1 } });
|
||||
}
|
||||
|
||||
private assertCanAssignRole(actorRole: Role, targetRole: Role): void {
|
||||
if (targetRole === Role.SUPER_ADMIN && actorRole !== Role.SUPER_ADMIN) {
|
||||
throw new ForbiddenException('Only SUPER_ADMIN can assign SUPER_ADMIN role');
|
||||
}
|
||||
if (!ADMIN_ROLES.includes(actorRole)) {
|
||||
throw new ForbiddenException('Insufficient permissions to manage users');
|
||||
}
|
||||
}
|
||||
|
||||
private generateApiKey(): string {
|
||||
return `esg_${randomBytes(32).toString('hex')}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user