xpeditis2.0/apps/backend/src/application/controllers/admin.controller.ts
David a1e255e816
Some checks failed
CI/CD Pipeline / Discord Notification (Failure) (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Deployment Summary (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Portainer (push) Blocked by required conditions
CI/CD Pipeline / Discord Notification (Success) (push) Blocked by required conditions
CI/CD Pipeline / Backend - Build, Test & Push (push) Failing after 1m20s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Has been cancelled
fix v1.0.0
2025-12-23 11:49:57 +01:00

601 lines
17 KiB
TypeScript

import {
Controller,
Get,
Patch,
Delete,
Param,
Body,
HttpCode,
HttpStatus,
Logger,
UsePipes,
ValidationPipe,
NotFoundException,
BadRequestException,
ParseUUIDPipe,
UseGuards,
Inject,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiNotFoundResponse,
ApiParam,
ApiBearerAuth,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RolesGuard } from '../guards/roles.guard';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
import { Roles } from '../decorators/roles.decorator';
// User imports
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
import { UserMapper } from '../mappers/user.mapper';
import { UpdateUserDto, UserResponseDto, UserListResponseDto } from '../dto/user.dto';
// Organization imports
import {
OrganizationRepository,
ORGANIZATION_REPOSITORY,
} from '@domain/ports/out/organization.repository';
import { OrganizationMapper } from '../mappers/organization.mapper';
import { OrganizationResponseDto, OrganizationListResponseDto } from '../dto/organization.dto';
// CSV Booking imports
import { TypeOrmCsvBookingRepository } from '@infrastructure/persistence/typeorm/repositories/csv-booking.repository';
/**
* Admin Controller
*
* Dedicated controller for admin-only endpoints that provide access to ALL data
* in the database without organization filtering.
*
* All endpoints require ADMIN role.
*/
@ApiTags('Admin')
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@ApiBearerAuth()
export class AdminController {
private readonly logger = new Logger(AdminController.name);
constructor(
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
@Inject(ORGANIZATION_REPOSITORY)
private readonly organizationRepository: OrganizationRepository,
private readonly csvBookingRepository: TypeOrmCsvBookingRepository
) {}
// ==================== USERS ENDPOINTS ====================
/**
* Get ALL users from database (admin only)
*
* Returns all users regardless of status (active/inactive) or organization
*/
@Get('users')
@ApiOperation({
summary: 'Get all users (Admin only)',
description:
'Retrieve ALL users from the database without any filters. Includes active and inactive users from all organizations.',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'All users retrieved successfully',
type: UserListResponseDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - missing or invalid token',
})
@ApiResponse({
status: 403,
description: 'Forbidden - requires admin role',
})
async getAllUsers(@CurrentUser() user: UserPayload): Promise<UserListResponseDto> {
this.logger.log(`[ADMIN: ${user.email}] Fetching ALL users from database`);
let users = await this.userRepository.findAll();
// Security: Non-admin users (MANAGER and below) cannot see ADMIN users
if (user.role !== 'ADMIN') {
users = users.filter(u => u.role !== 'ADMIN');
this.logger.log(`[SECURITY] Non-admin user ${user.email} - filtered out ADMIN users`);
}
const userDtos = UserMapper.toDtoArray(users);
this.logger.log(`[ADMIN] Retrieved ${users.length} users from database`);
return {
users: userDtos,
total: users.length,
page: 1,
pageSize: users.length,
totalPages: 1,
};
}
/**
* Get user by ID (admin only)
*/
@Get('users/:id')
@ApiOperation({
summary: 'Get user by ID (Admin only)',
description: 'Retrieve a specific user by ID',
})
@ApiParam({
name: 'id',
description: 'User ID (UUID)',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'User retrieved successfully',
type: UserResponseDto,
})
@ApiNotFoundResponse({
description: 'User not found',
})
async getUserById(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: UserPayload
): Promise<UserResponseDto> {
this.logger.log(`[ADMIN: ${user.email}] Fetching user: ${id}`);
const foundUser = await this.userRepository.findById(id);
if (!foundUser) {
throw new NotFoundException(`User ${id} not found`);
}
return UserMapper.toDto(foundUser);
}
/**
* Update user (admin only)
*/
@Patch('users/:id')
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@ApiOperation({
summary: 'Update user (Admin only)',
description: 'Update user details (any user, any organization)',
})
@ApiParam({
name: 'id',
description: 'User ID (UUID)',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'User updated successfully',
type: UserResponseDto,
})
@ApiNotFoundResponse({
description: 'User not found',
})
async updateUser(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateUserDto,
@CurrentUser() user: UserPayload
): Promise<UserResponseDto> {
this.logger.log(`[ADMIN: ${user.email}] Updating user: ${id}`);
const foundUser = await this.userRepository.findById(id);
if (!foundUser) {
throw new NotFoundException(`User ${id} not found`);
}
// Security: Prevent users from changing their own role
if (dto.role && id === user.id) {
this.logger.warn(`[SECURITY] User ${user.email} attempted to change their own role`);
throw new BadRequestException('You cannot change your own role');
}
// Apply updates
if (dto.firstName) {
foundUser.updateFirstName(dto.firstName);
}
if (dto.lastName) {
foundUser.updateLastName(dto.lastName);
}
if (dto.role) {
foundUser.updateRole(dto.role);
}
if (dto.isActive !== undefined) {
if (dto.isActive) {
foundUser.activate();
} else {
foundUser.deactivate();
}
}
const updatedUser = await this.userRepository.update(foundUser);
this.logger.log(`[ADMIN] User updated successfully: ${updatedUser.id}`);
return UserMapper.toDto(updatedUser);
}
/**
* Delete user (admin only)
*/
@Delete('users/:id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete user (Admin only)',
description: 'Permanently delete a user from the database',
})
@ApiParam({
name: 'id',
description: 'User ID (UUID)',
})
@ApiResponse({
status: HttpStatus.NO_CONTENT,
description: 'User deleted successfully',
})
@ApiNotFoundResponse({
description: 'User not found',
})
async deleteUser(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: UserPayload
): Promise<void> {
this.logger.log(`[ADMIN: ${user.email}] Deleting user: ${id}`);
const foundUser = await this.userRepository.findById(id);
if (!foundUser) {
throw new NotFoundException(`User ${id} not found`);
}
await this.userRepository.deleteById(id);
this.logger.log(`[ADMIN] User deleted successfully: ${id}`);
}
// ==================== ORGANIZATIONS ENDPOINTS ====================
/**
* Get ALL organizations from database (admin only)
*
* Returns all organizations regardless of status (active/inactive)
*/
@Get('organizations')
@ApiOperation({
summary: 'Get all organizations (Admin only)',
description:
'Retrieve ALL organizations from the database without any filters. Includes active and inactive organizations.',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'All organizations retrieved successfully',
type: OrganizationListResponseDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - missing or invalid token',
})
@ApiResponse({
status: 403,
description: 'Forbidden - requires admin role',
})
async getAllOrganizations(
@CurrentUser() user: UserPayload
): Promise<OrganizationListResponseDto> {
this.logger.log(`[ADMIN: ${user.email}] Fetching ALL organizations from database`);
const organizations = await this.organizationRepository.findAll();
const orgDtos = OrganizationMapper.toDtoArray(organizations);
this.logger.log(`[ADMIN] Retrieved ${organizations.length} organizations from database`);
return {
organizations: orgDtos,
total: organizations.length,
page: 1,
pageSize: organizations.length,
totalPages: 1,
};
}
/**
* Get organization by ID (admin only)
*/
@Get('organizations/:id')
@ApiOperation({
summary: 'Get organization by ID (Admin only)',
description: 'Retrieve a specific organization by ID',
})
@ApiParam({
name: 'id',
description: 'Organization ID (UUID)',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Organization retrieved successfully',
type: OrganizationResponseDto,
})
@ApiNotFoundResponse({
description: 'Organization not found',
})
async getOrganizationById(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: UserPayload
): Promise<OrganizationResponseDto> {
this.logger.log(`[ADMIN: ${user.email}] Fetching organization: ${id}`);
const organization = await this.organizationRepository.findById(id);
if (!organization) {
throw new NotFoundException(`Organization ${id} not found`);
}
return OrganizationMapper.toDto(organization);
}
// ==================== CSV BOOKINGS ENDPOINTS ====================
/**
* Get ALL csv bookings from database (admin only)
*
* Returns all csv bookings from all organizations
*/
@Get('bookings')
@ApiOperation({
summary: 'Get all CSV bookings (Admin only)',
description:
'Retrieve ALL CSV bookings from the database without any filters. Includes bookings from all organizations and all statuses.',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'All CSV bookings retrieved successfully',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - missing or invalid token',
})
@ApiResponse({
status: 403,
description: 'Forbidden - requires admin role',
})
async getAllBookings(@CurrentUser() user: UserPayload) {
this.logger.log(`[ADMIN: ${user.email}] Fetching ALL csv bookings from database`);
const csvBookings = await this.csvBookingRepository.findAll();
const bookingDtos = csvBookings.map(booking => this.csvBookingToDto(booking));
this.logger.log(`[ADMIN] Retrieved ${csvBookings.length} csv bookings from database`);
return {
bookings: bookingDtos,
total: csvBookings.length,
page: 1,
pageSize: csvBookings.length,
totalPages: csvBookings.length > 0 ? 1 : 0,
};
}
/**
* Get csv booking by ID (admin only)
*/
@Get('bookings/:id')
@ApiOperation({
summary: 'Get CSV booking by ID (Admin only)',
description: 'Retrieve a specific CSV booking by ID',
})
@ApiParam({
name: 'id',
description: 'Booking ID (UUID)',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'CSV booking retrieved successfully',
})
@ApiNotFoundResponse({
description: 'CSV booking not found',
})
async getBookingById(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: UserPayload) {
this.logger.log(`[ADMIN: ${user.email}] Fetching csv booking: ${id}`);
const csvBooking = await this.csvBookingRepository.findById(id);
if (!csvBooking) {
throw new NotFoundException(`CSV booking ${id} not found`);
}
return this.csvBookingToDto(csvBooking);
}
/**
* Update csv booking (admin only)
*/
@Patch('bookings/:id')
@ApiOperation({
summary: 'Update CSV booking (Admin only)',
description: 'Update CSV booking status or details',
})
@ApiParam({
name: 'id',
description: 'Booking ID (UUID)',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'CSV booking updated successfully',
})
@ApiNotFoundResponse({
description: 'CSV booking not found',
})
async updateBooking(
@Param('id', ParseUUIDPipe) id: string,
@Body() updateDto: any,
@CurrentUser() user: UserPayload
) {
this.logger.log(`[ADMIN: ${user.email}] Updating csv booking: ${id}`);
const csvBooking = await this.csvBookingRepository.findById(id);
if (!csvBooking) {
throw new NotFoundException(`CSV booking ${id} not found`);
}
// Apply updates to the domain entity
// Note: This is a simplified version. You may want to add proper domain methods
const updatedBooking = await this.csvBookingRepository.update(csvBooking);
this.logger.log(`[ADMIN] CSV booking updated successfully: ${id}`);
return this.csvBookingToDto(updatedBooking);
}
/**
* Delete csv booking (admin only)
*/
@Delete('bookings/:id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete CSV booking (Admin only)',
description: 'Permanently delete a CSV booking from the database',
})
@ApiParam({
name: 'id',
description: 'Booking ID (UUID)',
})
@ApiResponse({
status: HttpStatus.NO_CONTENT,
description: 'CSV booking deleted successfully',
})
@ApiNotFoundResponse({
description: 'CSV booking not found',
})
async deleteBooking(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: UserPayload
): Promise<void> {
this.logger.log(`[ADMIN: ${user.email}] Deleting csv booking: ${id}`);
const csvBooking = await this.csvBookingRepository.findById(id);
if (!csvBooking) {
throw new NotFoundException(`CSV booking ${id} not found`);
}
await this.csvBookingRepository.delete(id);
this.logger.log(`[ADMIN] CSV booking deleted successfully: ${id}`);
}
/**
* Helper method to convert CSV booking domain entity to DTO
*/
private csvBookingToDto(booking: any) {
const primaryCurrency = booking.primaryCurrency as 'USD' | 'EUR';
return {
id: booking.id,
userId: booking.userId,
organizationId: booking.organizationId,
carrierName: booking.carrierName,
carrierEmail: booking.carrierEmail,
origin: booking.origin.getValue(),
destination: booking.destination.getValue(),
volumeCBM: booking.volumeCBM,
weightKG: booking.weightKG,
palletCount: booking.palletCount,
priceUSD: booking.priceUSD,
priceEUR: booking.priceEUR,
primaryCurrency: booking.primaryCurrency,
transitDays: booking.transitDays,
containerType: booking.containerType,
status: booking.status,
documents: booking.documents || [],
confirmationToken: booking.confirmationToken,
requestedAt: booking.requestedAt,
respondedAt: booking.respondedAt || null,
notes: booking.notes,
rejectionReason: booking.rejectionReason,
routeDescription: booking.getRouteDescription(),
isExpired: booking.isExpired(),
price: booking.getPriceInCurrency(primaryCurrency),
};
}
// ==================== DOCUMENTS ENDPOINTS ====================
/**
* Get ALL documents from all organizations (admin only)
*
* Returns documents grouped by organization
*/
@Get('documents')
@ApiOperation({
summary: 'Get all documents (Admin only)',
description: 'Retrieve ALL documents from all organizations in the database.',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'All documents retrieved successfully',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - missing or invalid token',
})
@ApiResponse({
status: 403,
description: 'Forbidden - requires admin role',
})
async getAllDocuments(@CurrentUser() user: UserPayload): Promise<any> {
this.logger.log(`[ADMIN: ${user.email}] Fetching ALL documents from database`);
// Get all organizations
const organizations = await this.organizationRepository.findAll();
// Extract documents from all organizations
const allDocuments = organizations.flatMap(org =>
org.documents.map(doc => ({
...doc,
organizationId: org.id,
organizationName: org.name,
}))
);
this.logger.log(
`[ADMIN] Retrieved ${allDocuments.length} documents from ${organizations.length} organizations`
);
return {
documents: allDocuments,
total: allDocuments.length,
organizationCount: organizations.length,
};
}
/**
* Get documents for a specific organization (admin only)
*/
@Get('organizations/:id/documents')
@ApiOperation({
summary: 'Get organization documents (Admin only)',
description: 'Retrieve all documents for a specific organization',
})
@ApiParam({
name: 'id',
description: 'Organization ID (UUID)',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Organization documents retrieved successfully',
})
@ApiNotFoundResponse({
description: 'Organization not found',
})
async getOrganizationDocuments(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: UserPayload
): Promise<any> {
this.logger.log(`[ADMIN: ${user.email}] Fetching documents for organization: ${id}`);
const organization = await this.organizationRepository.findById(id);
if (!organization) {
throw new NotFoundException(`Organization ${id} not found`);
}
return {
organizationId: organization.id,
organizationName: organization.name,
documents: organization.documents,
total: organization.documents.length,
};
}
}