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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, }; } }