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
601 lines
17 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|