Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdf52c7041 | ||
|
|
6b832ab4ca | ||
|
|
2f2bbc8df1 | ||
|
|
528d746904 | ||
|
|
f86389cb84 | ||
|
|
cb2dcf4d3a | ||
|
|
30ddb9b631 | ||
|
|
c4356adcb2 |
166
.env.example
Normal file
166
.env.example
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
# ===========================================
|
||||||
|
# XPEDITIS Backend Environment Configuration
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# SERVER CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
SERVER_PORT=8080
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# DATABASE CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Development Database (H2) - No changes needed
|
||||||
|
SPRING_H2_CONSOLE_ENABLED=false
|
||||||
|
SPRING_H2_DATASOURCE_URL=jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||||
|
SPRING_H2_DATASOURCE_DRIVER_CLASS_NAME=org.h2.Driver
|
||||||
|
SPRING_H2_DATASOURCE_USERNAME=sa
|
||||||
|
SPRING_H2_DATASOURCE_PASSWORD=
|
||||||
|
|
||||||
|
# Production Database (MySQL) - FILL IN YOUR VALUES
|
||||||
|
SPRING_DATASOURCE_URL=jdbc:mysql://your-mysql-host:3306/xpeditis?useSSL=false&serverTimezone=UTC
|
||||||
|
SPRING_DATASOURCE_USERNAME=your_mysql_username
|
||||||
|
SPRING_DATASOURCE_PASSWORD=your_mysql_password
|
||||||
|
SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# JPA/HIBERNATE CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
SPRING_JPA_SHOW_SQL=false
|
||||||
|
SPRING_JPA_FORMAT_SQL=false
|
||||||
|
SPRING_JPA_DATABASE_PLATFORM_H2=org.hibernate.dialect.H2Dialect
|
||||||
|
SPRING_JPA_DATABASE_PLATFORM_MYSQL=org.hibernate.dialect.MySQLDialect
|
||||||
|
|
||||||
|
# Development
|
||||||
|
SPRING_JPA_HIBERNATE_DDL_AUTO_DEV=create-drop
|
||||||
|
SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_DEV=true
|
||||||
|
|
||||||
|
# Production
|
||||||
|
SPRING_JPA_HIBERNATE_DDL_AUTO_PROD=validate
|
||||||
|
SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_PROD=false
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# FLYWAY CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
# Development
|
||||||
|
SPRING_FLYWAY_ENABLED_DEV=false
|
||||||
|
|
||||||
|
# Production
|
||||||
|
SPRING_FLYWAY_ENABLED_PROD=true
|
||||||
|
SPRING_FLYWAY_LOCATIONS=classpath:db/migration/structure,classpath:db/migration/data
|
||||||
|
SPRING_FLYWAY_VALIDATE_ON_MIGRATE=true
|
||||||
|
SPRING_FLYWAY_BASELINE_ON_MIGRATE=true
|
||||||
|
SPRING_FLYWAY_BASELINE_VERSION=0
|
||||||
|
SPRING_FLYWAY_DEFAULT_SCHEMA=leblr
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# EMAIL CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Development (Mailtrap) - FILL IN YOUR MAILTRAP CREDENTIALS
|
||||||
|
SPRING_MAIL_HOST_DEV=sandbox.smtp.mailtrap.io
|
||||||
|
SPRING_MAIL_PORT_DEV=2525
|
||||||
|
SPRING_MAIL_USERNAME_DEV=your_mailtrap_username
|
||||||
|
SPRING_MAIL_PASSWORD_DEV=your_mailtrap_password
|
||||||
|
APPLICATION_EMAIL_FROM_DEV=noreply@xpeditis.local
|
||||||
|
|
||||||
|
# Production (OVH or your SMTP provider) - FILL IN YOUR VALUES
|
||||||
|
SPRING_MAIL_PROTOCOL_PROD=smtp
|
||||||
|
SPRING_MAIL_HOST_PROD=your-smtp-host
|
||||||
|
SPRING_MAIL_PORT_PROD=587
|
||||||
|
SPRING_MAIL_USERNAME_PROD=your-email@domain.com
|
||||||
|
SPRING_MAIL_PASSWORD_PROD=your_email_password
|
||||||
|
APPLICATION_EMAIL_FROM_PROD=your-email@domain.com
|
||||||
|
|
||||||
|
# Email Properties
|
||||||
|
SPRING_MAIL_SMTP_AUTH=true
|
||||||
|
SPRING_MAIL_SMTP_STARTTLS_ENABLE=true
|
||||||
|
SPRING_MAIL_SMTP_SSL_TRUST=*
|
||||||
|
SPRING_MAIL_SMTP_CONNECTION_TIMEOUT=5000
|
||||||
|
SPRING_MAIL_SMTP_TIMEOUT=3000
|
||||||
|
SPRING_MAIL_SMTP_WRITE_TIMEOUT=5000
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# OAUTH2 / GOOGLE CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
# FILL IN YOUR GOOGLE OAUTH2 CREDENTIALS
|
||||||
|
GOOGLE_CLIENT_ID=your-google-client-id-from-console
|
||||||
|
GOOGLE_CLIENT_SECRET=your-google-client-secret-from-console
|
||||||
|
|
||||||
|
# Development
|
||||||
|
OAUTH2_REDIRECT_URI_DEV=http://localhost:8080/login/oauth2/code/google
|
||||||
|
|
||||||
|
# Production - FILL IN YOUR DOMAIN
|
||||||
|
OAUTH2_REDIRECT_URI_PROD=https://your-domain.com/login/oauth2/code/google
|
||||||
|
|
||||||
|
# Google OAuth2 URLs (standard - don't change)
|
||||||
|
GOOGLE_AUTHORIZATION_URI=https://accounts.google.com/o/oauth2/v2/auth
|
||||||
|
GOOGLE_TOKEN_URI=https://oauth2.googleapis.com/token
|
||||||
|
GOOGLE_USER_INFO_URI=https://www.googleapis.com/oauth2/v2/userinfo
|
||||||
|
GOOGLE_USER_NAME_ATTRIBUTE=sub
|
||||||
|
GOOGLE_OAUTH2_SCOPE=openid,email,profile
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# SECURITY / JWT CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
# GENERATE A SECURE SECRET KEY FOR PRODUCTION
|
||||||
|
JWT_SECRET_KEY=your-very-secure-jwt-secret-key-here-make-it-long-and-random
|
||||||
|
JWT_EXPIRATION=86400000
|
||||||
|
JWT_REFRESH_TOKEN_EXPIRATION=604800000
|
||||||
|
|
||||||
|
# CSRF Configuration
|
||||||
|
APPLICATION_CSRF_ENABLED_DEV=false
|
||||||
|
APPLICATION_CSRF_ENABLED_PROD=true
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# APPLICATION SPECIFIC CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# OAuth2 Settings
|
||||||
|
APPLICATION_OAUTH2_GOOGLE_ENABLED=true
|
||||||
|
|
||||||
|
# License Configuration
|
||||||
|
APPLICATION_LICENSE_TRIAL_DURATION_DAYS=30
|
||||||
|
APPLICATION_LICENSE_TRIAL_MAX_USERS=5
|
||||||
|
APPLICATION_LICENSE_BASIC_MAX_USERS=50
|
||||||
|
APPLICATION_LICENSE_PREMIUM_MAX_USERS=200
|
||||||
|
APPLICATION_LICENSE_ENTERPRISE_MAX_USERS=1000
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# FILE UPLOAD CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
FILE_UPLOAD_DIR=/upload
|
||||||
|
SPRING_SERVLET_MULTIPART_ENABLED=true
|
||||||
|
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=50MB
|
||||||
|
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE=50MB
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# APPLICATION METADATA
|
||||||
|
# ===========================================
|
||||||
|
SPRING_APPLICATION_NAME=XPEDITIS Backend
|
||||||
|
SPRING_APPLICATION_VERSION=0.0.1-SNAPSHOT
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# ACTIVE PROFILES
|
||||||
|
# ===========================================
|
||||||
|
# Use 'dev' for development, 'prod' for production
|
||||||
|
SPRING_PROFILES_ACTIVE=dev
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# LOGGING CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
LOGGING_LEVEL_ROOT=INFO
|
||||||
|
LOGGING_LEVEL_SPRINGFRAMEWORK_BOOT_AUTOCONFIGURE=OFF
|
||||||
|
LOGGING_LEVEL_COMMONS_REQUEST_LOGGING_FILTER=INFO
|
||||||
|
LOGGING_LEVEL_HIBERNATE_SQL=OFF
|
||||||
|
LOGGING_LEVEL_HIBERNATE_TYPE=OFF
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# DEVELOPMENT ONLY
|
||||||
|
# ===========================================
|
||||||
|
# Uncomment for development debugging
|
||||||
|
# SPRING_JPA_SHOW_SQL=true
|
||||||
|
# SPRING_JPA_FORMAT_SQL=true
|
||||||
|
# LOGGING_LEVEL_ROOT=DEBUG
|
||||||
300
CLAUDE.md
Normal file
300
CLAUDE.md
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Xpeditis is a Spring Boot-based logistics/shipping application built with a modular Maven architecture. The project follows Hexagonal Architecture (Clean Architecture) principles with clear separation of concerns.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Modular Structure
|
||||||
|
The project is organized into Maven modules:
|
||||||
|
|
||||||
|
- **bootstrap**: Main application entry point and configuration (XpeditisApplication.java)
|
||||||
|
- **application**: REST controllers and web layer (AuthenticationRestController, UserRestController)
|
||||||
|
- **domain**: Core business logic split into:
|
||||||
|
- **api**: Service interfaces (UserService, AuthenticationService, etc.)
|
||||||
|
- **data**: DTOs and exceptions (UserAccount, AuthenticationRequest/Response)
|
||||||
|
- **service**: Service implementations (UserServiceImpl, AuthenticationServiceImpl)
|
||||||
|
- **spi**: Repository interfaces (UserRepository, AuthenticationRepository)
|
||||||
|
- **infrastructure**: Data access layer with JPA entities, repositories, and external integrations
|
||||||
|
- **common**: Shared utilities (CommonUtil)
|
||||||
|
|
||||||
|
### Key Technologies
|
||||||
|
- Spring Boot 3.4.1 with Java 23
|
||||||
|
- Spring Security with JWT authentication
|
||||||
|
- JPA/Hibernate for data persistence
|
||||||
|
- MapStruct for entity mapping
|
||||||
|
- Lombok for boilerplate reduction
|
||||||
|
- Flyway for database migrations
|
||||||
|
- MySQL (prod) / H2 (dev) databases
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Build and Test
|
||||||
|
```bash
|
||||||
|
# Clean build and install all modules
|
||||||
|
./mvnw clean install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
./mvnw test
|
||||||
|
|
||||||
|
# Run single test class
|
||||||
|
./mvnw test -Dtest=UserServiceImplTest
|
||||||
|
|
||||||
|
# Run specific test method
|
||||||
|
./mvnw test -Dtest=UserServiceImplTest#shouldCreateUser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
```bash
|
||||||
|
# Development (H2) - migrate database
|
||||||
|
./mvnw clean install flyway:migrate '-Dflyway.configFiles=flyway-h2.conf' -Pdev
|
||||||
|
|
||||||
|
# Production (MySQL) - migrate database
|
||||||
|
./mvnw clean install flyway:migrate -Pprod
|
||||||
|
|
||||||
|
# Start application with dev profile (default)
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
|
||||||
|
# Start with production profile
|
||||||
|
./mvnw spring-boot:run -Pprod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Operations
|
||||||
|
```bash
|
||||||
|
# Build and start services with Docker Compose
|
||||||
|
docker-compose up --build
|
||||||
|
|
||||||
|
# Start database only
|
||||||
|
docker-compose up db
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f back
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Profiles
|
||||||
|
|
||||||
|
- **dev** (default): Uses H2 in-memory database, simplified configuration
|
||||||
|
- **prod**: Uses MySQL database, includes Flyway migrations, production-ready settings
|
||||||
|
|
||||||
|
## Key Configuration Files
|
||||||
|
|
||||||
|
- `bootstrap/src/main/resources/application.yml`: Main configuration
|
||||||
|
- `bootstrap/src/main/resources/application-dev.yml`: Development overrides
|
||||||
|
- `bootstrap/src/main/resources/application-prod.yml`: Production overrides
|
||||||
|
- `infrastructure/flyway.conf`: Database migration configuration
|
||||||
|
- `compose.yml`: Docker services configuration
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
Swagger UI is available at: http://localhost:8080/swagger-ui.html
|
||||||
|
|
||||||
|
## Security Implementation
|
||||||
|
|
||||||
|
The application uses JWT-based authentication with:
|
||||||
|
- JWT token generation and validation (JwtUtil)
|
||||||
|
- Security configuration (SecurityConfiguration)
|
||||||
|
- Authentication filter (JwtAuthenticationFilter)
|
||||||
|
- Logout service (LogoutService)
|
||||||
|
- Role-based access control with Permission enum
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
- Unit tests for service layer implementations (AddressServiceImplTest, etc.)
|
||||||
|
- Repository tests for data access layer (AddressJpaRepositoryTest, etc.)
|
||||||
|
- Integration tests in bootstrap module (LeBlrApplicationTests)
|
||||||
|
- Use AssertJ for fluent assertions
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
Database migrations are in `infrastructure/src/main/resources/db/migration/`:
|
||||||
|
- `structure/V1__INIT_DB_SCHEMA.sql`: Initial schema
|
||||||
|
- `data/V1.1__INIT_DB_DATA.sql`: Initial data
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Structured logging configuration in `logback-spring.xml` with:
|
||||||
|
- Production logs written to `./logs/prod/`
|
||||||
|
- Request logging via CommonsRequestLoggingFilter
|
||||||
|
- Configurable log levels per package
|
||||||
|
|
||||||
|
## 🏗️ Checklist Architecture Hexagonale - Étapes de Validation Pré-Codage
|
||||||
|
|
||||||
|
### 📋 Phase 1 : Analyse et Compréhension du Contexte
|
||||||
|
|
||||||
|
#### ✅ 1.1 Identification du Domaine Métier
|
||||||
|
- [ ] Clarifier le domaine d'activité : Quel est le métier principal ?
|
||||||
|
- [ ] Identifier les entités métier principales : Quels sont les objets centraux ?
|
||||||
|
- [ ] Définir les règles de gestion : Quelles sont les contraintes business ?
|
||||||
|
- [ ] Mapper les cas d'usage : Que doit faire l'application ?
|
||||||
|
|
||||||
|
#### ✅ 1.2 Analyse des Besoins d'Intégration
|
||||||
|
- [ ] Identifier les acteurs externes : Qui utilise l'application ?
|
||||||
|
- [ ] Répertorier les services externes : BDD, APIs, systèmes de fichiers ?
|
||||||
|
- [ ] Définir les interfaces d'entrée : REST, GraphQL, CLI ?
|
||||||
|
- [ ] Spécifier les interfaces de sortie : Persistance, notifications, etc.
|
||||||
|
|
||||||
|
### 📐 Phase 2 : Conception Architecturale
|
||||||
|
|
||||||
|
#### ✅ 2.1 Structure des Modules Maven
|
||||||
|
- [ ] Valider la structure 4 modules :
|
||||||
|
- `domain` (cœur hexagonal)
|
||||||
|
- `application` (couche application)
|
||||||
|
- `infrastructure` (couche infrastructure)
|
||||||
|
- `bootstrap` (module de lancement)
|
||||||
|
|
||||||
|
#### ✅ 2.2 Définition des Ports
|
||||||
|
- [ ] Identifier les Ports API (interfaces exposées par le domaine) :
|
||||||
|
- Services métier que le domaine expose
|
||||||
|
- Interfaces appelées par les adaptateurs d'entrée
|
||||||
|
- [ ] Identifier les Ports SPI (interfaces requises par le domaine) :
|
||||||
|
- Repositories pour la persistance
|
||||||
|
- Services externes (notifications, intégrations)
|
||||||
|
- Interfaces que le domaine définit mais n'implémente pas
|
||||||
|
|
||||||
|
#### ✅ 2.3 Conception des Adaptateurs
|
||||||
|
- [ ] Adaptateurs d'Entrée (Driving) :
|
||||||
|
- Contrôleurs REST
|
||||||
|
- Interfaces utilisateur
|
||||||
|
- Tests automatisés
|
||||||
|
- [ ] Adaptateurs de Sortie (Driven) :
|
||||||
|
- Implémentations des repositories
|
||||||
|
- Clients pour services externes
|
||||||
|
- Systèmes de fichiers/messaging
|
||||||
|
|
||||||
|
### 🏛️ Phase 3 : Architecture en Couches
|
||||||
|
|
||||||
|
#### ✅ 3.1 Module Domain (Cœur)
|
||||||
|
- [ ] Vérifier l'isolation complète : Aucune dépendance externe
|
||||||
|
- [ ] Structure du package :
|
||||||
|
```
|
||||||
|
com.{project}.domain/
|
||||||
|
├── model/ # Entités métier
|
||||||
|
├── service/ # Services du domaine
|
||||||
|
├── port/
|
||||||
|
│ ├── in/ # Ports d'entrée (API)
|
||||||
|
│ └── out/ # Ports de sortie (SPI)
|
||||||
|
└── exception/ # Exceptions métier
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ 3.2 Module Application
|
||||||
|
- [ ] Responsabilités claires : Exposition des services
|
||||||
|
- [ ] Structure du package :
|
||||||
|
```
|
||||||
|
com.{project}.application/
|
||||||
|
├── controller/ # Contrôleurs REST
|
||||||
|
├── dto/ # Data Transfer Objects
|
||||||
|
├── mapper/ # Mappers DTO ↔ Domain
|
||||||
|
└── config/ # Configuration Spring
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ 3.3 Module Infrastructure
|
||||||
|
- [ ] Implémentation des SPI : Tous les ports de sortie
|
||||||
|
- [ ] Structure du package :
|
||||||
|
```
|
||||||
|
com.{project}.infrastructure/
|
||||||
|
├── adapter/
|
||||||
|
│ ├── in/ # Adaptateurs d'entrée (si applicable)
|
||||||
|
│ └── out/ # Adaptateurs de sortie
|
||||||
|
├── repository/ # Implémentations JPA
|
||||||
|
├── entity/ # Entités JPA
|
||||||
|
└── mapper/ # Mappers JPA ↔ Domain
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ 3.4 Module bootstrap
|
||||||
|
- [ ] Point d'entrée unique : Classe @SpringBootApplication
|
||||||
|
- [ ] Assemblage des dépendances : Configuration complète
|
||||||
|
- [ ] Dépendances vers tous les modules
|
||||||
|
|
||||||
|
### 🔧 Phase 4 : Validation Technique
|
||||||
|
|
||||||
|
#### ✅ 4.1 Gestion des Dépendances
|
||||||
|
- [ ] Module domain : Aucune dépendance externe (sauf tests)
|
||||||
|
- [ ] Module application : Dépend uniquement de domain
|
||||||
|
- [ ] Module infrastructure : Dépend uniquement de domain
|
||||||
|
- [ ] Module bootstrap : Dépend de tous les autres modules
|
||||||
|
|
||||||
|
#### ✅ 4.2 Respect des Patterns
|
||||||
|
- [ ] Domain-Driven Design : Entités, Value Objects, Aggregates
|
||||||
|
- [ ] SOLID Principles : SRP, DIP, ISP appliqués
|
||||||
|
- [ ] Repository Pattern : Abstraction de la persistance
|
||||||
|
- [ ] Clean Architecture : Règle de dépendance respectée
|
||||||
|
|
||||||
|
#### ✅ 4.3 Configuration Spring
|
||||||
|
- [ ] Injection de dépendance : Utilisation de @Configuration
|
||||||
|
- [ ] Éviter @Component dans le domaine
|
||||||
|
- [ ] Gestion des transactions : @Transactional dans l'application
|
||||||
|
|
||||||
|
### 🧪 Phase 5 : Stratégie de Test
|
||||||
|
|
||||||
|
#### ✅ 5.1 Tests par Couche
|
||||||
|
- [ ] Tests unitaires du domaine : Sans dépendances externes
|
||||||
|
- [ ] Tests d'intégration : Par adaptateur spécifique
|
||||||
|
- [ ] Tests end-to-end : Flux complets
|
||||||
|
- [ ] Mocking strategy : Interfaces bien définies
|
||||||
|
|
||||||
|
#### ✅ 5.2 Testabilité
|
||||||
|
- [ ] Isolation du domaine : Tests sans Spring Context
|
||||||
|
- [ ] Adaptateurs mockables : Interfaces clairement définies
|
||||||
|
- [ ] Données de test : Jeux de données cohérents
|
||||||
|
|
||||||
|
### 📝 Phase 6 : Naming et Conventions
|
||||||
|
|
||||||
|
#### ✅ 6.1 Conventions de Nommage
|
||||||
|
- [ ] Ports : Suffixe "Port" ou noms explicites d'interfaces
|
||||||
|
- [ ] Adaptateurs : Suffixe "Adapter"
|
||||||
|
- [ ] Services : Suffixe "Service"
|
||||||
|
- [ ] Repositories : Suffixe "Repository"
|
||||||
|
|
||||||
|
#### ✅ 6.2 Structure des Packages
|
||||||
|
- [ ] Cohérence : Nommage uniforme entre modules
|
||||||
|
- [ ] Lisibilité : Structure claire et intuitive
|
||||||
|
- [ ] Séparation : Concerns bien séparés
|
||||||
|
|
||||||
|
### 🎯 Phase 7 : Questions de Validation Finale
|
||||||
|
|
||||||
|
#### ✅ 7.1 Questions Critiques à Se Poser
|
||||||
|
- [ ] Le domaine est-il complètement isolé ?
|
||||||
|
- [ ] Les dépendances pointent-elles vers l'intérieur ?
|
||||||
|
- [ ] Peut-on tester le domaine sans Spring ?
|
||||||
|
- [ ] Peut-on changer de base de données sans impact sur le domaine ?
|
||||||
|
- [ ] Peut-on ajouter une nouvelle interface (GraphQL) facilement ?
|
||||||
|
|
||||||
|
#### ✅ 7.2 Validation des Flux
|
||||||
|
- [ ] Flux d'entrée : HTTP → Contrôleur → Service → Port API
|
||||||
|
- [ ] Flux de sortie : Port SPI → Adaptateur → Base/Service externe
|
||||||
|
- [ ] Mapping : DTO ↔ Domain ↔ JPA entities clairement défini
|
||||||
|
|
||||||
|
### 🚦 Phase 8 : Checklist Pré-Codage
|
||||||
|
|
||||||
|
#### ✅ 8.1 Avant de Commencer
|
||||||
|
- [ ] Architecture validée avec le demandeur
|
||||||
|
- [ ] Modules Maven structure approuvée
|
||||||
|
- [ ] Ports et Adaptateurs identifiés et documentés
|
||||||
|
- [ ] Stack technique confirmée (Spring Boot, H2, MapStruct)
|
||||||
|
- [ ] Stratégie de tests définie
|
||||||
|
|
||||||
|
#### ✅ 8.2 Ordre de Développement Recommandé
|
||||||
|
1. Module domain : Entités, services, ports
|
||||||
|
2. Module infrastructure : Adaptateurs de sortie, repositories
|
||||||
|
3. Module application : Contrôleurs, DTOs, mappers
|
||||||
|
4. Module bootstrap : Configuration, assemblage
|
||||||
|
5. Tests : Unitaires → Intégration → E2E
|
||||||
|
|
||||||
|
### 💡 Points d'Attention Spéciaux
|
||||||
|
|
||||||
|
#### ⚠️ Pièges à Éviter
|
||||||
|
- Dépendances circulaires entre modules
|
||||||
|
- Logique métier dans les adaptateurs
|
||||||
|
- Annotations Spring dans le domaine
|
||||||
|
- Couplage fort entre couches
|
||||||
|
- Tests qui nécessitent le contexte Spring pour le domaine
|
||||||
|
|
||||||
|
#### 🎯 Objectifs à Atteindre
|
||||||
|
- Domaine pur et testable isolément
|
||||||
|
- Flexibilité pour changer d'adaptateurs
|
||||||
|
- Maintenabilité et évolutivité
|
||||||
|
- Séparation claire des responsabilités
|
||||||
|
- Code réutilisable et modulaire
|
||||||
10
Dockerfile
10
Dockerfile
@ -12,8 +12,8 @@ COPY application/pom.xml application/
|
|||||||
COPY infrastructure/pom.xml infrastructure/
|
COPY infrastructure/pom.xml infrastructure/
|
||||||
COPY bootstrap/pom.xml bootstrap/
|
COPY bootstrap/pom.xml bootstrap/
|
||||||
COPY common/pom.xml common/
|
COPY common/pom.xml common/
|
||||||
ARG XPEDITIS_PROFILE
|
ARG SPRING_PROFILES_ACTIVE
|
||||||
RUN mvn -P${XPEDITIS_PROFILE} dependency:go-offline \
|
RUN mvn -P${SPRING_PROFILES_ACTIVE} dependency:go-offline \
|
||||||
&& mvn dependency:get -Dartifact=net.bytebuddy:byte-buddy-agent:1.15.11 -B \
|
&& mvn dependency:get -Dartifact=net.bytebuddy:byte-buddy-agent:1.15.11 -B \
|
||||||
&& mvn dependency:get -Dartifact=org.mapstruct:mapstruct-processor:1.6.3 -B \
|
&& mvn dependency:get -Dartifact=org.mapstruct:mapstruct-processor:1.6.3 -B \
|
||||||
&& mvn dependency:get -Dartifact=org.projectlombok:lombok-mapstruct-binding:0.2.0 -B \
|
&& mvn dependency:get -Dartifact=org.projectlombok:lombok-mapstruct-binding:0.2.0 -B \
|
||||||
@ -30,8 +30,8 @@ COPY application/src application/src
|
|||||||
COPY infrastructure/src infrastructure/src
|
COPY infrastructure/src infrastructure/src
|
||||||
COPY bootstrap/src bootstrap/src
|
COPY bootstrap/src bootstrap/src
|
||||||
COPY common/src common/src
|
COPY common/src common/src
|
||||||
ARG XPEDITIS_PROFILE
|
ARG SPRING_PROFILES_ACTIVE
|
||||||
RUN mvn -o clean package -P${XPEDITIS_PROFILE} -Dmaven.test.skip=true -DskipTests
|
RUN mvn -o clean package -P${SPRING_PROFILES_ACTIVE} -Dmaven.test.skip=true -DskipTests
|
||||||
|
|
||||||
#FROM builder AS test
|
#FROM builder AS test
|
||||||
#RUN mvn -o clean test
|
#RUN mvn -o clean test
|
||||||
@ -45,5 +45,3 @@ RUN chmod +x wait-for-it.sh
|
|||||||
COPY entrypoint.sh ./
|
COPY entrypoint.sh ./
|
||||||
RUN chmod +x entrypoint.sh
|
RUN chmod +x entrypoint.sh
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
#CMD ["java", "-jar", "-Dserver.port=8080", "leblr.jar"]
|
|
||||||
#HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "--fail", "http://localhost:8080/actuator/health", "|| exit 1" ]
|
|
||||||
|
|||||||
@ -17,8 +17,8 @@ COPY application/pom.xml application/
|
|||||||
COPY infrastructure/pom.xml infrastructure/
|
COPY infrastructure/pom.xml infrastructure/
|
||||||
COPY bootstrap/pom.xml bootstrap/
|
COPY bootstrap/pom.xml bootstrap/
|
||||||
COPY common/pom.xml common/
|
COPY common/pom.xml common/
|
||||||
ARG XPEDITIS_PROFILE
|
ARG SPRING_PROFILES_ACTIVE
|
||||||
RUN ./mvnw -P${XPEDITIS_PROFILE} dependency:go-offline \
|
RUN ./mvnw -P${SPRING_PROFILES_ACTIVE} dependency:go-offline \
|
||||||
&& ./mvnw dependency:get -Dartifact=net.bytebuddy:byte-buddy-agent:1.15.11 -B \
|
&& ./mvnw dependency:get -Dartifact=net.bytebuddy:byte-buddy-agent:1.15.11 -B \
|
||||||
&& ./mvnw dependency:get -Dartifact=org.mapstruct:mapstruct-processor:1.6.3 -B \
|
&& ./mvnw dependency:get -Dartifact=org.mapstruct:mapstruct-processor:1.6.3 -B \
|
||||||
&& ./mvnw dependency:get -Dartifact=org.projectlombok:lombok-mapstruct-binding:0.2.0 -B
|
&& ./mvnw dependency:get -Dartifact=org.projectlombok:lombok-mapstruct-binding:0.2.0 -B
|
||||||
|
|||||||
155
ENV_SETUP.md
Normal file
155
ENV_SETUP.md
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# Environment Configuration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Ce projet utilise des variables d'environnement pour la configuration. Toutes les variables ont été centralisées dans des fichiers `.env`.
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Copy the example file
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Edit the `.env` file with your values
|
||||||
|
|
||||||
|
#### Required for Development:
|
||||||
|
- `GOOGLE_CLIENT_ID` - Votre Google OAuth2 Client ID
|
||||||
|
- `GOOGLE_CLIENT_SECRET` - Votre Google OAuth2 Client Secret
|
||||||
|
- `JWT_SECRET_KEY` - Clé secrète pour JWT (générez une clé sécurisée)
|
||||||
|
|
||||||
|
#### Required for Production:
|
||||||
|
- Database credentials (`SPRING_DATASOURCE_*`)
|
||||||
|
- Email configuration (`SPRING_MAIL_*`)
|
||||||
|
- Production OAuth2 redirect URI
|
||||||
|
- Secure JWT secret key
|
||||||
|
|
||||||
|
## Environment Profiles
|
||||||
|
|
||||||
|
### Development (`SPRING_PROFILES_ACTIVE=dev`)
|
||||||
|
- Uses H2 in-memory database
|
||||||
|
- Uses Mailtrap for email testing
|
||||||
|
- CSRF disabled
|
||||||
|
- Flyway disabled
|
||||||
|
|
||||||
|
### Production (`SPRING_PROFILES_ACTIVE=prod`)
|
||||||
|
- Uses MySQL database
|
||||||
|
- Uses production SMTP server
|
||||||
|
- CSRF enabled
|
||||||
|
- Flyway enabled for migrations
|
||||||
|
|
||||||
|
## Key Variables by Category
|
||||||
|
|
||||||
|
### 🗄️ Database
|
||||||
|
```bash
|
||||||
|
# Development (H2)
|
||||||
|
SPRING_H2_DATASOURCE_URL=jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||||
|
|
||||||
|
# Production (MySQL)
|
||||||
|
SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/xpeditis
|
||||||
|
SPRING_DATASOURCE_USERNAME=your_username
|
||||||
|
SPRING_DATASOURCE_PASSWORD=your_password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📧 Email
|
||||||
|
```bash
|
||||||
|
# Development (Mailtrap)
|
||||||
|
SPRING_MAIL_HOST_DEV=sandbox.smtp.mailtrap.io
|
||||||
|
SPRING_MAIL_USERNAME_DEV=your_mailtrap_username
|
||||||
|
SPRING_MAIL_PASSWORD_DEV=your_mailtrap_password
|
||||||
|
|
||||||
|
# Production
|
||||||
|
SPRING_MAIL_HOST_PROD=your-smtp-host
|
||||||
|
SPRING_MAIL_USERNAME_PROD=your-email@domain.com
|
||||||
|
SPRING_MAIL_PASSWORD_PROD=your_password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔐 Security & OAuth2
|
||||||
|
```bash
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET_KEY=your-secure-secret-key
|
||||||
|
JWT_EXPIRATION=86400000
|
||||||
|
|
||||||
|
# Google OAuth2
|
||||||
|
GOOGLE_CLIENT_ID=your-google-client-id
|
||||||
|
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📋 License Limits
|
||||||
|
```bash
|
||||||
|
APPLICATION_LICENSE_TRIAL_MAX_USERS=5
|
||||||
|
APPLICATION_LICENSE_BASIC_MAX_USERS=50
|
||||||
|
APPLICATION_LICENSE_PREMIUM_MAX_USERS=200
|
||||||
|
APPLICATION_LICENSE_ENTERPRISE_MAX_USERS=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
### 🛡️ Important Security Practices:
|
||||||
|
1. **Never commit `.env` files** to version control
|
||||||
|
2. **Generate secure JWT secret keys** for production
|
||||||
|
3. **Use environment-specific secrets**
|
||||||
|
4. **Rotate keys regularly** in production
|
||||||
|
|
||||||
|
### 🔑 JWT Secret Key Generation:
|
||||||
|
```bash
|
||||||
|
# Generate a secure 256-bit key
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Or use online generator (ensure HTTPS):
|
||||||
|
# https://generate-secret.vercel.app/32
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage in Application
|
||||||
|
|
||||||
|
### 🎯 Easy Startup Scripts
|
||||||
|
Use the provided scripts for easy development and production startup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development mode (loads .env automatically)
|
||||||
|
./run-dev.sh
|
||||||
|
|
||||||
|
# Production mode (validates required variables)
|
||||||
|
./run-prod.sh
|
||||||
|
|
||||||
|
# Or traditional way
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spring Boot Configuration Loading Order:
|
||||||
|
1. System environment variables (highest priority)
|
||||||
|
2. `.env` file variables
|
||||||
|
3. `application-{profile}.yml` files
|
||||||
|
4. `application.yml` (lowest priority)
|
||||||
|
|
||||||
|
### 🔧 All YAML files now support .env variables:
|
||||||
|
- `application.yml` - Base configuration with .env support
|
||||||
|
- `application-dev.yml` - Development overrides with .env support
|
||||||
|
- `application-prod.yml` - Production overrides with .env support
|
||||||
|
|
||||||
|
## Docker Support
|
||||||
|
|
||||||
|
For Docker deployments, mount the `.env` file:
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--env-file .env \
|
||||||
|
-p 8080:8080 \
|
||||||
|
xpeditis-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues:
|
||||||
|
1. **Missing variables**: Check `.env.example` for required variables
|
||||||
|
2. **Database connection**: Verify database credentials and host
|
||||||
|
3. **Email not sending**: Check SMTP configuration
|
||||||
|
4. **OAuth2 not working**: Verify Google Console settings and redirect URIs
|
||||||
|
|
||||||
|
### Debugging:
|
||||||
|
```bash
|
||||||
|
# Enable debug logging
|
||||||
|
LOGGING_LEVEL_ROOT=DEBUG
|
||||||
|
|
||||||
|
# Show SQL queries
|
||||||
|
SPRING_JPA_SHOW_SQL=true
|
||||||
|
SPRING_JPA_FORMAT_SQL=true
|
||||||
|
```
|
||||||
@ -1,7 +0,0 @@
|
|||||||
# leblr-backend
|
|
||||||
|
|
||||||
Le BLR backend side<br>
|
|
||||||
SWAGGER UI : http://localhost:8080/swagger-ui.html<br>
|
|
||||||
<br>
|
|
||||||
.\mvnw clean install flyway:migrate -Pprod<br>
|
|
||||||
.\mvnw clean install flyway:migrate '-Dflyway.configFiles=flyway-h2.conf' -Pdev<br>
|
|
||||||
446
Xpeditis_API_Collection.postman_collection.json
Normal file
446
Xpeditis_API_Collection.postman_collection.json
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"name": "Xpeditis API Collection",
|
||||||
|
"description": "Collection complète des endpoints pour l'API Xpeditis - Plateforme de shipping maritime et logistique",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"bearer": [
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "{{jwt_token}}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "base_url",
|
||||||
|
"value": "http://localhost:8080",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "jwt_token",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "refresh_token",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "🏠 Service Info",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Service Information",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": [""]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Endpoints pour récupérer les informations du service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "🔐 Authentication",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Login",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"if (pm.response.code === 200) {",
|
||||||
|
" var jsonData = pm.response.json();",
|
||||||
|
" pm.collectionVariables.set('jwt_token', jsonData.accessToken);",
|
||||||
|
" pm.collectionVariables.set('refresh_token', jsonData.refreshToken);",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"username\": \"user@example.com\",\n \"password\": \"Password123!\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/auth/login",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "auth", "login"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Register",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"if (pm.response.code === 201) {",
|
||||||
|
" var jsonData = pm.response.json();",
|
||||||
|
" pm.collectionVariables.set('jwt_token', jsonData.accessToken);",
|
||||||
|
" pm.collectionVariables.set('refresh_token', jsonData.refreshToken);",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john.doe@example.com\",\n \"username\": \"johndoe\",\n \"password\": \"Password123!\",\n \"confirmPassword\": \"Password123!\",\n \"phoneNumber\": \"+33123456789\",\n \"companyName\": \"Maritime Solutions Inc\",\n \"companyCountry\": \"France\",\n \"authProvider\": \"LOCAL\",\n \"privacyPolicyAccepted\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/auth/register",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "auth", "register"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Google OAuth",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"if (pm.response.code === 200) {",
|
||||||
|
" var jsonData = pm.response.json();",
|
||||||
|
" pm.collectionVariables.set('jwt_token', jsonData.accessToken);",
|
||||||
|
" pm.collectionVariables.set('refresh_token', jsonData.refreshToken);",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"googleToken\": \"google_oauth_token_here\",\n \"companyName\": \"Optional Company Name\",\n \"phoneNumber\": \"+33123456789\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/auth/google",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "auth", "google"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Refresh Token",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"if (pm.response.code === 200) {",
|
||||||
|
" var jsonData = pm.response.json();",
|
||||||
|
" pm.collectionVariables.set('jwt_token', jsonData.accessToken);",
|
||||||
|
" pm.collectionVariables.set('refresh_token', jsonData.refreshToken);",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"refreshToken\": \"{{refresh_token}}\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/auth/refresh",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "auth", "refresh"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Logout",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{jwt_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/auth/logout",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "auth", "logout"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Endpoints d'authentification et gestion des sessions JWT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "👤 Profile Management",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Profile",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{jwt_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/profile",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "profile"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Profile",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{jwt_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"firstName\": \"John Updated\",\n \"lastName\": \"Doe Updated\",\n \"phoneNumber\": \"+33987654321\",\n \"username\": \"johnupdated\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/profile",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "profile"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Endpoints de gestion du profil utilisateur via ProfileController"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "👥 User Management",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get User Profile (Alternative)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{jwt_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/users/profile",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "users", "profile"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update User Profile (Alternative)",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{jwt_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"firstName\": \"John Updated\",\n \"lastName\": \"Doe Updated\",\n \"phoneNumber\": \"+33987654321\",\n \"username\": \"johnupdated\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/users/profile",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "users", "profile"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Change Password",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{jwt_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"currentPassword\": \"Password123!\",\n \"newPassword\": \"NewPassword123!\",\n \"confirmationPassword\": \"NewPassword123!\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/users/password",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "users", "password"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Account",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{jwt_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/users/account",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "users", "account"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "List Users (Admin Only)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{jwt_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/users?page=0&size=10",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "users"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "page",
|
||||||
|
"value": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "size",
|
||||||
|
"value": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "companyId",
|
||||||
|
"value": "uuid-company-id",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Endpoints de gestion des utilisateurs - profil, mot de passe, suppression de compte"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -29,6 +29,14 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
|||||||
@ -2,35 +2,101 @@ package com.dh7789dev.xpeditis.controller.api.v1;
|
|||||||
|
|
||||||
import com.dh7789dev.xpeditis.AuthenticationService;
|
import com.dh7789dev.xpeditis.AuthenticationService;
|
||||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
import com.dh7789dev.xpeditis.dto.request.GoogleAuthRequest;
|
||||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||||
|
import com.dh7789dev.xpeditis.dto.response.UserResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(value = "${apiPrefix}/api/v1/auth",
|
@RequestMapping(value = "${apiPrefix}/api/v1/auth", produces = APPLICATION_JSON_VALUE)
|
||||||
produces = APPLICATION_JSON_VALUE)
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Tag(name = "Authentication", description = "Authentication and registration endpoints")
|
||||||
public class AuthenticationRestController {
|
public class AuthenticationRestController {
|
||||||
|
|
||||||
private final AuthenticationService service;
|
private final AuthenticationService authenticationService;
|
||||||
|
|
||||||
|
@Operation(summary = "User login", description = "Authenticate user with email/username and password")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Successfully authenticated")
|
||||||
|
@ApiResponse(responseCode = "401", description = "Invalid credentials")
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<AuthenticationResponse> authenticate(
|
public ResponseEntity<AuthenticationResponse> authenticate(
|
||||||
@RequestBody @Valid AuthenticationRequest request) {
|
@RequestBody @Valid AuthenticationRequest request) {
|
||||||
return ResponseEntity.ok(service.authenticate(request));
|
log.info("Authentication attempt for user: {}", request.getUsername());
|
||||||
|
AuthenticationResponse response = authenticationService.authenticate(request);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "User registration", description = "Register a new user account")
|
||||||
|
@ApiResponse(responseCode = "201", description = "User successfully registered")
|
||||||
|
@ApiResponse(responseCode = "400", description = "Invalid registration data")
|
||||||
|
@ApiResponse(responseCode = "409", description = "User already exists")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<AuthenticationResponse> register(
|
public ResponseEntity<AuthenticationResponse> register(
|
||||||
@RequestBody @Valid RegisterRequest request) {
|
@RequestBody @Valid RegisterRequest request) {
|
||||||
return ResponseEntity.ok(service.register(request));
|
log.info("Registration attempt for email: {}", request.getEmail());
|
||||||
|
AuthenticationResponse response = authenticationService.register(request);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Google OAuth authentication", description = "Authenticate user with Google OAuth token")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Successfully authenticated with Google")
|
||||||
|
@ApiResponse(responseCode = "401", description = "Invalid Google token")
|
||||||
|
@PostMapping("/google")
|
||||||
|
public ResponseEntity<AuthenticationResponse> authenticateWithGoogle(
|
||||||
|
@RequestBody @Valid GoogleAuthRequest request) {
|
||||||
|
log.info("Google OAuth authentication attempt");
|
||||||
|
AuthenticationResponse response = authenticationService.authenticateWithGoogle(request.getGoogleToken());
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Refresh token", description = "Get a new access token using refresh token")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Token successfully refreshed")
|
||||||
|
@ApiResponse(responseCode = "401", description = "Invalid refresh token")
|
||||||
|
@PostMapping("/refresh")
|
||||||
|
public ResponseEntity<AuthenticationResponse> refreshToken(
|
||||||
|
@RequestBody RefreshTokenRequest request) {
|
||||||
|
log.info("Token refresh attempt");
|
||||||
|
AuthenticationResponse response = authenticationService.refreshToken(request.getRefreshToken());
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "User logout", description = "Invalidate user session and tokens")
|
||||||
|
@ApiResponse(responseCode = "204", description = "Successfully logged out")
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public ResponseEntity<Void> logout(HttpServletRequest request) {
|
||||||
|
String authHeader = request.getHeader("Authorization");
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
authenticationService.logout(token);
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner class for refresh token request
|
||||||
|
public static class RefreshTokenRequest {
|
||||||
|
@Valid
|
||||||
|
private String refreshToken;
|
||||||
|
|
||||||
|
public String getRefreshToken() {
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRefreshToken(String refreshToken) {
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,103 @@
|
|||||||
|
package com.dh7789dev.xpeditis.controller.api.v1;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.UserService;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.UpdateProfileRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.response.UserResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping(value = "${apiPrefix}/api/v1/profile", produces = APPLICATION_JSON_VALUE)
|
||||||
|
@Tag(name = "Profile", description = "User profile management endpoints")
|
||||||
|
public class ProfileController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@Operation(summary = "Get user profile", description = "Retrieve the current user's profile information")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Profile retrieved successfully")
|
||||||
|
@ApiResponse(responseCode = "401", description = "Unauthorized")
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<UserResponse> getProfile(Authentication authentication) {
|
||||||
|
log.info("Profile retrieval request for user: {}", authentication.getName());
|
||||||
|
|
||||||
|
UserAccount userAccount = userService.findByUsername(authentication.getName())
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
UserResponse userResponse = mapToUserResponse(userAccount);
|
||||||
|
return ResponseEntity.ok(userResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Update profile", description = "Update the current user's profile information")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Profile updated successfully")
|
||||||
|
@ApiResponse(responseCode = "400", description = "Invalid profile data")
|
||||||
|
@ApiResponse(responseCode = "401", description = "Unauthorized")
|
||||||
|
@PutMapping
|
||||||
|
public ResponseEntity<UserResponse> updateProfile(
|
||||||
|
@RequestBody @Valid UpdateProfileRequest request,
|
||||||
|
Authentication authentication) {
|
||||||
|
log.info("Profile update request for user: {}", authentication.getName());
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
UserAccount currentUser = userService.findByUsername(authentication.getName())
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
// Update fields if provided
|
||||||
|
if (request.getFirstName() != null && !request.getFirstName().trim().isEmpty()) {
|
||||||
|
currentUser.setFirstName(request.getFirstName().trim());
|
||||||
|
}
|
||||||
|
if (request.getLastName() != null && !request.getLastName().trim().isEmpty()) {
|
||||||
|
currentUser.setLastName(request.getLastName().trim());
|
||||||
|
}
|
||||||
|
if (request.getPhoneNumber() != null && !request.getPhoneNumber().trim().isEmpty()) {
|
||||||
|
currentUser.setPhoneNumber(new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(request.getPhoneNumber()));
|
||||||
|
}
|
||||||
|
if (request.getUsername() != null && !request.getUsername().trim().isEmpty()) {
|
||||||
|
// Check if username is available
|
||||||
|
if (!userService.existsByUsername(request.getUsername()) ||
|
||||||
|
request.getUsername().equals(currentUser.getUsername())) {
|
||||||
|
currentUser.setUsername(request.getUsername().trim());
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Username already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated user
|
||||||
|
UserAccount updatedUser = userService.updateProfile(currentUser);
|
||||||
|
UserResponse userResponse = mapToUserResponse(updatedUser);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(userResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to convert UserAccount to UserResponse
|
||||||
|
private UserResponse mapToUserResponse(UserAccount userAccount) {
|
||||||
|
return UserResponse.builder()
|
||||||
|
.id(userAccount.getId())
|
||||||
|
.firstName(userAccount.getFirstName())
|
||||||
|
.lastName(userAccount.getLastName())
|
||||||
|
.email(userAccount.getEmail() != null ? userAccount.getEmail().getValue() : null)
|
||||||
|
.username(userAccount.getUsername())
|
||||||
|
.phoneNumber(userAccount.getPhoneNumber() != null ? userAccount.getPhoneNumber().getValue() : null)
|
||||||
|
.authProvider(userAccount.getAuthProvider())
|
||||||
|
.privacyPolicyAccepted(userAccount.isPrivacyPolicyAccepted())
|
||||||
|
.privacyPolicyAcceptedAt(userAccount.getPrivacyPolicyAcceptedAt())
|
||||||
|
.createdAt(userAccount.getCreatedAt())
|
||||||
|
.lastLoginAt(userAccount.getLastLoginAt())
|
||||||
|
.isActive(userAccount.isActive())
|
||||||
|
.role(userAccount.getRole() != null ? userAccount.getRole().name() : null)
|
||||||
|
.companyName(userAccount.getCompany() != null ? userAccount.getCompany().getName() : null)
|
||||||
|
.companyId(userAccount.getCompany() != null ? userAccount.getCompany().getId() : null)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,38 +1,153 @@
|
|||||||
package com.dh7789dev.xpeditis.controller.api.v1;
|
package com.dh7789dev.xpeditis.controller.api.v1;
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.UserService;
|
import com.dh7789dev.xpeditis.UserService;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.UpdateProfileRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.response.UserResponse;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.PatchMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@Validated
|
@Validated
|
||||||
@RequestMapping(value = "${apiPrefix}/api/v1/users",
|
@RequiredArgsConstructor
|
||||||
produces = APPLICATION_JSON_VALUE)
|
@RequestMapping(value = "${apiPrefix}/api/v1/users", produces = APPLICATION_JSON_VALUE)
|
||||||
|
@Tag(name = "User Management", description = "User profile and management endpoints")
|
||||||
public class UserRestController {
|
public class UserRestController {
|
||||||
|
|
||||||
private final UserService service;
|
private final UserService userService;
|
||||||
|
|
||||||
public UserRestController(UserService service) {
|
@Operation(summary = "Get current user profile", description = "Retrieve the profile of the authenticated user")
|
||||||
this.service = service;
|
@ApiResponse(responseCode = "200", description = "User profile retrieved successfully")
|
||||||
|
@GetMapping("/profile")
|
||||||
|
public ResponseEntity<UserResponse> getProfile(Authentication authentication) {
|
||||||
|
log.info("Profile request for user: {}", authentication.getName());
|
||||||
|
|
||||||
|
// Get user by username from authentication
|
||||||
|
UserAccount userAccount = userService.findByUsername(authentication.getName())
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
// Convert to response DTO
|
||||||
|
UserResponse userResponse = mapToUserResponse(userAccount);
|
||||||
|
return ResponseEntity.ok(userResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "Change password of the connected user")
|
@Operation(summary = "Update user profile", description = "Update profile information of the authenticated user")
|
||||||
@PatchMapping("/password")
|
@ApiResponse(responseCode = "200", description = "Profile updated successfully")
|
||||||
|
@ApiResponse(responseCode = "400", description = "Invalid profile data")
|
||||||
|
@PutMapping("/profile")
|
||||||
|
public ResponseEntity<UserResponse> updateProfile(
|
||||||
|
@RequestBody @Valid UpdateProfileRequest request,
|
||||||
|
Authentication authentication) {
|
||||||
|
log.info("Profile update request for user: {}", authentication.getName());
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
UserAccount currentUser = userService.findByUsername(authentication.getName())
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if (request.getFirstName() != null) {
|
||||||
|
currentUser.setFirstName(request.getFirstName());
|
||||||
|
}
|
||||||
|
if (request.getLastName() != null) {
|
||||||
|
currentUser.setLastName(request.getLastName());
|
||||||
|
}
|
||||||
|
if (request.getPhoneNumber() != null) {
|
||||||
|
currentUser.setPhoneNumber(new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(request.getPhoneNumber()));
|
||||||
|
}
|
||||||
|
if (request.getUsername() != null && !userService.existsByUsername(request.getUsername())) {
|
||||||
|
currentUser.setUsername(request.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated user
|
||||||
|
UserAccount updatedUser = userService.updateProfile(currentUser);
|
||||||
|
UserResponse userResponse = mapToUserResponse(updatedUser);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(userResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Change password", description = "Change password of the authenticated user")
|
||||||
|
@ApiResponse(responseCode = "204", description = "Password changed successfully")
|
||||||
|
@ApiResponse(responseCode = "400", description = "Invalid password data")
|
||||||
|
@PutMapping("/password")
|
||||||
public ResponseEntity<Void> changePassword(
|
public ResponseEntity<Void> changePassword(
|
||||||
@RequestBody ChangePasswordRequest request,
|
@RequestBody @Valid ChangePasswordRequest request,
|
||||||
Principal connectedUser) {
|
Principal connectedUser) {
|
||||||
service.changePassword(request, connectedUser);
|
log.info("Password change request for user: {}", connectedUser.getName());
|
||||||
return new ResponseEntity<>(HttpStatus.OK);
|
userService.changePassword(request, connectedUser);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Delete user account", description = "Delete the authenticated user's account")
|
||||||
|
@ApiResponse(responseCode = "204", description = "Account deleted successfully")
|
||||||
|
@DeleteMapping("/account")
|
||||||
|
public ResponseEntity<Void> deleteAccount(Authentication authentication) {
|
||||||
|
log.info("Account deletion request for user: {}", authentication.getName());
|
||||||
|
|
||||||
|
UserAccount user = userService.findByUsername(authentication.getName())
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
userService.deleteUser(user.getId());
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "List users", description = "List all users (Admin only)")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Users retrieved successfully")
|
||||||
|
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<Page<UserResponse>> listUsers(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(required = false) UUID companyId) {
|
||||||
|
log.info("User list request - page: {}, size: {}, companyId: {}", page, size, companyId);
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(page, size);
|
||||||
|
|
||||||
|
// TODO: Implement pagination and company filtering in service layer
|
||||||
|
// For now, return an empty page
|
||||||
|
Page<UserResponse> users = Page.empty(pageable);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to convert UserAccount to UserResponse
|
||||||
|
private UserResponse mapToUserResponse(UserAccount userAccount) {
|
||||||
|
return UserResponse.builder()
|
||||||
|
.id(userAccount.getId())
|
||||||
|
.firstName(userAccount.getFirstName())
|
||||||
|
.lastName(userAccount.getLastName())
|
||||||
|
.email(userAccount.getEmail() != null ? userAccount.getEmail().getValue() : null)
|
||||||
|
.username(userAccount.getUsername())
|
||||||
|
.phoneNumber(userAccount.getPhoneNumber() != null ? userAccount.getPhoneNumber().getValue() : null)
|
||||||
|
.authProvider(userAccount.getAuthProvider())
|
||||||
|
.privacyPolicyAccepted(userAccount.isPrivacyPolicyAccepted())
|
||||||
|
.privacyPolicyAcceptedAt(userAccount.getPrivacyPolicyAcceptedAt())
|
||||||
|
.createdAt(userAccount.getCreatedAt())
|
||||||
|
.lastLoginAt(userAccount.getLastLoginAt())
|
||||||
|
.isActive(userAccount.isActive())
|
||||||
|
.role(userAccount.getRole() != null ? userAccount.getRole().name() : null)
|
||||||
|
.companyName(userAccount.getCompany() != null ? userAccount.getCompany().getName() : null)
|
||||||
|
.companyId(userAccount.getCompany() != null ? userAccount.getCompany().getId() : null)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.dh7789dev.xpeditis.configuration;
|
package com.dh7789dev.xpeditis.configuration;
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dao.UserDao;
|
import com.dh7789dev.xpeditis.dao.UserDao;
|
||||||
|
import com.dh7789dev.xpeditis.entity.UserEntity;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.dh7789dev.xpeditis.configuration;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "spring.security.oauth2.client.registration.google")
|
||||||
|
@Data
|
||||||
|
public class OAuth2Configuration {
|
||||||
|
|
||||||
|
private String clientId;
|
||||||
|
private String clientSecret;
|
||||||
|
private String redirectUri;
|
||||||
|
private String scope;
|
||||||
|
|
||||||
|
public static final String GOOGLE_TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s";
|
||||||
|
public static final String GOOGLE_USER_INFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=%s";
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.dh7789dev.xpeditis.configuration;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
|||||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
@ -27,7 +28,7 @@ import static org.springframework.security.web.util.matcher.AntPathRequestMatche
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
//@EnableMethodSecurity
|
@EnableMethodSecurity(prePostEnabled = true)
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Value("${application.csrf.enabled}")
|
@Value("${application.csrf.enabled}")
|
||||||
@ -42,6 +43,14 @@ public class SecurityConfiguration {
|
|||||||
"/api/v1/auth/**",
|
"/api/v1/auth/**",
|
||||||
"/actuator/health/**"};
|
"/actuator/health/**"};
|
||||||
|
|
||||||
|
private static final String[] ADMIN_ONLY_URL = {
|
||||||
|
"/api/v1/users"};
|
||||||
|
|
||||||
|
private static final String[] GENERAL_API_URL = {
|
||||||
|
"/api/v1/quotes/**",
|
||||||
|
"/api/v1/shipments/**",
|
||||||
|
"/api/v1/companies/profile"};
|
||||||
|
|
||||||
private static final String[] INTERNAL_WHITE_LIST_URL = {
|
private static final String[] INTERNAL_WHITE_LIST_URL = {
|
||||||
"/v2/api-docs",
|
"/v2/api-docs",
|
||||||
"/v3/api-docs",
|
"/v3/api-docs",
|
||||||
@ -99,10 +108,9 @@ public class SecurityConfiguration {
|
|||||||
http.authorizeHttpRequests(auth ->
|
http.authorizeHttpRequests(auth ->
|
||||||
auth.requestMatchers(WHITE_LIST_URL).permitAll()
|
auth.requestMatchers(WHITE_LIST_URL).permitAll()
|
||||||
.requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll()
|
.requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll()
|
||||||
.requestMatchers(antMatcher(HttpMethod.GET, API_V1_URI)).permitAll()
|
.requestMatchers("/api/v1/users/**").authenticated()
|
||||||
.requestMatchers(antMatcher(HttpMethod.PUT, API_V1_URI)).hasRole(ADMIN_ROLE)
|
.requestMatchers("/api/v1/profile/**").authenticated()
|
||||||
.requestMatchers(antMatcher(HttpMethod.DELETE, API_V1_URI)).hasRole(ADMIN_ROLE)
|
.requestMatchers(GENERAL_API_URL).authenticated()
|
||||||
.requestMatchers(antMatcher(HttpMethod.POST, API_V1_URI)).hasRole(ADMIN_ROLE)
|
|
||||||
.requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS)
|
.requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS)
|
||||||
.requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS)
|
.requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS)
|
||||||
.requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS)
|
.requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS)
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
---
|
|
||||||
spring:
|
spring:
|
||||||
h2:
|
h2:
|
||||||
console:
|
console:
|
||||||
enabled: 'false'
|
enabled: ${SPRING_H2_CONSOLE_ENABLED:false}
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
url: ${SPRING_H2_DATASOURCE_URL:jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE}
|
||||||
driverClassName: org.h2.Driver
|
driverClassName: ${SPRING_H2_DATASOURCE_DRIVER_CLASS_NAME:org.h2.Driver}
|
||||||
username: sa
|
username: ${SPRING_H2_DATASOURCE_USERNAME:sa}
|
||||||
password: ''
|
password: ${SPRING_H2_DATASOURCE_PASSWORD:}
|
||||||
|
|
||||||
sql:
|
sql:
|
||||||
init:
|
init:
|
||||||
@ -16,45 +15,74 @@ spring:
|
|||||||
mode: always
|
mode: always
|
||||||
|
|
||||||
jpa:
|
jpa:
|
||||||
show-sql: true
|
show-sql: ${SPRING_JPA_SHOW_SQL:false}
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
format_sql: true
|
format_sql: ${SPRING_JPA_FORMAT_SQL:false}
|
||||||
# show_sql: true
|
# show_sql: true
|
||||||
database-platform: org.hibernate.dialect.H2Dialect
|
# database-platform: ${SPRING_JPA_DATABASE_PLATFORM_H2:org.hibernate.dialect.H2Dialect}
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
ddl-auto: ${SPRING_JPA_HIBERNATE_DDL_AUTO_DEV:create-drop}
|
||||||
# Just to load initial data for the demo. DO NOT USE IT IN PRODUCTION
|
# Just to load initial data for the demo. DO NOT USE IT IN PRODUCTION
|
||||||
defer-datasource-initialization: true
|
defer-datasource-initialization: ${SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_DEV:true}
|
||||||
|
|
||||||
flyway: # flyway automatically uses the datasource from the application to connect to the DB
|
flyway: # flyway automatically uses the datasource from the application to connect to the DB
|
||||||
enabled: false # enables flyway database migration
|
enabled: ${SPRING_FLYWAY_ENABLED_DEV:false} # enables flyway database migration
|
||||||
|
|
||||||
mail:
|
mail:
|
||||||
host: sandbox.smtp.mailtrap.io
|
host: ${SPRING_MAIL_HOST_DEV:sandbox.smtp.mailtrap.io}
|
||||||
port: 2525
|
port: ${SPRING_MAIL_PORT_DEV:2525}
|
||||||
username: 2597bd31d265eb
|
username: ${SPRING_MAIL_USERNAME_DEV:your_mailtrap_username}
|
||||||
password: cd126234193c89
|
password: ${SPRING_MAIL_PASSWORD_DEV:your_mailtrap_password}
|
||||||
properties:
|
properties:
|
||||||
mail:
|
mail:
|
||||||
smtp:
|
smtp:
|
||||||
ssl:
|
ssl:
|
||||||
trust:"*"
|
trust: ${SPRING_MAIL_SMTP_SSL_TRUST:*}
|
||||||
auth: true
|
auth: ${SPRING_MAIL_SMTP_AUTH:true}
|
||||||
starttls:
|
starttls:
|
||||||
enable: true
|
enable: ${SPRING_MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||||
connectiontimeout: 5000
|
connectiontimeout: ${SPRING_MAIL_SMTP_CONNECTION_TIMEOUT:5000}
|
||||||
timeout: 3000
|
timeout: ${SPRING_MAIL_SMTP_TIMEOUT:3000}
|
||||||
writetimeout: 5000
|
writetimeout: ${SPRING_MAIL_SMTP_WRITE_TIMEOUT:5000}
|
||||||
|
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
client:
|
||||||
|
registration:
|
||||||
|
google:
|
||||||
|
client-id: ${GOOGLE_CLIENT_ID:your-google-client-id}
|
||||||
|
client-secret: ${GOOGLE_CLIENT_SECRET:your-google-client-secret}
|
||||||
|
scope: ${GOOGLE_OAUTH2_SCOPE:openid,email,profile}
|
||||||
|
redirect-uri: ${OAUTH2_REDIRECT_URI_DEV:http://localhost:8080/login/oauth2/code/google}
|
||||||
|
provider:
|
||||||
|
google:
|
||||||
|
authorization-uri: ${GOOGLE_AUTHORIZATION_URI:https://accounts.google.com/o/oauth2/v2/auth}
|
||||||
|
token-uri: ${GOOGLE_TOKEN_URI:https://oauth2.googleapis.com/token}
|
||||||
|
user-info-uri: ${GOOGLE_USER_INFO_URI:https://www.googleapis.com/oauth2/v2/userinfo}
|
||||||
|
user-name-attribute: ${GOOGLE_USER_NAME_ATTRIBUTE:sub}
|
||||||
|
|
||||||
application:
|
application:
|
||||||
email:
|
email:
|
||||||
from: randommailjf@gmail.com
|
from: ${APPLICATION_EMAIL_FROM_DEV:noreply@xpeditis.local}
|
||||||
csrf:
|
csrf:
|
||||||
enabled: false
|
enabled: ${APPLICATION_CSRF_ENABLED_DEV:false}
|
||||||
security:
|
security:
|
||||||
jwt:
|
jwt:
|
||||||
secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
|
secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970}
|
||||||
expiration: 86400000 # a day
|
expiration: ${JWT_EXPIRATION:86400000} # a day
|
||||||
refresh-token:
|
refresh-token:
|
||||||
expiration: 604800000 # 7 days
|
expiration: ${JWT_REFRESH_TOKEN_EXPIRATION:604800000} # 7 days
|
||||||
|
oauth2:
|
||||||
|
google:
|
||||||
|
enabled: ${APPLICATION_OAUTH2_GOOGLE_ENABLED:true}
|
||||||
|
license:
|
||||||
|
trial:
|
||||||
|
duration-days: ${APPLICATION_LICENSE_TRIAL_DURATION_DAYS:30}
|
||||||
|
max-users: ${APPLICATION_LICENSE_TRIAL_MAX_USERS:5}
|
||||||
|
basic:
|
||||||
|
max-users: ${APPLICATION_LICENSE_BASIC_MAX_USERS:50}
|
||||||
|
premium:
|
||||||
|
max-users: ${APPLICATION_LICENSE_PREMIUM_MAX_USERS:200}
|
||||||
|
enterprise:
|
||||||
|
max-users: ${APPLICATION_LICENSE_ENTERPRISE_MAX_USERS:1000}
|
||||||
@ -1,10 +1,9 @@
|
|||||||
---
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL}
|
url: ${SPRING_DATASOURCE_URL:}
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME}
|
username: ${SPRING_DATASOURCE_USERNAME:}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD}
|
password: ${SPRING_DATASOURCE_PASSWORD:}
|
||||||
#hikari:
|
#hikari:
|
||||||
#schema: leblr
|
#schema: leblr
|
||||||
|
|
||||||
@ -15,52 +14,83 @@ spring:
|
|||||||
#data-locations: import_users.sql
|
#data-locations: import_users.sql
|
||||||
|
|
||||||
jpa:
|
jpa:
|
||||||
# show-sql: true
|
show-sql: ${SPRING_JPA_SHOW_SQL:false}
|
||||||
properties:
|
properties:
|
||||||
hibernate:
|
hibernate:
|
||||||
format_sql: true
|
format_sql: ${SPRING_JPA_FORMAT_SQL:true}
|
||||||
#show_sql: true
|
#show_sql: true
|
||||||
database: mysql
|
database: mysql
|
||||||
database-platform: org.hibernate.dialect.MySQLDialect
|
database-platform: ${SPRING_JPA_DATABASE_PLATFORM_MYSQL:org.hibernate.dialect.MySQLDialect}
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: validate
|
ddl-auto: ${SPRING_JPA_HIBERNATE_DDL_AUTO_PROD:validate}
|
||||||
defer-datasource-initialization: false
|
defer-datasource-initialization: ${SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_PROD:false}
|
||||||
#open-in-view: false
|
#open-in-view: false
|
||||||
|
|
||||||
flyway: # flyway automatically uses the datasource from the application to connect to the DB
|
flyway: # flyway automatically uses the datasource from the application to connect to the DB
|
||||||
enabled: true # enables flyway database migration
|
enabled: ${SPRING_FLYWAY_ENABLED_PROD:true} # enables flyway database migration
|
||||||
locations: classpath:db/migration/structure, classpath:db/migration/data # the location where flyway should look for migration scripts
|
locations: ${SPRING_FLYWAY_LOCATIONS:classpath:db/migration/structure, classpath:db/migration/data} # the location where flyway should look for migration scripts
|
||||||
validate-on-migrate: true
|
validate-on-migrate: ${SPRING_FLYWAY_VALIDATE_ON_MIGRATE:true}
|
||||||
baseline-on-migrate: true
|
baseline-on-migrate: ${SPRING_FLYWAY_BASELINE_ON_MIGRATE:true}
|
||||||
baseline-version: 0
|
baseline-version: ${SPRING_FLYWAY_BASELINE_VERSION:0}
|
||||||
default-schema: leblr
|
default-schema: ${SPRING_FLYWAY_DEFAULT_SCHEMA:leblr}
|
||||||
|
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
client:
|
||||||
|
registration:
|
||||||
|
google:
|
||||||
|
client-id: ${GOOGLE_CLIENT_ID}
|
||||||
|
client-secret: ${GOOGLE_CLIENT_SECRET}
|
||||||
|
scope: ${GOOGLE_OAUTH2_SCOPE:openid,email,profile}
|
||||||
|
redirect-uri: ${OAUTH2_REDIRECT_URI_PROD:https://xpeditis.fr/login/oauth2/code/google}
|
||||||
|
provider:
|
||||||
|
google:
|
||||||
|
authorization-uri: ${GOOGLE_AUTHORIZATION_URI:https://accounts.google.com/o/oauth2/v2/auth}
|
||||||
|
token-uri: ${GOOGLE_TOKEN_URI:https://oauth2.googleapis.com/token}
|
||||||
|
user-info-uri: ${GOOGLE_USER_INFO_URI:https://www.googleapis.com/oauth2/v2/userinfo}
|
||||||
|
user-name-attribute: ${GOOGLE_USER_NAME_ATTRIBUTE:sub}
|
||||||
|
|
||||||
mail:
|
mail:
|
||||||
protocol: smtp
|
protocol: ${SPRING_MAIL_PROTOCOL_PROD:smtp}
|
||||||
host: ssl0.ovh.net
|
host: ${SPRING_MAIL_HOST_PROD:ssl0.ovh.net}
|
||||||
port: 587
|
port: ${SPRING_MAIL_PORT_PROD:587}
|
||||||
username: contact@xpeditis.fr
|
username: ${SPRING_MAIL_USERNAME_PROD:contact@xpeditis.fr}
|
||||||
password:
|
password: ${SPRING_MAIL_PASSWORD_PROD:}
|
||||||
properties:
|
properties:
|
||||||
mail:
|
mail:
|
||||||
smtp:
|
smtp:
|
||||||
auth: true
|
auth: ${SPRING_MAIL_SMTP_AUTH:true}
|
||||||
starttls:
|
starttls:
|
||||||
enable: true
|
enable: ${SPRING_MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||||
connectiontimeout: 5000
|
connectiontimeout: ${SPRING_MAIL_SMTP_CONNECTION_TIMEOUT:5000}
|
||||||
timeout: 3000
|
timeout: ${SPRING_MAIL_SMTP_TIMEOUT:3000}
|
||||||
writetimeout: 5000
|
writetimeout: ${SPRING_MAIL_SMTP_WRITE_TIMEOUT:5000}
|
||||||
|
|
||||||
application:
|
application:
|
||||||
email:
|
email:
|
||||||
from: contact@leblr.fr
|
from: ${APPLICATION_EMAIL_FROM_PROD:contact@xpeditis.fr}
|
||||||
|
|
||||||
csrf:
|
csrf:
|
||||||
enabled: false
|
enabled: ${APPLICATION_CSRF_ENABLED_PROD:true}
|
||||||
|
|
||||||
security:
|
security:
|
||||||
jwt:
|
jwt:
|
||||||
secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
|
secret-key: ${JWT_SECRET_KEY}
|
||||||
expiration: 86400000 # a day
|
expiration: ${JWT_EXPIRATION:86400000} # a day
|
||||||
refresh-token:
|
refresh-token:
|
||||||
expiration: 604800000 # 7 days
|
expiration: ${JWT_REFRESH_TOKEN_EXPIRATION:604800000} # 7 days
|
||||||
|
|
||||||
|
oauth2:
|
||||||
|
google:
|
||||||
|
enabled: ${APPLICATION_OAUTH2_GOOGLE_ENABLED:true}
|
||||||
|
|
||||||
|
license:
|
||||||
|
trial:
|
||||||
|
duration-days: ${APPLICATION_LICENSE_TRIAL_DURATION_DAYS:30}
|
||||||
|
max-users: ${APPLICATION_LICENSE_TRIAL_MAX_USERS:5}
|
||||||
|
basic:
|
||||||
|
max-users: ${APPLICATION_LICENSE_BASIC_MAX_USERS:50}
|
||||||
|
premium:
|
||||||
|
max-users: ${APPLICATION_LICENSE_PREMIUM_MAX_USERS:200}
|
||||||
|
enterprise:
|
||||||
|
max-users: ${APPLICATION_LICENSE_ENTERPRISE_MAX_USERS:1000}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
server:
|
server:
|
||||||
port: 8080
|
port: ${SERVER_PORT:8080}
|
||||||
|
|
||||||
file:
|
file:
|
||||||
upload-dir: /upload
|
upload-dir: ${FILE_UPLOAD_DIR:/upload}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
http:
|
http:
|
||||||
@ -12,11 +12,11 @@ spring:
|
|||||||
force: true
|
force: true
|
||||||
|
|
||||||
application:
|
application:
|
||||||
name: '@project.description@'
|
name: ${SPRING_APPLICATION_NAME:@project.description@}
|
||||||
version: '@project.version@'
|
version: ${SPRING_APPLICATION_VERSION:@project.version@}
|
||||||
|
|
||||||
profiles:
|
profiles:
|
||||||
active: '@spring.profiles.active@'
|
active: ${SPRING_PROFILES_ACTIVE:@spring.profiles.active@}
|
||||||
|
|
||||||
banner:
|
banner:
|
||||||
location: 'classpath:banner.txt'
|
location: 'classpath:banner.txt'
|
||||||
@ -30,21 +30,22 @@ spring:
|
|||||||
|
|
||||||
servlet:
|
servlet:
|
||||||
multipart:
|
multipart:
|
||||||
enabled: true
|
enabled: ${SPRING_SERVLET_MULTIPART_ENABLED:true}
|
||||||
max-file-size: 50MB
|
max-file-size: ${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}
|
||||||
max-request-size: 50MB
|
max-request-size: ${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}
|
||||||
#location: ${java.io.tmpdir}
|
#location: ${java.io.tmpdir}
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
root: ${LOGGING_LEVEL_ROOT:INFO}
|
||||||
org:
|
org:
|
||||||
org.hibernate.orm.query.sqm.ast.logTree: OFF
|
org.hibernate.orm.query.sqm.ast.logTree: ${LOGGING_LEVEL_HIBERNATE_SQL:OFF}
|
||||||
springframework:
|
springframework:
|
||||||
boot:
|
boot:
|
||||||
autoconfigure: OFF
|
autoconfigure: ${LOGGING_LEVEL_SPRINGFRAMEWORK_BOOT_AUTOCONFIGURE:OFF}
|
||||||
web:
|
web:
|
||||||
filter:
|
filter:
|
||||||
CommonsRequestLoggingFilter: INFO
|
CommonsRequestLoggingFilter: ${LOGGING_LEVEL_COMMONS_REQUEST_LOGGING_FILTER:INFO}
|
||||||
security:
|
security:
|
||||||
config:
|
config:
|
||||||
annotation:
|
annotation:
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
@ActiveProfiles("dev")
|
|
||||||
class LeBlrApplicationTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void contextLoads() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -33,13 +33,13 @@ services:
|
|||||||
container_name: db
|
container_name: db
|
||||||
|
|
||||||
back:
|
back:
|
||||||
image: leblr-backend
|
image: xpeditis-backend
|
||||||
restart: always
|
restart: always
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
XPEDITIS_PROFILE: prod
|
SPRING_PROFILES_ACTIVE: dev
|
||||||
secrets:
|
secrets:
|
||||||
- mysql-user
|
- mysql-user
|
||||||
- mysql-password
|
- mysql-password
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
|
||||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||||
|
import com.dh7789dev.xpeditis.port.in.AuthenticationUseCase;
|
||||||
|
|
||||||
public interface AuthenticationService {
|
public interface AuthenticationService extends AuthenticationUseCase {
|
||||||
|
|
||||||
AuthenticationResponse authenticate(AuthenticationRequest request);
|
AuthenticationResponse authenticate(AuthenticationRequest request);
|
||||||
|
|
||||||
AuthenticationResponse register(RegisterRequest request);
|
AuthenticationResponse register(RegisterRequest request);
|
||||||
|
|
||||||
|
AuthenticationResponse authenticateWithGoogle(String googleToken);
|
||||||
|
|
||||||
|
UserAccount getCurrentUser(String token);
|
||||||
|
|
||||||
|
void logout(String token);
|
||||||
|
|
||||||
|
boolean validateToken(String token);
|
||||||
|
|
||||||
|
AuthenticationResponse refreshToken(String refreshToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,24 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.License;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.CreateCompanyRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.UpdateCompanyRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface CompanyService {
|
public interface CompanyService {
|
||||||
|
|
||||||
|
Company createCompany(CreateCompanyRequest request);
|
||||||
|
Company updateCompany(UUID id, UpdateCompanyRequest request);
|
||||||
|
Optional<Company> findCompanyById(UUID id);
|
||||||
|
List<Company> findAllCompanies();
|
||||||
|
void deleteCompany(UUID id);
|
||||||
|
boolean validateLicense(UUID companyId, int requestedUsers);
|
||||||
|
boolean isLicenseActive(UUID companyId);
|
||||||
|
License upgradeLicense(UUID companyId, LicenseType newLicenseType);
|
||||||
|
int getMaxUsersForLicense(LicenseType licenseType);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,25 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
public interface LicenseService {
|
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.License;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||||
|
import com.dh7789dev.xpeditis.port.in.LicenseValidationUseCase;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface LicenseService extends LicenseValidationUseCase {
|
||||||
|
|
||||||
|
boolean validateLicense(UUID companyId);
|
||||||
|
|
||||||
|
boolean canAddUser(UUID companyId);
|
||||||
|
|
||||||
|
License createTrialLicense(Company company);
|
||||||
|
|
||||||
|
License upgradeLicense(UUID companyId, LicenseType newType);
|
||||||
|
|
||||||
|
void deactivateLicense(UUID licenseId);
|
||||||
|
|
||||||
|
License getActiveLicense(UUID companyId);
|
||||||
|
|
||||||
|
long getDaysUntilExpiration(UUID companyId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,35 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
|
import com.dh7789dev.xpeditis.port.in.UserManagementUseCase;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface UserService {
|
public interface UserService extends UserManagementUseCase {
|
||||||
|
|
||||||
void changePassword(ChangePasswordRequest request, Principal connectedUser);
|
void changePassword(ChangePasswordRequest request, Principal connectedUser);
|
||||||
|
|
||||||
|
UserAccount createUser(RegisterRequest request);
|
||||||
|
|
||||||
|
UserAccount createGoogleUser(String googleToken);
|
||||||
|
|
||||||
|
Optional<UserAccount> findById(UUID id);
|
||||||
|
|
||||||
|
Optional<UserAccount> findByEmail(String email);
|
||||||
|
|
||||||
|
Optional<UserAccount> findByUsername(String username);
|
||||||
|
|
||||||
|
UserAccount updateProfile(UserAccount userAccount);
|
||||||
|
|
||||||
|
void deactivateUser(UUID userId);
|
||||||
|
|
||||||
|
void deleteUser(UUID userId);
|
||||||
|
|
||||||
|
boolean existsByEmail(String email);
|
||||||
|
|
||||||
|
boolean existsByUsername(String username);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.dh7789dev.xpeditis.port.in;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||||
|
|
||||||
|
public interface AuthenticationUseCase {
|
||||||
|
|
||||||
|
AuthenticationResponse authenticate(AuthenticationRequest request);
|
||||||
|
|
||||||
|
AuthenticationResponse authenticateWithGoogle(String googleToken);
|
||||||
|
|
||||||
|
UserAccount getCurrentUser(String token);
|
||||||
|
|
||||||
|
void logout(String token);
|
||||||
|
|
||||||
|
boolean validateToken(String token);
|
||||||
|
|
||||||
|
AuthenticationResponse refreshToken(String refreshToken);
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.dh7789dev.xpeditis.port.in;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.License;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface LicenseValidationUseCase {
|
||||||
|
|
||||||
|
boolean validateLicense(UUID companyId);
|
||||||
|
|
||||||
|
boolean canAddUser(UUID companyId);
|
||||||
|
|
||||||
|
License createTrialLicense(Company company);
|
||||||
|
|
||||||
|
License upgradeLicense(UUID companyId, LicenseType newType);
|
||||||
|
|
||||||
|
void deactivateLicense(UUID licenseId);
|
||||||
|
|
||||||
|
License getActiveLicense(UUID companyId);
|
||||||
|
|
||||||
|
long getDaysUntilExpiration(UUID companyId);
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.dh7789dev.xpeditis.port.in;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface UserManagementUseCase {
|
||||||
|
|
||||||
|
UserAccount createUser(RegisterRequest request);
|
||||||
|
|
||||||
|
UserAccount createGoogleUser(String googleToken);
|
||||||
|
|
||||||
|
Optional<UserAccount> findById(UUID id);
|
||||||
|
|
||||||
|
Optional<UserAccount> findByEmail(String email);
|
||||||
|
|
||||||
|
Optional<UserAccount> findByUsername(String username);
|
||||||
|
|
||||||
|
UserAccount updateProfile(UserAccount userAccount);
|
||||||
|
|
||||||
|
void changePassword(ChangePasswordRequest request, Principal connectedUser);
|
||||||
|
|
||||||
|
void deactivateUser(UUID userId);
|
||||||
|
|
||||||
|
void deleteUser(UUID userId);
|
||||||
|
|
||||||
|
boolean existsByEmail(String email);
|
||||||
|
|
||||||
|
boolean existsByUsername(String username);
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.app;
|
||||||
|
|
||||||
|
public enum AuthProvider {
|
||||||
|
LOCAL,
|
||||||
|
GOOGLE
|
||||||
|
}
|
||||||
@ -1,19 +1,50 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
package com.dh7789dev.xpeditis.dto.app;
|
||||||
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class Company {
|
public class Company {
|
||||||
private Long id;
|
private UUID id;
|
||||||
private String name;
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String website;
|
||||||
|
private String industry;
|
||||||
private String country;
|
private String country;
|
||||||
|
private String siren;
|
||||||
|
private String numEori;
|
||||||
|
private String phone;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private boolean isActive;
|
||||||
private List<UserAccount> users;
|
private List<UserAccount> users;
|
||||||
|
private List<License> licenses;
|
||||||
|
private License license; // Current active license
|
||||||
private List<Quote> quotes;
|
private List<Quote> quotes;
|
||||||
private List<ExportFolder> exports;
|
private List<ExportFolder> exports;
|
||||||
|
|
||||||
|
public License getActiveLicense() {
|
||||||
|
return licenses != null ? licenses.stream()
|
||||||
|
.filter(License::isActive)
|
||||||
|
.filter(license -> license.getExpirationDate() == null ||
|
||||||
|
license.getExpirationDate().isAfter(LocalDateTime.now().toLocalDate()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActiveUserCount() {
|
||||||
|
return users != null ? (int) users.stream()
|
||||||
|
.filter(UserAccount::isActive)
|
||||||
|
.count() : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.app;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class GoogleUserInfo {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String email;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
private String picture;
|
||||||
|
private boolean verified;
|
||||||
|
private String locale;
|
||||||
|
}
|
||||||
@ -1,16 +1,54 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
package com.dh7789dev.xpeditis.dto.app;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class License {
|
public class License {
|
||||||
private Long id;
|
private UUID id;
|
||||||
private String licenseKey;
|
private String licenseKey;
|
||||||
|
private LicenseType type;
|
||||||
|
private LocalDate startDate;
|
||||||
private LocalDate expirationDate;
|
private LocalDate expirationDate;
|
||||||
private boolean active;
|
private LocalDateTime issuedDate;
|
||||||
private UserAccount user;
|
private LocalDateTime expiryDate;
|
||||||
|
private int maxUsers;
|
||||||
|
private boolean isActive;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Company company;
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return expirationDate != null && expirationDate.isBefore(LocalDate.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getExpiryDate() {
|
||||||
|
return expiryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return isActive && !isExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canAddUser(int currentUserCount) {
|
||||||
|
return !hasUserLimit() || currentUserCount < maxUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUserLimit() {
|
||||||
|
return type != null && type.hasUserLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDaysUntilExpiration() {
|
||||||
|
return expirationDate != null ?
|
||||||
|
java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), expirationDate) :
|
||||||
|
Long.MAX_VALUE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.app;
|
||||||
|
|
||||||
|
public enum LicenseType {
|
||||||
|
TRIAL(5, 30),
|
||||||
|
BASIC(10, -1),
|
||||||
|
PREMIUM(50, -1),
|
||||||
|
ENTERPRISE(-1, -1);
|
||||||
|
|
||||||
|
private final int maxUsers;
|
||||||
|
private final int durationDays;
|
||||||
|
|
||||||
|
LicenseType(int maxUsers, int durationDays) {
|
||||||
|
this.maxUsers = maxUsers;
|
||||||
|
this.durationDays = durationDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxUsers() {
|
||||||
|
return maxUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDurationDays() {
|
||||||
|
return durationDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUserLimit() {
|
||||||
|
return maxUsers > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasTimeLimit() {
|
||||||
|
return durationDays > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.app;
|
||||||
|
|
||||||
|
public enum Role {
|
||||||
|
USER,
|
||||||
|
MANAGER,
|
||||||
|
ADMIN,
|
||||||
|
ADMIN_PLATFORM
|
||||||
|
}
|
||||||
@ -1,20 +1,51 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
package com.dh7789dev.xpeditis.dto.app;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.valueobject.Email;
|
||||||
|
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class UserAccount {
|
public class UserAccount {
|
||||||
private String username;
|
private UUID id;
|
||||||
private String firstName;
|
private String firstName;
|
||||||
private String lastName;
|
private String lastName;
|
||||||
private String email;
|
private Email email;
|
||||||
|
private String username;
|
||||||
private String password;
|
private String password;
|
||||||
private String role; // or "ADMIN"
|
private PhoneNumber phoneNumber;
|
||||||
|
private AuthProvider authProvider;
|
||||||
|
private String googleId;
|
||||||
|
private boolean privacyPolicyAccepted;
|
||||||
|
private LocalDateTime privacyPolicyAcceptedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private LocalDateTime lastLoginAt;
|
||||||
|
private boolean isActive;
|
||||||
|
private Role role;
|
||||||
private Company company;
|
private Company company;
|
||||||
private License license;
|
private License license;
|
||||||
private List<Quote> quotes;
|
private List<Quote> quotes;
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return (firstName != null ? firstName : "") +
|
||||||
|
(lastName != null ? " " + lastName : "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGoogleAuth() {
|
||||||
|
return AuthProvider.GOOGLE.equals(authProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLocalAuth() {
|
||||||
|
return AuthProvider.LOCAL.equals(authProvider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CreateCompanyRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "Company name is required")
|
||||||
|
@Size(min = 2, max = 100, message = "Company name must be between 2 and 100 characters")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Size(max = 500, message = "Description must not exceed 500 characters")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Size(max = 255, message = "Website must not exceed 255 characters")
|
||||||
|
private String website;
|
||||||
|
|
||||||
|
@Size(max = 100, message = "Industry must not exceed 100 characters")
|
||||||
|
private String industry;
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class GoogleAuthRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "Google token is required")
|
||||||
|
String googleToken;
|
||||||
|
|
||||||
|
String companyName;
|
||||||
|
|
||||||
|
String phoneNumber;
|
||||||
|
}
|
||||||
@ -1,41 +1,70 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.request;
|
package com.dh7789dev.xpeditis.dto.request;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
|
||||||
|
import jakarta.validation.constraints.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import lombok.experimental.FieldDefaults;
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Builder(toBuilder = true)
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class RegisterRequest {
|
public class RegisterRequest {
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank(message = "First name is required")
|
||||||
|
@Size(max = 50, message = "First name must not exceed 50 characters")
|
||||||
String firstName;
|
String firstName;
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank(message = "Last name is required")
|
||||||
|
@Size(max = 50, message = "Last name must not exceed 50 characters")
|
||||||
String lastName;
|
String lastName;
|
||||||
|
|
||||||
@NotBlank
|
@Email(message = "Invalid email format")
|
||||||
String username;
|
@NotBlank(message = "Email is required")
|
||||||
|
|
||||||
@NotBlank
|
|
||||||
String email;
|
String email;
|
||||||
|
|
||||||
@NotBlank
|
@Size(max = 50, message = "Username must not exceed 50 characters")
|
||||||
|
String username;
|
||||||
|
|
||||||
|
@Pattern(
|
||||||
|
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$",
|
||||||
|
message = "Password must be at least 8 characters with uppercase, lowercase and digit"
|
||||||
|
)
|
||||||
String password;
|
String password;
|
||||||
|
|
||||||
@NotBlank
|
String confirmPassword;
|
||||||
String phone;
|
|
||||||
|
|
||||||
String company_uuid = "";
|
@NotBlank(message = "Phone number is required")
|
||||||
|
@Pattern(
|
||||||
|
regexp = "^\\+?[1-9]\\d{1,14}$",
|
||||||
|
message = "Invalid phone number format"
|
||||||
|
)
|
||||||
|
String phoneNumber;
|
||||||
|
|
||||||
String company_name;
|
@NotBlank(message = "Company name is required")
|
||||||
|
@Size(max = 100, message = "Company name must not exceed 100 characters")
|
||||||
|
String companyName;
|
||||||
|
|
||||||
|
String companyCountry;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
AuthProvider authProvider = AuthProvider.LOCAL;
|
||||||
|
|
||||||
|
String googleId;
|
||||||
|
|
||||||
|
@AssertTrue(message = "Privacy policy must be accepted")
|
||||||
|
boolean privacyPolicyAccepted;
|
||||||
|
|
||||||
|
@AssertTrue(message = "Password confirmation must match")
|
||||||
|
public boolean isPasswordConfirmed() {
|
||||||
|
return password != null && password.equals(confirmPassword);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UpdateCompanyRequest {
|
||||||
|
|
||||||
|
@Size(min = 2, max = 100, message = "Company name must be between 2 and 100 characters")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Size(max = 500, message = "Description must not exceed 500 characters")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Size(max = 255, message = "Website must not exceed 255 characters")
|
||||||
|
private String website;
|
||||||
|
|
||||||
|
@Size(max = 100, message = "Industry must not exceed 100 characters")
|
||||||
|
private String industry;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class UpdateProfileRequest {
|
||||||
|
|
||||||
|
@Size(max = 50, message = "First name must not exceed 50 characters")
|
||||||
|
String firstName;
|
||||||
|
|
||||||
|
@Size(max = 50, message = "Last name must not exceed 50 characters")
|
||||||
|
String lastName;
|
||||||
|
|
||||||
|
@Pattern(
|
||||||
|
regexp = "^\\+?[1-9]\\d{1,14}$",
|
||||||
|
message = "Invalid phone number format"
|
||||||
|
)
|
||||||
|
String phoneNumber;
|
||||||
|
|
||||||
|
@Size(max = 50, message = "Username must not exceed 50 characters")
|
||||||
|
String username;
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
|||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
@ -12,6 +13,7 @@ import lombok.experimental.FieldDefaults;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Builder
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
@ -25,12 +27,22 @@ public class AuthenticationResponse {
|
|||||||
@JsonProperty("refresh_token")
|
@JsonProperty("refresh_token")
|
||||||
String refreshToken;
|
String refreshToken;
|
||||||
|
|
||||||
|
@JsonProperty("token_type")
|
||||||
|
@Builder.Default
|
||||||
|
String tokenType = "Bearer";
|
||||||
|
|
||||||
|
@JsonProperty("expires_in")
|
||||||
|
long expiresIn;
|
||||||
|
|
||||||
@JsonProperty("created_at")
|
@JsonProperty("created_at")
|
||||||
Date createdAt;
|
Date createdAt;
|
||||||
|
|
||||||
@JsonProperty("expires_at")
|
@JsonProperty("expires_at")
|
||||||
Date expiresAt;
|
Date expiresAt;
|
||||||
|
|
||||||
|
@JsonProperty("user")
|
||||||
|
UserResponse user;
|
||||||
|
|
||||||
@JsonProperty("error_message")
|
@JsonProperty("error_message")
|
||||||
String error;
|
String error;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.response;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class TokenResponse {
|
||||||
|
|
||||||
|
String token;
|
||||||
|
String refreshToken;
|
||||||
|
long expiresIn;
|
||||||
|
@Builder.Default
|
||||||
|
String tokenType = "Bearer";
|
||||||
|
UserResponse user;
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.response;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class UserResponse {
|
||||||
|
|
||||||
|
UUID id;
|
||||||
|
String firstName;
|
||||||
|
String lastName;
|
||||||
|
String email;
|
||||||
|
String username;
|
||||||
|
String phoneNumber;
|
||||||
|
AuthProvider authProvider;
|
||||||
|
boolean privacyPolicyAccepted;
|
||||||
|
LocalDateTime privacyPolicyAcceptedAt;
|
||||||
|
LocalDateTime createdAt;
|
||||||
|
LocalDateTime lastLoginAt;
|
||||||
|
boolean isActive;
|
||||||
|
String role;
|
||||||
|
String companyName;
|
||||||
|
UUID companyId;
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return (firstName != null ? firstName : "") +
|
||||||
|
(lastName != null ? " " + lastName : "").trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.valueobject;
|
||||||
|
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public class Email {
|
||||||
|
private static final Pattern EMAIL_PATTERN = Pattern.compile(
|
||||||
|
"^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$"
|
||||||
|
);
|
||||||
|
|
||||||
|
String value;
|
||||||
|
|
||||||
|
public Email(String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Email cannot be null or empty");
|
||||||
|
}
|
||||||
|
if (!EMAIL_PATTERN.matcher(value.trim()).matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid email format: " + value);
|
||||||
|
}
|
||||||
|
this.value = value.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.dh7789dev.xpeditis.dto.valueobject;
|
||||||
|
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public class PhoneNumber {
|
||||||
|
private static final Pattern PHONE_PATTERN = Pattern.compile(
|
||||||
|
"^\\+?[1-9]\\d{1,14}$"
|
||||||
|
);
|
||||||
|
|
||||||
|
String value;
|
||||||
|
|
||||||
|
public PhoneNumber(String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Phone number cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleanedPhone = value.replaceAll("[\\s\\-\\(\\)]", "");
|
||||||
|
|
||||||
|
if (!PHONE_PATTERN.matcher(cleanedPhone).matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid phone number format: " + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = cleanedPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.dh7789dev.xpeditis.exception;
|
||||||
|
|
||||||
|
public class AuthenticationException extends RuntimeException {
|
||||||
|
|
||||||
|
public AuthenticationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthenticationException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.dh7789dev.xpeditis.exception;
|
||||||
|
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
public BusinessException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BusinessException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.dh7789dev.xpeditis.exception;
|
||||||
|
|
||||||
|
public class CompanyInactiveException extends RuntimeException {
|
||||||
|
public CompanyInactiveException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompanyInactiveException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.dh7789dev.xpeditis.exception;
|
||||||
|
|
||||||
|
public class InvalidCredentialsException extends RuntimeException {
|
||||||
|
public InvalidCredentialsException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidCredentialsException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.dh7789dev.xpeditis.exception;
|
||||||
|
|
||||||
|
public class LicenseExpiredException extends RuntimeException {
|
||||||
|
public LicenseExpiredException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LicenseExpiredException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.dh7789dev.xpeditis.exception;
|
||||||
|
|
||||||
|
public class LicenseUserLimitExceededException extends RuntimeException {
|
||||||
|
public LicenseUserLimitExceededException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LicenseUserLimitExceededException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.dh7789dev.xpeditis.exception;
|
||||||
|
|
||||||
|
public class ResourceNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public ResourceNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceNotFoundException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.dh7789dev.xpeditis.exception;
|
||||||
|
|
||||||
|
public class UserAlreadyExistsException extends RuntimeException {
|
||||||
|
public UserAlreadyExistsException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserAlreadyExistsException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -62,6 +62,10 @@
|
|||||||
<groupId>org.springframework.security</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>spring-security-crypto</artifactId>
|
<artifactId>spring-security-crypto</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-tx</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -1,26 +1,244 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.valueobject.Email;
|
||||||
|
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Role;
|
||||||
|
import com.dh7789dev.xpeditis.exception.AuthenticationException;
|
||||||
|
import com.dh7789dev.xpeditis.exception.BusinessException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@Transactional
|
||||||
public class AuthenticationServiceImpl implements AuthenticationService {
|
public class AuthenticationServiceImpl implements AuthenticationService {
|
||||||
|
|
||||||
private final AuthenticationRepository authenticationRepository;
|
private final AuthenticationRepository authenticationRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
public AuthenticationServiceImpl(AuthenticationRepository authenticationRepository) {
|
private final OAuth2Provider oAuth2Provider;
|
||||||
this.authenticationRepository = authenticationRepository;
|
private final CompanyService companyService;
|
||||||
}
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationResponse authenticate(AuthenticationRequest request) {
|
public AuthenticationResponse authenticate(AuthenticationRequest request) {
|
||||||
|
log.info("Authenticating user with username/email: {}", request.getUsername());
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
|
||||||
|
throw new AuthenticationException("Username or email is required");
|
||||||
|
}
|
||||||
|
if (request.getPassword() == null || request.getPassword().trim().isEmpty()) {
|
||||||
|
throw new AuthenticationException("Password is required");
|
||||||
|
}
|
||||||
|
|
||||||
return authenticationRepository.authenticate(request);
|
return authenticationRepository.authenticate(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationResponse register(RegisterRequest request) {
|
public AuthenticationResponse register(RegisterRequest request) {
|
||||||
|
log.info("Registering new user with email: {}", request.getEmail());
|
||||||
|
|
||||||
|
// Validate business rules
|
||||||
|
validateRegistrationRequest(request);
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
if (userRepository.existsByEmail(request.getEmail())) {
|
||||||
|
throw new BusinessException("User with this email already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getUsername() != null && userRepository.existsByUsername(request.getUsername())) {
|
||||||
|
throw new BusinessException("Username already taken");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and validate user account
|
||||||
|
UserAccount userAccount = createUserAccountFromRequest(request);
|
||||||
|
|
||||||
return authenticationRepository.register(request);
|
return authenticationRepository.register(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationResponse authenticateWithGoogle(String googleToken) {
|
||||||
|
log.info("Authenticating user with Google OAuth2");
|
||||||
|
|
||||||
|
if (googleToken == null || googleToken.trim().isEmpty()) {
|
||||||
|
throw new AuthenticationException("Google token is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Google token
|
||||||
|
if (!oAuth2Provider.validateToken(googleToken)) {
|
||||||
|
throw new AuthenticationException("Invalid Google token");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user info from Google
|
||||||
|
Optional<GoogleUserInfo> googleUserInfo = oAuth2Provider.getUserInfo(googleToken);
|
||||||
|
if (googleUserInfo.isEmpty()) {
|
||||||
|
throw new AuthenticationException("Failed to retrieve user information from Google");
|
||||||
|
}
|
||||||
|
|
||||||
|
GoogleUserInfo userInfo = googleUserInfo.get();
|
||||||
|
|
||||||
|
// Check if user exists by Google ID or email
|
||||||
|
Optional<UserAccount> existingUser = userRepository.findByGoogleId(userInfo.getId());
|
||||||
|
if (existingUser.isEmpty()) {
|
||||||
|
existingUser = userRepository.findByEmail(userInfo.getEmail());
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAccount userAccount;
|
||||||
|
if (existingUser.isPresent()) {
|
||||||
|
// Update existing user
|
||||||
|
userAccount = existingUser.get();
|
||||||
|
updateUserFromGoogleInfo(userAccount, userInfo);
|
||||||
|
} else {
|
||||||
|
// Create new user from Google info
|
||||||
|
userAccount = createUserAccountFromGoogleInfo(userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save/update user
|
||||||
|
userAccount = userRepository.save(userAccount);
|
||||||
|
|
||||||
|
return authenticationRepository.authenticateWithGoogle(userAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserAccount getCurrentUser(String token) {
|
||||||
|
if (token == null || token.trim().isEmpty()) {
|
||||||
|
throw new AuthenticationException("Token is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticationRepository.getCurrentUser(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout(String token) {
|
||||||
|
if (token == null || token.trim().isEmpty()) {
|
||||||
|
throw new AuthenticationException("Token is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Logging out user with token");
|
||||||
|
authenticationRepository.logout(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
if (token == null || token.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticationRepository.validateToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationResponse refreshToken(String refreshToken) {
|
||||||
|
if (refreshToken == null || refreshToken.trim().isEmpty()) {
|
||||||
|
throw new AuthenticationException("Refresh token is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Refreshing token");
|
||||||
|
return authenticationRepository.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRegistrationRequest(RegisterRequest request) {
|
||||||
|
if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("Email is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getFirstName() == null || request.getFirstName().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("First name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getLastName() == null || request.getLastName().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("Last name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getPassword() == null || request.getPassword().length() < 8) {
|
||||||
|
throw new BusinessException("Password must be at least 8 characters long");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email format by creating Email value object
|
||||||
|
try {
|
||||||
|
new Email(request.getEmail());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new BusinessException("Invalid email format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate phone number if provided
|
||||||
|
if (request.getPhoneNumber() != null && !request.getPhoneNumber().trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
new PhoneNumber(request.getPhoneNumber());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new BusinessException("Invalid phone number format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserAccount createUserAccountFromRequest(RegisterRequest request) {
|
||||||
|
return UserAccount.builder()
|
||||||
|
.id(UUID.randomUUID())
|
||||||
|
.firstName(request.getFirstName().trim())
|
||||||
|
.lastName(request.getLastName().trim())
|
||||||
|
.email(new Email(request.getEmail()))
|
||||||
|
.phoneNumber(request.getPhoneNumber() != null ? new PhoneNumber(request.getPhoneNumber()) : null)
|
||||||
|
.username(request.getUsername() != null ? request.getUsername().trim() : null)
|
||||||
|
.password(passwordEncoder.encode(request.getPassword()))
|
||||||
|
.authProvider(AuthProvider.LOCAL)
|
||||||
|
.role(Role.USER)
|
||||||
|
.isActive(true)
|
||||||
|
.privacyPolicyAccepted(request.isPrivacyPolicyAccepted())
|
||||||
|
.privacyPolicyAcceptedAt(request.isPrivacyPolicyAccepted() ? LocalDateTime.now() : null)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserAccount createUserAccountFromGoogleInfo(GoogleUserInfo googleInfo) {
|
||||||
|
return UserAccount.builder()
|
||||||
|
.id(UUID.randomUUID())
|
||||||
|
.firstName(googleInfo.getFirstName())
|
||||||
|
.lastName(googleInfo.getLastName())
|
||||||
|
.email(new Email(googleInfo.getEmail()))
|
||||||
|
.googleId(googleInfo.getId())
|
||||||
|
.authProvider(AuthProvider.GOOGLE)
|
||||||
|
.role(Role.USER)
|
||||||
|
.isActive(true)
|
||||||
|
.privacyPolicyAccepted(false) // User will need to accept on first login
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.lastLoginAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUserFromGoogleInfo(UserAccount userAccount, GoogleUserInfo googleInfo) {
|
||||||
|
// Update Google ID if not set
|
||||||
|
if (userAccount.getGoogleId() == null) {
|
||||||
|
userAccount.setGoogleId(googleInfo.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update auth provider if it was local
|
||||||
|
if (userAccount.getAuthProvider() == AuthProvider.LOCAL) {
|
||||||
|
userAccount.setAuthProvider(AuthProvider.GOOGLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last login time
|
||||||
|
userAccount.setLastLoginAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
// Optionally update profile information if changed
|
||||||
|
if (!googleInfo.getFirstName().equals(userAccount.getFirstName())) {
|
||||||
|
userAccount.setFirstName(googleInfo.getFirstName());
|
||||||
|
}
|
||||||
|
if (!googleInfo.getLastName().equals(userAccount.getLastName())) {
|
||||||
|
userAccount.setLastName(googleInfo.getLastName());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,314 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.License;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.CreateCompanyRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.UpdateCompanyRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||||
|
import com.dh7789dev.xpeditis.exception.BusinessException;
|
||||||
|
import com.dh7789dev.xpeditis.exception.ResourceNotFoundException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@Transactional
|
||||||
public class CompanyServiceImpl implements CompanyService {
|
public class CompanyServiceImpl implements CompanyService {
|
||||||
|
|
||||||
|
private final CompanyRepository companyRepository;
|
||||||
|
private final LicenseRepository licenseRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Value("${application.license.trial.duration-days:30}")
|
||||||
|
private int trialDurationDays;
|
||||||
|
|
||||||
|
@Value("${application.license.trial.max-users:5}")
|
||||||
|
private int trialMaxUsers;
|
||||||
|
|
||||||
|
@Value("${application.license.basic.max-users:50}")
|
||||||
|
private int basicMaxUsers;
|
||||||
|
|
||||||
|
@Value("${application.license.premium.max-users:200}")
|
||||||
|
private int premiumMaxUsers;
|
||||||
|
|
||||||
|
@Value("${application.license.enterprise.max-users:1000}")
|
||||||
|
private int enterpriseMaxUsers;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Company createCompany(CreateCompanyRequest request) {
|
||||||
|
log.info("Creating company: {}", request.getName());
|
||||||
|
|
||||||
|
// Validate business rules
|
||||||
|
validateCreateCompanyRequest(request);
|
||||||
|
|
||||||
|
// Check if company name is unique
|
||||||
|
if (companyRepository.existsByName(request.getName())) {
|
||||||
|
throw new BusinessException("Company with this name already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create company
|
||||||
|
Company company = Company.builder()
|
||||||
|
.id(UUID.randomUUID())
|
||||||
|
.name(request.getName().trim())
|
||||||
|
.description(request.getDescription() != null ? request.getDescription().trim() : null)
|
||||||
|
.website(request.getWebsite() != null ? request.getWebsite().trim() : null)
|
||||||
|
.industry(request.getIndustry() != null ? request.getIndustry().trim() : null)
|
||||||
|
.isActive(true)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Save company
|
||||||
|
company = companyRepository.save(company);
|
||||||
|
|
||||||
|
// Create trial license automatically
|
||||||
|
License trialLicense = createTrialLicense(company);
|
||||||
|
company.setLicense(trialLicense);
|
||||||
|
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Company updateCompany(UUID id, UpdateCompanyRequest request) {
|
||||||
|
log.info("Updating company with ID: {}", id);
|
||||||
|
|
||||||
|
Company company = getCompanyById(id);
|
||||||
|
|
||||||
|
// Validate business rules
|
||||||
|
validateUpdateCompanyRequest(request);
|
||||||
|
|
||||||
|
// Check if new name is unique (excluding current company)
|
||||||
|
if (request.getName() != null && !request.getName().equals(company.getName())) {
|
||||||
|
if (companyRepository.existsByName(request.getName())) {
|
||||||
|
throw new BusinessException("Company with this name already exists");
|
||||||
|
}
|
||||||
|
company.setName(request.getName().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields if provided
|
||||||
|
if (request.getDescription() != null) {
|
||||||
|
company.setDescription(request.getDescription().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getWebsite() != null) {
|
||||||
|
company.setWebsite(request.getWebsite().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getIndustry() != null) {
|
||||||
|
company.setIndustry(request.getIndustry().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return companyRepository.save(company);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Company> findCompanyById(UUID id) {
|
||||||
|
return companyRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Company> findAllCompanies() {
|
||||||
|
return companyRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteCompany(UUID id) {
|
||||||
|
log.info("Deleting company with ID: {}", id);
|
||||||
|
|
||||||
|
Company company = getCompanyById(id);
|
||||||
|
|
||||||
|
// Check if company has active users
|
||||||
|
List<UserAccount> activeUsers = userRepository.findByCompanyIdAndIsActive(id, true);
|
||||||
|
if (!activeUsers.isEmpty()) {
|
||||||
|
throw new BusinessException("Cannot delete company with active users. Please deactivate or transfer users first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
companyRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateLicense(UUID companyId, int requestedUsers) {
|
||||||
|
Optional<Company> companyOpt = companyRepository.findById(companyId);
|
||||||
|
if (companyOpt.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Company company = companyOpt.get();
|
||||||
|
License license = company.getLicense();
|
||||||
|
if (license == null) {
|
||||||
|
log.warn("Company {} has no license", companyId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateLicenseConstraints(license, requestedUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLicenseActive(UUID companyId) {
|
||||||
|
Optional<Company> companyOpt = companyRepository.findById(companyId);
|
||||||
|
if (companyOpt.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Company company = companyOpt.get();
|
||||||
|
License license = company.getLicense();
|
||||||
|
if (license == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return license.isActive() && license.getExpiryDate().isAfter(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public License upgradeLicense(UUID companyId, LicenseType newLicenseType) {
|
||||||
|
log.info("Upgrading license for company {} to {}", companyId, newLicenseType);
|
||||||
|
|
||||||
|
Company company = getCompanyById(companyId);
|
||||||
|
|
||||||
|
License currentLicense = company.getLicense();
|
||||||
|
if (currentLicense == null) {
|
||||||
|
throw new BusinessException("Company has no current license");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate upgrade is allowed
|
||||||
|
if (!isUpgradeAllowed(currentLicense.getType(), newLicenseType)) {
|
||||||
|
throw new BusinessException("License upgrade from " + currentLicense.getType() + " to " + newLicenseType + " is not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new license
|
||||||
|
License newLicense = createLicense(company, newLicenseType);
|
||||||
|
|
||||||
|
// Deactivate old license
|
||||||
|
currentLicense.setActive(false);
|
||||||
|
licenseRepository.save(currentLicense);
|
||||||
|
|
||||||
|
company.setLicense(newLicense);
|
||||||
|
companyRepository.save(company);
|
||||||
|
|
||||||
|
return newLicense;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxUsersForLicense(LicenseType licenseType) {
|
||||||
|
return switch (licenseType) {
|
||||||
|
case TRIAL -> trialMaxUsers;
|
||||||
|
case BASIC -> basicMaxUsers;
|
||||||
|
case PREMIUM -> premiumMaxUsers;
|
||||||
|
case ENTERPRISE -> enterpriseMaxUsers;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Company getCompanyById(UUID id) {
|
||||||
|
return companyRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Company not found with ID: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateCreateCompanyRequest(CreateCompanyRequest request) {
|
||||||
|
if (request.getName() == null || request.getName().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("Company name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getName().trim().length() < 2) {
|
||||||
|
throw new BusinessException("Company name must be at least 2 characters long");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getName().trim().length() > 100) {
|
||||||
|
throw new BusinessException("Company name must not exceed 100 characters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateUpdateCompanyRequest(UpdateCompanyRequest request) {
|
||||||
|
if (request.getName() != null && request.getName().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("Company name cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getName() != null && request.getName().trim().length() < 2) {
|
||||||
|
throw new BusinessException("Company name must be at least 2 characters long");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getName() != null && request.getName().trim().length() > 100) {
|
||||||
|
throw new BusinessException("Company name must not exceed 100 characters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private License createTrialLicense(Company company) {
|
||||||
|
License license = License.builder()
|
||||||
|
.id(UUID.randomUUID())
|
||||||
|
.company(company)
|
||||||
|
.type(LicenseType.TRIAL)
|
||||||
|
.issuedDate(LocalDateTime.now())
|
||||||
|
.expiryDate(LocalDateTime.now().plusDays(trialDurationDays))
|
||||||
|
.maxUsers(trialMaxUsers)
|
||||||
|
.isActive(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return licenseRepository.save(license);
|
||||||
|
}
|
||||||
|
|
||||||
|
private License createLicense(Company company, LicenseType licenseType) {
|
||||||
|
LocalDateTime expiryDate = calculateExpiryDate(licenseType);
|
||||||
|
int maxUsers = getMaxUsersForLicense(licenseType);
|
||||||
|
|
||||||
|
License license = License.builder()
|
||||||
|
.id(UUID.randomUUID())
|
||||||
|
.company(company)
|
||||||
|
.type(licenseType)
|
||||||
|
.issuedDate(LocalDateTime.now())
|
||||||
|
.expiryDate(expiryDate)
|
||||||
|
.maxUsers(maxUsers)
|
||||||
|
.isActive(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return licenseRepository.save(license);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime calculateExpiryDate(LicenseType licenseType) {
|
||||||
|
return switch (licenseType) {
|
||||||
|
case TRIAL -> LocalDateTime.now().plusDays(trialDurationDays);
|
||||||
|
case BASIC, PREMIUM -> LocalDateTime.now().plusYears(1);
|
||||||
|
case ENTERPRISE -> LocalDateTime.now().plusYears(2);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateLicenseConstraints(License license, int requestedUsers) {
|
||||||
|
// Check if license is active
|
||||||
|
if (!license.isActive()) {
|
||||||
|
log.warn("License is not active for company {}", license.getCompany().getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if license has expired
|
||||||
|
if (license.getExpiryDate().isBefore(LocalDateTime.now())) {
|
||||||
|
log.warn("License has expired for company {}", license.getCompany().getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user limit
|
||||||
|
if (requestedUsers > license.getMaxUsers()) {
|
||||||
|
log.warn("Requested users {} exceeds license limit {} for company {}",
|
||||||
|
requestedUsers, license.getMaxUsers(), license.getCompany().getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUpgradeAllowed(LicenseType current, LicenseType target) {
|
||||||
|
// Define upgrade paths
|
||||||
|
return switch (current) {
|
||||||
|
case TRIAL -> target == LicenseType.BASIC || target == LicenseType.PREMIUM || target == LicenseType.ENTERPRISE;
|
||||||
|
case BASIC -> target == LicenseType.PREMIUM || target == LicenseType.ENTERPRISE;
|
||||||
|
case PREMIUM -> target == LicenseType.ENTERPRISE;
|
||||||
|
case ENTERPRISE -> false; // Cannot upgrade from enterprise
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,60 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.License;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class LicenseServiceImpl implements LicenseService {
|
public class LicenseServiceImpl implements LicenseService {
|
||||||
|
|
||||||
|
private final LicenseRepository licenseRepository;
|
||||||
|
|
||||||
|
public LicenseServiceImpl(LicenseRepository licenseRepository) {
|
||||||
|
this.licenseRepository = licenseRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateLicense(UUID companyId) {
|
||||||
|
// TODO: Implement license validation logic
|
||||||
|
return true; // Temporary implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canAddUser(UUID companyId) {
|
||||||
|
// TODO: Implement user addition validation logic
|
||||||
|
return true; // Temporary implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public License createTrialLicense(Company company) {
|
||||||
|
// TODO: Implement trial license creation logic
|
||||||
|
throw new UnsupportedOperationException("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public License upgradeLicense(UUID companyId, LicenseType newType) {
|
||||||
|
// TODO: Implement license upgrade logic
|
||||||
|
throw new UnsupportedOperationException("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivateLicense(UUID licenseId) {
|
||||||
|
// TODO: Implement license deactivation logic
|
||||||
|
throw new UnsupportedOperationException("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public License getActiveLicense(UUID companyId) {
|
||||||
|
// TODO: Implement active license retrieval logic
|
||||||
|
throw new UnsupportedOperationException("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDaysUntilExpiration(UUID companyId) {
|
||||||
|
// TODO: Implement days until expiration calculation logic
|
||||||
|
return Long.MAX_VALUE; // Temporary implementation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserServiceImpl implements UserService {
|
public class UserServiceImpl implements UserService {
|
||||||
@ -18,4 +22,67 @@ public class UserServiceImpl implements UserService {
|
|||||||
public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
|
public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
|
||||||
userRepository.changePassword(request, connectedUser);
|
userRepository.changePassword(request, connectedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserAccount createUser(RegisterRequest request) {
|
||||||
|
// Create UserAccount from RegisterRequest
|
||||||
|
UserAccount userAccount = UserAccount.builder()
|
||||||
|
.firstName(request.getFirstName())
|
||||||
|
.lastName(request.getLastName())
|
||||||
|
.email(new com.dh7789dev.xpeditis.dto.valueobject.Email(request.getEmail()))
|
||||||
|
.username(request.getUsername())
|
||||||
|
.phoneNumber(new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(request.getPhoneNumber()))
|
||||||
|
.authProvider(request.getAuthProvider())
|
||||||
|
.privacyPolicyAccepted(request.isPrivacyPolicyAccepted())
|
||||||
|
.isActive(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return userRepository.save(userAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserAccount createGoogleUser(String googleToken) {
|
||||||
|
// TODO: Implement Google OAuth integration to extract user info from token
|
||||||
|
throw new UnsupportedOperationException("Google OAuth integration not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<UserAccount> findById(UUID id) {
|
||||||
|
return userRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<UserAccount> findByEmail(String email) {
|
||||||
|
return userRepository.findByEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<UserAccount> findByUsername(String username) {
|
||||||
|
return userRepository.findByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserAccount updateProfile(UserAccount userAccount) {
|
||||||
|
return userRepository.save(userAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivateUser(UUID userId) {
|
||||||
|
userRepository.deactivateUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteUser(UUID userId) {
|
||||||
|
userRepository.deleteById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean existsByEmail(String email) {
|
||||||
|
return userRepository.existsByEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean existsByUsername(String username) {
|
||||||
|
return userRepository.existsByUsername(username);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,503 @@
|
|||||||
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Role;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||||
|
import com.dh7789dev.xpeditis.dto.valueobject.Email;
|
||||||
|
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
|
||||||
|
import com.dh7789dev.xpeditis.exception.AuthenticationException;
|
||||||
|
import com.dh7789dev.xpeditis.exception.BusinessException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("Authentication Service Tests")
|
||||||
|
class AuthenticationServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AuthenticationRepository authenticationRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OAuth2Provider oAuth2Provider;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CompanyService companyService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private AuthenticationServiceImpl authenticationService;
|
||||||
|
|
||||||
|
private AuthenticationRequest validAuthRequest;
|
||||||
|
private RegisterRequest validRegisterRequest;
|
||||||
|
private UserAccount testUserAccount;
|
||||||
|
private AuthenticationResponse testAuthResponse;
|
||||||
|
private GoogleUserInfo testGoogleUserInfo;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
validAuthRequest = new AuthenticationRequest("test@example.com", "password123");
|
||||||
|
|
||||||
|
validRegisterRequest = RegisterRequest.builder()
|
||||||
|
.firstName("John")
|
||||||
|
.lastName("Doe")
|
||||||
|
.email("john.doe@example.com")
|
||||||
|
.username("johndoe")
|
||||||
|
.password("password123")
|
||||||
|
.phoneNumber("+1234567890")
|
||||||
|
.privacyPolicyAccepted(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
testUserAccount = UserAccount.builder()
|
||||||
|
.id(UUID.randomUUID())
|
||||||
|
.firstName("John")
|
||||||
|
.lastName("Doe")
|
||||||
|
.email(new Email("john.doe@example.com"))
|
||||||
|
.phoneNumber(new PhoneNumber("+1234567890"))
|
||||||
|
.username("johndoe")
|
||||||
|
.authProvider(AuthProvider.LOCAL)
|
||||||
|
.role(Role.USER)
|
||||||
|
.isActive(true)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
testAuthResponse = AuthenticationResponse.builder()
|
||||||
|
.accessToken("mock-jwt-token")
|
||||||
|
.refreshToken("mock-refresh-token")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
testGoogleUserInfo = GoogleUserInfo.builder()
|
||||||
|
.id("google123")
|
||||||
|
.email("john.doe@example.com")
|
||||||
|
.firstName("John")
|
||||||
|
.lastName("Doe")
|
||||||
|
.verified(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Authentication Tests")
|
||||||
|
class AuthenticationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should authenticate valid user successfully")
|
||||||
|
void shouldAuthenticateValidUser() {
|
||||||
|
// Given
|
||||||
|
when(authenticationRepository.authenticate(validAuthRequest)).thenReturn(testAuthResponse);
|
||||||
|
|
||||||
|
// When
|
||||||
|
AuthenticationResponse result = authenticationService.authenticate(validAuthRequest);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getAccessToken()).isEqualTo("mock-jwt-token");
|
||||||
|
assertThat(result.getRefreshToken()).isEqualTo("mock-refresh-token");
|
||||||
|
verify(authenticationRepository).authenticate(validAuthRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when username is null")
|
||||||
|
void shouldThrowExceptionWhenUsernameIsNull() {
|
||||||
|
// Given
|
||||||
|
AuthenticationRequest invalidRequest = new AuthenticationRequest(null, "password123");
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.authenticate(invalidRequest))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Username or email is required");
|
||||||
|
|
||||||
|
verify(authenticationRepository, never()).authenticate(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when username is empty")
|
||||||
|
void shouldThrowExceptionWhenUsernameIsEmpty() {
|
||||||
|
// Given
|
||||||
|
AuthenticationRequest invalidRequest = new AuthenticationRequest(" ", "password123");
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.authenticate(invalidRequest))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Username or email is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when password is null")
|
||||||
|
void shouldThrowExceptionWhenPasswordIsNull() {
|
||||||
|
// Given
|
||||||
|
AuthenticationRequest invalidRequest = new AuthenticationRequest("test@example.com", null);
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.authenticate(invalidRequest))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Password is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when password is empty")
|
||||||
|
void shouldThrowExceptionWhenPasswordIsEmpty() {
|
||||||
|
// Given
|
||||||
|
AuthenticationRequest invalidRequest = new AuthenticationRequest("test@example.com", " ");
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.authenticate(invalidRequest))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Password is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Registration Tests")
|
||||||
|
class RegistrationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should register new user successfully")
|
||||||
|
void shouldRegisterNewUserSuccessfully() {
|
||||||
|
// Given
|
||||||
|
when(userRepository.existsByEmail("john.doe@example.com")).thenReturn(false);
|
||||||
|
when(userRepository.existsByUsername("johndoe")).thenReturn(false);
|
||||||
|
when(passwordEncoder.encode("password123")).thenReturn("encoded-password");
|
||||||
|
when(authenticationRepository.register(validRegisterRequest)).thenReturn(testAuthResponse);
|
||||||
|
|
||||||
|
// When
|
||||||
|
AuthenticationResponse result = authenticationService.register(validRegisterRequest);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getAccessToken()).isEqualTo("mock-jwt-token");
|
||||||
|
verify(userRepository).existsByEmail("john.doe@example.com");
|
||||||
|
verify(userRepository).existsByUsername("johndoe");
|
||||||
|
verify(authenticationRepository).register(validRegisterRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when email already exists")
|
||||||
|
void shouldThrowExceptionWhenEmailAlreadyExists() {
|
||||||
|
// Given
|
||||||
|
when(userRepository.existsByEmail("john.doe@example.com")).thenReturn(true);
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.register(validRegisterRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("User with this email already exists");
|
||||||
|
|
||||||
|
verify(userRepository, never()).existsByUsername(any());
|
||||||
|
verify(authenticationRepository, never()).register(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when username already exists")
|
||||||
|
void shouldThrowExceptionWhenUsernameAlreadyExists() {
|
||||||
|
// Given
|
||||||
|
when(userRepository.existsByEmail("john.doe@example.com")).thenReturn(false);
|
||||||
|
when(userRepository.existsByUsername("johndoe")).thenReturn(true);
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.register(validRegisterRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("Username already taken");
|
||||||
|
|
||||||
|
verify(authenticationRepository, never()).register(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when email is null")
|
||||||
|
void shouldThrowExceptionWhenEmailIsNull() {
|
||||||
|
// Given
|
||||||
|
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
|
||||||
|
.email(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("Email is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when first name is null")
|
||||||
|
void shouldThrowExceptionWhenFirstNameIsNull() {
|
||||||
|
// Given
|
||||||
|
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
|
||||||
|
.firstName(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("First name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when password is too short")
|
||||||
|
void shouldThrowExceptionWhenPasswordIsTooShort() {
|
||||||
|
// Given
|
||||||
|
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
|
||||||
|
.password("short")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("Password must be at least 8 characters long");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when email format is invalid")
|
||||||
|
void shouldThrowExceptionWhenEmailFormatIsInvalid() {
|
||||||
|
// Given
|
||||||
|
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
|
||||||
|
.email("invalid-email")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("Invalid email format");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when phone number format is invalid")
|
||||||
|
void shouldThrowExceptionWhenPhoneNumberFormatIsInvalid() {
|
||||||
|
// Given
|
||||||
|
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
|
||||||
|
.phoneNumber("invalid-phone")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("Invalid phone number format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Google OAuth2 Authentication Tests")
|
||||||
|
class GoogleOAuth2Tests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should authenticate with Google successfully for new user")
|
||||||
|
void shouldAuthenticateWithGoogleForNewUser() {
|
||||||
|
// Given
|
||||||
|
String googleToken = "valid-google-token";
|
||||||
|
when(oAuth2Provider.validateToken(googleToken)).thenReturn(true);
|
||||||
|
when(oAuth2Provider.getUserInfo(googleToken)).thenReturn(Optional.of(testGoogleUserInfo));
|
||||||
|
when(userRepository.findByGoogleId("google123")).thenReturn(Optional.empty());
|
||||||
|
when(userRepository.findByEmail("john.doe@example.com")).thenReturn(Optional.empty());
|
||||||
|
when(userRepository.save(any(UserAccount.class))).thenReturn(testUserAccount);
|
||||||
|
when(authenticationRepository.authenticateWithGoogle(any(UserAccount.class))).thenReturn(testAuthResponse);
|
||||||
|
|
||||||
|
// When
|
||||||
|
AuthenticationResponse result = authenticationService.authenticateWithGoogle(googleToken);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getAccessToken()).isEqualTo("mock-jwt-token");
|
||||||
|
verify(oAuth2Provider).validateToken(googleToken);
|
||||||
|
verify(oAuth2Provider).getUserInfo(googleToken);
|
||||||
|
verify(userRepository).save(any(UserAccount.class));
|
||||||
|
verify(authenticationRepository).authenticateWithGoogle(any(UserAccount.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should authenticate with Google successfully for existing user")
|
||||||
|
void shouldAuthenticateWithGoogleForExistingUser() {
|
||||||
|
// Given
|
||||||
|
String googleToken = "valid-google-token";
|
||||||
|
when(oAuth2Provider.validateToken(googleToken)).thenReturn(true);
|
||||||
|
when(oAuth2Provider.getUserInfo(googleToken)).thenReturn(Optional.of(testGoogleUserInfo));
|
||||||
|
when(userRepository.findByGoogleId("google123")).thenReturn(Optional.of(testUserAccount));
|
||||||
|
when(userRepository.save(any(UserAccount.class))).thenReturn(testUserAccount);
|
||||||
|
when(authenticationRepository.authenticateWithGoogle(any(UserAccount.class))).thenReturn(testAuthResponse);
|
||||||
|
|
||||||
|
// When
|
||||||
|
AuthenticationResponse result = authenticationService.authenticateWithGoogle(googleToken);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
verify(userRepository).findByGoogleId("google123");
|
||||||
|
verify(userRepository, never()).findByEmail(any());
|
||||||
|
verify(userRepository).save(testUserAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when Google token is null")
|
||||||
|
void shouldThrowExceptionWhenGoogleTokenIsNull() {
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.authenticateWithGoogle(null))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Google token is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when Google token is invalid")
|
||||||
|
void shouldThrowExceptionWhenGoogleTokenIsInvalid() {
|
||||||
|
// Given
|
||||||
|
String invalidToken = "invalid-token";
|
||||||
|
when(oAuth2Provider.validateToken(invalidToken)).thenReturn(false);
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.authenticateWithGoogle(invalidToken))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Invalid Google token");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when Google user info cannot be retrieved")
|
||||||
|
void shouldThrowExceptionWhenGoogleUserInfoCannotBeRetrieved() {
|
||||||
|
// Given
|
||||||
|
String validToken = "valid-token";
|
||||||
|
when(oAuth2Provider.validateToken(validToken)).thenReturn(true);
|
||||||
|
when(oAuth2Provider.getUserInfo(validToken)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.authenticateWithGoogle(validToken))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Failed to retrieve user information from Google");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Token Management Tests")
|
||||||
|
class TokenManagementTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should validate token successfully")
|
||||||
|
void shouldValidateTokenSuccessfully() {
|
||||||
|
// Given
|
||||||
|
String validToken = "valid-token";
|
||||||
|
when(authenticationRepository.validateToken(validToken)).thenReturn(true);
|
||||||
|
|
||||||
|
// When
|
||||||
|
boolean result = authenticationService.validateToken(validToken);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isTrue();
|
||||||
|
verify(authenticationRepository).validateToken(validToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return false for invalid token")
|
||||||
|
void shouldReturnFalseForInvalidToken() {
|
||||||
|
// Given
|
||||||
|
String invalidToken = "invalid-token";
|
||||||
|
when(authenticationRepository.validateToken(invalidToken)).thenReturn(false);
|
||||||
|
|
||||||
|
// When
|
||||||
|
boolean result = authenticationService.validateToken(invalidToken);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return false for null token")
|
||||||
|
void shouldReturnFalseForNullToken() {
|
||||||
|
// When
|
||||||
|
boolean result = authenticationService.validateToken(null);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
verify(authenticationRepository, never()).validateToken(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should refresh token successfully")
|
||||||
|
void shouldRefreshTokenSuccessfully() {
|
||||||
|
// Given
|
||||||
|
String refreshToken = "valid-refresh-token";
|
||||||
|
when(authenticationRepository.refreshToken(refreshToken)).thenReturn(testAuthResponse);
|
||||||
|
|
||||||
|
// When
|
||||||
|
AuthenticationResponse result = authenticationService.refreshToken(refreshToken);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
verify(authenticationRepository).refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when refresh token is null")
|
||||||
|
void shouldThrowExceptionWhenRefreshTokenIsNull() {
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.refreshToken(null))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Refresh token is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should logout successfully")
|
||||||
|
void shouldLogoutSuccessfully() {
|
||||||
|
// Given
|
||||||
|
String token = "valid-token";
|
||||||
|
|
||||||
|
// When
|
||||||
|
authenticationService.logout(token);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify(authenticationRepository).logout(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when logout token is null")
|
||||||
|
void shouldThrowExceptionWhenLogoutTokenIsNull() {
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.logout(null))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Token is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("User Retrieval Tests")
|
||||||
|
class UserRetrievalTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should get current user successfully")
|
||||||
|
void shouldGetCurrentUserSuccessfully() {
|
||||||
|
// Given
|
||||||
|
String token = "valid-token";
|
||||||
|
when(authenticationRepository.getCurrentUser(token)).thenReturn(testUserAccount);
|
||||||
|
|
||||||
|
// When
|
||||||
|
UserAccount result = authenticationService.getCurrentUser(token);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getEmail().getValue()).isEqualTo("john.doe@example.com");
|
||||||
|
verify(authenticationRepository).getCurrentUser(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when token is null for getCurrentUser")
|
||||||
|
void shouldThrowExceptionWhenTokenIsNullForGetCurrentUser() {
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> authenticationService.getCurrentUser(null))
|
||||||
|
.isInstanceOf(AuthenticationException.class)
|
||||||
|
.hasMessage("Token is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +1,273 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.License;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.CreateCompanyRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.UpdateCompanyRequest;
|
||||||
|
import com.dh7789dev.xpeditis.exception.BusinessException;
|
||||||
|
import com.dh7789dev.xpeditis.exception.ResourceNotFoundException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("Company Service Tests")
|
||||||
class CompanyServiceImplTest {
|
class CompanyServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CompanyRepository companyRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LicenseRepository licenseRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CompanyServiceImpl companyService;
|
||||||
|
|
||||||
@Test
|
private CreateCompanyRequest validCreateRequest;
|
||||||
void test(){
|
private UpdateCompanyRequest validUpdateRequest;
|
||||||
int test = 1 +1;
|
private Company testCompany;
|
||||||
assertEquals(2,test);
|
private License testLicense;
|
||||||
|
private UUID companyId;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// Set configuration properties
|
||||||
|
ReflectionTestUtils.setField(companyService, "trialDurationDays", 30);
|
||||||
|
ReflectionTestUtils.setField(companyService, "trialMaxUsers", 5);
|
||||||
|
ReflectionTestUtils.setField(companyService, "basicMaxUsers", 50);
|
||||||
|
ReflectionTestUtils.setField(companyService, "premiumMaxUsers", 200);
|
||||||
|
ReflectionTestUtils.setField(companyService, "enterpriseMaxUsers", 1000);
|
||||||
|
|
||||||
|
companyId = UUID.randomUUID();
|
||||||
|
|
||||||
|
validCreateRequest = CreateCompanyRequest.builder()
|
||||||
|
.name("Test Company")
|
||||||
|
.description("A test company")
|
||||||
|
.website("https://testcompany.com")
|
||||||
|
.industry("Technology")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
validUpdateRequest = UpdateCompanyRequest.builder()
|
||||||
|
.name("Updated Company")
|
||||||
|
.description("Updated description")
|
||||||
|
.website("https://updated.com")
|
||||||
|
.industry("Software")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
testLicense = License.builder()
|
||||||
|
.id(UUID.randomUUID())
|
||||||
|
.type(LicenseType.TRIAL)
|
||||||
|
.issuedDate(LocalDateTime.now())
|
||||||
|
.expiryDate(LocalDateTime.now().plusDays(30))
|
||||||
|
.maxUsers(5)
|
||||||
|
.isActive(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
testCompany = Company.builder()
|
||||||
|
.id(companyId)
|
||||||
|
.name("Test Company")
|
||||||
|
.description("A test company")
|
||||||
|
.website("https://testcompany.com")
|
||||||
|
.industry("Technology")
|
||||||
|
.isActive(true)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.license(testLicense)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Set bidirectional relationship
|
||||||
|
testLicense.setCompany(testCompany);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Company Creation Tests")
|
||||||
|
class CompanyCreationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should create company successfully")
|
||||||
|
void shouldCreateCompanySuccessfully() {
|
||||||
|
// Given
|
||||||
|
when(companyRepository.existsByName("Test Company")).thenReturn(false);
|
||||||
|
when(companyRepository.save(any(Company.class))).thenReturn(testCompany);
|
||||||
|
when(licenseRepository.save(any(License.class))).thenReturn(testLicense);
|
||||||
|
|
||||||
|
// When
|
||||||
|
Company result = companyService.createCompany(validCreateRequest);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getName()).isEqualTo("Test Company");
|
||||||
|
assertThat(result.getDescription()).isEqualTo("A test company");
|
||||||
|
assertThat(result.getWebsite()).isEqualTo("https://testcompany.com");
|
||||||
|
assertThat(result.getIndustry()).isEqualTo("Technology");
|
||||||
|
assertThat(result.isActive()).isTrue();
|
||||||
|
assertThat(result.getLicense()).isNotNull();
|
||||||
|
|
||||||
|
verify(companyRepository).existsByName("Test Company");
|
||||||
|
verify(companyRepository).save(any(Company.class));
|
||||||
|
verify(licenseRepository).save(any(License.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when company name already exists")
|
||||||
|
void shouldThrowExceptionWhenCompanyNameAlreadyExists() {
|
||||||
|
// Given
|
||||||
|
when(companyRepository.existsByName("Test Company")).thenReturn(true);
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> companyService.createCompany(validCreateRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("Company with this name already exists");
|
||||||
|
|
||||||
|
verify(companyRepository, never()).save(any());
|
||||||
|
verify(licenseRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when company name is null")
|
||||||
|
void shouldThrowExceptionWhenCompanyNameIsNull() {
|
||||||
|
// Given
|
||||||
|
CreateCompanyRequest invalidRequest = validCreateRequest.toBuilder()
|
||||||
|
.name(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> companyService.createCompany(invalidRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("Company name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw exception when company name is too short")
|
||||||
|
void shouldThrowExceptionWhenCompanyNameIsTooShort() {
|
||||||
|
// Given
|
||||||
|
CreateCompanyRequest invalidRequest = validCreateRequest.toBuilder()
|
||||||
|
.name("A")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// When & Then
|
||||||
|
assertThatThrownBy(() -> companyService.createCompany(invalidRequest))
|
||||||
|
.isInstanceOf(BusinessException.class)
|
||||||
|
.hasMessage("Company name must be at least 2 characters long");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("License Validation Tests")
|
||||||
|
class LicenseValidationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should validate license successfully")
|
||||||
|
void shouldValidateLicenseSuccessfully() {
|
||||||
|
// Given
|
||||||
|
testLicense.setMaxUsers(10);
|
||||||
|
testLicense.setActive(true);
|
||||||
|
testLicense.setExpiryDate(LocalDateTime.now().plusDays(10));
|
||||||
|
testCompany.setLicense(testLicense);
|
||||||
|
|
||||||
|
when(companyRepository.findById(companyId)).thenReturn(Optional.of(testCompany));
|
||||||
|
|
||||||
|
// When
|
||||||
|
boolean result = companyService.validateLicense(companyId, 5);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return false when license is inactive")
|
||||||
|
void shouldReturnFalseWhenLicenseIsInactive() {
|
||||||
|
// Given
|
||||||
|
testLicense.setActive(false);
|
||||||
|
testCompany.setLicense(testLicense);
|
||||||
|
|
||||||
|
when(companyRepository.findById(companyId)).thenReturn(Optional.of(testCompany));
|
||||||
|
|
||||||
|
// When
|
||||||
|
boolean result = companyService.validateLicense(companyId, 5);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return false when requested users exceed license limit")
|
||||||
|
void shouldReturnFalseWhenRequestedUsersExceedLicenseLimit() {
|
||||||
|
// Given
|
||||||
|
testLicense.setMaxUsers(5);
|
||||||
|
testCompany.setLicense(testLicense);
|
||||||
|
|
||||||
|
when(companyRepository.findById(companyId)).thenReturn(Optional.of(testCompany));
|
||||||
|
|
||||||
|
// When
|
||||||
|
boolean result = companyService.validateLicense(companyId, 10);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("License Type Limits Tests")
|
||||||
|
class LicenseTypeLimitsTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return correct max users for trial license")
|
||||||
|
void shouldReturnCorrectMaxUsersForTrialLicense() {
|
||||||
|
// When
|
||||||
|
int result = companyService.getMaxUsersForLicense(LicenseType.TRIAL);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return correct max users for basic license")
|
||||||
|
void shouldReturnCorrectMaxUsersForBasicLicense() {
|
||||||
|
// When
|
||||||
|
int result = companyService.getMaxUsersForLicense(LicenseType.BASIC);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isEqualTo(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return correct max users for premium license")
|
||||||
|
void shouldReturnCorrectMaxUsersForPremiumLicense() {
|
||||||
|
// When
|
||||||
|
int result = companyService.getMaxUsersForLicense(LicenseType.PREMIUM);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isEqualTo(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return correct max users for enterprise license")
|
||||||
|
void shouldReturnCorrectMaxUsersForEnterpriseLicense() {
|
||||||
|
// When
|
||||||
|
int result = companyService.getMaxUsersForLicense(LicenseType.ENTERPRISE);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isEqualTo(1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,187 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.request.UpdateProfileRequest;
|
||||||
|
import com.dh7789dev.xpeditis.dto.valueobject.Email;
|
||||||
|
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import java.security.Principal;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("UserServiceImpl Tests")
|
||||||
class UserServiceImplTest {
|
class UserServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
@Test
|
@InjectMocks
|
||||||
void test(){
|
private UserServiceImpl userService;
|
||||||
int test = 1 +1;
|
|
||||||
assertEquals(2,test);
|
private UserAccount testUserAccount;
|
||||||
|
private UpdateProfileRequest validUpdateProfileRequest;
|
||||||
|
private ChangePasswordRequest validChangePasswordRequest;
|
||||||
|
private RegisterRequest validRegisterRequest;
|
||||||
|
private Principal mockPrincipal;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
testUserAccount = UserAccount.builder()
|
||||||
|
.id(UUID.randomUUID())
|
||||||
|
.firstName("John")
|
||||||
|
.lastName("Doe")
|
||||||
|
.email(new Email("john.doe@example.com"))
|
||||||
|
.username("johndoe")
|
||||||
|
.phoneNumber(new PhoneNumber("+1234567890"))
|
||||||
|
.isActive(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
validUpdateProfileRequest = UpdateProfileRequest.builder()
|
||||||
|
.firstName("John")
|
||||||
|
.lastName("Doe")
|
||||||
|
.phoneNumber("+1234567890")
|
||||||
|
.username("johndoe")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
validChangePasswordRequest = new ChangePasswordRequest();
|
||||||
|
// Assuming ChangePasswordRequest has appropriate fields
|
||||||
|
|
||||||
|
mockPrincipal = java.security.Principal.class.cast(org.mockito.Mockito.mock(Principal.class));
|
||||||
|
|
||||||
|
validRegisterRequest = RegisterRequest.builder()
|
||||||
|
.firstName("John")
|
||||||
|
.lastName("Doe")
|
||||||
|
.email("john.doe@example.com")
|
||||||
|
.username("johndoe")
|
||||||
|
.password("Password123")
|
||||||
|
.confirmPassword("Password123")
|
||||||
|
.phoneNumber("+1234567890")
|
||||||
|
.companyName("Test Company")
|
||||||
|
.companyCountry("US")
|
||||||
|
.privacyPolicyAccepted(true)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Password Change Tests")
|
||||||
|
class PasswordChangeTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should delegate password change to repository")
|
||||||
|
void shouldDelegatePasswordChangeToRepository() {
|
||||||
|
// When
|
||||||
|
userService.changePassword(validChangePasswordRequest, mockPrincipal);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify(userRepository).changePassword(validChangePasswordRequest, mockPrincipal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("User Existence Tests")
|
||||||
|
class UserExistenceTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return false for existsByEmail (not implemented)")
|
||||||
|
void shouldReturnFalseForExistsByEmail() {
|
||||||
|
// When
|
||||||
|
boolean result = userService.existsByEmail("test@example.com");
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return false for existsByUsername (not implemented)")
|
||||||
|
void shouldReturnFalseForExistsByUsername() {
|
||||||
|
// When
|
||||||
|
boolean result = userService.existsByUsername("testuser");
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Not Implemented Methods Tests")
|
||||||
|
class NotImplementedMethodsTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw UnsupportedOperationException for createUser")
|
||||||
|
void shouldThrowExceptionForCreateUser() {
|
||||||
|
assertThatThrownBy(() -> userService.createUser(validRegisterRequest))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class)
|
||||||
|
.hasMessage("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw UnsupportedOperationException for createGoogleUser")
|
||||||
|
void shouldThrowExceptionForCreateGoogleUser() {
|
||||||
|
assertThatThrownBy(() -> userService.createGoogleUser("google-token"))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class)
|
||||||
|
.hasMessage("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw UnsupportedOperationException for findById")
|
||||||
|
void shouldThrowExceptionForFindById() {
|
||||||
|
assertThatThrownBy(() -> userService.findById(UUID.randomUUID()))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class)
|
||||||
|
.hasMessage("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw UnsupportedOperationException for findByEmail")
|
||||||
|
void shouldThrowExceptionForFindByEmail() {
|
||||||
|
assertThatThrownBy(() -> userService.findByEmail("test@example.com"))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class)
|
||||||
|
.hasMessage("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw UnsupportedOperationException for findByUsername")
|
||||||
|
void shouldThrowExceptionForFindByUsername() {
|
||||||
|
assertThatThrownBy(() -> userService.findByUsername("testuser"))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class)
|
||||||
|
.hasMessage("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw UnsupportedOperationException for updateProfile")
|
||||||
|
void shouldThrowExceptionForUpdateProfile() {
|
||||||
|
assertThatThrownBy(() -> userService.updateProfile(testUserAccount))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class)
|
||||||
|
.hasMessage("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw UnsupportedOperationException for deactivateUser")
|
||||||
|
void shouldThrowExceptionForDeactivateUser() {
|
||||||
|
assertThatThrownBy(() -> userService.deactivateUser(UUID.randomUUID()))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class)
|
||||||
|
.hasMessage("Not implemented yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw UnsupportedOperationException for deleteUser")
|
||||||
|
void shouldThrowExceptionForDeleteUser() {
|
||||||
|
assertThatThrownBy(() -> userService.deleteUser(UUID.randomUUID()))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class)
|
||||||
|
.hasMessage("Not implemented yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
@ -8,4 +9,9 @@ public interface AuthenticationRepository {
|
|||||||
|
|
||||||
AuthenticationResponse authenticate(AuthenticationRequest request);
|
AuthenticationResponse authenticate(AuthenticationRequest request);
|
||||||
AuthenticationResponse register(RegisterRequest request);
|
AuthenticationResponse register(RegisterRequest request);
|
||||||
|
AuthenticationResponse authenticateWithGoogle(UserAccount userAccount);
|
||||||
|
UserAccount getCurrentUser(String token);
|
||||||
|
void logout(String token);
|
||||||
|
boolean validateToken(String token);
|
||||||
|
AuthenticationResponse refreshToken(String refreshToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,22 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface CompanyRepository {
|
public interface CompanyRepository {
|
||||||
|
|
||||||
|
Company save(Company company);
|
||||||
|
|
||||||
|
Optional<Company> findById(UUID id);
|
||||||
|
|
||||||
|
Optional<Company> findByName(String name);
|
||||||
|
|
||||||
|
List<Company> findAll();
|
||||||
|
|
||||||
|
boolean existsByName(String name);
|
||||||
|
|
||||||
|
void deleteById(UUID id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,24 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.License;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface LicenseRepository {
|
public interface LicenseRepository {
|
||||||
|
|
||||||
|
License save(License license);
|
||||||
|
|
||||||
|
Optional<License> findById(UUID id);
|
||||||
|
|
||||||
|
Optional<License> findActiveLicenseByCompanyId(UUID companyId);
|
||||||
|
|
||||||
|
List<License> findByCompanyId(UUID companyId);
|
||||||
|
|
||||||
|
Optional<License> findByLicenseKey(String licenseKey);
|
||||||
|
|
||||||
|
void deleteById(UUID id);
|
||||||
|
|
||||||
|
void deactivateLicense(UUID id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface OAuth2Provider {
|
||||||
|
|
||||||
|
boolean validateToken(String accessToken);
|
||||||
|
|
||||||
|
Optional<GoogleUserInfo> getUserInfo(String accessToken);
|
||||||
|
}
|
||||||
@ -1,10 +1,34 @@
|
|||||||
package com.dh7789dev.xpeditis;
|
package com.dh7789dev.xpeditis;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface UserRepository {
|
public interface UserRepository {
|
||||||
|
|
||||||
void changePassword(ChangePasswordRequest request, Principal connectedUser);
|
void changePassword(ChangePasswordRequest request, Principal connectedUser);
|
||||||
|
|
||||||
|
UserAccount save(UserAccount userAccount);
|
||||||
|
|
||||||
|
Optional<UserAccount> findById(UUID id);
|
||||||
|
|
||||||
|
Optional<UserAccount> findByEmail(String email);
|
||||||
|
|
||||||
|
Optional<UserAccount> findByUsername(String username);
|
||||||
|
|
||||||
|
Optional<UserAccount> findByGoogleId(String googleId);
|
||||||
|
|
||||||
|
boolean existsByEmail(String email);
|
||||||
|
|
||||||
|
boolean existsByUsername(String username);
|
||||||
|
|
||||||
|
void deleteById(UUID id);
|
||||||
|
|
||||||
|
void deactivateUser(UUID id);
|
||||||
|
|
||||||
|
List<UserAccount> findByCompanyIdAndIsActive(UUID companyId, boolean isActive);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,10 +32,11 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<!-- MapStruct disabled - using manual mappers -->
|
||||||
|
<!-- <dependency>
|
||||||
<groupId>org.mapstruct</groupId>
|
<groupId>org.mapstruct</groupId>
|
||||||
<artifactId>mapstruct</artifactId>
|
<artifactId>mapstruct</artifactId>
|
||||||
</dependency>
|
</dependency> -->
|
||||||
|
|
||||||
<!-- spring-boot dependencies -->
|
<!-- spring-boot dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -128,7 +129,8 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>${org.projectlombok.version}</version>
|
<version>${org.projectlombok.version}</version>
|
||||||
</path>
|
</path>
|
||||||
<path>
|
<!-- MapStruct processors disabled - using manual mappers -->
|
||||||
|
<!-- <path>
|
||||||
<groupId>org.mapstruct</groupId>
|
<groupId>org.mapstruct</groupId>
|
||||||
<artifactId>mapstruct-processor</artifactId>
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
<version>${org.mapstruct.version}</version>
|
<version>${org.mapstruct.version}</version>
|
||||||
@ -137,7 +139,7 @@
|
|||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||||
<version>0.2.0</version>
|
<version>0.2.0</version>
|
||||||
</path>
|
</path> -->
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|||||||
@ -2,7 +2,15 @@ package com.dh7789dev.xpeditis.dao;
|
|||||||
|
|
||||||
import com.dh7789dev.xpeditis.entity.CompanyEntity;
|
import com.dh7789dev.xpeditis.entity.CompanyEntity;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface CompanyDao extends JpaRepository<CompanyEntity, Long> {
|
@Repository
|
||||||
|
public interface CompanyDao extends JpaRepository<CompanyEntity, UUID> {
|
||||||
|
|
||||||
|
Optional<CompanyEntity> findByName(String name);
|
||||||
|
|
||||||
|
boolean existsByName(String name);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,21 @@ package com.dh7789dev.xpeditis.dao;
|
|||||||
|
|
||||||
import com.dh7789dev.xpeditis.entity.LicenseEntity;
|
import com.dh7789dev.xpeditis.entity.LicenseEntity;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
public interface LicenseDao extends JpaRepository<LicenseEntity, Long> {
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface LicenseDao extends JpaRepository<LicenseEntity, UUID> {
|
||||||
|
|
||||||
|
Optional<LicenseEntity> findByLicenseKey(String licenseKey);
|
||||||
|
|
||||||
|
List<LicenseEntity> findByCompanyId(UUID companyId);
|
||||||
|
|
||||||
|
@Query("SELECT l FROM LicenseEntity l WHERE l.company.id = :companyId AND l.isActive = true AND (l.expirationDate IS NULL OR l.expirationDate > CURRENT_DATE)")
|
||||||
|
Optional<LicenseEntity> findActiveLicenseByCompanyId(@Param("companyId") UUID companyId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
|
||||||
@ -19,5 +20,5 @@ public interface TokenDao extends JpaRepository<TokenEntity, Integer> {
|
|||||||
on t.user.id = u.id\s
|
on t.user.id = u.id\s
|
||||||
where u.id = :userId and (t.expired = false or t.revoked = false)\s
|
where u.id = :userId and (t.expired = false or t.revoked = false)\s
|
||||||
""")
|
""")
|
||||||
List<TokenEntity> findAllValidTokenByUserId(String userId);
|
List<TokenEntity> findAllValidTokenByUserId(UUID userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,16 +2,21 @@ package com.dh7789dev.xpeditis.dao;
|
|||||||
|
|
||||||
import com.dh7789dev.xpeditis.entity.UserEntity;
|
import com.dh7789dev.xpeditis.entity.UserEntity;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
public interface UserDao extends JpaRepository<UserEntity, Long> {
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserDao extends JpaRepository<UserEntity, UUID> {
|
||||||
|
|
||||||
@Query("SELECT u FROM UserEntity u WHERE u.username = :username")
|
|
||||||
Optional<UserEntity> findByUsername(String username);
|
Optional<UserEntity> findByUsername(String username);
|
||||||
|
|
||||||
|
Optional<UserEntity> findByEmail(String email);
|
||||||
|
|
||||||
|
Optional<UserEntity> findByGoogleId(String googleId);
|
||||||
|
|
||||||
boolean existsByUsername(String username);
|
boolean existsByUsername(String username);
|
||||||
|
|
||||||
|
boolean existsByEmail(String email);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.dh7789dev.xpeditis.entity;
|
||||||
|
|
||||||
|
public enum AuthProviderEntity {
|
||||||
|
LOCAL,
|
||||||
|
GOOGLE
|
||||||
|
}
|
||||||
@ -10,58 +10,102 @@ import lombok.experimental.FieldNameConstants;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@FieldNameConstants
|
@FieldNameConstants
|
||||||
@FieldDefaults( level = AccessLevel.PRIVATE)
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
@Table(name = "Company")
|
@Table(name = "companies")
|
||||||
public class CompanyEntity extends BaseEntity {
|
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
|
||||||
@Id
|
public class CompanyEntity {
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "name", length = 50)
|
@Id
|
||||||
private String name;
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
@Column(columnDefinition = "BINARY(16)")
|
||||||
|
UUID id;
|
||||||
|
|
||||||
|
@Column(name = "name", nullable = false, unique = true, length = 100)
|
||||||
|
String name;
|
||||||
|
|
||||||
@Column(name = "country", length = 50)
|
@Column(name = "country", length = 50)
|
||||||
private String country;
|
String country;
|
||||||
|
|
||||||
@Column(name = "siren")
|
@Column(name = "siren", length = 20)
|
||||||
private String siren;
|
String siren;
|
||||||
|
|
||||||
@Column(name = "num_eori")
|
@Column(name = "num_eori", length = 50)
|
||||||
private String num_eori;
|
String numEori;
|
||||||
|
|
||||||
@Column(name = "phone", length = 20)
|
@Column(name = "phone", length = 20)
|
||||||
private String phone;
|
String phone;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
|
@Column(name = "is_active", nullable = false)
|
||||||
private List<UserEntity> users;
|
boolean isActive = true;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
|
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
private List<QuoteEntity> quotes;
|
List<UserEntity> users;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
|
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
private List<ExportFolderEntity> exports;
|
List<LicenseEntity> licenses;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
|
List<QuoteEntity> quotes;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
|
List<ExportFolderEntity> exports;
|
||||||
|
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", updatable = false)
|
||||||
private LocalDateTime createdAt;
|
LocalDateTime createdAt;
|
||||||
|
|
||||||
@Column(name = "modified_at")
|
@Column(name = "updated_at")
|
||||||
private LocalDateTime modifiedAt;
|
LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.CreatedDate
|
||||||
|
@Column(name = "created_date", updatable = false, nullable = false)
|
||||||
|
java.time.Instant createdDate;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.LastModifiedDate
|
||||||
|
@Column(name = "modified_date", nullable = false)
|
||||||
|
java.time.Instant modifiedDate;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.CreatedBy
|
||||||
|
@Column(name = "created_by", updatable = false, nullable = false)
|
||||||
|
String createdBy = "SYSTEM";
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.LastModifiedBy
|
||||||
|
@Column(name = "modified_by")
|
||||||
|
String modifiedBy;
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
if (id == null) {
|
||||||
|
id = UUID.randomUUID();
|
||||||
|
}
|
||||||
createdAt = LocalDateTime.now();
|
createdAt = LocalDateTime.now();
|
||||||
modifiedAt = LocalDateTime.now();
|
updatedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
public void onUpdate() {
|
public void onUpdate() {
|
||||||
modifiedAt = LocalDateTime.now();
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActiveUserCount() {
|
||||||
|
return users != null ? (int) users.stream()
|
||||||
|
.filter(UserEntity::isActive)
|
||||||
|
.count() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LicenseEntity getActiveLicense() {
|
||||||
|
return licenses != null ? licenses.stream()
|
||||||
|
.filter(LicenseEntity::isActive)
|
||||||
|
.filter(license -> license.getExpirationDate() == null ||
|
||||||
|
license.getExpirationDate().isAfter(java.time.LocalDate.now()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,46 +10,100 @@ import lombok.experimental.FieldNameConstants;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@FieldNameConstants
|
@FieldNameConstants
|
||||||
@FieldDefaults( level = AccessLevel.PRIVATE)
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
@Table(name = "License")
|
@Table(name = "licenses")
|
||||||
public class LicenseEntity extends BaseEntity {
|
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
|
||||||
|
public class LicenseEntity {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
private Long id;
|
@Column(columnDefinition = "BINARY(16)")
|
||||||
|
UUID id;
|
||||||
|
|
||||||
@Column(unique = true)
|
@Column(name = "license_key", unique = true, nullable = false)
|
||||||
private String licenseKey;
|
String licenseKey;
|
||||||
|
|
||||||
@Column(name = "expirationDate")
|
@Enumerated(EnumType.STRING)
|
||||||
private LocalDate expirationDate;
|
@Column(name = "type", nullable = false)
|
||||||
|
LicenseTypeEntity type;
|
||||||
|
|
||||||
private boolean active;
|
@Column(name = "start_date", nullable = false)
|
||||||
|
LocalDate startDate;
|
||||||
|
|
||||||
@OneToOne
|
@Column(name = "expiration_date")
|
||||||
@JoinColumn(name = "user_id", unique = true)
|
LocalDate expirationDate;
|
||||||
private UserEntity user;
|
|
||||||
|
@Column(name = "max_users", nullable = false)
|
||||||
|
int maxUsers;
|
||||||
|
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
boolean isActive = true;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "company_id", nullable = false)
|
||||||
|
CompanyEntity company;
|
||||||
|
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", updatable = false)
|
||||||
private LocalDateTime createdAt;
|
LocalDateTime createdAt;
|
||||||
|
|
||||||
@Column(name = "modified_at")
|
@org.springframework.data.annotation.CreatedDate
|
||||||
private LocalDateTime modifiedAt;
|
@Column(name = "created_date", updatable = false, nullable = false)
|
||||||
|
java.time.Instant createdDate;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.LastModifiedDate
|
||||||
|
@Column(name = "modified_date", nullable = false)
|
||||||
|
java.time.Instant modifiedDate;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.CreatedBy
|
||||||
|
@Column(name = "created_by", updatable = false, nullable = false)
|
||||||
|
String createdBy = "SYSTEM";
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.LastModifiedBy
|
||||||
|
@Column(name = "modified_by")
|
||||||
|
String modifiedBy;
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
if (id == null) {
|
||||||
|
id = UUID.randomUUID();
|
||||||
|
}
|
||||||
|
if (licenseKey == null) {
|
||||||
|
licenseKey = generateLicenseKey();
|
||||||
|
}
|
||||||
createdAt = LocalDateTime.now();
|
createdAt = LocalDateTime.now();
|
||||||
modifiedAt = LocalDateTime.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreUpdate
|
public boolean isExpired() {
|
||||||
public void onUpdate() {
|
return expirationDate != null && expirationDate.isBefore(LocalDate.now());
|
||||||
modifiedAt = LocalDateTime.now();
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return isActive && !isExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canAddUser(int currentUserCount) {
|
||||||
|
return !hasUserLimit() || currentUserCount < maxUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUserLimit() {
|
||||||
|
return type != null && type.hasUserLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDaysUntilExpiration() {
|
||||||
|
return expirationDate != null ?
|
||||||
|
java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), expirationDate) :
|
||||||
|
Long.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateLicenseKey() {
|
||||||
|
return "LIC-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.dh7789dev.xpeditis.entity;
|
||||||
|
|
||||||
|
public enum LicenseTypeEntity {
|
||||||
|
TRIAL(5, 30),
|
||||||
|
BASIC(10, -1),
|
||||||
|
PREMIUM(50, -1),
|
||||||
|
ENTERPRISE(-1, -1);
|
||||||
|
|
||||||
|
private final int maxUsers;
|
||||||
|
private final int durationDays;
|
||||||
|
|
||||||
|
LicenseTypeEntity(int maxUsers, int durationDays) {
|
||||||
|
this.maxUsers = maxUsers;
|
||||||
|
this.durationDays = durationDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxUsers() {
|
||||||
|
return maxUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDurationDays() {
|
||||||
|
return durationDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUserLimit() {
|
||||||
|
return maxUsers > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasTimeLimit() {
|
||||||
|
return durationDays > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package com.dh7789dev.xpeditis.entity;
|
package com.dh7789dev.xpeditis.entity;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Role;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -7,91 +8,143 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.experimental.FieldDefaults;
|
import lombok.experimental.FieldDefaults;
|
||||||
import lombok.experimental.FieldNameConstants;
|
import lombok.experimental.FieldNameConstants;
|
||||||
import org.hibernate.annotations.NaturalId;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@FieldNameConstants
|
@FieldNameConstants
|
||||||
@FieldDefaults( level = AccessLevel.PRIVATE)
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
@Table(name = "Users")
|
@Table(name = "users")
|
||||||
public class UserEntity extends BaseEntity implements UserDetails {
|
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
|
||||||
|
public class UserEntity implements UserDetails {
|
||||||
|
|
||||||
@NaturalId
|
@Id
|
||||||
@Column(nullable = false, unique = true, length = 50)
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
private String username;
|
@Column(columnDefinition = "BINARY(16)")
|
||||||
|
UUID id;
|
||||||
|
|
||||||
@Column(name = "first_name", length = 50)
|
@Column(name = "first_name", nullable = false, length = 50)
|
||||||
private String firstName;
|
String firstName;
|
||||||
|
|
||||||
@Column(name = "last_name", length = 50)
|
@Column(name = "last_name", nullable = false, length = 50)
|
||||||
private String lastName;
|
String lastName;
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
@Column(nullable = false, unique = true)
|
||||||
private String email;
|
String email;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(name = "username", unique = true, length = 50)
|
||||||
private String password;
|
String username;
|
||||||
|
|
||||||
@Column(name = "phone", length = 20)
|
@Column(name = "password")
|
||||||
private String phone;
|
String password;
|
||||||
|
|
||||||
|
@Column(name = "phone_number", nullable = true, length = 20)
|
||||||
|
String phoneNumber;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false)
|
@Column(name = "auth_provider", nullable = false)
|
||||||
private Role role;
|
AuthProviderEntity authProvider = AuthProviderEntity.LOCAL;
|
||||||
|
|
||||||
@Column(name = "enabled", nullable = false, columnDefinition = "BOOLEAN DEFAULT TRUE NOT NULL")
|
@Column(name = "google_id")
|
||||||
private boolean enabled;
|
String googleId;
|
||||||
|
|
||||||
@ManyToOne
|
@Column(name = "privacy_policy_accepted", nullable = false)
|
||||||
private CompanyEntity company;
|
boolean privacyPolicyAccepted = false;
|
||||||
|
|
||||||
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
|
@Column(name = "privacy_policy_accepted_at")
|
||||||
private LicenseEntity license;
|
LocalDateTime privacyPolicyAcceptedAt;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
|
@Column(name = "last_login_at")
|
||||||
private List<QuoteEntity> quotes;
|
LocalDateTime lastLoginAt;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "user")
|
@Column(name = "is_active", nullable = false)
|
||||||
private List<TokenEntity> tokens;
|
boolean isActive = true;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "role", nullable = false)
|
||||||
|
Role role = Role.USER;
|
||||||
|
|
||||||
|
@Column(name = "enabled", nullable = false)
|
||||||
|
boolean enabled = true;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "company_id")
|
||||||
|
CompanyEntity company;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
|
List<QuoteEntity> quotes;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
|
List<TokenEntity> tokens;
|
||||||
|
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", updatable = false)
|
||||||
private LocalDateTime createdAt;
|
LocalDateTime createdAt;
|
||||||
|
|
||||||
@Column(name = "modified_at")
|
@Column(name = "updated_at")
|
||||||
private LocalDateTime modifiedAt;
|
LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.CreatedDate
|
||||||
|
@Column(name = "created_date", updatable = false, nullable = false)
|
||||||
|
java.time.Instant createdDate;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.LastModifiedDate
|
||||||
|
@Column(name = "modified_date", nullable = false)
|
||||||
|
java.time.Instant modifiedDate;
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.CreatedBy
|
||||||
|
@Column(name = "created_by", updatable = false, nullable = false)
|
||||||
|
String createdBy = "SYSTEM";
|
||||||
|
|
||||||
|
@org.springframework.data.annotation.LastModifiedBy
|
||||||
|
@Column(name = "modified_by")
|
||||||
|
String modifiedBy;
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
if (id == null) {
|
||||||
|
id = UUID.randomUUID();
|
||||||
|
}
|
||||||
createdAt = LocalDateTime.now();
|
createdAt = LocalDateTime.now();
|
||||||
modifiedAt = LocalDateTime.now();
|
updatedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
public void onUpdate() {
|
public void onUpdate() {
|
||||||
modifiedAt = LocalDateTime.now();
|
updatedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
return role.getAuthorities();
|
return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return username != null ? username : email;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAccountNonExpired() {
|
public boolean isAccountNonExpired() {
|
||||||
return true;
|
return isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAccountNonLocked() {
|
public boolean isAccountNonLocked() {
|
||||||
return true;
|
return isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -101,14 +154,19 @@ public class UserEntity extends BaseEntity implements UserDetails {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled && isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return (firstName != null ? firstName : "") +
|
||||||
|
(lastName != null ? " " + lastName : "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "UserEntity(" + super.toString() + String.format(
|
return "UserEntity(" + super.toString() + String.format(
|
||||||
"username=%s, firstName=%s, lastName=%s, email=%s, role=%s)",
|
"id=%s, username=%s, firstName=%s, lastName=%s, email=%s, role=%s)",
|
||||||
username, firstName, lastName, email, role.name()
|
id, username, firstName, lastName, email, role.name()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.Address;
|
|
||||||
import com.dh7789dev.xpeditis.entity.AddressEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.MappingConstants;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { CompanyMapper.class })
|
|
||||||
public interface AddressMapper {
|
|
||||||
|
|
||||||
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
|
|
||||||
|
|
||||||
AddressEntity addressToAddressEntity(Address address);
|
|
||||||
|
|
||||||
Address addressEntityToAddress(AddressEntity addressEntity);
|
|
||||||
}
|
|
||||||
@ -1,25 +1,39 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
package com.dh7789dev.xpeditis.mapper;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Company;
|
||||||
import com.dh7789dev.xpeditis.dto.app.Company;
|
import com.dh7789dev.xpeditis.entity.CompanyEntity;
|
||||||
import com.dh7789dev.xpeditis.entity.CompanyEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
public class CompanyMapper {
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.MappingConstants;
|
public static CompanyEntity companyToCompanyEntity(Company company) {
|
||||||
import org.mapstruct.factory.Mappers;
|
if (company == null) return null;
|
||||||
|
|
||||||
|
CompanyEntity entity = new CompanyEntity();
|
||||||
|
entity.setId(company.getId());
|
||||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
|
entity.setName(company.getName());
|
||||||
public interface CompanyMapper {
|
entity.setCountry(company.getCountry());
|
||||||
|
entity.setSiren(company.getSiren());
|
||||||
CompanyMapper INSTANCE = Mappers.getMapper(CompanyMapper.class);
|
entity.setNumEori(company.getNumEori());
|
||||||
|
entity.setPhone(company.getPhone());
|
||||||
@Mapping(target = "createdDate", ignore = true)
|
entity.setActive(company.isActive());
|
||||||
@Mapping(target = "modifiedDate", ignore = true)
|
entity.setCreatedAt(company.getCreatedAt());
|
||||||
@Mapping(target = "createdBy", ignore = true)
|
entity.setUpdatedAt(company.getUpdatedAt());
|
||||||
@Mapping(target = "modifiedBy", ignore = true)
|
return entity;
|
||||||
CompanyEntity companyToCompanyEntity(Company company);
|
}
|
||||||
|
|
||||||
Company companyEntityToCompany(CompanyEntity companyEntity);
|
public static Company companyEntityToCompany(CompanyEntity companyEntity) {
|
||||||
}
|
if (companyEntity == null) return null;
|
||||||
|
|
||||||
|
return Company.builder()
|
||||||
|
.id(companyEntity.getId())
|
||||||
|
.name(companyEntity.getName())
|
||||||
|
.country(companyEntity.getCountry())
|
||||||
|
.siren(companyEntity.getSiren())
|
||||||
|
.numEori(companyEntity.getNumEori())
|
||||||
|
.phone(companyEntity.getPhone())
|
||||||
|
.isActive(companyEntity.isActive())
|
||||||
|
.createdAt(companyEntity.getCreatedAt())
|
||||||
|
.updatedAt(companyEntity.getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.Dimension;
|
|
||||||
import com.dh7789dev.xpeditis.entity.DimensionEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(componentModel = "spring")
|
|
||||||
public interface DimensionMapper {
|
|
||||||
DimensionMapper INSTANCE = Mappers.getMapper(DimensionMapper.class);
|
|
||||||
|
|
||||||
DimensionEntity dimensionToDimensionEntity(Dimension dimension);
|
|
||||||
|
|
||||||
Dimension dimensionEntityToDimension(DimensionEntity dimensionEntity);
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.Document;
|
|
||||||
import com.dh7789dev.xpeditis.entity.DocumentEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(componentModel = "spring")
|
|
||||||
public interface DocumentMapper {
|
|
||||||
DocumentMapper INSTANCE = Mappers.getMapper(DocumentMapper.class);
|
|
||||||
|
|
||||||
DocumentEntity documentToDocumentEntity(Document document);
|
|
||||||
Document documentEntityToDocument(DocumentEntity documentEntity);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.ExportFolder;
|
|
||||||
import com.dh7789dev.xpeditis.entity.ExportFolderEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.MappingConstants;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { DocumentMapper.class, CompanyMapper.class })
|
|
||||||
public interface ExportFolderMapper {
|
|
||||||
ExportFolderMapper INSTANCE = Mappers.getMapper(ExportFolderMapper.class);
|
|
||||||
|
|
||||||
@Mapping(target = "createdDate", ignore = true)
|
|
||||||
@Mapping(target = "modifiedDate", ignore = true)
|
|
||||||
@Mapping(target = "createdBy", ignore = true)
|
|
||||||
@Mapping(target = "modifiedBy", ignore = true)
|
|
||||||
ExportFolderEntity exportFolderToCompanyEntity(ExportFolder exportFolder);
|
|
||||||
|
|
||||||
ExportFolder exportFolderEntityToExportFolder(ExportFolderEntity exportFolderEntity);
|
|
||||||
}
|
|
||||||
@ -1,22 +1,64 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
package com.dh7789dev.xpeditis.mapper;
|
||||||
|
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.License;
|
||||||
import com.dh7789dev.xpeditis.dto.app.License;
|
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||||
import com.dh7789dev.xpeditis.entity.LicenseEntity;
|
import com.dh7789dev.xpeditis.entity.LicenseEntity;
|
||||||
import org.mapstruct.Mapper;
|
import com.dh7789dev.xpeditis.entity.LicenseTypeEntity;
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.MappingConstants;
|
public class LicenseMapper {
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
public static LicenseEntity licenseToLicenseEntity(License license) {
|
||||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
|
if (license == null) return null;
|
||||||
public interface LicenseMapper {
|
|
||||||
LicenseMapper INSTANCE = Mappers.getMapper(LicenseMapper.class);
|
LicenseEntity entity = new LicenseEntity();
|
||||||
|
entity.setId(license.getId());
|
||||||
@Mapping(target = "createdDate", ignore = true)
|
entity.setLicenseKey(license.getLicenseKey());
|
||||||
@Mapping(target = "modifiedDate", ignore = true)
|
entity.setStartDate(license.getStartDate());
|
||||||
@Mapping(target = "createdBy", ignore = true)
|
entity.setExpirationDate(license.getExpirationDate());
|
||||||
@Mapping(target = "modifiedBy", ignore = true)
|
entity.setMaxUsers(license.getMaxUsers());
|
||||||
LicenseEntity licenseToLicenseEntity(License license);
|
entity.setActive(license.isActive());
|
||||||
|
entity.setCreatedAt(license.getCreatedAt());
|
||||||
License licenseEntityToLicense(LicenseEntity licenseEntity);
|
|
||||||
}
|
// Convert LicenseType
|
||||||
|
if (license.getType() != null) {
|
||||||
|
entity.setType(mapLicenseType(license.getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static License licenseEntityToLicense(LicenseEntity licenseEntity) {
|
||||||
|
if (licenseEntity == null) return null;
|
||||||
|
|
||||||
|
return License.builder()
|
||||||
|
.id(licenseEntity.getId())
|
||||||
|
.licenseKey(licenseEntity.getLicenseKey())
|
||||||
|
.startDate(licenseEntity.getStartDate())
|
||||||
|
.expirationDate(licenseEntity.getExpirationDate())
|
||||||
|
.maxUsers(licenseEntity.getMaxUsers())
|
||||||
|
.isActive(licenseEntity.isActive())
|
||||||
|
.createdAt(licenseEntity.getCreatedAt())
|
||||||
|
.type(mapLicenseTypeEntity(licenseEntity.getType()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LicenseTypeEntity mapLicenseType(LicenseType licenseType) {
|
||||||
|
if (licenseType == null) return LicenseTypeEntity.TRIAL;
|
||||||
|
return switch (licenseType) {
|
||||||
|
case TRIAL -> LicenseTypeEntity.TRIAL;
|
||||||
|
case BASIC -> LicenseTypeEntity.BASIC;
|
||||||
|
case PREMIUM -> LicenseTypeEntity.PREMIUM;
|
||||||
|
case ENTERPRISE -> LicenseTypeEntity.ENTERPRISE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LicenseType mapLicenseTypeEntity(LicenseTypeEntity licenseTypeEntity) {
|
||||||
|
if (licenseTypeEntity == null) return LicenseType.TRIAL;
|
||||||
|
return switch (licenseTypeEntity) {
|
||||||
|
case TRIAL -> LicenseType.TRIAL;
|
||||||
|
case BASIC -> LicenseType.BASIC;
|
||||||
|
case PREMIUM -> LicenseType.PREMIUM;
|
||||||
|
case ENTERPRISE -> LicenseType.ENTERPRISE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.Notification;
|
|
||||||
import com.dh7789dev.xpeditis.entity.NotificationEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
|
|
||||||
@Mapper(componentModel = "spring" , uses = { ExportFolderMapper.class })
|
|
||||||
public interface NotificationMapper {
|
|
||||||
NotificationMapper INSTANCE = Mappers.getMapper(NotificationMapper.class);
|
|
||||||
|
|
||||||
NotificationEntity notificationToNotificationEntity(Notification notification);
|
|
||||||
|
|
||||||
Notification notificationEntityToNotification(NotificationEntity notificationEntity);
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.QuoteDetail;
|
|
||||||
import com.dh7789dev.xpeditis.entity.QuoteDetailEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(componentModel = "spring", uses = { DimensionMapper.class })
|
|
||||||
public interface QuoteDetailMapper {
|
|
||||||
QuoteDetailMapper INSTANCE = Mappers.getMapper(QuoteDetailMapper.class);
|
|
||||||
|
|
||||||
@Mapping(source = "quoteId", target = "quote.id")
|
|
||||||
QuoteDetailEntity quoteDetailsToQuoteDetailsEntity(QuoteDetail quoteDetail);
|
|
||||||
|
|
||||||
@Mapping(source = "quote.id", target = "quoteId")
|
|
||||||
QuoteDetail quoteDetailsEntityToQuoteDetails(QuoteDetailEntity quoteDetailEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.Quote;
|
|
||||||
import com.dh7789dev.xpeditis.entity.QuoteEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.Mapping;
|
|
||||||
import org.mapstruct.MappingConstants;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { QuoteDetailMapper.class })
|
|
||||||
public interface QuoteMapper {
|
|
||||||
QuoteMapper INSTANCE = Mappers.getMapper(QuoteMapper.class);
|
|
||||||
|
|
||||||
@Mapping(target = "createdDate", ignore = true)
|
|
||||||
@Mapping(target = "modifiedDate", ignore = true)
|
|
||||||
@Mapping(target = "createdBy", ignore = true)
|
|
||||||
@Mapping(target = "modifiedBy", ignore = true)
|
|
||||||
QuoteEntity quoteToQuoteEntity(Quote quote);
|
|
||||||
|
|
||||||
Quote quoteEntityToQuote(QuoteEntity quoteEntity);
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.ShipmentTracking;
|
|
||||||
import com.dh7789dev.xpeditis.entity.ShipmentTrackingEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(componentModel = "spring", uses = { ExportFolderMapper.class })
|
|
||||||
public interface ShipmentTrackingMapper {
|
|
||||||
|
|
||||||
ShipmentTrackingMapper INSTANCE = Mappers.getMapper(ShipmentTrackingMapper.class);
|
|
||||||
|
|
||||||
ShipmentTrackingEntity shipmentTrackingToShipmentTrackingEntity(ShipmentTracking shipmentTracking);
|
|
||||||
|
|
||||||
ShipmentTracking shipmentTrackingEntityToShipmentTracking(ShipmentTrackingEntity shipmentTrackingEntity);
|
|
||||||
}
|
|
||||||
@ -1,24 +1,64 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
package com.dh7789dev.xpeditis.mapper;
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.entity.UserEntity;
|
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
|
||||||
import org.mapstruct.Mapper;
|
import com.dh7789dev.xpeditis.dto.valueobject.Email;
|
||||||
import org.mapstruct.Mapping;
|
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
|
||||||
import org.mapstruct.MappingConstants;
|
import com.dh7789dev.xpeditis.entity.AuthProviderEntity;
|
||||||
import org.mapstruct.factory.Mappers;
|
import com.dh7789dev.xpeditis.entity.UserEntity;
|
||||||
|
|
||||||
|
public class UserMapper {
|
||||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
|
|
||||||
public interface UserMapper {
|
public static UserEntity userAccountToUserEntity(UserAccount user) {
|
||||||
|
if (user == null) return null;
|
||||||
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
|
|
||||||
|
UserEntity entity = new UserEntity();
|
||||||
@Mapping(target = "createdDate", ignore = true)
|
entity.setId(user.getId());
|
||||||
@Mapping(target = "modifiedDate", ignore = true)
|
entity.setFirstName(user.getFirstName());
|
||||||
@Mapping(target = "createdBy", ignore = true)
|
entity.setLastName(user.getLastName());
|
||||||
@Mapping(target = "modifiedBy", ignore = true)
|
entity.setEmail(user.getEmail() != null ? user.getEmail().getValue() : null);
|
||||||
UserEntity userAccountToUserEntity(UserAccount user);
|
entity.setUsername(user.getUsername());
|
||||||
|
entity.setPassword(user.getPassword());
|
||||||
UserAccount userEntityToUserAccount(UserEntity userEntity);
|
entity.setPhoneNumber(user.getPhoneNumber() != null ? user.getPhoneNumber().getValue() : null);
|
||||||
}
|
entity.setGoogleId(user.getGoogleId());
|
||||||
|
entity.setPrivacyPolicyAccepted(user.isPrivacyPolicyAccepted());
|
||||||
|
entity.setPrivacyPolicyAcceptedAt(user.getPrivacyPolicyAcceptedAt());
|
||||||
|
entity.setLastLoginAt(user.getLastLoginAt());
|
||||||
|
entity.setActive(user.isActive());
|
||||||
|
entity.setRole(user.getRole());
|
||||||
|
entity.setCreatedAt(user.getCreatedAt());
|
||||||
|
entity.setUpdatedAt(user.getUpdatedAt());
|
||||||
|
|
||||||
|
// Convert AuthProvider
|
||||||
|
if (user.getAuthProvider() != null) {
|
||||||
|
entity.setAuthProvider(user.getAuthProvider() == AuthProvider.GOOGLE ?
|
||||||
|
AuthProviderEntity.GOOGLE : AuthProviderEntity.LOCAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserAccount userEntityToUserAccount(UserEntity userEntity) {
|
||||||
|
if (userEntity == null) return null;
|
||||||
|
|
||||||
|
return UserAccount.builder()
|
||||||
|
.id(userEntity.getId())
|
||||||
|
.firstName(userEntity.getFirstName())
|
||||||
|
.lastName(userEntity.getLastName())
|
||||||
|
.email(userEntity.getEmail() != null ? new Email(userEntity.getEmail()) : null)
|
||||||
|
.username(userEntity.getUsername())
|
||||||
|
.password(userEntity.getPassword())
|
||||||
|
.phoneNumber(userEntity.getPhoneNumber() != null ? new PhoneNumber(userEntity.getPhoneNumber()) : null)
|
||||||
|
.googleId(userEntity.getGoogleId())
|
||||||
|
.privacyPolicyAccepted(userEntity.isPrivacyPolicyAccepted())
|
||||||
|
.privacyPolicyAcceptedAt(userEntity.getPrivacyPolicyAcceptedAt())
|
||||||
|
.lastLoginAt(userEntity.getLastLoginAt())
|
||||||
|
.isActive(userEntity.isActive())
|
||||||
|
.role(userEntity.getRole())
|
||||||
|
.createdAt(userEntity.getCreatedAt())
|
||||||
|
.updatedAt(userEntity.getUpdatedAt())
|
||||||
|
.authProvider(userEntity.getAuthProvider() == AuthProviderEntity.GOOGLE ?
|
||||||
|
AuthProvider.GOOGLE : AuthProvider.LOCAL)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.mapper;
|
|
||||||
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.VesselSchedule;
|
|
||||||
import com.dh7789dev.xpeditis.entity.VesselScheduleEntity;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
@Mapper(componentModel = "spring", uses = { ExportFolderMapper.class })
|
|
||||||
public interface VesselScheduleMapper {
|
|
||||||
|
|
||||||
VesselScheduleMapper INSTANCE = Mappers.getMapper(VesselScheduleMapper.class);
|
|
||||||
|
|
||||||
VesselScheduleEntity vesselScheduleToVesselScheduleEntity(VesselSchedule vesselSchedule);
|
|
||||||
|
|
||||||
VesselSchedule vesselScheduleEntityToVesselSchedule(VesselScheduleEntity vesselScheduleEntity);
|
|
||||||
}
|
|
||||||
@ -4,6 +4,8 @@ import com.dh7789dev.xpeditis.AuthenticationRepository;
|
|||||||
import com.dh7789dev.xpeditis.dao.CompanyDao;
|
import com.dh7789dev.xpeditis.dao.CompanyDao;
|
||||||
import com.dh7789dev.xpeditis.dao.TokenDao;
|
import com.dh7789dev.xpeditis.dao.TokenDao;
|
||||||
import com.dh7789dev.xpeditis.dao.UserDao;
|
import com.dh7789dev.xpeditis.dao.UserDao;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.Role;
|
||||||
|
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||||
@ -69,11 +71,24 @@ public class AuthenticationJwtRepository implements AuthenticationRepository {
|
|||||||
userEntity.setEmail(request.getEmail());
|
userEntity.setEmail(request.getEmail());
|
||||||
userEntity.setUsername(request.getUsername());
|
userEntity.setUsername(request.getUsername());
|
||||||
userEntity.setEnabled(true);
|
userEntity.setEnabled(true);
|
||||||
if(request.getCompany_uuid().isEmpty()){
|
if(request.getCompanyName() != null && !request.getCompanyName().isEmpty()){
|
||||||
userEntity.setRole(Role.ADMIN);
|
userEntity.setRole(Role.ADMIN);
|
||||||
CompanyEntity companyEntity = new CompanyEntity();
|
|
||||||
companyEntity.setName(request.getCompany_name());
|
// Rechercher l'entreprise existante ou en créer une nouvelle
|
||||||
companyDao.save(companyEntity);
|
CompanyEntity companyEntity = companyDao.findByName(request.getCompanyName())
|
||||||
|
.orElseGet(() -> {
|
||||||
|
log.info("Creating new company: {}", request.getCompanyName());
|
||||||
|
CompanyEntity newCompany = new CompanyEntity();
|
||||||
|
newCompany.setName(request.getCompanyName());
|
||||||
|
if (request.getCompanyCountry() != null && !request.getCompanyCountry().isEmpty()) {
|
||||||
|
newCompany.setCountry(request.getCompanyCountry());
|
||||||
|
}
|
||||||
|
return companyDao.save(newCompany);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lier l'utilisateur à l'entreprise
|
||||||
|
userEntity.setCompany(companyEntity);
|
||||||
|
log.info("User {} linked to company: {}", request.getUsername(), companyEntity.getName());
|
||||||
} else {
|
} else {
|
||||||
userEntity.setRole(Role.ADMIN);
|
userEntity.setRole(Role.ADMIN);
|
||||||
}
|
}
|
||||||
@ -94,7 +109,7 @@ public class AuthenticationJwtRepository implements AuthenticationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void revokeAllUserTokens(UserEntity userEntity) {
|
private void revokeAllUserTokens(UserEntity userEntity) {
|
||||||
var validUserTokens = tokenDao.findAllValidTokenByUserId(String.valueOf(userEntity.getId()));
|
var validUserTokens = tokenDao.findAllValidTokenByUserId(userEntity.getId());
|
||||||
if (validUserTokens.isEmpty()) return;
|
if (validUserTokens.isEmpty()) return;
|
||||||
validUserTokens.forEach(token -> {
|
validUserTokens.forEach(token -> {
|
||||||
token.setExpired(true);
|
token.setExpired(true);
|
||||||
@ -102,4 +117,92 @@ public class AuthenticationJwtRepository implements AuthenticationRepository {
|
|||||||
});
|
});
|
||||||
tokenDao.saveAll(validUserTokens);
|
tokenDao.saveAll(validUserTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationResponse authenticateWithGoogle(UserAccount userAccount) {
|
||||||
|
// For Google authentication, we assume the user is already validated
|
||||||
|
// Find or create the UserEntity equivalent
|
||||||
|
UserEntity userEntity = userDao.findByEmail(userAccount.getEmail().getValue())
|
||||||
|
.orElseGet(() -> {
|
||||||
|
UserEntity newUser = new UserEntity();
|
||||||
|
newUser.setFirstName(userAccount.getFirstName());
|
||||||
|
newUser.setLastName(userAccount.getLastName());
|
||||||
|
newUser.setEmail(userAccount.getEmail().getValue());
|
||||||
|
newUser.setEnabled(true);
|
||||||
|
newUser.setRole(Role.USER);
|
||||||
|
return userDao.save(newUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
var jwtToken = jwtUtil.generateToken(userEntity);
|
||||||
|
var refreshToken = jwtUtil.generateRefreshToken(userEntity);
|
||||||
|
|
||||||
|
revokeAllUserTokens(userEntity);
|
||||||
|
saveUserToken(userEntity, jwtToken);
|
||||||
|
|
||||||
|
return new AuthenticationResponse()
|
||||||
|
.setAccessToken(jwtToken)
|
||||||
|
.setRefreshToken(refreshToken)
|
||||||
|
.setCreatedAt(jwtUtil.extractCreatedAt(jwtToken))
|
||||||
|
.setExpiresAt(jwtUtil.extractExpiration(jwtToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserAccount getCurrentUser(String token) {
|
||||||
|
try {
|
||||||
|
// Extract username from JWT token
|
||||||
|
String username = jwtUtil.extractUsername(token);
|
||||||
|
|
||||||
|
// Find user in database
|
||||||
|
UserEntity userEntity = userDao.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||||
|
|
||||||
|
// Convert to UserAccount DTO
|
||||||
|
return convertToUserAccount(userEntity);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error getting current user from token", e);
|
||||||
|
throw new UsernameNotFoundException("Invalid token or user not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserAccount convertToUserAccount(UserEntity userEntity) {
|
||||||
|
return UserAccount.builder()
|
||||||
|
.id(userEntity.getId())
|
||||||
|
.firstName(userEntity.getFirstName())
|
||||||
|
.lastName(userEntity.getLastName())
|
||||||
|
.email(userEntity.getEmail() != null ? new com.dh7789dev.xpeditis.dto.valueobject.Email(userEntity.getEmail()) : null)
|
||||||
|
.username(userEntity.getUsername())
|
||||||
|
.phoneNumber(userEntity.getPhoneNumber() != null ? new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(userEntity.getPhoneNumber()) : null)
|
||||||
|
.authProvider(com.dh7789dev.xpeditis.dto.app.AuthProvider.LOCAL)
|
||||||
|
.isActive(userEntity.isEnabled())
|
||||||
|
.role(userEntity.getRole())
|
||||||
|
.createdAt(userEntity.getCreatedAt())
|
||||||
|
.lastLoginAt(userEntity.getLastLoginAt())
|
||||||
|
.privacyPolicyAccepted(userEntity.isPrivacyPolicyAccepted())
|
||||||
|
.privacyPolicyAcceptedAt(userEntity.getPrivacyPolicyAcceptedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout(String token) {
|
||||||
|
var tokenEntity = tokenDao.findByToken(token);
|
||||||
|
if (tokenEntity.isPresent()) {
|
||||||
|
tokenEntity.get().setExpired(true);
|
||||||
|
tokenEntity.get().setRevoked(true);
|
||||||
|
tokenDao.save(tokenEntity.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
return tokenDao.findByToken(token)
|
||||||
|
.map(t -> !t.isExpired() && !t.isRevoked())
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationResponse refreshToken(String refreshToken) {
|
||||||
|
// Implementation would validate refresh token and generate new access token
|
||||||
|
// For now, returning empty response to allow compilation
|
||||||
|
return new AuthenticationResponse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.repository;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.CompanyRepository;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Repository
|
|
||||||
public class CompanyJpaRepository implements CompanyRepository {
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.repository;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.LicenseRepository;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Repository
|
|
||||||
public class LicenseJpaRepository implements LicenseRepository {
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.repository;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.UserRepository;
|
|
||||||
import com.dh7789dev.xpeditis.dao.UserDao;
|
|
||||||
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
|
|
||||||
import com.dh7789dev.xpeditis.entity.UserEntity;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.security.Principal;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public class UserJpaRepository implements UserRepository {
|
|
||||||
|
|
||||||
private final UserDao userDao;
|
|
||||||
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public UserJpaRepository(UserDao userDao, PasswordEncoder passwordEncoder) {
|
|
||||||
this.userDao = userDao;
|
|
||||||
this.passwordEncoder = passwordEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
|
|
||||||
|
|
||||||
var userEntity = (UserEntity) ((UsernamePasswordAuthenticationToken) connectedUser).getPrincipal();
|
|
||||||
|
|
||||||
// check if the current password is correct
|
|
||||||
if (!passwordEncoder.matches(request.getCurrentPassword(), userEntity.getPassword())) {
|
|
||||||
throw new IllegalStateException("Wrong password");
|
|
||||||
}
|
|
||||||
// check if the two new passwords are the same
|
|
||||||
if (!request.getNewPassword().equals(request.getConfirmationPassword())) {
|
|
||||||
throw new IllegalStateException("Password are not the same");
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the password
|
|
||||||
userEntity.setPassword(passwordEncoder.encode(request.getNewPassword()));
|
|
||||||
userDao.save(userEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -76,6 +76,11 @@ public class JwtUtil {
|
|||||||
final String username = extractUsername(token);
|
final String username = extractUsername(token);
|
||||||
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
|
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRefreshTokenValid(String refreshToken, UserDetails userDetails) {
|
||||||
|
final String username = extractUsername(refreshToken);
|
||||||
|
return (username.equals(userDetails.getUsername())) && !isTokenExpired(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTokenExpired(String token) {
|
private boolean isTokenExpired(String token) {
|
||||||
return extractExpiration(token).before(new Date());
|
return extractExpiration(token).before(new Date());
|
||||||
|
|||||||
@ -0,0 +1,148 @@
|
|||||||
|
-- Enhanced User Management Schema Migration
|
||||||
|
-- This migration enhances the existing user management system with UUID support,
|
||||||
|
-- OAuth2 authentication, licensing system, and improved company management
|
||||||
|
|
||||||
|
-- Drop existing constraints that will be recreated
|
||||||
|
ALTER TABLE token DROP FOREIGN KEY FK_TOKEN_ON_USER;
|
||||||
|
|
||||||
|
-- Create companies table with UUID support
|
||||||
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
|
id BINARY(16) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
country VARCHAR(50),
|
||||||
|
siren VARCHAR(20),
|
||||||
|
num_eori VARCHAR(50),
|
||||||
|
phone VARCHAR(20),
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(255) DEFAULT 'SYSTEM',
|
||||||
|
modified_by VARCHAR(255),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Create licenses table with UUID support
|
||||||
|
CREATE TABLE IF NOT EXISTS licenses (
|
||||||
|
id BINARY(16) NOT NULL,
|
||||||
|
license_key VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
expiration_date DATE,
|
||||||
|
max_users INT NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
company_id BINARY(16) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(255) DEFAULT 'SYSTEM',
|
||||||
|
modified_by VARCHAR(255),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Create new users table with enhanced fields
|
||||||
|
CREATE TABLE IF NOT EXISTS users_new (
|
||||||
|
id BINARY(16) NOT NULL,
|
||||||
|
first_name VARCHAR(50) NOT NULL,
|
||||||
|
last_name VARCHAR(50) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
username VARCHAR(50) UNIQUE,
|
||||||
|
password VARCHAR(255),
|
||||||
|
phone_number VARCHAR(20) NOT NULL,
|
||||||
|
auth_provider VARCHAR(20) NOT NULL DEFAULT 'LOCAL',
|
||||||
|
google_id VARCHAR(255),
|
||||||
|
privacy_policy_accepted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
privacy_policy_accepted_at TIMESTAMP NULL,
|
||||||
|
last_login_at TIMESTAMP NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
role VARCHAR(255) NOT NULL DEFAULT 'USER',
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
company_id BINARY(16),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(255) DEFAULT 'SYSTEM',
|
||||||
|
modified_by VARCHAR(255),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Create tokens table with UUID foreign keys
|
||||||
|
CREATE TABLE IF NOT EXISTS tokens_new (
|
||||||
|
id BIGINT AUTO_INCREMENT NOT NULL,
|
||||||
|
token VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
token_type VARCHAR(50) NOT NULL,
|
||||||
|
revoked BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
expired BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
user_id BINARY(16) NOT NULL,
|
||||||
|
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
created_by VARCHAR(255) DEFAULT 'SYSTEM' NOT NULL,
|
||||||
|
modified_by VARCHAR(255),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users_new(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Insert default company for existing users
|
||||||
|
INSERT IGNORE INTO companies (id, name, country, is_active)
|
||||||
|
VALUES (UNHEX(REPLACE(UUID(), '-', '')), 'Default Company', 'Unknown', TRUE);
|
||||||
|
|
||||||
|
-- Set the default company ID for use in migration
|
||||||
|
SET @default_company_id = (SELECT id FROM companies WHERE name = 'Default Company' LIMIT 1);
|
||||||
|
|
||||||
|
-- Migrate existing users to new table structure
|
||||||
|
INSERT INTO users_new (
|
||||||
|
id, first_name, last_name, email, username, password, phone_number,
|
||||||
|
auth_provider, privacy_policy_accepted, is_active, role, enabled, company_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
UNHEX(REPLACE(UUID(), '-', '')),
|
||||||
|
COALESCE(first_name, 'Unknown'),
|
||||||
|
COALESCE(last_name, 'User'),
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
COALESCE(phone, '+1234567890'),
|
||||||
|
'LOCAL',
|
||||||
|
TRUE,
|
||||||
|
TRUE,
|
||||||
|
role,
|
||||||
|
enabled,
|
||||||
|
@default_company_id
|
||||||
|
FROM users
|
||||||
|
WHERE email IS NOT NULL;
|
||||||
|
|
||||||
|
-- Create trial license for default company
|
||||||
|
INSERT INTO licenses (
|
||||||
|
id, license_key, type, start_date, max_users, is_active, company_id
|
||||||
|
) VALUES (
|
||||||
|
UNHEX(REPLACE(UUID(), '-', '')),
|
||||||
|
CONCAT('TRIAL-', UPPER(SUBSTRING(REPLACE(UUID(), '-', ''), 1, 8))),
|
||||||
|
'TRIAL',
|
||||||
|
CURDATE(),
|
||||||
|
5,
|
||||||
|
TRUE,
|
||||||
|
@default_company_id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Migrate tokens with proper user references
|
||||||
|
INSERT INTO tokens_new (token, token_type, revoked, expired, user_id)
|
||||||
|
SELECT
|
||||||
|
t.token,
|
||||||
|
t.token_type,
|
||||||
|
t.revoked,
|
||||||
|
t.expired,
|
||||||
|
un.id
|
||||||
|
FROM token t
|
||||||
|
INNER JOIN users u ON t.user_id = u.id
|
||||||
|
INNER JOIN users_new un ON u.email = un.email;
|
||||||
|
|
||||||
|
-- Drop old tables and rename new ones
|
||||||
|
DROP TABLE IF EXISTS token;
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
|
|
||||||
|
RENAME TABLE users_new TO users;
|
||||||
|
RENAME TABLE tokens_new TO token;
|
||||||
26
run-dev.sh
Executable file
26
run-dev.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# XPEDITIS Backend - Development Runner
|
||||||
|
|
||||||
|
echo "Starting XPEDITIS Backend in Development Mode..."
|
||||||
|
echo "Loading environment variables from .env file..."
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "ERROR: .env file not found!"
|
||||||
|
echo "Please copy .env.example to .env and configure your variables:"
|
||||||
|
echo " cp .env.example .env"
|
||||||
|
echo " # Then edit .env with your values"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load .env file
|
||||||
|
set -o allexport
|
||||||
|
source .env
|
||||||
|
set +o allexport
|
||||||
|
|
||||||
|
echo "Environment variables loaded successfully"
|
||||||
|
echo "Starting application with profile: ${SPRING_PROFILES_ACTIVE:-dev}"
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
./mvnw spring-boot:run -Dspring-boot.run.profiles=${SPRING_PROFILES_ACTIVE:-dev}
|
||||||
57
run-prod.sh
Executable file
57
run-prod.sh
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# XPEDITIS Backend - Production Runner
|
||||||
|
|
||||||
|
echo "Starting XPEDITIS Backend in Production Mode..."
|
||||||
|
echo "Loading environment variables from .env file..."
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "ERROR: .env file not found!"
|
||||||
|
echo "Please copy .env.example to .env and configure your production variables:"
|
||||||
|
echo " cp .env.example .env"
|
||||||
|
echo " # Then edit .env with your production values"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load .env file
|
||||||
|
set -o allexport
|
||||||
|
source .env
|
||||||
|
set +o allexport
|
||||||
|
|
||||||
|
# Override profile for production
|
||||||
|
export SPRING_PROFILES_ACTIVE=prod
|
||||||
|
|
||||||
|
# Validate required production variables
|
||||||
|
echo "Validating required production environment variables..."
|
||||||
|
|
||||||
|
REQUIRED_VARS=(
|
||||||
|
"SPRING_DATASOURCE_URL"
|
||||||
|
"SPRING_DATASOURCE_USERNAME"
|
||||||
|
"SPRING_DATASOURCE_PASSWORD"
|
||||||
|
"JWT_SECRET_KEY"
|
||||||
|
"GOOGLE_CLIENT_ID"
|
||||||
|
"GOOGLE_CLIENT_SECRET"
|
||||||
|
"SPRING_MAIL_PASSWORD_PROD"
|
||||||
|
)
|
||||||
|
|
||||||
|
MISSING_VARS=()
|
||||||
|
for var in "${REQUIRED_VARS[@]}"; do
|
||||||
|
if [ -z "${!var}" ]; then
|
||||||
|
MISSING_VARS+=($var)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#MISSING_VARS[@]} -ne 0 ]; then
|
||||||
|
echo "ERROR: Missing required production environment variables:"
|
||||||
|
printf " - %s\n" "${MISSING_VARS[@]}"
|
||||||
|
echo ""
|
||||||
|
echo "Please update your .env file with production values."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "All required production variables are set"
|
||||||
|
echo "Starting application with profile: prod"
|
||||||
|
|
||||||
|
# Start the application in production mode
|
||||||
|
./mvnw spring-boot:run -Dspring-boot.run.profiles=prod
|
||||||
28
test-env.sh
Executable file
28
test-env.sh
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test Environment Variables Loading
|
||||||
|
|
||||||
|
echo "Testing .env file loading..."
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "ERROR: .env file not found!"
|
||||||
|
echo "Please copy .env.example to .env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load .env file
|
||||||
|
set -o allexport
|
||||||
|
source .env
|
||||||
|
set +o allexport
|
||||||
|
|
||||||
|
echo "Environment variables loaded successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Key variables:"
|
||||||
|
echo " SPRING_PROFILES_ACTIVE = ${SPRING_PROFILES_ACTIVE:-dev}"
|
||||||
|
echo " SERVER_PORT = ${SERVER_PORT:-8080}"
|
||||||
|
echo " GOOGLE_CLIENT_ID = ${GOOGLE_CLIENT_ID:0:20}..." # Show only first 20 chars for security
|
||||||
|
echo " JWT_SECRET_KEY = ${JWT_SECRET_KEY:0:10}..." # Show only first 10 chars for security
|
||||||
|
echo " SPRING_DATASOURCE_URL = ${SPRING_DATASOURCE_URL:-Not set}"
|
||||||
|
echo ""
|
||||||
|
echo "Test completed successfully!"
|
||||||
Loading…
Reference in New Issue
Block a user