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:
@@ -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')
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user