fix register
This commit is contained in:
parent
b2f5d9968d
commit
a34c850e67
@ -29,7 +29,14 @@
|
||||
"Bash(npx ts-node:*)",
|
||||
"Bash(python3:*)",
|
||||
"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": [],
|
||||
"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`
|
||||
- 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`
|
||||
2. Create value objects if needed in `src/domain/value-objects/`
|
||||
3. Write unit tests: `entity-name.entity.spec.ts`
|
||||
4. Add repository port in `src/domain/ports/out/entity-name.repository.ts`
|
||||
5. Create ORM entity in `src/infrastructure/persistence/typeorm/entities/`
|
||||
6. Implement repository in `src/infrastructure/persistence/typeorm/repositories/`
|
||||
7. Create mapper in `src/infrastructure/persistence/typeorm/mappers/`
|
||||
8. Generate migration: `npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName`
|
||||
**Admin Features** (Branch: `users_admin`):
|
||||
- User CRUD operations (Create, Read, Update, Delete)
|
||||
- Organization management
|
||||
- Role assignment and permissions
|
||||
- Argon2 password hash generation for new users
|
||||
- Accessible at `/admin/users` (ADMIN role required)
|
||||
|
||||
### Adding a New API Endpoint
|
||||
|
||||
1. Create DTO in `src/application/dto/feature-name.dto.ts`
|
||||
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
|
||||
**Password Hashing Utility**:
|
||||
- Use `apps/backend/generate-hash.js` to generate Argon2 password hashes
|
||||
- Example: `node apps/backend/generate-hash.js mypassword`
|
||||
|
||||
## Deployment
|
||||
|
||||
@ -542,23 +504,23 @@ docker build -t xpeditis-frontend:latest -f apps/frontend/Dockerfile .
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Production Deployment (AWS)
|
||||
### Production Deployment (Portainer)
|
||||
|
||||
See [DEPLOYMENT.md](DEPLOYMENT.md) for complete instructions:
|
||||
- AWS RDS (PostgreSQL)
|
||||
- AWS ElastiCache (Redis)
|
||||
- AWS S3 (documents)
|
||||
- AWS ECS/Fargate (containers)
|
||||
- AWS ALB (load balancer)
|
||||
- AWS CloudWatch (logs + metrics)
|
||||
- Sentry (error tracking)
|
||||
See [docker/PORTAINER_DEPLOYMENT_GUIDE.md](docker/PORTAINER_DEPLOYMENT_GUIDE.md) for complete instructions:
|
||||
- Scaleway Container Registry (rg.fr-par.scw.cloud/weworkstudio)
|
||||
- Docker Swarm stack deployment
|
||||
- Traefik reverse proxy configuration
|
||||
- Environment-specific configs (staging/production)
|
||||
|
||||
**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
|
||||
- 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
|
||||
|
||||
@ -612,9 +574,32 @@ See [docker/PORTAINER_DEPLOYMENT_GUIDE.md](docker/PORTAINER_DEPLOYMENT_GUIDE.md)
|
||||
- Document APIs with Swagger decorators
|
||||
- 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
|
||||
2. Domain layer has zero external dependencies
|
||||
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
|
||||
9. Prettier formatting applied
|
||||
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 { 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 { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
|
||||
import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity';
|
||||
import { OrganizationOrmEntity } from '../../infrastructure/persistence/typeorm/entities/organization.orm-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -29,8 +32,8 @@ import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities
|
||||
}),
|
||||
}),
|
||||
|
||||
// 👇 Add this to register TypeORM repository for UserOrmEntity
|
||||
TypeOrmModule.forFeature([UserOrmEntity]),
|
||||
// 👇 Add this to register TypeORM repositories
|
||||
TypeOrmModule.forFeature([UserOrmEntity, OrganizationOrmEntity]),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
@ -40,6 +43,10 @@ import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities
|
||||
provide: USER_REPOSITORY,
|
||||
useClass: TypeOrmUserRepository,
|
||||
},
|
||||
{
|
||||
provide: ORGANIZATION_REPOSITORY,
|
||||
useClass: TypeOrmOrganizationRepository,
|
||||
},
|
||||
],
|
||||
exports: [AuthService, JwtStrategy, PassportModule],
|
||||
})
|
||||
|
||||
@ -4,14 +4,18 @@ import {
|
||||
ConflictException,
|
||||
Logger,
|
||||
Inject,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as argon2 from 'argon2';
|
||||
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
||||
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 { DEFAULT_ORG_ID } from '@infrastructure/persistence/typeorm/seeds/test-organizations.seed';
|
||||
import { RegisterOrganizationDto } from '../dto/auth-login.dto';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string; // user ID
|
||||
@ -27,7 +31,9 @@ export class AuthService {
|
||||
|
||||
constructor(
|
||||
@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 configService: ConfigService
|
||||
) {}
|
||||
@ -40,7 +46,8 @@ export class AuthService {
|
||||
password: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
organizationId?: string
|
||||
organizationId?: string,
|
||||
organizationData?: RegisterOrganizationDto
|
||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
||||
this.logger.log(`Registering new user: ${email}`);
|
||||
|
||||
@ -57,8 +64,11 @@ export class AuthService {
|
||||
parallelism: 4,
|
||||
});
|
||||
|
||||
// Validate or generate organization ID
|
||||
const finalOrganizationId = this.validateOrGenerateOrganizationId(organizationId);
|
||||
// Determine organization ID:
|
||||
// 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({
|
||||
id: uuidv4(),
|
||||
@ -209,20 +219,80 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate or generate a valid organization ID
|
||||
* If provided ID is invalid (not a UUID), generate a new one
|
||||
* Resolve organization ID for registration
|
||||
* 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 {
|
||||
// UUID v4 regex pattern
|
||||
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;
|
||||
private async resolveOrganizationId(
|
||||
organizationId?: string,
|
||||
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;
|
||||
}
|
||||
|
||||
// Use default organization "Test Freight Forwarder Inc." if not provided
|
||||
// This ID comes from the seed migration 1730000000006-SeedOrganizations
|
||||
this.logger.log(`Using default organization ID for user registration: ${DEFAULT_ORG_ID}`);
|
||||
return DEFAULT_ORG_ID;
|
||||
// Case 2: New user - create a new organization
|
||||
if (organizationData) {
|
||||
this.logger.log(`Creating new organization for user registration: ${organizationData.name}`);
|
||||
|
||||
// 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.firstName,
|
||||
dto.lastName,
|
||||
dto.organizationId
|
||||
dto.organizationId,
|
||||
dto.organization
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsString, MinLength, IsOptional, ValidateNested, IsEnum, MaxLength, Matches } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { OrganizationType } from '@domain/entities/organization.entity';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({
|
||||
@ -19,6 +21,84 @@ export class LoginDto {
|
||||
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 {
|
||||
@ApiProperty({
|
||||
example: 'john.doe@acme.com',
|
||||
@ -52,14 +132,24 @@ export class RegisterDto {
|
||||
@MinLength(2, { message: 'Last name must be at least 2 characters' })
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
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,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
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 {
|
||||
|
||||
@ -11,6 +11,7 @@ import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { register } from '@/lib/api';
|
||||
import type { OrganizationType } from '@/types/api';
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
@ -19,6 +20,16 @@ export default function RegisterPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = 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 [error, setError] = useState('');
|
||||
|
||||
@ -38,6 +49,17 @@ export default function RegisterPage() {
|
||||
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);
|
||||
|
||||
try {
|
||||
@ -46,7 +68,15 @@ export default function RegisterPage() {
|
||||
password,
|
||||
firstName,
|
||||
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');
|
||||
} catch (err: any) {
|
||||
@ -181,6 +211,131 @@ export default function RegisterPage() {
|
||||
/>
|
||||
</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 */}
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@ -8,12 +8,24 @@
|
||||
// Authentication
|
||||
// ============================================================================
|
||||
|
||||
export interface RegisterOrganizationData {
|
||||
name: string;
|
||||
type: OrganizationType;
|
||||
street: string;
|
||||
city: string;
|
||||
state?: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
scac?: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
organizationId: string;
|
||||
organizationId?: string; // For invited users
|
||||
organization?: RegisterOrganizationData; // For new users
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user