diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..766f514
--- /dev/null
+++ b/.env.example
@@ -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
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..bcd489b
--- /dev/null
+++ b/CLAUDE.md
@@ -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
\ No newline at end of file
diff --git a/ENV_SETUP.md b/ENV_SETUP.md
new file mode 100644
index 0000000..abfd492
--- /dev/null
+++ b/ENV_SETUP.md
@@ -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
+```
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index ef1301f..0000000
--- a/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# leblr-backend
-
-Le BLR backend side
-SWAGGER UI : http://localhost:8080/swagger-ui.html
-
-.\mvnw clean install flyway:migrate -Pprod
-.\mvnw clean install flyway:migrate '-Dflyway.configFiles=flyway-h2.conf' -Pdev
\ No newline at end of file
diff --git a/application/pom.xml b/application/pom.xml
index dbdf3de..e1a48bd 100755
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -29,6 +29,14 @@
org.springframework.boot
spring-boot-starter-validation
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
org.springframework.boot
spring-boot-starter-test
diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java
index 90de943..71dcf60 100644
--- a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java
+++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java
@@ -2,35 +2,101 @@ package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.AuthenticationService;
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.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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+@Slf4j
@RestController
-@RequestMapping(value = "${apiPrefix}/api/v1/auth",
- produces = APPLICATION_JSON_VALUE)
+@RequestMapping(value = "${apiPrefix}/api/v1/auth", produces = APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
+@Tag(name = "Authentication", description = "Authentication and registration endpoints")
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")
public ResponseEntity authenticate(
@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")
public ResponseEntity register(
@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 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 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 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;
+ }
}
}
diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/ProfileController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/ProfileController.java
new file mode 100644
index 0000000..fb2c4bb
--- /dev/null
+++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/ProfileController.java
@@ -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 getProfile(Authentication authentication) {
+ log.info("Profile retrieval request for user: {}", authentication.getName());
+
+ UserAccount userAccount = userService.findByEmail(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 updateProfile(
+ @RequestBody @Valid UpdateProfileRequest request,
+ Authentication authentication) {
+ log.info("Profile update request for user: {}", authentication.getName());
+
+ // Get current user
+ UserAccount currentUser = userService.findByEmail(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();
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/UserRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/UserRestController.java
index eef434b..c6317b3 100644
--- a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/UserRestController.java
+++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/UserRestController.java
@@ -1,38 +1,153 @@
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.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.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.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.PatchMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import java.security.Principal;
+import java.util.UUID;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+@Slf4j
@RestController
@Validated
-@RequestMapping(value = "${apiPrefix}/api/v1/users",
- produces = APPLICATION_JSON_VALUE)
+@RequiredArgsConstructor
+@RequestMapping(value = "${apiPrefix}/api/v1/users", produces = APPLICATION_JSON_VALUE)
+@Tag(name = "User Management", description = "User profile and management endpoints")
public class UserRestController {
- private final UserService service;
+ private final UserService userService;
- public UserRestController(UserService service) {
- this.service = service;
+ @Operation(summary = "Get current user profile", description = "Retrieve the profile of the authenticated user")
+ @ApiResponse(responseCode = "200", description = "User profile retrieved successfully")
+ @GetMapping("/profile")
+ public ResponseEntity getProfile(Authentication authentication) {
+ log.info("Profile request for user: {}", authentication.getName());
+
+ // Get user by email from authentication
+ UserAccount userAccount = userService.findByEmail(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")
- @PatchMapping("/password")
+ @Operation(summary = "Update user profile", description = "Update profile information of the authenticated user")
+ @ApiResponse(responseCode = "200", description = "Profile updated successfully")
+ @ApiResponse(responseCode = "400", description = "Invalid profile data")
+ @PutMapping("/profile")
+ public ResponseEntity updateProfile(
+ @RequestBody @Valid UpdateProfileRequest request,
+ Authentication authentication) {
+ log.info("Profile update request for user: {}", authentication.getName());
+
+ // Get current user
+ UserAccount currentUser = userService.findByEmail(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 changePassword(
- @RequestBody ChangePasswordRequest request,
+ @RequestBody @Valid ChangePasswordRequest request,
Principal connectedUser) {
- service.changePassword(request, connectedUser);
- return new ResponseEntity<>(HttpStatus.OK);
+ log.info("Password change request for user: {}", connectedUser.getName());
+ 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 deleteAccount(Authentication authentication) {
+ log.info("Account deletion request for user: {}", authentication.getName());
+
+ UserAccount user = userService.findByEmail(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> 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 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().getValue())
+ .username(userAccount.getUsername())
+ .phoneNumber(userAccount.getPhoneNumber().getValue())
+ .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();
}
}
diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java
index f52b15e..2832287 100755
--- a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java
+++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java
@@ -1,6 +1,7 @@
package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.UserDao;
+import com.dh7789dev.xpeditis.entity.UserEntity;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/OAuth2Configuration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/OAuth2Configuration.java
new file mode 100644
index 0000000..54e4aa8
--- /dev/null
+++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/OAuth2Configuration.java
@@ -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";
+}
\ No newline at end of file
diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/RestTemplateConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/RestTemplateConfiguration.java
new file mode 100644
index 0000000..e7fc16a
--- /dev/null
+++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/RestTemplateConfiguration.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java
index e1b6d77..5862a3f 100755
--- a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java
+++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java
@@ -42,6 +42,20 @@ public class SecurityConfiguration {
"/api/v1/auth/**",
"/actuator/health/**"};
+ private static final String[] USER_AUTHENTICATED_URL = {
+ "/api/v1/profile/**",
+ "/api/v1/users/profile",
+ "/api/v1/users/password",
+ "/api/v1/users/account"};
+
+ 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 = {
"/v2/api-docs",
"/v3/api-docs",
@@ -99,10 +113,10 @@ public class SecurityConfiguration {
http.authorizeHttpRequests(auth ->
auth.requestMatchers(WHITE_LIST_URL).permitAll()
.requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll()
- .requestMatchers(antMatcher(HttpMethod.GET, API_V1_URI)).permitAll()
- .requestMatchers(antMatcher(HttpMethod.PUT, API_V1_URI)).hasRole(ADMIN_ROLE)
- .requestMatchers(antMatcher(HttpMethod.DELETE, API_V1_URI)).hasRole(ADMIN_ROLE)
- .requestMatchers(antMatcher(HttpMethod.POST, API_V1_URI)).hasRole(ADMIN_ROLE)
+ .requestMatchers(USER_AUTHENTICATED_URL).authenticated()
+ .requestMatchers(GENERAL_API_URL).authenticated()
+ .requestMatchers(ADMIN_ONLY_URL).hasRole(ADMIN_ROLE)
+ .requestMatchers(antMatcher(HttpMethod.DELETE, "/api/v1/users/**")).hasRole(ADMIN_ROLE)
.requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS)
.requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS)
.requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS)
diff --git a/bootstrap/src/main/resources/application-dev.yml b/bootstrap/src/main/resources/application-dev.yml
index ea2ec7e..5d744f1 100644
--- a/bootstrap/src/main/resources/application-dev.yml
+++ b/bootstrap/src/main/resources/application-dev.yml
@@ -1,14 +1,13 @@
----
spring:
h2:
console:
- enabled: 'false'
+ enabled: ${SPRING_H2_CONSOLE_ENABLED:false}
datasource:
- url: jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
- driverClassName: org.h2.Driver
- username: sa
- password: ''
+ url: ${SPRING_H2_DATASOURCE_URL:jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE}
+ driverClassName: ${SPRING_H2_DATASOURCE_DRIVER_CLASS_NAME:org.h2.Driver}
+ username: ${SPRING_H2_DATASOURCE_USERNAME:sa}
+ password: ${SPRING_H2_DATASOURCE_PASSWORD:}
sql:
init:
@@ -16,45 +15,74 @@ spring:
mode: always
jpa:
- show-sql: true
+ show-sql: ${SPRING_JPA_SHOW_SQL:false}
properties:
hibernate:
- format_sql: true
+ format_sql: ${SPRING_JPA_FORMAT_SQL:false}
# show_sql: true
- database-platform: org.hibernate.dialect.H2Dialect
+ database-platform: ${SPRING_JPA_DATABASE_PLATFORM_H2:org.hibernate.dialect.H2Dialect}
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
- 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
- enabled: false # enables flyway database migration
+ enabled: ${SPRING_FLYWAY_ENABLED_DEV:false} # enables flyway database migration
mail:
- host: sandbox.smtp.mailtrap.io
- port: 2525
- username: 2597bd31d265eb
- password: cd126234193c89
+ host: ${SPRING_MAIL_HOST_DEV:sandbox.smtp.mailtrap.io}
+ port: ${SPRING_MAIL_PORT_DEV:2525}
+ username: ${SPRING_MAIL_USERNAME_DEV:your_mailtrap_username}
+ password: ${SPRING_MAIL_PASSWORD_DEV:your_mailtrap_password}
properties:
mail:
smtp:
ssl:
- trust:"*"
- auth: true
+ trust: ${SPRING_MAIL_SMTP_SSL_TRUST:*}
+ auth: ${SPRING_MAIL_SMTP_AUTH:true}
starttls:
- enable: true
- connectiontimeout: 5000
- timeout: 3000
- writetimeout: 5000
+ enable: ${SPRING_MAIL_SMTP_STARTTLS_ENABLE:true}
+ connectiontimeout: ${SPRING_MAIL_SMTP_CONNECTION_TIMEOUT:5000}
+ timeout: ${SPRING_MAIL_SMTP_TIMEOUT:3000}
+ 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:
email:
- from: randommailjf@gmail.com
+ from: ${APPLICATION_EMAIL_FROM_DEV:noreply@xpeditis.local}
csrf:
- enabled: false
+ enabled: ${APPLICATION_CSRF_ENABLED_DEV:false}
security:
jwt:
- secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
- expiration: 86400000 # a day
+ secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970}
+ expiration: ${JWT_EXPIRATION:86400000} # a day
refresh-token:
- expiration: 604800000 # 7 days
\ No newline at end of file
+ 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}
\ No newline at end of file
diff --git a/bootstrap/src/main/resources/application-prod.yml b/bootstrap/src/main/resources/application-prod.yml
index 1dcc1ab..eeb2e91 100644
--- a/bootstrap/src/main/resources/application-prod.yml
+++ b/bootstrap/src/main/resources/application-prod.yml
@@ -1,10 +1,9 @@
----
spring:
datasource:
- url: ${SPRING_DATASOURCE_URL}
- driver-class-name: com.mysql.cj.jdbc.Driver
- username: ${SPRING_DATASOURCE_USERNAME}
- password: ${SPRING_DATASOURCE_PASSWORD}
+ url: ${SPRING_DATASOURCE_URL:}
+ driver-class-name: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}
+ username: ${SPRING_DATASOURCE_USERNAME:}
+ password: ${SPRING_DATASOURCE_PASSWORD:}
#hikari:
#schema: leblr
@@ -15,52 +14,83 @@ spring:
#data-locations: import_users.sql
jpa:
-# show-sql: true
+ show-sql: ${SPRING_JPA_SHOW_SQL:false}
properties:
hibernate:
- format_sql: true
+ format_sql: ${SPRING_JPA_FORMAT_SQL:true}
#show_sql: true
database: mysql
- database-platform: org.hibernate.dialect.MySQLDialect
+ database-platform: ${SPRING_JPA_DATABASE_PLATFORM_MYSQL:org.hibernate.dialect.MySQLDialect}
hibernate:
- ddl-auto: validate
- defer-datasource-initialization: false
+ ddl-auto: ${SPRING_JPA_HIBERNATE_DDL_AUTO_PROD:validate}
+ defer-datasource-initialization: ${SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_PROD:false}
#open-in-view: false
flyway: # flyway automatically uses the datasource from the application to connect to the DB
- enabled: true # enables flyway database migration
- locations: classpath:db/migration/structure, classpath:db/migration/data # the location where flyway should look for migration scripts
- validate-on-migrate: true
- baseline-on-migrate: true
- baseline-version: 0
- default-schema: leblr
+ enabled: ${SPRING_FLYWAY_ENABLED_PROD:true} # enables flyway database migration
+ locations: ${SPRING_FLYWAY_LOCATIONS:classpath:db/migration/structure, classpath:db/migration/data} # the location where flyway should look for migration scripts
+ validate-on-migrate: ${SPRING_FLYWAY_VALIDATE_ON_MIGRATE:true}
+ baseline-on-migrate: ${SPRING_FLYWAY_BASELINE_ON_MIGRATE:true}
+ baseline-version: ${SPRING_FLYWAY_BASELINE_VERSION:0}
+ 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:
- protocol: smtp
- host: ssl0.ovh.net
- port: 587
- username: contact@xpeditis.fr
- password:
+ protocol: ${SPRING_MAIL_PROTOCOL_PROD:smtp}
+ host: ${SPRING_MAIL_HOST_PROD:ssl0.ovh.net}
+ port: ${SPRING_MAIL_PORT_PROD:587}
+ username: ${SPRING_MAIL_USERNAME_PROD:contact@xpeditis.fr}
+ password: ${SPRING_MAIL_PASSWORD_PROD:}
properties:
mail:
smtp:
- auth: true
+ auth: ${SPRING_MAIL_SMTP_AUTH:true}
starttls:
- enable: true
- connectiontimeout: 5000
- timeout: 3000
- writetimeout: 5000
+ enable: ${SPRING_MAIL_SMTP_STARTTLS_ENABLE:true}
+ connectiontimeout: ${SPRING_MAIL_SMTP_CONNECTION_TIMEOUT:5000}
+ timeout: ${SPRING_MAIL_SMTP_TIMEOUT:3000}
+ writetimeout: ${SPRING_MAIL_SMTP_WRITE_TIMEOUT:5000}
application:
email:
- from: contact@leblr.fr
+ from: ${APPLICATION_EMAIL_FROM_PROD:contact@xpeditis.fr}
csrf:
- enabled: false
+ enabled: ${APPLICATION_CSRF_ENABLED_PROD:true}
security:
jwt:
- secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
- expiration: 86400000 # a day
+ secret-key: ${JWT_SECRET_KEY}
+ expiration: ${JWT_EXPIRATION:86400000} # a day
refresh-token:
- expiration: 604800000 # 7 days
\ No newline at end of file
+ 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}
\ No newline at end of file
diff --git a/bootstrap/src/main/resources/application.yml b/bootstrap/src/main/resources/application.yml
index 305483c..36f241c 100755
--- a/bootstrap/src/main/resources/application.yml
+++ b/bootstrap/src/main/resources/application.yml
@@ -1,8 +1,8 @@
server:
- port: 8080
+ port: ${SERVER_PORT:8080}
file:
- upload-dir: /upload
+ upload-dir: ${FILE_UPLOAD_DIR:/upload}
spring:
http:
@@ -12,11 +12,11 @@ spring:
force: true
application:
- name: '@project.description@'
- version: '@project.version@'
+ name: ${SPRING_APPLICATION_NAME:@project.description@}
+ version: ${SPRING_APPLICATION_VERSION:@project.version@}
profiles:
- active: '@spring.profiles.active@'
+ active: ${SPRING_PROFILES_ACTIVE:@spring.profiles.active@}
banner:
location: 'classpath:banner.txt'
@@ -30,21 +30,22 @@ spring:
servlet:
multipart:
- enabled: true
- max-file-size: 50MB
- max-request-size: 50MB
+ enabled: ${SPRING_SERVLET_MULTIPART_ENABLED:true}
+ max-file-size: ${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}
+ max-request-size: ${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}
#location: ${java.io.tmpdir}
logging:
level:
+ root: ${LOGGING_LEVEL_ROOT:INFO}
org:
- org.hibernate.orm.query.sqm.ast.logTree: OFF
+ org.hibernate.orm.query.sqm.ast.logTree: ${LOGGING_LEVEL_HIBERNATE_SQL:OFF}
springframework:
boot:
- autoconfigure: OFF
+ autoconfigure: ${LOGGING_LEVEL_SPRINGFRAMEWORK_BOOT_AUTOCONFIGURE:OFF}
web:
filter:
- CommonsRequestLoggingFilter: INFO
+ CommonsRequestLoggingFilter: ${LOGGING_LEVEL_COMMONS_REQUEST_LOGGING_FILTER:INFO}
security:
config:
annotation:
diff --git a/bootstrap/src/test/java/com/dh7789dev/xpeditis/LeBlrApplicationTests.java b/bootstrap/src/test/java/com/dh7789dev/xpeditis/LeBlrApplicationTests.java
deleted file mode 100755
index 0d8751c..0000000
--- a/bootstrap/src/test/java/com/dh7789dev/xpeditis/LeBlrApplicationTests.java
+++ /dev/null
@@ -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() {
- }
-}
diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/AuthenticationService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/AuthenticationService.java
index 99476e7..857e282 100644
--- a/domain/api/src/main/java/com/dh7789dev/xpeditis/AuthenticationService.java
+++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/AuthenticationService.java
@@ -1,12 +1,24 @@
package com.dh7789dev.xpeditis;
+import com.dh7789dev.xpeditis.dto.app.UserAccount;
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.response.AuthenticationResponse;
+import com.dh7789dev.xpeditis.port.in.AuthenticationUseCase;
-public interface AuthenticationService {
+public interface AuthenticationService extends AuthenticationUseCase {
AuthenticationResponse authenticate(AuthenticationRequest 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);
}
diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/CompanyService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/CompanyService.java
index d0a00d4..79e1f48 100644
--- a/domain/api/src/main/java/com/dh7789dev/xpeditis/CompanyService.java
+++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/CompanyService.java
@@ -1,4 +1,24 @@
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 {
+
+ Company createCompany(CreateCompanyRequest request);
+ Company updateCompany(UUID id, UpdateCompanyRequest request);
+ Optional findCompanyById(UUID id);
+ List 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);
}
diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/LicenseService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/LicenseService.java
index 7914daa..f2c0404 100644
--- a/domain/api/src/main/java/com/dh7789dev/xpeditis/LicenseService.java
+++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/LicenseService.java
@@ -1,4 +1,25 @@
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);
}
diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/UserService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/UserService.java
index 964e2c1..f59417e 100644
--- a/domain/api/src/main/java/com/dh7789dev/xpeditis/UserService.java
+++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/UserService.java
@@ -1,10 +1,35 @@
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.port.in.UserManagementUseCase;
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);
+
+ UserAccount createUser(RegisterRequest request);
+
+ UserAccount createGoogleUser(String googleToken);
+
+ Optional findById(UUID id);
+
+ Optional findByEmail(String email);
+
+ Optional findByUsername(String username);
+
+ UserAccount updateProfile(UserAccount userAccount);
+
+ void deactivateUser(UUID userId);
+
+ void deleteUser(UUID userId);
+
+ boolean existsByEmail(String email);
+
+ boolean existsByUsername(String username);
}
diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/AuthenticationUseCase.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/AuthenticationUseCase.java
new file mode 100644
index 0000000..7352746
--- /dev/null
+++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/AuthenticationUseCase.java
@@ -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);
+}
\ No newline at end of file
diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/LicenseValidationUseCase.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/LicenseValidationUseCase.java
new file mode 100644
index 0000000..9834293
--- /dev/null
+++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/LicenseValidationUseCase.java
@@ -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);
+}
\ No newline at end of file
diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/UserManagementUseCase.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/UserManagementUseCase.java
new file mode 100644
index 0000000..ac3e134
--- /dev/null
+++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/port/in/UserManagementUseCase.java
@@ -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 findById(UUID id);
+
+ Optional findByEmail(String email);
+
+ Optional 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);
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/AuthProvider.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/AuthProvider.java
new file mode 100644
index 0000000..51313c3
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/AuthProvider.java
@@ -0,0 +1,6 @@
+package com.dh7789dev.xpeditis.dto.app;
+
+public enum AuthProvider {
+ LOCAL,
+ GOOGLE
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Company.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Company.java
index fd69b84..d3cb833 100644
--- a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Company.java
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Company.java
@@ -1,19 +1,50 @@
package com.dh7789dev.xpeditis.dto.app;
-
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.time.LocalDateTime;
import java.util.List;
+import java.util.UUID;
@Data
+@Builder
+@NoArgsConstructor
@AllArgsConstructor
public class Company {
- private Long id;
+ private UUID id;
private String name;
+ private String description;
+ private String website;
+ private String industry;
private String country;
+ private String siren;
+ private String numEori;
+ private String phone;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+ private boolean isActive;
private List users;
+ private List licenses;
+ private License license; // Current active license
private List quotes;
private List 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;
+ }
}
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/GoogleUserInfo.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/GoogleUserInfo.java
new file mode 100644
index 0000000..56d5702
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/GoogleUserInfo.java
@@ -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;
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/License.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/License.java
index a2e3996..c680a44 100644
--- a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/License.java
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/License.java
@@ -1,16 +1,54 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.UUID;
@Data
+@Builder
+@NoArgsConstructor
@AllArgsConstructor
public class License {
- private Long id;
+ private UUID id;
private String licenseKey;
+ private LicenseType type;
+ private LocalDate startDate;
private LocalDate expirationDate;
- private boolean active;
- private UserAccount user;
+ private LocalDateTime issuedDate;
+ 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;
+ }
}
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/LicenseType.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/LicenseType.java
new file mode 100644
index 0000000..31a08ec
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/LicenseType.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Role.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Role.java
new file mode 100644
index 0000000..0b36579
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/Role.java
@@ -0,0 +1,8 @@
+package com.dh7789dev.xpeditis.dto.app;
+
+public enum Role {
+ USER,
+ MANAGER,
+ ADMIN,
+ ADMIN_PLATFORM
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/UserAccount.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/UserAccount.java
index 31a6c7f..dc8d8d2 100644
--- a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/UserAccount.java
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/UserAccount.java
@@ -1,20 +1,51 @@
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.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.time.LocalDateTime;
import java.util.List;
+import java.util.UUID;
@Data
+@Builder
+@NoArgsConstructor
@AllArgsConstructor
public class UserAccount {
- private String username;
+ private UUID id;
private String firstName;
private String lastName;
- private String email;
+ private Email email;
+ private String username;
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 License license;
private List 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);
+ }
}
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/CreateCompanyRequest.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/CreateCompanyRequest.java
new file mode 100644
index 0000000..df63a91
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/CreateCompanyRequest.java
@@ -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;
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/GoogleAuthRequest.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/GoogleAuthRequest.java
new file mode 100644
index 0000000..600b6e5
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/GoogleAuthRequest.java
@@ -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;
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/RegisterRequest.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/RegisterRequest.java
index c6183a9..8759eab 100644
--- a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/RegisterRequest.java
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/RegisterRequest.java
@@ -1,41 +1,69 @@
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.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.FieldDefaults;
@Data
+@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@Accessors(chain = true)
public class RegisterRequest {
- @NotBlank
+ @NotBlank(message = "First name is required")
+ @Size(max = 50, message = "First name must not exceed 50 characters")
String firstName;
- @NotBlank
+ @NotBlank(message = "Last name is required")
+ @Size(max = 50, message = "Last name must not exceed 50 characters")
String lastName;
- @NotBlank
- String username;
-
- @NotBlank
+ @Email(message = "Invalid email format")
+ @NotBlank(message = "Email is required")
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;
- @NotBlank
- String phone;
+ String confirmPassword;
- 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;
+
+ 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);
+ }
}
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/UpdateCompanyRequest.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/UpdateCompanyRequest.java
new file mode 100644
index 0000000..77fa5e7
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/UpdateCompanyRequest.java
@@ -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;
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/UpdateProfileRequest.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/UpdateProfileRequest.java
new file mode 100644
index 0000000..7b3ca4a
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/request/UpdateProfileRequest.java
@@ -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;
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/AuthenticationResponse.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/AuthenticationResponse.java
index dec8e31..26ffd81 100644
--- a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/AuthenticationResponse.java
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/AuthenticationResponse.java
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@@ -12,6 +13,7 @@ import lombok.experimental.FieldDefaults;
import java.util.Date;
@Data
+@Builder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
@@ -25,12 +27,21 @@ public class AuthenticationResponse {
@JsonProperty("refresh_token")
String refreshToken;
+ @JsonProperty("token_type")
+ String tokenType = "Bearer";
+
+ @JsonProperty("expires_in")
+ long expiresIn;
+
@JsonProperty("created_at")
Date createdAt;
@JsonProperty("expires_at")
Date expiresAt;
+ @JsonProperty("user")
+ UserResponse user;
+
@JsonProperty("error_message")
String error;
}
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/TokenResponse.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/TokenResponse.java
new file mode 100644
index 0000000..874e917
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/TokenResponse.java
@@ -0,0 +1,22 @@
+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;
+ String tokenType = "Bearer";
+ UserResponse user;
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/UserResponse.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/UserResponse.java
new file mode 100644
index 0000000..009d62a
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/response/UserResponse.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/valueobject/Email.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/valueobject/Email.java
new file mode 100644
index 0000000..8d750b9
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/valueobject/Email.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/valueobject/PhoneNumber.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/valueobject/PhoneNumber.java
new file mode 100644
index 0000000..2d30dcb
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/valueobject/PhoneNumber.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/AuthenticationException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/AuthenticationException.java
new file mode 100644
index 0000000..b53a275
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/AuthenticationException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/BusinessException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/BusinessException.java
new file mode 100644
index 0000000..96409a6
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/BusinessException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/CompanyInactiveException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/CompanyInactiveException.java
new file mode 100644
index 0000000..a868f31
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/CompanyInactiveException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/InvalidCredentialsException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/InvalidCredentialsException.java
new file mode 100644
index 0000000..c0c22cf
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/InvalidCredentialsException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/LicenseExpiredException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/LicenseExpiredException.java
new file mode 100644
index 0000000..8292bc5
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/LicenseExpiredException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/LicenseUserLimitExceededException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/LicenseUserLimitExceededException.java
new file mode 100644
index 0000000..fd180d3
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/LicenseUserLimitExceededException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/ResourceNotFoundException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/ResourceNotFoundException.java
new file mode 100644
index 0000000..08b3e66
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/ResourceNotFoundException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/UserAlreadyExistsException.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/UserAlreadyExistsException.java
new file mode 100644
index 0000000..c8b1caf
--- /dev/null
+++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/exception/UserAlreadyExistsException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/domain/service/pom.xml b/domain/service/pom.xml
index 57a24e6..a098e50 100755
--- a/domain/service/pom.xml
+++ b/domain/service/pom.xml
@@ -62,6 +62,10 @@
org.springframework.security
spring-security-crypto
+
+ org.springframework
+ spring-tx
+
diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/AuthenticationServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/AuthenticationServiceImpl.java
index 01086b8..fd55abc 100644
--- a/domain/service/src/main/java/com/dh7789dev/xpeditis/AuthenticationServiceImpl.java
+++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/AuthenticationServiceImpl.java
@@ -1,26 +1,244 @@
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.response.AuthenticationResponse;
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.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+import java.util.UUID;
@Service
+@RequiredArgsConstructor
+@Slf4j
+@Transactional
public class AuthenticationServiceImpl implements AuthenticationService {
private final AuthenticationRepository authenticationRepository;
-
- public AuthenticationServiceImpl(AuthenticationRepository authenticationRepository) {
- this.authenticationRepository = authenticationRepository;
- }
+ private final UserRepository userRepository;
+ private final OAuth2Provider oAuth2Provider;
+ private final CompanyService companyService;
+ private final PasswordEncoder passwordEncoder;
@Override
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);
}
@Override
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);
}
+
+ @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 = 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 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());
+ }
+ }
}
diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/CompanyServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/CompanyServiceImpl.java
index 43e1c65..cee3808 100644
--- a/domain/service/src/main/java/com/dh7789dev/xpeditis/CompanyServiceImpl.java
+++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/CompanyServiceImpl.java
@@ -1,7 +1,314 @@
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.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
@Service
+@RequiredArgsConstructor
+@Slf4j
+@Transactional
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 findCompanyById(UUID id) {
+ return companyRepository.findById(id);
+ }
+
+ @Override
+ public List 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 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 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 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
+ };
+ }
}
diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/LicenseServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/LicenseServiceImpl.java
index 91da424..d7266ce 100644
--- a/domain/service/src/main/java/com/dh7789dev/xpeditis/LicenseServiceImpl.java
+++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/LicenseServiceImpl.java
@@ -1,7 +1,60 @@
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 java.util.UUID;
+
@Service
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
+ }
}
diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/UserServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/UserServiceImpl.java
index 5e1c014..bbc3b7b 100644
--- a/domain/service/src/main/java/com/dh7789dev/xpeditis/UserServiceImpl.java
+++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/UserServiceImpl.java
@@ -1,9 +1,13 @@
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 org.springframework.stereotype.Service;
import java.security.Principal;
+import java.util.Optional;
+import java.util.UUID;
@Service
public class UserServiceImpl implements UserService {
@@ -18,4 +22,64 @@ public class UserServiceImpl implements UserService {
public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
userRepository.changePassword(request, connectedUser);
}
+
+ @Override
+ public UserAccount createUser(RegisterRequest request) {
+ // TODO: Implement user creation logic
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public UserAccount createGoogleUser(String googleToken) {
+ // TODO: Implement Google user creation logic
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public Optional findById(UUID id) {
+ // TODO: Implement find by id logic
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public Optional findByEmail(String email) {
+ // TODO: Implement find by email logic
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public Optional findByUsername(String username) {
+ // TODO: Implement find by username logic
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public UserAccount updateProfile(UserAccount userAccount) {
+ // TODO: Implement profile update logic
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public void deactivateUser(UUID userId) {
+ // TODO: Implement user deactivation logic
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public void deleteUser(UUID userId) {
+ // TODO: Implement user deletion logic
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public boolean existsByEmail(String email) {
+ // TODO: Implement email existence check
+ return false;
+ }
+
+ @Override
+ public boolean existsByUsername(String username) {
+ // TODO: Implement username existence check
+ return false;
+ }
}
diff --git a/domain/service/src/test/java/com/dh7789dev/xpeditis/AuthenticationServiceImplTest.java b/domain/service/src/test/java/com/dh7789dev/xpeditis/AuthenticationServiceImplTest.java
new file mode 100644
index 0000000..da3d9c3
--- /dev/null
+++ b/domain/service/src/test/java/com/dh7789dev/xpeditis/AuthenticationServiceImplTest.java
@@ -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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/domain/service/src/test/java/com/dh7789dev/xpeditis/CompanyServiceImplTest.java b/domain/service/src/test/java/com/dh7789dev/xpeditis/CompanyServiceImplTest.java
index cb54882..d7ea9b7 100644
--- a/domain/service/src/test/java/com/dh7789dev/xpeditis/CompanyServiceImplTest.java
+++ b/domain/service/src/test/java/com/dh7789dev/xpeditis/CompanyServiceImplTest.java
@@ -1,19 +1,273 @@
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.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
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)
+@DisplayName("Company Service Tests")
class CompanyServiceImplTest {
+ @Mock
+ private CompanyRepository companyRepository;
+
+ @Mock
+ private LicenseRepository licenseRepository;
+
+ @Mock
+ private UserRepository userRepository;
+
+ @InjectMocks
+ private CompanyServiceImpl companyService;
- @Test
- void test(){
- int test = 1 +1;
- assertEquals(2,test);
+ private CreateCompanyRequest validCreateRequest;
+ private UpdateCompanyRequest validUpdateRequest;
+ private Company testCompany;
+ 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);
+ }
}
}
diff --git a/domain/service/src/test/java/com/dh7789dev/xpeditis/UserServiceImplTest.java b/domain/service/src/test/java/com/dh7789dev/xpeditis/UserServiceImplTest.java
index c653f3f..66b525e 100644
--- a/domain/service/src/test/java/com/dh7789dev/xpeditis/UserServiceImplTest.java
+++ b/domain/service/src/test/java/com/dh7789dev/xpeditis/UserServiceImplTest.java
@@ -1,19 +1,187 @@
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.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
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)
+@DisplayName("UserServiceImpl Tests")
class UserServiceImplTest {
+ @Mock
+ private UserRepository userRepository;
- @Test
- void test(){
- int test = 1 +1;
- assertEquals(2,test);
+ @InjectMocks
+ private UserServiceImpl userService;
+
+ 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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/AuthenticationRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/AuthenticationRepository.java
index 6107d94..3a9b926 100644
--- a/domain/spi/src/main/java/com/dh7789dev/xpeditis/AuthenticationRepository.java
+++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/AuthenticationRepository.java
@@ -1,5 +1,6 @@
package com.dh7789dev.xpeditis;
+import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
@@ -8,4 +9,9 @@ public interface AuthenticationRepository {
AuthenticationResponse authenticate(AuthenticationRequest 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);
}
diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/CompanyRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/CompanyRepository.java
index b619576..d937627 100644
--- a/domain/spi/src/main/java/com/dh7789dev/xpeditis/CompanyRepository.java
+++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/CompanyRepository.java
@@ -1,4 +1,22 @@
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 {
+
+ Company save(Company company);
+
+ Optional findById(UUID id);
+
+ Optional findByName(String name);
+
+ List findAll();
+
+ boolean existsByName(String name);
+
+ void deleteById(UUID id);
}
diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/LicenseRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/LicenseRepository.java
index 4b3ec59..f98d988 100644
--- a/domain/spi/src/main/java/com/dh7789dev/xpeditis/LicenseRepository.java
+++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/LicenseRepository.java
@@ -1,4 +1,24 @@
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 {
+
+ License save(License license);
+
+ Optional findById(UUID id);
+
+ Optional findActiveLicenseByCompanyId(UUID companyId);
+
+ List findByCompanyId(UUID companyId);
+
+ Optional findByLicenseKey(String licenseKey);
+
+ void deleteById(UUID id);
+
+ void deactivateLicense(UUID id);
}
diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/OAuth2Provider.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/OAuth2Provider.java
new file mode 100644
index 0000000..ebb1d57
--- /dev/null
+++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/OAuth2Provider.java
@@ -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 getUserInfo(String accessToken);
+}
\ No newline at end of file
diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/UserRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/UserRepository.java
index cdba3d7..78aa283 100644
--- a/domain/spi/src/main/java/com/dh7789dev/xpeditis/UserRepository.java
+++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/UserRepository.java
@@ -1,10 +1,34 @@
package com.dh7789dev.xpeditis;
+import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import java.security.Principal;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
public interface UserRepository {
void changePassword(ChangePasswordRequest request, Principal connectedUser);
+
+ UserAccount save(UserAccount userAccount);
+
+ Optional findById(UUID id);
+
+ Optional findByEmail(String email);
+
+ Optional findByUsername(String username);
+
+ Optional findByGoogleId(String googleId);
+
+ boolean existsByEmail(String email);
+
+ boolean existsByUsername(String username);
+
+ void deleteById(UUID id);
+
+ void deactivateUser(UUID id);
+
+ List findByCompanyIdAndIsActive(UUID companyId, boolean isActive);
}
diff --git a/infrastructure/pom.xml b/infrastructure/pom.xml
index d6ac811..f34b02b 100755
--- a/infrastructure/pom.xml
+++ b/infrastructure/pom.xml
@@ -32,10 +32,11 @@
lombok
provided
-
+
+
@@ -128,7 +129,8 @@
lombok
${org.projectlombok.version}
-
+
+
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/CompanyDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/CompanyDao.java
index 9da92f2..237dd52 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/CompanyDao.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/CompanyDao.java
@@ -2,7 +2,15 @@ package com.dh7789dev.xpeditis.dao;
import com.dh7789dev.xpeditis.entity.CompanyEntity;
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 {
+@Repository
+public interface CompanyDao extends JpaRepository {
+
+ Optional findByName(String name);
+
+ boolean existsByName(String name);
}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/LicenseDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/LicenseDao.java
index 5b6dc6d..6a4ff8e 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/LicenseDao.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/LicenseDao.java
@@ -2,6 +2,21 @@ package com.dh7789dev.xpeditis.dao;
import com.dh7789dev.xpeditis.entity.LicenseEntity;
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 {
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@Repository
+public interface LicenseDao extends JpaRepository {
+
+ Optional findByLicenseKey(String licenseKey);
+
+ List 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 findActiveLicenseByCompanyId(@Param("companyId") UUID companyId);
}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/TokenDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/TokenDao.java
index 9a33689..901f8a4 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/TokenDao.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/TokenDao.java
@@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
+import java.util.UUID;
import org.springframework.stereotype.Repository;
@@ -19,5 +20,5 @@ public interface TokenDao extends JpaRepository {
on t.user.id = u.id\s
where u.id = :userId and (t.expired = false or t.revoked = false)\s
""")
- List findAllValidTokenByUserId(String userId);
+ List findAllValidTokenByUserId(UUID userId);
}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java
index c033c42..b9f43a4 100755
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java
@@ -2,16 +2,21 @@ package com.dh7789dev.xpeditis.dao;
import com.dh7789dev.xpeditis.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
-
-import java.util.Optional;
-
-import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
-public interface UserDao extends JpaRepository {
+import java.util.Optional;
+import java.util.UUID;
+
+@Repository
+public interface UserDao extends JpaRepository {
- @Query("SELECT u FROM UserEntity u WHERE u.username = :username")
Optional findByUsername(String username);
+
+ Optional findByEmail(String email);
+
+ Optional findByGoogleId(String googleId);
+
boolean existsByUsername(String username);
-
+
+ boolean existsByEmail(String email);
}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/AuthProviderEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/AuthProviderEntity.java
new file mode 100644
index 0000000..03e65d5
--- /dev/null
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/AuthProviderEntity.java
@@ -0,0 +1,6 @@
+package com.dh7789dev.xpeditis.entity;
+
+public enum AuthProviderEntity {
+ LOCAL,
+ GOOGLE
+}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/CompanyEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/CompanyEntity.java
index c7b2059..5a1ca1b 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/CompanyEntity.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/CompanyEntity.java
@@ -7,61 +7,107 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
+import org.hibernate.annotations.GenericGenerator;
import java.time.LocalDateTime;
import java.util.List;
+import java.util.UUID;
@Entity
@Getter
@Setter
@NoArgsConstructor
@FieldNameConstants
-@FieldDefaults( level = AccessLevel.PRIVATE)
-@Table(name = "Company")
-public class CompanyEntity extends BaseEntity {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
+@FieldDefaults(level = AccessLevel.PRIVATE)
+@Table(name = "companies")
+@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
+public class CompanyEntity {
- @Column(name = "name", length = 50)
- private String name;
+ @Id
+ @GeneratedValue(generator = "UUID")
+ @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
+ @Column(columnDefinition = "BINARY(16)")
+ UUID id;
+
+ @Column(name = "name", nullable = false, unique = true, length = 100)
+ String name;
@Column(name = "country", length = 50)
- private String country;
+ String country;
- @Column(name = "siren")
- private String siren;
+ @Column(name = "siren", length = 20)
+ String siren;
- @Column(name = "num_eori")
- private String num_eori;
+ @Column(name = "num_eori", length = 50)
+ String numEori;
@Column(name = "phone", length = 20)
- private String phone;
+ String phone;
- @OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
- private List users;
+ @Column(name = "is_active", nullable = false)
+ boolean isActive = true;
- @OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
- private List quotes;
+ @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ List users;
- @OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
- private List exports;
+ @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ List licenses;
+
+ @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ List quotes;
+
+ @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ List exports;
@Column(name = "created_at", updatable = false)
- private LocalDateTime createdAt;
+ LocalDateTime createdAt;
- @Column(name = "modified_at")
- private LocalDateTime modifiedAt;
+ @Column(name = "updated_at")
+ 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
public void onCreate() {
+ if (id == null) {
+ id = UUID.randomUUID();
+ }
createdAt = LocalDateTime.now();
- modifiedAt = LocalDateTime.now();
+ updatedAt = LocalDateTime.now();
}
@PreUpdate
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;
}
}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/LicenseEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/LicenseEntity.java
index b8ff10a..f564f6c 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/LicenseEntity.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/LicenseEntity.java
@@ -7,49 +7,105 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
+import org.hibernate.annotations.GenericGenerator;
import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.util.UUID;
@Entity
@Getter
@Setter
@NoArgsConstructor
@FieldNameConstants
-@FieldDefaults( level = AccessLevel.PRIVATE)
-@Table(name = "License")
-public class LicenseEntity extends BaseEntity {
+@FieldDefaults(level = AccessLevel.PRIVATE)
+@Table(name = "licenses")
+@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
+public class LicenseEntity {
+
@Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
+ @GeneratedValue(generator = "UUID")
+ @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
+ @Column(columnDefinition = "BINARY(16)")
+ UUID id;
- @Column(unique = true)
- private String licenseKey;
+ @Column(name = "license_key", unique = true, nullable = false)
+ String licenseKey;
- @Column(name = "expirationDate")
- private LocalDate expirationDate;
+ @Enumerated(EnumType.STRING)
+ @Column(name = "type", nullable = false)
+ LicenseTypeEntity type;
- private boolean active;
+ @Column(name = "start_date", nullable = false)
+ LocalDate startDate;
- @OneToOne
- @JoinColumn(name = "user_id", unique = true)
- private UserEntity user;
+ @Column(name = "expiration_date")
+ LocalDate expirationDate;
+
+ @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)
- private LocalDateTime createdAt;
+ LocalDateTime createdAt;
- @Column(name = "modified_at")
- private LocalDateTime modifiedAt;
+ @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
public void onCreate() {
+ if (id == null) {
+ id = UUID.randomUUID();
+ }
+ if (licenseKey == null) {
+ licenseKey = generateLicenseKey();
+ }
createdAt = LocalDateTime.now();
- modifiedAt = LocalDateTime.now();
}
- @PreUpdate
- public void onUpdate() {
- modifiedAt = LocalDateTime.now();
+ public boolean isExpired() {
+ return expirationDate != null && expirationDate.isBefore(LocalDate.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();
}
}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/LicenseTypeEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/LicenseTypeEntity.java
new file mode 100644
index 0000000..0c7b011
--- /dev/null
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/LicenseTypeEntity.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/UserEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/UserEntity.java
index e6c3bb1..3690e2d 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/UserEntity.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/UserEntity.java
@@ -1,5 +1,6 @@
package com.dh7789dev.xpeditis.entity;
+import com.dh7789dev.xpeditis.dto.app.Role;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
@@ -7,91 +8,145 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
-import org.hibernate.annotations.NaturalId;
+import org.hibernate.annotations.GenericGenerator;
import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
+import java.util.UUID;
@Entity
@Getter
@Setter
@NoArgsConstructor
@FieldNameConstants
-@FieldDefaults( level = AccessLevel.PRIVATE)
-@Table(name = "Users")
-public class UserEntity extends BaseEntity implements UserDetails {
+@FieldDefaults(level = AccessLevel.PRIVATE)
+@Table(name = "users")
+@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class)
+public class UserEntity implements UserDetails {
- @NaturalId
- @Column(nullable = false, unique = true, length = 50)
- private String username;
+ @Id
+ @GeneratedValue(generator = "UUID")
+ @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
+ @Column(columnDefinition = "BINARY(16)")
+ UUID id;
- @Column(name = "first_name", length = 50)
- private String firstName;
+ @Column(name = "first_name", nullable = false, length = 50)
+ String firstName;
- @Column(name = "last_name", length = 50)
- private String lastName;
+ @Column(name = "last_name", nullable = false, length = 50)
+ String lastName;
- @Column(unique = true, nullable = false)
- private String email;
+ @Column(nullable = false, unique = true)
+ String email;
- @Column(nullable = false)
- private String password;
+ @Column(name = "username", unique = true, length = 50)
+ String username;
- @Column(name = "phone", length = 20)
- private String phone;
+ @Column(name = "password")
+ String password;
+
+ @Column(name = "phone_number", nullable = true, length = 20)
+ String phoneNumber;
@Enumerated(EnumType.STRING)
- @Column(nullable = false)
- private Role role;
+ @Column(name = "auth_provider", nullable = false)
+ AuthProviderEntity authProvider = AuthProviderEntity.LOCAL;
- @Column(name = "enabled", nullable = false, columnDefinition = "BOOLEAN DEFAULT TRUE NOT NULL")
- private boolean enabled;
+ @Column(name = "google_id")
+ String googleId;
- @ManyToOne
- private CompanyEntity company;
+ @Column(name = "privacy_policy_accepted", nullable = false)
+ boolean privacyPolicyAccepted = false;
- @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
- private LicenseEntity license;
+ @Column(name = "privacy_policy_accepted_at")
+ LocalDateTime privacyPolicyAcceptedAt;
- @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
- private List quotes;
+ @Column(name = "last_login_at")
+ LocalDateTime lastLoginAt;
- @OneToMany(mappedBy = "user")
- private List tokens;
+ @Column(name = "is_active", nullable = false)
+ 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 quotes;
+
+ @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ List tokens;
@Column(name = "created_at", updatable = false)
- private LocalDateTime createdAt;
+ LocalDateTime createdAt;
- @Column(name = "modified_at")
- private LocalDateTime modifiedAt;
+ @Column(name = "updated_at")
+ 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
public void onCreate() {
+ if (id == null) {
+ id = UUID.randomUUID();
+ }
createdAt = LocalDateTime.now();
- modifiedAt = LocalDateTime.now();
+ updatedAt = LocalDateTime.now();
}
@PreUpdate
public void onUpdate() {
- modifiedAt = LocalDateTime.now();
+ updatedAt = LocalDateTime.now();
}
@Override
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
public boolean isAccountNonExpired() {
- return true;
+ return isActive;
}
@Override
public boolean isAccountNonLocked() {
- return true;
+ return isActive;
}
@Override
@@ -101,14 +156,19 @@ public class UserEntity extends BaseEntity implements UserDetails {
@Override
public boolean isEnabled() {
- return enabled;
+ return enabled && isActive;
+ }
+
+ public String getFullName() {
+ return (firstName != null ? firstName : "") +
+ (lastName != null ? " " + lastName : "").trim();
}
@Override
public String toString() {
return "UserEntity(" + super.toString() + String.format(
- "username=%s, firstName=%s, lastName=%s, email=%s, role=%s)",
- username, firstName, lastName, email, role.name()
+ "id=%s, username=%s, firstName=%s, lastName=%s, email=%s, role=%s)",
+ id, username, firstName, lastName, email, role.name()
);
}
}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/AddressMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/AddressMapper.java
deleted file mode 100644
index 15421a6..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/AddressMapper.java
+++ /dev/null
@@ -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);
-}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/CompanyMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/CompanyMapper.java
index 05a2dd4..30ae57f 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/CompanyMapper.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/CompanyMapper.java
@@ -1,25 +1,39 @@
-package com.dh7789dev.xpeditis.mapper;
-
-
-import com.dh7789dev.xpeditis.dto.app.Company;
-import com.dh7789dev.xpeditis.entity.CompanyEntity;
-import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-import org.mapstruct.MappingConstants;
-import org.mapstruct.factory.Mappers;
-
-
-
-@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
-public interface CompanyMapper {
-
- CompanyMapper INSTANCE = Mappers.getMapper(CompanyMapper.class);
-
- @Mapping(target = "createdDate", ignore = true)
- @Mapping(target = "modifiedDate", ignore = true)
- @Mapping(target = "createdBy", ignore = true)
- @Mapping(target = "modifiedBy", ignore = true)
- CompanyEntity companyToCompanyEntity(Company company);
-
- Company companyEntityToCompany(CompanyEntity companyEntity);
-}
+package com.dh7789dev.xpeditis.mapper;
+
+import com.dh7789dev.xpeditis.dto.app.Company;
+import com.dh7789dev.xpeditis.entity.CompanyEntity;
+
+public class CompanyMapper {
+
+ public static CompanyEntity companyToCompanyEntity(Company company) {
+ if (company == null) return null;
+
+ CompanyEntity entity = new CompanyEntity();
+ entity.setId(company.getId());
+ entity.setName(company.getName());
+ entity.setCountry(company.getCountry());
+ entity.setSiren(company.getSiren());
+ entity.setNumEori(company.getNumEori());
+ entity.setPhone(company.getPhone());
+ entity.setActive(company.isActive());
+ entity.setCreatedAt(company.getCreatedAt());
+ entity.setUpdatedAt(company.getUpdatedAt());
+ return entity;
+ }
+
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/DimensionMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/DimensionMapper.java
deleted file mode 100644
index 233ce8b..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/DimensionMapper.java
+++ /dev/null
@@ -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);
-
-}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/DocumentMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/DocumentMapper.java
deleted file mode 100644
index f445fc9..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/DocumentMapper.java
+++ /dev/null
@@ -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);
-
-}
-
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ExportFolderMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ExportFolderMapper.java
deleted file mode 100644
index b8751fa..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ExportFolderMapper.java
+++ /dev/null
@@ -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);
-}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/LicenseMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/LicenseMapper.java
index 5f63f10..c7301db 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/LicenseMapper.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/LicenseMapper.java
@@ -1,22 +1,64 @@
-package com.dh7789dev.xpeditis.mapper;
-
-
-import com.dh7789dev.xpeditis.dto.app.License;
-import com.dh7789dev.xpeditis.entity.LicenseEntity;
-import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-import org.mapstruct.MappingConstants;
-import org.mapstruct.factory.Mappers;
-
-@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
-public interface LicenseMapper {
- LicenseMapper INSTANCE = Mappers.getMapper(LicenseMapper.class);
-
- @Mapping(target = "createdDate", ignore = true)
- @Mapping(target = "modifiedDate", ignore = true)
- @Mapping(target = "createdBy", ignore = true)
- @Mapping(target = "modifiedBy", ignore = true)
- LicenseEntity licenseToLicenseEntity(License license);
-
- License licenseEntityToLicense(LicenseEntity licenseEntity);
-}
+package com.dh7789dev.xpeditis.mapper;
+
+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.LicenseTypeEntity;
+
+public class LicenseMapper {
+
+ public static LicenseEntity licenseToLicenseEntity(License license) {
+ if (license == null) return null;
+
+ LicenseEntity entity = new LicenseEntity();
+ entity.setId(license.getId());
+ entity.setLicenseKey(license.getLicenseKey());
+ entity.setStartDate(license.getStartDate());
+ entity.setExpirationDate(license.getExpirationDate());
+ entity.setMaxUsers(license.getMaxUsers());
+ entity.setActive(license.isActive());
+ entity.setCreatedAt(license.getCreatedAt());
+
+ // 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;
+ };
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/NotificationMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/NotificationMapper.java
deleted file mode 100644
index 0e0e7c3..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/NotificationMapper.java
+++ /dev/null
@@ -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);
-
-}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/QuoteDetailMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/QuoteDetailMapper.java
deleted file mode 100644
index 77d991b..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/QuoteDetailMapper.java
+++ /dev/null
@@ -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);
-}
-
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/QuoteMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/QuoteMapper.java
deleted file mode 100644
index 48903ea..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/QuoteMapper.java
+++ /dev/null
@@ -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);
-}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ShipmentTrackingMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ShipmentTrackingMapper.java
deleted file mode 100644
index c80fb1f..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/ShipmentTrackingMapper.java
+++ /dev/null
@@ -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);
-}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/UserMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/UserMapper.java
index b1b8e29..511343b 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/UserMapper.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/UserMapper.java
@@ -1,24 +1,64 @@
-package com.dh7789dev.xpeditis.mapper;
-
-import com.dh7789dev.xpeditis.dto.app.UserAccount;
-import com.dh7789dev.xpeditis.entity.UserEntity;
-import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-import org.mapstruct.MappingConstants;
-import org.mapstruct.factory.Mappers;
-
-
-@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
-public interface UserMapper {
-
- UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
-
- @Mapping(target = "createdDate", ignore = true)
- @Mapping(target = "modifiedDate", ignore = true)
- @Mapping(target = "createdBy", ignore = true)
- @Mapping(target = "modifiedBy", ignore = true)
- UserEntity userAccountToUserEntity(UserAccount user);
-
- UserAccount userEntityToUserAccount(UserEntity userEntity);
-}
-
+package com.dh7789dev.xpeditis.mapper;
+
+import com.dh7789dev.xpeditis.dto.app.UserAccount;
+import com.dh7789dev.xpeditis.dto.app.AuthProvider;
+import com.dh7789dev.xpeditis.dto.valueobject.Email;
+import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
+import com.dh7789dev.xpeditis.entity.AuthProviderEntity;
+import com.dh7789dev.xpeditis.entity.UserEntity;
+
+public class UserMapper {
+
+ public static UserEntity userAccountToUserEntity(UserAccount user) {
+ if (user == null) return null;
+
+ UserEntity entity = new UserEntity();
+ entity.setId(user.getId());
+ entity.setFirstName(user.getFirstName());
+ entity.setLastName(user.getLastName());
+ entity.setEmail(user.getEmail() != null ? user.getEmail().getValue() : null);
+ entity.setUsername(user.getUsername());
+ entity.setPassword(user.getPassword());
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/VesselScheduleMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/VesselScheduleMapper.java
deleted file mode 100644
index 9716d51..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/VesselScheduleMapper.java
+++ /dev/null
@@ -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);
-}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java
index 4ef586c..d42b385 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java
@@ -4,6 +4,8 @@ import com.dh7789dev.xpeditis.AuthenticationRepository;
import com.dh7789dev.xpeditis.dao.CompanyDao;
import com.dh7789dev.xpeditis.dao.TokenDao;
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.response.AuthenticationResponse;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
@@ -69,10 +71,10 @@ public class AuthenticationJwtRepository implements AuthenticationRepository {
userEntity.setEmail(request.getEmail());
userEntity.setUsername(request.getUsername());
userEntity.setEnabled(true);
- if(request.getCompany_uuid().isEmpty()){
+ if(request.getCompanyName() != null && !request.getCompanyName().isEmpty()){
userEntity.setRole(Role.ADMIN);
CompanyEntity companyEntity = new CompanyEntity();
- companyEntity.setName(request.getCompany_name());
+ companyEntity.setName(request.getCompanyName());
companyDao.save(companyEntity);
} else {
userEntity.setRole(Role.ADMIN);
@@ -94,7 +96,7 @@ public class AuthenticationJwtRepository implements AuthenticationRepository {
}
private void revokeAllUserTokens(UserEntity userEntity) {
- var validUserTokens = tokenDao.findAllValidTokenByUserId(String.valueOf(userEntity.getId()));
+ var validUserTokens = tokenDao.findAllValidTokenByUserId(userEntity.getId());
if (validUserTokens.isEmpty()) return;
validUserTokens.forEach(token -> {
token.setExpired(true);
@@ -102,4 +104,63 @@ public class AuthenticationJwtRepository implements AuthenticationRepository {
});
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) {
+ // Implementation would extract user from token
+ // For now, returning null to allow compilation
+ return null;
+ }
+
+ @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();
+ }
}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/CompanyJpaRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/CompanyJpaRepository.java
deleted file mode 100644
index 664d5bb..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/CompanyJpaRepository.java
+++ /dev/null
@@ -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 {
-}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/LicenseJpaRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/LicenseJpaRepository.java
deleted file mode 100644
index 890ac30..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/LicenseJpaRepository.java
+++ /dev/null
@@ -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 {
-}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/UserJpaRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/UserJpaRepository.java
deleted file mode 100644
index 97fc413..0000000
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/UserJpaRepository.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/util/JwtUtil.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/util/JwtUtil.java
index 3afa46a..4c411a9 100644
--- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/util/JwtUtil.java
+++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/util/JwtUtil.java
@@ -76,6 +76,11 @@ public class JwtUtil {
final String username = extractUsername(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) {
return extractExpiration(token).before(new Date());
diff --git a/infrastructure/src/main/resources/db/migration/structure/V2__ENHANCED_USER_MANAGEMENT_SCHEMA.sql b/infrastructure/src/main/resources/db/migration/structure/V2__ENHANCED_USER_MANAGEMENT_SCHEMA.sql
new file mode 100644
index 0000000..daea7ff
--- /dev/null
+++ b/infrastructure/src/main/resources/db/migration/structure/V2__ENHANCED_USER_MANAGEMENT_SCHEMA.sql
@@ -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;
\ No newline at end of file
diff --git a/run-dev.sh b/run-dev.sh
new file mode 100755
index 0000000..b93f28e
--- /dev/null
+++ b/run-dev.sh
@@ -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}
\ No newline at end of file
diff --git a/run-prod.sh b/run-prod.sh
new file mode 100755
index 0000000..fdafcc0
--- /dev/null
+++ b/run-prod.sh
@@ -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
\ No newline at end of file
diff --git a/test-env.sh b/test-env.sh
new file mode 100755
index 0000000..84aec08
--- /dev/null
+++ b/test-env.sh
@@ -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!"
\ No newline at end of file