fix register
This commit is contained in:
parent
b2f5d9968d
commit
a34c850e67
@ -29,7 +29,14 @@
|
|||||||
"Bash(npx ts-node:*)",
|
"Bash(npx ts-node:*)",
|
||||||
"Bash(python3:*)",
|
"Bash(python3:*)",
|
||||||
"Read(//Users/david/.docker/**)",
|
"Read(//Users/david/.docker/**)",
|
||||||
"Bash(env)"
|
"Bash(env)",
|
||||||
|
"Bash(ssh david@xpeditis-cloud \"docker ps --filter name=xpeditis-backend --format ''{{.ID}} {{.Status}}''\")",
|
||||||
|
"Bash(git revert:*)",
|
||||||
|
"Bash(git log:*)",
|
||||||
|
"Bash(xargs -r docker rm:*)",
|
||||||
|
"Bash(npm run migration:run:*)",
|
||||||
|
"Bash(npm run dev:*)",
|
||||||
|
"Bash(npm run backend:dev:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
115
CLAUDE.md
115
CLAUDE.md
@ -474,58 +474,20 @@ The platform supports CSV-based operations for bulk data management:
|
|||||||
- Export to PDF documents using `pdfkit`
|
- Export to PDF documents using `pdfkit`
|
||||||
- File downloads using `file-saver` on frontend
|
- File downloads using `file-saver` on frontend
|
||||||
|
|
||||||
## Common Development Tasks
|
## Admin User Management
|
||||||
|
|
||||||
### Adding a New Domain Entity
|
The platform includes a dedicated admin interface for user management:
|
||||||
|
|
||||||
1. Create entity in `src/domain/entities/entity-name.entity.ts`
|
**Admin Features** (Branch: `users_admin`):
|
||||||
2. Create value objects if needed in `src/domain/value-objects/`
|
- User CRUD operations (Create, Read, Update, Delete)
|
||||||
3. Write unit tests: `entity-name.entity.spec.ts`
|
- Organization management
|
||||||
4. Add repository port in `src/domain/ports/out/entity-name.repository.ts`
|
- Role assignment and permissions
|
||||||
5. Create ORM entity in `src/infrastructure/persistence/typeorm/entities/`
|
- Argon2 password hash generation for new users
|
||||||
6. Implement repository in `src/infrastructure/persistence/typeorm/repositories/`
|
- Accessible at `/admin/users` (ADMIN role required)
|
||||||
7. Create mapper in `src/infrastructure/persistence/typeorm/mappers/`
|
|
||||||
8. Generate migration: `npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName`
|
|
||||||
|
|
||||||
### Adding a New API Endpoint
|
**Password Hashing Utility**:
|
||||||
|
- Use `apps/backend/generate-hash.js` to generate Argon2 password hashes
|
||||||
1. Create DTO in `src/application/dto/feature-name.dto.ts`
|
- Example: `node apps/backend/generate-hash.js mypassword`
|
||||||
2. Add endpoint to controller in `src/application/controllers/`
|
|
||||||
3. Add Swagger decorators (`@ApiOperation`, `@ApiResponse`)
|
|
||||||
4. Create domain service in `src/domain/services/` if needed
|
|
||||||
5. Write unit tests for domain logic
|
|
||||||
6. Write integration tests for infrastructure
|
|
||||||
7. Update Postman collection in `postman/`
|
|
||||||
|
|
||||||
### Adding a New Carrier Integration
|
|
||||||
|
|
||||||
1. Create connector in `src/infrastructure/carriers/carrier-name/`
|
|
||||||
2. Implement `CarrierConnectorPort` interface
|
|
||||||
3. Add request/response mappers
|
|
||||||
4. Implement circuit breaker (5s timeout)
|
|
||||||
5. Add retry logic with exponential backoff
|
|
||||||
6. Write integration tests
|
|
||||||
7. Update carrier seed data
|
|
||||||
8. Add API credentials to `.env.example`
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
**Architecture & Planning**:
|
|
||||||
- [ARCHITECTURE.md](ARCHITECTURE.md) - System architecture (5,800 words)
|
|
||||||
- [DEPLOYMENT.md](DEPLOYMENT.md) - Deployment guide (4,500 words)
|
|
||||||
- [PRD.md](PRD.md) - Product requirements
|
|
||||||
- [TODO.md](TODO.md) - 30-week development roadmap
|
|
||||||
|
|
||||||
**Implementation Summaries**:
|
|
||||||
- [PHASE4_SUMMARY.md](PHASE4_SUMMARY.md) - Security, monitoring, testing
|
|
||||||
- [PHASE3_COMPLETE.md](PHASE3_COMPLETE.md) - Booking workflow, exports
|
|
||||||
- [PHASE2_COMPLETE.md](PHASE2_COMPLETE.md) - Authentication, RBAC
|
|
||||||
- [PHASE-1-WEEK5-COMPLETE.md](PHASE-1-WEEK5-COMPLETE.md) - Rate search, cache
|
|
||||||
|
|
||||||
**Testing**:
|
|
||||||
- [TEST_EXECUTION_GUIDE.md](TEST_EXECUTION_GUIDE.md) - How to run all tests
|
|
||||||
- [TEST_COVERAGE_REPORT.md](TEST_COVERAGE_REPORT.md) - Coverage metrics
|
|
||||||
- [GUIDE_TESTS_POSTMAN.md](GUIDE_TESTS_POSTMAN.md) - Postman API tests
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
@ -542,23 +504,23 @@ docker build -t xpeditis-frontend:latest -f apps/frontend/Dockerfile .
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production Deployment (AWS)
|
### Production Deployment (Portainer)
|
||||||
|
|
||||||
See [DEPLOYMENT.md](DEPLOYMENT.md) for complete instructions:
|
See [docker/PORTAINER_DEPLOYMENT_GUIDE.md](docker/PORTAINER_DEPLOYMENT_GUIDE.md) for complete instructions:
|
||||||
- AWS RDS (PostgreSQL)
|
- Scaleway Container Registry (rg.fr-par.scw.cloud/weworkstudio)
|
||||||
- AWS ElastiCache (Redis)
|
- Docker Swarm stack deployment
|
||||||
- AWS S3 (documents)
|
- Traefik reverse proxy configuration
|
||||||
- AWS ECS/Fargate (containers)
|
- Environment-specific configs (staging/production)
|
||||||
- AWS ALB (load balancer)
|
|
||||||
- AWS CloudWatch (logs + metrics)
|
|
||||||
- Sentry (error tracking)
|
|
||||||
|
|
||||||
**CI/CD**: Automated via GitHub Actions
|
**CI/CD**: Automated via GitHub Actions
|
||||||
- Build and push Docker images
|
- Build and push Docker images to Scaleway Registry
|
||||||
- Deploy to staging/production via Portainer
|
- Deploy to staging/production via Portainer
|
||||||
- Run smoke tests post-deployment
|
- Run smoke tests post-deployment
|
||||||
|
|
||||||
See [docker/PORTAINER_DEPLOYMENT_GUIDE.md](docker/PORTAINER_DEPLOYMENT_GUIDE.md) for Portainer setup.
|
**Deployment Scripts**:
|
||||||
|
- `docker/build-images.sh` - Build and tag Docker images
|
||||||
|
- `deploy-to-portainer.sh` - Automated deployment script
|
||||||
|
- `docker/portainer-stack.yml` - Production stack configuration
|
||||||
|
|
||||||
## Performance Targets
|
## Performance Targets
|
||||||
|
|
||||||
@ -612,9 +574,32 @@ See [docker/PORTAINER_DEPLOYMENT_GUIDE.md](docker/PORTAINER_DEPLOYMENT_GUIDE.md)
|
|||||||
- Document APIs with Swagger decorators
|
- Document APIs with Swagger decorators
|
||||||
- Run migrations before deployment
|
- Run migrations before deployment
|
||||||
|
|
||||||
## Support & Contribution
|
## Documentation
|
||||||
|
|
||||||
|
**Architecture & Planning**:
|
||||||
|
- [ARCHITECTURE.md](ARCHITECTURE.md) - System architecture (5,800 words)
|
||||||
|
- [DEPLOYMENT.md](DEPLOYMENT.md) - Deployment guide (4,500 words)
|
||||||
|
- [PRD.md](PRD.md) - Product requirements
|
||||||
|
- [TODO.md](TODO.md) - 30-week development roadmap
|
||||||
|
|
||||||
|
**Implementation Summaries**:
|
||||||
|
- [PHASE4_SUMMARY.md](PHASE4_SUMMARY.md) - Security, monitoring, testing
|
||||||
|
- [PHASE3_COMPLETE.md](PHASE3_COMPLETE.md) - Booking workflow, exports
|
||||||
|
- [PHASE2_COMPLETE.md](PHASE2_COMPLETE.md) - Authentication, RBAC
|
||||||
|
- [PHASE-1-WEEK5-COMPLETE.md](PHASE-1-WEEK5-COMPLETE.md) - Rate search, cache
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
- [TEST_EXECUTION_GUIDE.md](TEST_EXECUTION_GUIDE.md) - How to run all tests
|
||||||
|
- [TEST_COVERAGE_REPORT.md](TEST_COVERAGE_REPORT.md) - Coverage metrics
|
||||||
|
- [GUIDE_TESTS_POSTMAN.md](GUIDE_TESTS_POSTMAN.md) - Postman API tests
|
||||||
|
|
||||||
|
**Deployment**:
|
||||||
|
- [docker/PORTAINER_DEPLOYMENT_GUIDE.md](docker/PORTAINER_DEPLOYMENT_GUIDE.md) - Portainer setup
|
||||||
|
- [docker/DOCKER_BUILD_GUIDE.md](docker/DOCKER_BUILD_GUIDE.md) - Docker build instructions
|
||||||
|
- [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) - Pre-deployment checklist
|
||||||
|
|
||||||
|
## Code Review Checklist
|
||||||
|
|
||||||
**Code Review Checklist**:
|
|
||||||
1. Hexagonal architecture principles followed
|
1. Hexagonal architecture principles followed
|
||||||
2. Domain layer has zero external dependencies
|
2. Domain layer has zero external dependencies
|
||||||
3. Unit tests written (90%+ coverage for domain)
|
3. Unit tests written (90%+ coverage for domain)
|
||||||
@ -625,9 +610,3 @@ See [docker/PORTAINER_DEPLOYMENT_GUIDE.md](docker/PORTAINER_DEPLOYMENT_GUIDE.md)
|
|||||||
8. TypeScript strict mode passes
|
8. TypeScript strict mode passes
|
||||||
9. Prettier formatting applied
|
9. Prettier formatting applied
|
||||||
10. ESLint passes with no warnings
|
10. ESLint passes with no warnings
|
||||||
|
|
||||||
**Getting Help**:
|
|
||||||
- Check existing documentation (ARCHITECTURE.md, DEPLOYMENT.md)
|
|
||||||
- Review Swagger API docs (http://localhost:4000/api/docs)
|
|
||||||
- Check GitHub Actions for CI failures
|
|
||||||
- Review Sentry for production errors
|
|
||||||
|
|||||||
14
apps/backend/generate-hash.js
Normal file
14
apps/backend/generate-hash.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const argon2 = require('argon2');
|
||||||
|
|
||||||
|
async function generateHash() {
|
||||||
|
const hash = await argon2.hash('Password123!', {
|
||||||
|
type: argon2.argon2id,
|
||||||
|
memoryCost: 65536, // 64 MB
|
||||||
|
timeCost: 3,
|
||||||
|
parallelism: 4,
|
||||||
|
});
|
||||||
|
console.log('Argon2id hash for "Password123!":');
|
||||||
|
console.log(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHash().catch(console.error);
|
||||||
16250
apps/backend/package-lock.json
generated
Normal file
16250
apps/backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,8 +9,11 @@ import { AuthController } from '../controllers/auth.controller';
|
|||||||
|
|
||||||
// Import domain and infrastructure dependencies
|
// Import domain and infrastructure dependencies
|
||||||
import { USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
import { USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
||||||
|
import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository';
|
||||||
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
||||||
|
import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
|
||||||
import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity';
|
import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity';
|
||||||
|
import { OrganizationOrmEntity } from '../../infrastructure/persistence/typeorm/entities/organization.orm-entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -29,8 +32,8 @@ import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 👇 Add this to register TypeORM repository for UserOrmEntity
|
// 👇 Add this to register TypeORM repositories
|
||||||
TypeOrmModule.forFeature([UserOrmEntity]),
|
TypeOrmModule.forFeature([UserOrmEntity, OrganizationOrmEntity]),
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [
|
providers: [
|
||||||
@ -40,6 +43,10 @@ import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities
|
|||||||
provide: USER_REPOSITORY,
|
provide: USER_REPOSITORY,
|
||||||
useClass: TypeOrmUserRepository,
|
useClass: TypeOrmUserRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ORGANIZATION_REPOSITORY,
|
||||||
|
useClass: TypeOrmOrganizationRepository,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
exports: [AuthService, JwtStrategy, PassportModule],
|
exports: [AuthService, JwtStrategy, PassportModule],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,14 +4,18 @@ import {
|
|||||||
ConflictException,
|
ConflictException,
|
||||||
Logger,
|
Logger,
|
||||||
Inject,
|
Inject,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
||||||
import { User, UserRole } from '@domain/entities/user.entity';
|
import { User, UserRole } from '@domain/entities/user.entity';
|
||||||
|
import { OrganizationRepository, ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository';
|
||||||
|
import { Organization, OrganizationType } from '@domain/entities/organization.entity';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { DEFAULT_ORG_ID } from '@infrastructure/persistence/typeorm/seeds/test-organizations.seed';
|
import { DEFAULT_ORG_ID } from '@infrastructure/persistence/typeorm/seeds/test-organizations.seed';
|
||||||
|
import { RegisterOrganizationDto } from '../dto/auth-login.dto';
|
||||||
|
|
||||||
export interface JwtPayload {
|
export interface JwtPayload {
|
||||||
sub: string; // user ID
|
sub: string; // user ID
|
||||||
@ -27,7 +31,9 @@ export class AuthService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(USER_REPOSITORY)
|
@Inject(USER_REPOSITORY)
|
||||||
private readonly userRepository: UserRepository, // ✅ Correct injection
|
private readonly userRepository: UserRepository,
|
||||||
|
@Inject(ORGANIZATION_REPOSITORY)
|
||||||
|
private readonly organizationRepository: OrganizationRepository,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly configService: ConfigService
|
private readonly configService: ConfigService
|
||||||
) {}
|
) {}
|
||||||
@ -40,7 +46,8 @@ export class AuthService {
|
|||||||
password: string,
|
password: string,
|
||||||
firstName: string,
|
firstName: string,
|
||||||
lastName: string,
|
lastName: string,
|
||||||
organizationId?: string
|
organizationId?: string,
|
||||||
|
organizationData?: RegisterOrganizationDto
|
||||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
||||||
this.logger.log(`Registering new user: ${email}`);
|
this.logger.log(`Registering new user: ${email}`);
|
||||||
|
|
||||||
@ -57,8 +64,11 @@ export class AuthService {
|
|||||||
parallelism: 4,
|
parallelism: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate or generate organization ID
|
// Determine organization ID:
|
||||||
const finalOrganizationId = this.validateOrGenerateOrganizationId(organizationId);
|
// 1. If organizationId is provided (invited user), use it
|
||||||
|
// 2. If organizationData is provided (new user), create a new organization
|
||||||
|
// 3. Otherwise, use default organization
|
||||||
|
const finalOrganizationId = await this.resolveOrganizationId(organizationId, organizationData);
|
||||||
|
|
||||||
const user = User.create({
|
const user = User.create({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
@ -209,20 +219,80 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate or generate a valid organization ID
|
* Resolve organization ID for registration
|
||||||
* If provided ID is invalid (not a UUID), generate a new one
|
* 1. If organizationId is provided (invited user), validate and use it
|
||||||
|
* 2. If organizationData is provided (new user), create a new organization
|
||||||
|
* 3. Otherwise, throw an error (both are required)
|
||||||
*/
|
*/
|
||||||
private validateOrGenerateOrganizationId(organizationId?: string): string {
|
private async resolveOrganizationId(
|
||||||
// UUID v4 regex pattern
|
organizationId?: string,
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
organizationData?: RegisterOrganizationDto
|
||||||
|
): Promise<string> {
|
||||||
|
// Case 1: Invited user - organizationId is provided
|
||||||
|
if (organizationId) {
|
||||||
|
this.logger.log(`Using existing organization for invited user: ${organizationId}`);
|
||||||
|
|
||||||
|
// Validate that the organization exists
|
||||||
|
const organization = await this.organizationRepository.findById(organizationId);
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestException('Invalid organization ID - organization does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!organization.isActive) {
|
||||||
|
throw new BadRequestException('Organization is not active');
|
||||||
|
}
|
||||||
|
|
||||||
if (organizationId && uuidRegex.test(organizationId)) {
|
|
||||||
return organizationId;
|
return organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use default organization "Test Freight Forwarder Inc." if not provided
|
// Case 2: New user - create a new organization
|
||||||
// This ID comes from the seed migration 1730000000006-SeedOrganizations
|
if (organizationData) {
|
||||||
this.logger.log(`Using default organization ID for user registration: ${DEFAULT_ORG_ID}`);
|
this.logger.log(`Creating new organization for user registration: ${organizationData.name}`);
|
||||||
return DEFAULT_ORG_ID;
|
|
||||||
|
// Check if organization name already exists
|
||||||
|
const existingOrg = await this.organizationRepository.findByName(organizationData.name);
|
||||||
|
|
||||||
|
if (existingOrg) {
|
||||||
|
throw new ConflictException('An organization with this name already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if SCAC code already exists (for carriers)
|
||||||
|
if (organizationData.scac) {
|
||||||
|
const existingScac = await this.organizationRepository.findBySCAC(organizationData.scac);
|
||||||
|
|
||||||
|
if (existingScac) {
|
||||||
|
throw new ConflictException('An organization with this SCAC code already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new organization
|
||||||
|
const newOrganization = Organization.create({
|
||||||
|
id: uuidv4(),
|
||||||
|
name: organizationData.name,
|
||||||
|
type: organizationData.type,
|
||||||
|
scac: organizationData.scac,
|
||||||
|
address: {
|
||||||
|
street: organizationData.street,
|
||||||
|
city: organizationData.city,
|
||||||
|
state: organizationData.state,
|
||||||
|
postalCode: organizationData.postalCode,
|
||||||
|
country: organizationData.country,
|
||||||
|
},
|
||||||
|
documents: [],
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedOrganization = await this.organizationRepository.save(newOrganization);
|
||||||
|
|
||||||
|
this.logger.log(`New organization created: ${savedOrganization.id} - ${savedOrganization.name}`);
|
||||||
|
|
||||||
|
return savedOrganization.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3: Neither provided - error
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Either organizationId (for invited users) or organization data (for new users) must be provided'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,7 +70,8 @@ export class AuthController {
|
|||||||
dto.password,
|
dto.password,
|
||||||
dto.firstName,
|
dto.firstName,
|
||||||
dto.lastName,
|
dto.lastName,
|
||||||
dto.organizationId
|
dto.organizationId,
|
||||||
|
dto.organization
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
import { IsEmail, IsString, MinLength, IsOptional, ValidateNested, IsEnum, MaxLength, Matches } from 'class-validator';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { OrganizationType } from '@domain/entities/organization.entity';
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
@ -19,6 +21,84 @@ export class LoginDto {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization data for registration (nested in RegisterDto)
|
||||||
|
*/
|
||||||
|
export class RegisterOrganizationDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Acme Freight Forwarding',
|
||||||
|
description: 'Organization name',
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 200,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2)
|
||||||
|
@MaxLength(200)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: OrganizationType.FREIGHT_FORWARDER,
|
||||||
|
description: 'Organization type',
|
||||||
|
enum: OrganizationType,
|
||||||
|
})
|
||||||
|
@IsEnum(OrganizationType)
|
||||||
|
type: OrganizationType;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '123 Main Street',
|
||||||
|
description: 'Street address',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
street: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Rotterdam',
|
||||||
|
description: 'City',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
city: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: 'South Holland',
|
||||||
|
description: 'State or province',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
state?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '3000 AB',
|
||||||
|
description: 'Postal code',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
postalCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'NL',
|
||||||
|
description: 'Country code (ISO 3166-1 alpha-2)',
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 2,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2)
|
||||||
|
@MaxLength(2)
|
||||||
|
@Matches(/^[A-Z]{2}$/, { message: 'Country must be a 2-letter ISO code (e.g., NL, US, CN)' })
|
||||||
|
country: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: 'MAEU',
|
||||||
|
description: 'Standard Carrier Alpha Code (4 uppercase letters, required for carriers only)',
|
||||||
|
minLength: 4,
|
||||||
|
maxLength: 4,
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@MinLength(4)
|
||||||
|
@MaxLength(4)
|
||||||
|
@Matches(/^[A-Z]{4}$/, { message: 'SCAC must be 4 uppercase letters' })
|
||||||
|
scac?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class RegisterDto {
|
export class RegisterDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 'john.doe@acme.com',
|
example: 'john.doe@acme.com',
|
||||||
@ -52,14 +132,24 @@ export class RegisterDto {
|
|||||||
@MinLength(2, { message: 'Last name must be at least 2 characters' })
|
@MinLength(2, { message: 'Last name must be at least 2 characters' })
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiPropertyOptional({
|
||||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||||
description: 'Organization ID (optional, will create default organization if not provided)',
|
description: 'Organization ID (optional - for invited users). If not provided, organization data must be provided.',
|
||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Organization data (required if organizationId is not provided)',
|
||||||
|
type: RegisterOrganizationDto,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => RegisterOrganizationDto)
|
||||||
|
@IsOptional()
|
||||||
|
organization?: RegisterOrganizationDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthResponseDto {
|
export class AuthResponseDto {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { register } from '@/lib/api';
|
import { register } from '@/lib/api';
|
||||||
|
import type { OrganizationType } from '@/types/api';
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -19,6 +20,16 @@ export default function RegisterPage() {
|
|||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
|
||||||
|
// Organization fields
|
||||||
|
const [organizationName, setOrganizationName] = useState('');
|
||||||
|
const [organizationType, setOrganizationType] = useState<OrganizationType>('FREIGHT_FORWARDER');
|
||||||
|
const [street, setStreet] = useState('');
|
||||||
|
const [city, setCity] = useState('');
|
||||||
|
const [state, setState] = useState('');
|
||||||
|
const [postalCode, setPostalCode] = useState('');
|
||||||
|
const [country, setCountry] = useState('FR');
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
@ -38,6 +49,17 @@ export default function RegisterPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate organization fields
|
||||||
|
if (!organizationName.trim()) {
|
||||||
|
setError('Le nom de l\'organisation est requis');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!street.trim() || !city.trim() || !postalCode.trim() || !country.trim()) {
|
||||||
|
setError('Tous les champs d\'adresse sont requis');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -46,7 +68,15 @@ export default function RegisterPage() {
|
|||||||
password,
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
organizationId: 'a1234567-0000-4000-8000-000000000001', // Test Organization
|
organization: {
|
||||||
|
name: organizationName,
|
||||||
|
type: organizationType,
|
||||||
|
street,
|
||||||
|
city,
|
||||||
|
state: state || undefined,
|
||||||
|
postalCode,
|
||||||
|
country: country.toUpperCase(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -181,6 +211,131 @@ export default function RegisterPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Organization Section */}
|
||||||
|
<div className="pt-4 border-t border-neutral-200">
|
||||||
|
<h3 className="text-h5 text-brand-navy mb-4">Informations de votre organisation</h3>
|
||||||
|
|
||||||
|
{/* Organization Name */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="organizationName" className="label">
|
||||||
|
Nom de l'organisation
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="organizationName"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={organizationName}
|
||||||
|
onChange={e => setOrganizationName(e.target.value)}
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="Acme Logistics"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Organization Type */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="organizationType" className="label">
|
||||||
|
Type d'organisation
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="organizationType"
|
||||||
|
value={organizationType}
|
||||||
|
onChange={e => setOrganizationType(e.target.value as OrganizationType)}
|
||||||
|
className="input w-full"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<option value="FREIGHT_FORWARDER">Transitaire</option>
|
||||||
|
<option value="SHIPPER">Expéditeur</option>
|
||||||
|
<option value="CARRIER">Transporteur</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Street Address */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="street" className="label">
|
||||||
|
Adresse
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="street"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={street}
|
||||||
|
onChange={e => setStreet(e.target.value)}
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="123 Rue de la République"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* City & Postal Code */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="city" className="label">
|
||||||
|
Ville
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="city"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={city}
|
||||||
|
onChange={e => setCity(e.target.value)}
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="Paris"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="postalCode" className="label">
|
||||||
|
Code postal
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="postalCode"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={postalCode}
|
||||||
|
onChange={e => setPostalCode(e.target.value)}
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="75001"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* State & Country */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="state" className="label">
|
||||||
|
Région (optionnel)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="state"
|
||||||
|
type="text"
|
||||||
|
value={state}
|
||||||
|
onChange={e => setState(e.target.value)}
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="Île-de-France"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="country" className="label">
|
||||||
|
Pays (code ISO)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="country"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={country}
|
||||||
|
onChange={e => setCountry(e.target.value)}
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="FR"
|
||||||
|
maxLength={2}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@ -8,12 +8,24 @@
|
|||||||
// Authentication
|
// Authentication
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface RegisterOrganizationData {
|
||||||
|
name: string;
|
||||||
|
type: OrganizationType;
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
state?: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
scac?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RegisterRequest {
|
export interface RegisterRequest {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
organizationId: string;
|
organizationId?: string; // For invited users
|
||||||
|
organization?: RegisterOrganizationData; // For new users
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user