fix backend
This commit is contained in:
parent
4baffe0b7a
commit
79ea90b165
@ -26,7 +26,7 @@ import { AuditModule } from './application/audit/audit.module';
|
|||||||
import { NotificationsModule } from './application/notifications/notifications.module';
|
import { NotificationsModule } from './application/notifications/notifications.module';
|
||||||
import { WebhooksModule } from './application/webhooks/webhooks.module';
|
import { WebhooksModule } from './application/webhooks/webhooks.module';
|
||||||
import { GDPRModule } from './application/gdpr/gdpr.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 { AdminModule } from './application/admin/admin.module';
|
||||||
import { BlogModule } from './application/blog/blog.module';
|
import { BlogModule } from './application/blog/blog.module';
|
||||||
import { LogsModule } from './application/logs/logs.module';
|
import { LogsModule } from './application/logs/logs.module';
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import { SIRET_VERIFICATION_PORT } from '@domain/ports/out/siret-verification.po
|
|||||||
import { PappersSiretAdapter } from '@infrastructure/external/pappers-siret.adapter';
|
import { PappersSiretAdapter } from '@infrastructure/external/pappers-siret.adapter';
|
||||||
|
|
||||||
// CSV Booking Service
|
// CSV Booking Service
|
||||||
import { CsvBookingsModule } from '../csv-bookings.module';
|
import { CsvBookingsModule } from '../csv-bookings/csv-bookings.module';
|
||||||
|
|
||||||
// Email
|
// Email
|
||||||
import { EmailModule } from '@infrastructure/email/email.module';
|
import { EmailModule } from '@infrastructure/email/email.module';
|
||||||
|
|||||||
@ -1,2 +1,16 @@
|
|||||||
export * from './rates.controller';
|
export * from './rates.controller';
|
||||||
export * from './bookings.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';
|
||||||
|
|||||||
@ -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 {}
|
|
||||||
@ -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 {}
|
||||||
@ -7,7 +7,7 @@ import { DashboardController } from './dashboard.controller';
|
|||||||
import { AnalyticsService } from '../services/analytics.service';
|
import { AnalyticsService } from '../services/analytics.service';
|
||||||
import { BookingsModule } from '../bookings/bookings.module';
|
import { BookingsModule } from '../bookings/bookings.module';
|
||||||
import { RatesModule } from '../rates/rates.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 { SubscriptionsModule } from '../subscriptions/subscriptions.module';
|
||||||
import { FeatureFlagGuard } from '../guards/feature-flag.guard';
|
import { FeatureFlagGuard } from '../guards/feature-flag.guard';
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './current-user.decorator';
|
export * from './current-user.decorator';
|
||||||
export * from './public.decorator';
|
export * from './public.decorator';
|
||||||
export * from './roles.decorator';
|
export * from './roles.decorator';
|
||||||
|
export * from './requires-feature.decorator';
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
export * from './jwt-auth.guard';
|
export * from './jwt-auth.guard';
|
||||||
export * from './roles.guard';
|
export * from './roles.guard';
|
||||||
export * from './api-key-or-jwt.guard';
|
export * from './api-key-or-jwt.guard';
|
||||||
|
export * from './feature-flag.guard';
|
||||||
|
export * from './throttle.guard';
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
export * from './rate-quote.mapper';
|
export * from './rate-quote.mapper';
|
||||||
export * from './booking.mapper';
|
export * from './booking.mapper';
|
||||||
export * from './port.mapper';
|
export * from './port.mapper';
|
||||||
|
export * from './user.mapper';
|
||||||
|
export * from './organization.mapper';
|
||||||
|
export * from './csv-rate.mapper';
|
||||||
|
|||||||
@ -45,12 +45,16 @@ import { CarrierOrmEntity } from '../../infrastructure/persistence/typeorm/entit
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: RateSearchService,
|
provide: RateSearchService,
|
||||||
useFactory: (cache: any, rateQuoteRepo: any, portRepo: any, carrierRepo: any) => {
|
useFactory: (
|
||||||
// For now, create service with empty connectors array
|
connectors: any[],
|
||||||
// TODO: Inject actual carrier connectors
|
cache: any,
|
||||||
return new RateSearchService([], cache, rateQuoteRepo, portRepo, carrierRepo);
|
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],
|
exports: [RATE_QUOTE_REPOSITORY, RateSearchService],
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,3 +7,4 @@
|
|||||||
export * from './search-rates.port';
|
export * from './search-rates.port';
|
||||||
export * from './get-ports.port';
|
export * from './get-ports.port';
|
||||||
export * from './validate-availability.port';
|
export * from './validate-availability.port';
|
||||||
|
export * from './search-csv-rates.port';
|
||||||
|
|||||||
@ -15,6 +15,11 @@ export * from './notification.repository';
|
|||||||
export * from './audit-log.repository';
|
export * from './audit-log.repository';
|
||||||
export * from './webhook.repository';
|
export * from './webhook.repository';
|
||||||
export * from './csv-booking.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
|
// Infrastructure Ports
|
||||||
export * from './cache.port';
|
export * from './cache.port';
|
||||||
@ -23,6 +28,6 @@ export * from './pdf.port';
|
|||||||
export * from './storage.port';
|
export * from './storage.port';
|
||||||
export * from './carrier-connector.port';
|
export * from './carrier-connector.port';
|
||||||
export * from './csv-rate-loader.port';
|
export * from './csv-rate-loader.port';
|
||||||
export * from './subscription.repository';
|
export * from './shipment-counter.port';
|
||||||
export * from './license.repository';
|
export * from './siret-verification.port';
|
||||||
export * from './stripe.port';
|
export * from './stripe.port';
|
||||||
|
|||||||
@ -8,3 +8,6 @@ export * from './rate-search.service';
|
|||||||
export * from './port-search.service';
|
export * from './port-search.service';
|
||||||
export * from './availability-validation.service';
|
export * from './availability-validation.service';
|
||||||
export * from './booking.service';
|
export * from './booking.service';
|
||||||
|
export * from './csv-rate-search.service';
|
||||||
|
export * from './csv-rate-price-calculator.service';
|
||||||
|
export * from './rate-offer-generator.service';
|
||||||
|
|||||||
@ -15,3 +15,6 @@ export * from './subscription-plan.vo';
|
|||||||
export * from './subscription-status.vo';
|
export * from './subscription-status.vo';
|
||||||
export * from './license-status.vo';
|
export * from './license-status.vo';
|
||||||
export * from './locale.vo';
|
export * from './locale.vo';
|
||||||
|
export * from './surcharge.vo';
|
||||||
|
export * from './volume.vo';
|
||||||
|
export * from './plan-feature.vo';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user