feat: admin user management (create/search/role) and cluster resource monitoring

- Add POST /users endpoint for admin to create users with hashed passwords
- Add GET /users?search= with ILike search on email/firstName/lastName
- Add PATCH /users/:id/role for role assignment (user/admin)
- Return appCount per user in the users list
- Add GET /clusters/:id/resources for node, CPU, memory, pod monitoring
- Parse K8s node capacity/allocatable with CPU millicores and memory MiB helpers
- Frontend: admin users page with search bar, create form, role dropdown, app count
- Frontend: cluster resource panel with nodes table, CPU/memory bars, summary cards
This commit is contained in:
keyhan
2026-04-05 17:48:15 +03:30
parent 2621dc0cc6
commit e97af36740
8 changed files with 564 additions and 69 deletions
+50 -4
View File
@@ -1,19 +1,49 @@
import {
Controller,
Get,
Post,
Patch,
Param,
Body,
Query,
UseGuards,
Request,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsEnum } from 'class-validator';
import { UsersService } from './users.service';
import { RolesGuard } from '../common/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';
import { UserRole } from '../common/enums';
class AdminCreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
@MaxLength(64)
password: string;
@IsString()
@MinLength(1)
firstName: string;
@IsString()
@MinLength(1)
lastName: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
class UpdateRoleDto {
@IsEnum(UserRole)
role: UserRole;
}
@ApiTags('Users')
@ApiBearerAuth()
@Controller('users')
@@ -34,9 +64,25 @@ export class UsersController {
@Get()
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'List all users (Admin only)' })
async findAll() {
return this.usersService.findAll();
@ApiOperation({ summary: 'List all users with optional search (Admin only)' })
@ApiQuery({ name: 'search', required: false, description: 'Search by name or email' })
async findAll(@Query('search') search?: string) {
return this.usersService.findAll(search);
}
@Post()
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Create a new user (Admin only)' })
async adminCreate(@Body() dto: AdminCreateUserDto) {
return this.usersService.adminCreate(dto);
}
@Patch(':id/role')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Update user role (Admin only)' })
async updateRole(@Param('id') id: string, @Body() dto: UpdateRoleDto) {
await this.usersService.updateRole(id, dto.role);
return { message: 'Role updated' };
}
@Patch(':id/deactivate')
+61 -4
View File
@@ -1,6 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Repository, ILike } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './entities/user.entity';
import { UserRole } from '../common/enums';
@@ -19,6 +20,30 @@ export class UsersService {
return this.usersRepository.save(saved);
}
/**
* Admin create user — hashes password, checks duplicate email
*/
async adminCreate(data: {
email: string;
password: string;
firstName: string;
lastName: string;
role?: UserRole;
}): Promise<Omit<User, 'password'>> {
const existing = await this.findByEmail(data.email);
if (existing) {
throw new ConflictException('Email already registered');
}
const hashedPassword = await bcrypt.hash(data.password, 12);
const user = await this.create({
...data,
password: hashedPassword,
role: data.role || UserRole.USER,
});
const { password, ...result } = user;
return result as Omit<User, 'password'>;
}
async findByEmail(email: string): Promise<User | null> {
return this.usersRepository.findOne({ where: { email } });
}
@@ -27,10 +52,33 @@ export class UsersService {
return this.usersRepository.findOne({ where: { id } });
}
async findAll(): Promise<User[]> {
return this.usersRepository.find({
async findAll(search?: string): Promise<any[]> {
const where = search
? [
{ email: ILike(`%${search}%`) },
{ firstName: ILike(`%${search}%`) },
{ lastName: ILike(`%${search}%`) },
]
: undefined;
const users = await this.usersRepository.find({
where,
select: ['id', 'email', 'firstName', 'lastName', 'role', 'isActive', 'namespace', 'createdAt'],
relations: ['applications'],
order: { createdAt: 'DESC' },
});
return users.map((u) => ({
id: u.id,
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
role: u.role,
isActive: u.isActive,
namespace: u.namespace,
createdAt: u.createdAt,
appCount: u.applications?.length || 0,
}));
}
async update(id: string, data: Partial<User>): Promise<User> {
@@ -42,6 +90,15 @@ export class UsersService {
return this.usersRepository.save(user);
}
async updateRole(id: string, role: UserRole): Promise<void> {
const user = await this.findById(id);
if (!user) {
throw new NotFoundException('User not found');
}
user.role = role;
await this.usersRepository.save(user);
}
async deactivate(id: string): Promise<void> {
await this.usersRepository.update(id, { isActive: false });
}