fix backend

This commit is contained in:
David 2026-05-14 21:21:57 +02:00
parent 4baffe0b7a
commit 79ea90b165
15 changed files with 103 additions and 385 deletions

View File

@ -26,7 +26,7 @@ import { AuditModule } from './application/audit/audit.module';
import { NotificationsModule } from './application/notifications/notifications.module';
import { WebhooksModule } from './application/webhooks/webhooks.module';
import { GDPRModule } from './application/gdpr/gdpr.module';
import { CsvBookingsModule } from './application/csv-bookings.module';
import { CsvBookingsModule } from './application/csv-bookings/csv-bookings.module';
import { AdminModule } from './application/admin/admin.module';
import { BlogModule } from './application/blog/blog.module';
import { LogsModule } from './application/logs/logs.module';

View File

@ -24,7 +24,7 @@ import { SIRET_VERIFICATION_PORT } from '@domain/ports/out/siret-verification.po
import { PappersSiretAdapter } from '@infrastructure/external/pappers-siret.adapter';
// CSV Booking Service
import { CsvBookingsModule } from '../csv-bookings.module';
import { CsvBookingsModule } from '../csv-bookings/csv-bookings.module';
// Email
import { EmailModule } from '@infrastructure/email/email.module';

View File

@ -1,2 +1,16 @@
export * from './rates.controller';
export * from './bookings.controller';
export * from './auth.controller';
export * from './users.controller';
export * from './organizations.controller';
export * from './ports.controller';
export * from './notifications.controller';
export * from './webhooks.controller';
export * from './audit.controller';
export * from './subscriptions.controller';
export * from './invitations.controller';
export * from './gdpr.controller';
export * from './health.controller';
export * from './blog.controller';
export * from './csv-bookings.controller';
export * from './csv-booking-actions.controller';

View File

@ -1,57 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { CsvBookingsController } from './controllers/csv-bookings.controller';
import { CsvBookingActionsController } from './controllers/csv-booking-actions.controller';
import { CsvBookingService } from './services/csv-booking.service';
import { CsvBookingOrmEntity } from '../infrastructure/persistence/typeorm/entities/csv-booking.orm-entity';
import { TypeOrmCsvBookingRepository } from '../infrastructure/persistence/typeorm/repositories/csv-booking.repository';
import { TypeOrmShipmentCounterRepository } from '../infrastructure/persistence/typeorm/repositories/shipment-counter.repository';
import { SHIPMENT_COUNTER_PORT } from '@domain/ports/out/shipment-counter.port';
import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository';
import { OrganizationOrmEntity } from '../infrastructure/persistence/typeorm/entities/organization.orm-entity';
import { TypeOrmOrganizationRepository } from '../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
import { USER_REPOSITORY } from '@domain/ports/out/user.repository';
import { UserOrmEntity } from '../infrastructure/persistence/typeorm/entities/user.orm-entity';
import { TypeOrmUserRepository } from '../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
import { NotificationsModule } from './notifications/notifications.module';
import { EmailModule } from '../infrastructure/email/email.module';
import { StorageModule } from '../infrastructure/storage/storage.module';
import { SubscriptionsModule } from './subscriptions/subscriptions.module';
import { StripeModule } from '../infrastructure/stripe/stripe.module';
/**
* CSV Bookings Module
*
* Handles CSV-based booking workflow with carrier email confirmations
*/
@Module({
imports: [
TypeOrmModule.forFeature([CsvBookingOrmEntity, OrganizationOrmEntity, UserOrmEntity]),
ConfigModule,
NotificationsModule,
EmailModule,
StorageModule,
SubscriptionsModule,
StripeModule,
],
controllers: [CsvBookingsController, CsvBookingActionsController],
providers: [
CsvBookingService,
TypeOrmCsvBookingRepository,
{
provide: SHIPMENT_COUNTER_PORT,
useClass: TypeOrmShipmentCounterRepository,
},
{
provide: ORGANIZATION_REPOSITORY,
useClass: TypeOrmOrganizationRepository,
},
{
provide: USER_REPOSITORY,
useClass: TypeOrmUserRepository,
},
],
exports: [CsvBookingService, TypeOrmCsvBookingRepository],
})
export class CsvBookingsModule {}

View File

@ -0,0 +1,57 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { CsvBookingsController } from '../controllers/csv-bookings.controller';
import { CsvBookingActionsController } from '../controllers/csv-booking-actions.controller';
import { CsvBookingService } from '../services/csv-booking.service';
import { CsvBookingOrmEntity } from '../../infrastructure/persistence/typeorm/entities/csv-booking.orm-entity';
import { TypeOrmCsvBookingRepository } from '../../infrastructure/persistence/typeorm/repositories/csv-booking.repository';
import { TypeOrmShipmentCounterRepository } from '../../infrastructure/persistence/typeorm/repositories/shipment-counter.repository';
import { SHIPMENT_COUNTER_PORT } from '@domain/ports/out/shipment-counter.port';
import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository';
import { OrganizationOrmEntity } from '../../infrastructure/persistence/typeorm/entities/organization.orm-entity';
import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
import { USER_REPOSITORY } from '@domain/ports/out/user.repository';
import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity';
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
import { NotificationsModule } from '../notifications/notifications.module';
import { EmailModule } from '../../infrastructure/email/email.module';
import { StorageModule } from '../../infrastructure/storage/storage.module';
import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
import { StripeModule } from '../../infrastructure/stripe/stripe.module';
/**
* CSV Bookings Module
*
* Handles CSV-based booking workflow with carrier email confirmations
*/
@Module({
imports: [
TypeOrmModule.forFeature([CsvBookingOrmEntity, OrganizationOrmEntity, UserOrmEntity]),
ConfigModule,
NotificationsModule,
EmailModule,
StorageModule,
SubscriptionsModule,
StripeModule,
],
controllers: [CsvBookingsController, CsvBookingActionsController],
providers: [
CsvBookingService,
TypeOrmCsvBookingRepository,
{
provide: SHIPMENT_COUNTER_PORT,
useClass: TypeOrmShipmentCounterRepository,
},
{
provide: ORGANIZATION_REPOSITORY,
useClass: TypeOrmOrganizationRepository,
},
{
provide: USER_REPOSITORY,
useClass: TypeOrmUserRepository,
},
],
exports: [CsvBookingService, TypeOrmCsvBookingRepository],
})
export class CsvBookingsModule {}

View File

@ -7,7 +7,7 @@ import { DashboardController } from './dashboard.controller';
import { AnalyticsService } from '../services/analytics.service';
import { BookingsModule } from '../bookings/bookings.module';
import { RatesModule } from '../rates/rates.module';
import { CsvBookingsModule } from '../csv-bookings.module';
import { CsvBookingsModule } from '../csv-bookings/csv-bookings.module';
import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
import { FeatureFlagGuard } from '../guards/feature-flag.guard';

View File

@ -1,3 +1,4 @@
export * from './current-user.decorator';
export * from './public.decorator';
export * from './roles.decorator';
export * from './requires-feature.decorator';

View File

@ -1,3 +1,5 @@
export * from './jwt-auth.guard';
export * from './roles.guard';
export * from './api-key-or-jwt.guard';
export * from './feature-flag.guard';
export * from './throttle.guard';

View File

@ -1,3 +1,6 @@
export * from './rate-quote.mapper';
export * from './booking.mapper';
export * from './port.mapper';
export * from './user.mapper';
export * from './organization.mapper';
export * from './csv-rate.mapper';

View File

@ -45,12 +45,16 @@ import { CarrierOrmEntity } from '../../infrastructure/persistence/typeorm/entit
},
{
provide: RateSearchService,
useFactory: (cache: any, rateQuoteRepo: any, portRepo: any, carrierRepo: any) => {
// For now, create service with empty connectors array
// TODO: Inject actual carrier connectors
return new RateSearchService([], cache, rateQuoteRepo, portRepo, carrierRepo);
useFactory: (
connectors: any[],
cache: any,
rateQuoteRepo: any,
portRepo: any,
carrierRepo: any,
) => {
return new RateSearchService(connectors, cache, rateQuoteRepo, portRepo, carrierRepo);
},
inject: [CACHE_PORT, RATE_QUOTE_REPOSITORY, PORT_REPOSITORY, CARRIER_REPOSITORY],
inject: ['CarrierConnectors', CACHE_PORT, RATE_QUOTE_REPOSITORY, PORT_REPOSITORY, CARRIER_REPOSITORY],
},
],
exports: [RATE_QUOTE_REPOSITORY, RateSearchService],

View File

@ -1,318 +0,0 @@
/**
* Carrier Auth Service
*
* Handles carrier authentication and automatic account creation
*/
import { Injectable, Logger, UnauthorizedException, Inject } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CarrierProfileRepository } from '@infrastructure/persistence/typeorm/repositories/carrier-profile.repository';
import { UserOrmEntity } from '@infrastructure/persistence/typeorm/entities/user.orm-entity';
import { OrganizationOrmEntity } from '@infrastructure/persistence/typeorm/entities/organization.orm-entity';
import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port';
import * as argon2 from 'argon2';
import { randomBytes } from 'crypto';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class CarrierAuthService {
private readonly logger = new Logger(CarrierAuthService.name);
constructor(
private readonly carrierProfileRepository: CarrierProfileRepository,
@InjectRepository(UserOrmEntity)
private readonly userRepository: Repository<UserOrmEntity>,
@InjectRepository(OrganizationOrmEntity)
private readonly organizationRepository: Repository<OrganizationOrmEntity>,
private readonly jwtService: JwtService,
@Inject(EMAIL_PORT)
private readonly emailAdapter: EmailPort
) {}
/**
* Create carrier account automatically when clicking accept/reject link
*/
async createCarrierAccountIfNotExists(
carrierEmail: string,
carrierName: string
): Promise<{
carrierId: string;
userId: string;
isNewAccount: boolean;
temporaryPassword?: string;
}> {
this.logger.log(`Checking/creating carrier account for: ${carrierEmail}`);
// Check if carrier already exists
const existingCarrier = await this.carrierProfileRepository.findByEmail(carrierEmail);
if (existingCarrier) {
this.logger.log(`Carrier already exists: ${carrierEmail}`);
return {
carrierId: existingCarrier.id,
userId: existingCarrier.userId,
isNewAccount: false,
};
}
// Create new organization for the carrier
const organizationId = uuidv4(); // Generate UUID for organization
const organization = this.organizationRepository.create({
id: organizationId, // Provide explicit ID since @PrimaryColumn requires it
name: carrierName,
type: 'CARRIER',
isCarrier: true,
carrierType: 'LCL', // Default
addressStreet: 'TBD',
addressCity: 'TBD',
addressPostalCode: 'TBD',
addressCountry: 'FR', // Default to France
isActive: true,
});
const savedOrganization = await this.organizationRepository.save(organization);
this.logger.log(`Created organization: ${savedOrganization.id}`);
// Generate temporary password
const temporaryPassword = this.generateTemporaryPassword();
const hashedPassword = await argon2.hash(temporaryPassword);
// Create user account
const nameParts = carrierName.split(' ');
const user = this.userRepository.create({
id: uuidv4(),
email: carrierEmail.toLowerCase(),
passwordHash: hashedPassword,
firstName: nameParts[0] || 'Carrier',
lastName: nameParts.slice(1).join(' ') || 'Account',
role: 'CARRIER', // New role for carriers
organizationId: savedOrganization.id,
isActive: true,
isEmailVerified: true, // Auto-verified since created via email
});
const savedUser = await this.userRepository.save(user);
this.logger.log(`Created user: ${savedUser.id}`);
// Create carrier profile
const carrierProfile = await this.carrierProfileRepository.create({
userId: savedUser.id,
organizationId: savedOrganization.id,
companyName: carrierName,
notificationEmail: carrierEmail,
preferredCurrency: 'USD',
isActive: true,
isVerified: false, // Will be verified later
});
this.logger.log(`Created carrier profile: ${carrierProfile.id}`);
// Send welcome email with credentials and WAIT for confirmation
try {
await this.emailAdapter.sendCarrierAccountCreated(
carrierEmail,
carrierName,
temporaryPassword
);
this.logger.log(`Account creation email sent to ${carrierEmail}`);
} catch (error: any) {
this.logger.error(`Failed to send account creation email: ${error?.message}`, error?.stack);
// Continue even if email fails - account is already created
}
return {
carrierId: carrierProfile.id,
userId: savedUser.id,
isNewAccount: true,
temporaryPassword,
};
}
/**
* Generate auto-login JWT token for carrier
*/
async generateAutoLoginToken(userId: string, carrierId: string): Promise<string> {
this.logger.log(`Generating auto-login token for carrier: ${carrierId}`);
const payload = {
sub: userId,
carrierId,
type: 'carrier',
autoLogin: true,
};
const token = this.jwtService.sign(payload, { expiresIn: '1h' });
this.logger.log(`Auto-login token generated for carrier: ${carrierId}`);
return token;
}
/**
* Standard login for carriers
*/
async login(
email: string,
password: string
): Promise<{
accessToken: string;
refreshToken: string;
carrier: {
id: string;
companyName: string;
email: string;
};
}> {
this.logger.log(`Carrier login attempt: ${email}`);
const carrier = await this.carrierProfileRepository.findByEmail(email);
if (!carrier || !carrier.user) {
this.logger.warn(`Login failed: Carrier not found for email ${email}`);
throw new UnauthorizedException('Invalid credentials');
}
// Verify password
const isPasswordValid = await argon2.verify(carrier.user.passwordHash, password);
if (!isPasswordValid) {
this.logger.warn(`Login failed: Invalid password for ${email}`);
throw new UnauthorizedException('Invalid credentials');
}
// Check if carrier is active
if (!carrier.isActive) {
this.logger.warn(`Login failed: Carrier account is inactive ${email}`);
throw new UnauthorizedException('Account is inactive');
}
// Update last login
await this.carrierProfileRepository.updateLastLogin(carrier.id);
// Generate JWT tokens
const payload = {
sub: carrier.userId,
email: carrier.user.email,
carrierId: carrier.id,
organizationId: carrier.organizationId,
role: 'CARRIER',
};
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
this.logger.log(`Login successful for carrier: ${carrier.id}`);
return {
accessToken,
refreshToken,
carrier: {
id: carrier.id,
companyName: carrier.companyName,
email: carrier.user.email,
},
};
}
/**
* Verify auto-login token
*/
async verifyAutoLoginToken(token: string): Promise<{
userId: string;
carrierId: string;
}> {
try {
const payload = this.jwtService.verify(token);
if (!payload.autoLogin || payload.type !== 'carrier') {
throw new UnauthorizedException('Invalid auto-login token');
}
return {
userId: payload.sub,
carrierId: payload.carrierId,
};
} catch (error: any) {
this.logger.error(`Auto-login token verification failed: ${error?.message}`);
throw new UnauthorizedException('Invalid or expired token');
}
}
/**
* Change carrier password
*/
async changePassword(carrierId: string, oldPassword: string, newPassword: string): Promise<void> {
this.logger.log(`Password change request for carrier: ${carrierId}`);
const carrier = await this.carrierProfileRepository.findById(carrierId);
if (!carrier || !carrier.user) {
throw new UnauthorizedException('Carrier not found');
}
// Verify old password
const isOldPasswordValid = await argon2.verify(carrier.user.passwordHash, oldPassword);
if (!isOldPasswordValid) {
this.logger.warn(`Password change failed: Invalid old password for carrier ${carrierId}`);
throw new UnauthorizedException('Invalid old password');
}
// Hash new password
const hashedNewPassword = await argon2.hash(newPassword);
// Update password
carrier.user.passwordHash = hashedNewPassword;
await this.userRepository.save(carrier.user);
this.logger.log(`Password changed successfully for carrier: ${carrierId}`);
}
/**
* Request password reset (sends temporary password via email)
*/
async requestPasswordReset(email: string): Promise<{ temporaryPassword: string }> {
this.logger.log(`Password reset request for: ${email}`);
const carrier = await this.carrierProfileRepository.findByEmail(email);
if (!carrier || !carrier.user) {
// Don't reveal if email exists or not for security
this.logger.warn(`Password reset requested for non-existent carrier: ${email}`);
throw new UnauthorizedException('If this email exists, a password reset will be sent');
}
// Generate temporary password
const temporaryPassword = this.generateTemporaryPassword();
const hashedPassword = await argon2.hash(temporaryPassword);
// Update password
carrier.user.passwordHash = hashedPassword;
await this.userRepository.save(carrier.user);
this.logger.log(`Temporary password generated for carrier: ${carrier.id}`);
// Send password reset email and WAIT for confirmation
try {
await this.emailAdapter.sendCarrierPasswordReset(
email,
carrier.companyName,
temporaryPassword
);
this.logger.log(`Password reset email sent to ${email}`);
} catch (error: any) {
this.logger.error(`Failed to send password reset email: ${error?.message}`, error?.stack);
// Continue even if email fails - password is already reset
}
return { temporaryPassword };
}
/**
* Generate a secure temporary password
*/
private generateTemporaryPassword(): string {
return randomBytes(16).toString('hex').slice(0, 12);
}
}

View File

@ -7,3 +7,4 @@
export * from './search-rates.port';
export * from './get-ports.port';
export * from './validate-availability.port';
export * from './search-csv-rates.port';

View File

@ -15,6 +15,11 @@ export * from './notification.repository';
export * from './audit-log.repository';
export * from './webhook.repository';
export * from './csv-booking.repository';
export * from './api-key.repository';
export * from './blog-post.repository';
export * from './invitation-token.repository';
export * from './subscription.repository';
export * from './license.repository';
// Infrastructure Ports
export * from './cache.port';
@ -23,6 +28,6 @@ export * from './pdf.port';
export * from './storage.port';
export * from './carrier-connector.port';
export * from './csv-rate-loader.port';
export * from './subscription.repository';
export * from './license.repository';
export * from './shipment-counter.port';
export * from './siret-verification.port';
export * from './stripe.port';

View File

@ -8,3 +8,6 @@ export * from './rate-search.service';
export * from './port-search.service';
export * from './availability-validation.service';
export * from './booking.service';
export * from './csv-rate-search.service';
export * from './csv-rate-price-calculator.service';
export * from './rate-offer-generator.service';

View File

@ -15,3 +15,6 @@ export * from './subscription-plan.vo';
export * from './subscription-status.vo';
export * from './license-status.vo';
export * from './locale.vo';
export * from './surcharge.vo';
export * from './volume.vo';
export * from './plan-feature.vo';