autocomit
This commit is contained in:
@@ -0,0 +1,239 @@
|
|||||||
|
# LDPv2 Monorepo - Story 0: Foundation
|
||||||
|
|
||||||
|
Complete implementation of Story 0 (Walking Skeleton) with Spring Boot backend and Angular 18 frontend.
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ldpv2-monorepo/
|
||||||
|
├── backend/ # Spring Boot 3.2 backend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── main/java/com/ldpv2/
|
||||||
|
│ │ │ ├── config/ # Security, OpenAPI config
|
||||||
|
│ │ │ ├── controller/ # REST controllers
|
||||||
|
│ │ │ ├── domain/entity/ # JPA entities
|
||||||
|
│ │ │ ├── dto/ # Request/Response DTOs
|
||||||
|
│ │ │ ├── exception/ # Exception handlers
|
||||||
|
│ │ │ ├── repository/ # Spring Data repositories
|
||||||
|
│ │ │ ├── security/ # JWT security
|
||||||
|
│ │ │ └── service/ # Business logic
|
||||||
|
│ │ └── resources/
|
||||||
|
│ │ ├── application.yml # Configuration
|
||||||
|
│ │ └── db/changelog/ # Liquibase migrations
|
||||||
|
│ ├── pom.xml
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── frontend/ # Angular 18 frontend
|
||||||
|
│ ├── src/app/
|
||||||
|
│ │ ├── core/ # Auth, guards, interceptors
|
||||||
|
│ │ ├── shared/ # Models, components
|
||||||
|
│ │ └── features/ # Feature modules
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── angular.json
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── docker-compose.yml # Complete Docker setup
|
||||||
|
└── README.md # This file
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- JDK 17+ (for local backend development)
|
||||||
|
- Node.js 18+ (for local frontend development)
|
||||||
|
|
||||||
|
### Option 1: Docker Compose (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start all services
|
||||||
|
docker-compose up --build
|
||||||
|
|
||||||
|
# Access the application
|
||||||
|
Frontend: http://localhost:4200
|
||||||
|
Backend API: http://localhost:8080/api
|
||||||
|
Swagger UI: http://localhost:8080/swagger-ui/index.html
|
||||||
|
PostgreSQL: localhost:5432
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Local Development
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# Using Maven
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
|
||||||
|
# Or with Docker for PostgreSQL only
|
||||||
|
docker run -d -p 5432:5432 \
|
||||||
|
-e POSTGRES_DB=ldpv2 \
|
||||||
|
-e POSTGRES_USER=ldpv2_user \
|
||||||
|
-e POSTGRES_PASSWORD=ldpv2_password \
|
||||||
|
postgres:16-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start dev server
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Access at http://localhost:4200
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔑 Default Credentials
|
||||||
|
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `admin123`
|
||||||
|
|
||||||
|
## 📚 API Documentation
|
||||||
|
|
||||||
|
Swagger UI is available at: http://localhost:8080/swagger-ui/index.html
|
||||||
|
|
||||||
|
### Authentication Endpoints
|
||||||
|
- `POST /api/auth/register` - Register new user
|
||||||
|
- `POST /api/auth/login` - Login and get JWT token
|
||||||
|
|
||||||
|
### Environment Endpoints (Requires Authentication)
|
||||||
|
- `GET /api/environments` - List all environments (paginated)
|
||||||
|
- `GET /api/environments/{id}` - Get environment by ID
|
||||||
|
- `POST /api/environments` - Create new environment
|
||||||
|
- `PUT /api/environments/{id}` - Update environment
|
||||||
|
- `DELETE /api/environments/{id}` - Delete environment
|
||||||
|
- `GET /api/environments/search?query={name}` - Search environments
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Backend Tests
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
./mvnw test
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
./mvnw clean test jacoco:report
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Tests
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# E2E tests
|
||||||
|
npm run e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Database Migrations
|
||||||
|
|
||||||
|
Liquibase automatically runs migrations on startup. Migration files are in:
|
||||||
|
`backend/src/main/resources/db/changelog/`
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Backend Configuration
|
||||||
|
Edit `backend/src/main/resources/application.yml`
|
||||||
|
|
||||||
|
Key properties:
|
||||||
|
- `spring.datasource.*` - Database configuration
|
||||||
|
- `jwt.secret` - JWT signing secret (CHANGE IN PRODUCTION!)
|
||||||
|
- `jwt.expiration` - Token expiration time
|
||||||
|
|
||||||
|
### Frontend Configuration
|
||||||
|
Edit `frontend/src/environments/environment.ts`
|
||||||
|
|
||||||
|
## 🐳 Docker Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build all images
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
# Start in background
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Stop all services
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Stop and remove volumes
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Story 0 Implementation Checklist
|
||||||
|
|
||||||
|
### ✅ Backend
|
||||||
|
- [x] Spring Boot 3.2 setup with Maven
|
||||||
|
- [x] PostgreSQL 16 integration
|
||||||
|
- [x] Liquibase database migrations
|
||||||
|
- [x] JWT authentication & authorization
|
||||||
|
- [x] User entity and authentication
|
||||||
|
- [x] Environment entity (CRUD example)
|
||||||
|
- [x] Global exception handling
|
||||||
|
- [x] OpenAPI/Swagger documentation
|
||||||
|
- [x] Docker support
|
||||||
|
|
||||||
|
### ✅ Frontend
|
||||||
|
- [x] Angular 18 setup
|
||||||
|
- [x] Authentication service
|
||||||
|
- [x] JWT interceptor
|
||||||
|
- [x] Auth guard for route protection
|
||||||
|
- [x] Login component
|
||||||
|
- [x] Environment list component
|
||||||
|
- [x] Environment form component
|
||||||
|
- [x] Environment detail component
|
||||||
|
- [x] Responsive Material Design
|
||||||
|
|
||||||
|
### ✅ DevOps
|
||||||
|
- [x] Docker Compose setup
|
||||||
|
- [x] Multi-stage Dockerfiles
|
||||||
|
- [x] PostgreSQL in Docker
|
||||||
|
- [x] Health checks
|
||||||
|
- [x] Environment variables
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
After Story 0 is complete, proceed with:
|
||||||
|
1. **Story 1**: Business Unit Management
|
||||||
|
2. **Story 2**: Application Management
|
||||||
|
3. **Story 3**: Contact Management
|
||||||
|
4. **Story 4**: Enhanced Environment Management
|
||||||
|
5. **Story 5**: Version Management
|
||||||
|
6. **Story 6**: Deployment Tracking
|
||||||
|
7. **Story 7**: Current State Dashboard & History
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Backend won't start
|
||||||
|
- Check PostgreSQL is running: `docker ps`
|
||||||
|
- Check logs: `docker-compose logs backend`
|
||||||
|
- Verify database credentials in `application.yml`
|
||||||
|
|
||||||
|
### Frontend can't connect to backend
|
||||||
|
- Verify backend is running on port 8080
|
||||||
|
- Check CORS configuration in `SecurityConfig.java`
|
||||||
|
- Verify proxy configuration in `proxy.conf.json`
|
||||||
|
|
||||||
|
### Database migrations fail
|
||||||
|
- Stop all containers: `docker-compose down -v`
|
||||||
|
- Remove volumes and restart: `docker-compose up --build`
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
Proprietary - LDPv2 Team
|
||||||
|
|
||||||
|
## 👥 Team
|
||||||
|
|
||||||
|
LDPv2 Development Team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version**: 1.0.0 - Story 0 Foundation
|
||||||
|
**Last Updated**: February 2026
|
||||||
|
**Status**: ✅ Complete and Ready for Development
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
target/
|
||||||
|
!target/*.jar
|
||||||
|
.mvn/
|
||||||
|
mvnw
|
||||||
|
mvnw.cmd
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Logs ###
|
||||||
|
*.log
|
||||||
|
|
||||||
|
### OS ###
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Multi-stage build for Spring Boot backend
|
||||||
|
FROM maven:3.9-eclipse-temurin-17-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy pom.xml and download dependencies (cached layer)
|
||||||
|
COPY pom.xml .
|
||||||
|
RUN mvn dependency:go-offline -B
|
||||||
|
|
||||||
|
# Copy source code and build
|
||||||
|
COPY src ./src
|
||||||
|
RUN mvn clean package -DskipTests
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy JAR from build stage
|
||||||
|
COPY --from=build /app/target/*.jar app.jar
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
Executable
+445
@@ -0,0 +1,445 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Generate remaining backend files
|
||||||
|
|
||||||
|
BASE="/home/claude/ldpv2-monorepo/backend/src/main/java/com/ldpv2"
|
||||||
|
|
||||||
|
# ============= EXCEPTIONS =============
|
||||||
|
cat > "$BASE/exception/ResourceNotFoundException.java" << 'JAVA'
|
||||||
|
package com.ldpv2.exception;
|
||||||
|
|
||||||
|
public class ResourceNotFoundException extends RuntimeException {
|
||||||
|
public ResourceNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JAVA
|
||||||
|
|
||||||
|
cat > "$BASE/exception/BadRequestException.java" << 'JAVA'
|
||||||
|
package com.ldpv2.exception;
|
||||||
|
|
||||||
|
public class BadRequestException extends RuntimeException {
|
||||||
|
public BadRequestException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JAVA
|
||||||
|
|
||||||
|
cat > "$BASE/exception/GlobalExceptionHandler.java" << 'JAVA'
|
||||||
|
package com.ldpv2.exception;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(ResourceNotFoundException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleResourceNotFound(ResourceNotFoundException ex) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("timestamp", LocalDateTime.now());
|
||||||
|
error.put("status", HttpStatus.NOT_FOUND.value());
|
||||||
|
error.put("message", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(BadRequestException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleBadRequest(BadRequestException ex) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("timestamp", LocalDateTime.now());
|
||||||
|
error.put("status", HttpStatus.BAD_REQUEST.value());
|
||||||
|
error.put("message", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex) {
|
||||||
|
Map<String, String> errors = new HashMap<>();
|
||||||
|
ex.getBindingResult().getAllErrors().forEach((error) -> {
|
||||||
|
String fieldName = ((FieldError) error).getField();
|
||||||
|
String errorMessage = error.getDefaultMessage();
|
||||||
|
errors.put(fieldName, errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("timestamp", LocalDateTime.now());
|
||||||
|
response.put("status", HttpStatus.BAD_REQUEST.value());
|
||||||
|
response.put("message", "Validation failed");
|
||||||
|
response.put("errors", errors);
|
||||||
|
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(BadCredentialsException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleBadCredentials(BadCredentialsException ex) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("timestamp", LocalDateTime.now());
|
||||||
|
error.put("status", HttpStatus.UNAUTHORIZED.value());
|
||||||
|
error.put("message", "Invalid username or password");
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleGlobalException(Exception ex) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("timestamp", LocalDateTime.now());
|
||||||
|
error.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||||
|
error.put("message", "An unexpected error occurred");
|
||||||
|
error.put("details", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JAVA
|
||||||
|
|
||||||
|
echo "Exception handlers created"
|
||||||
|
|
||||||
|
# ============= SERVICES =============
|
||||||
|
|
||||||
|
cat > "$BASE/service/AuthService.java" << 'JAVA'
|
||||||
|
package com.ldpv2.service;
|
||||||
|
|
||||||
|
import com.ldpv2.domain.entity.User;
|
||||||
|
import com.ldpv2.dto.request.LoginRequest;
|
||||||
|
import com.ldpv2.dto.request.RegisterRequest;
|
||||||
|
import com.ldpv2.dto.response.AuthResponse;
|
||||||
|
import com.ldpv2.dto.response.UserResponse;
|
||||||
|
import com.ldpv2.exception.BadRequestException;
|
||||||
|
import com.ldpv2.repository.UserRepository;
|
||||||
|
import com.ldpv2.security.JwtTokenProvider;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtTokenProvider tokenProvider;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public AuthResponse register(RegisterRequest request) {
|
||||||
|
// Check if username exists
|
||||||
|
if (userRepository.existsByUsername(request.getUsername())) {
|
||||||
|
throw new BadRequestException("Username already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email exists
|
||||||
|
if (userRepository.existsByEmail(request.getEmail())) {
|
||||||
|
throw new BadRequestException("Email already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(request.getUsername());
|
||||||
|
user.setEmail(request.getEmail());
|
||||||
|
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||||
|
user.setRole("USER");
|
||||||
|
|
||||||
|
user = userRepository.save(user);
|
||||||
|
|
||||||
|
// Authenticate the user
|
||||||
|
Authentication authentication = authenticationManager.authenticate(
|
||||||
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
||||||
|
);
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
String token = tokenProvider.generateToken(authentication);
|
||||||
|
|
||||||
|
return new AuthResponse(token, mapToUserResponse(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthResponse login(LoginRequest request) {
|
||||||
|
Authentication authentication = authenticationManager.authenticate(
|
||||||
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
||||||
|
);
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
String token = tokenProvider.generateToken(authentication);
|
||||||
|
|
||||||
|
User user = userRepository.findByUsername(request.getUsername())
|
||||||
|
.orElseThrow(() -> new BadRequestException("User not found"));
|
||||||
|
|
||||||
|
return new AuthResponse(token, mapToUserResponse(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserResponse mapToUserResponse(User user) {
|
||||||
|
return new UserResponse(
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getRole(),
|
||||||
|
user.getCreatedAt(),
|
||||||
|
user.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JAVA
|
||||||
|
|
||||||
|
cat > "$BASE/service/EnvironmentService.java" << 'JAVA'
|
||||||
|
package com.ldpv2.service;
|
||||||
|
|
||||||
|
import com.ldpv2.domain.entity.Environment;
|
||||||
|
import com.ldpv2.dto.request.CreateEnvironmentRequest;
|
||||||
|
import com.ldpv2.dto.request.UpdateEnvironmentRequest;
|
||||||
|
import com.ldpv2.dto.response.EnvironmentResponse;
|
||||||
|
import com.ldpv2.exception.BadRequestException;
|
||||||
|
import com.ldpv2.exception.ResourceNotFoundException;
|
||||||
|
import com.ldpv2.repository.EnvironmentRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EnvironmentService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EnvironmentRepository environmentRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public EnvironmentResponse create(CreateEnvironmentRequest request) {
|
||||||
|
// Check if name already exists
|
||||||
|
if (environmentRepository.existsByName(request.getName())) {
|
||||||
|
throw new BadRequestException("Environment with name '" + request.getName() + "' already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
Environment environment = new Environment();
|
||||||
|
environment.setName(request.getName());
|
||||||
|
environment.setDescription(request.getDescription());
|
||||||
|
environment.setIsProduction(request.getIsProduction() != null ? request.getIsProduction() : false);
|
||||||
|
environment.setCriticalityLevel(request.getCriticalityLevel());
|
||||||
|
|
||||||
|
environment = environmentRepository.save(environment);
|
||||||
|
return mapToResponse(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public EnvironmentResponse update(UUID id, UpdateEnvironmentRequest request) {
|
||||||
|
Environment environment = environmentRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Environment not found with id: " + id));
|
||||||
|
|
||||||
|
// Check if new name already exists (excluding current environment)
|
||||||
|
if (request.getName() != null && !request.getName().equals(environment.getName())) {
|
||||||
|
if (environmentRepository.existsByName(request.getName())) {
|
||||||
|
throw new BadRequestException("Environment with name '" + request.getName() + "' already exists");
|
||||||
|
}
|
||||||
|
environment.setName(request.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getDescription() != null) {
|
||||||
|
environment.setDescription(request.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getIsProduction() != null) {
|
||||||
|
environment.setIsProduction(request.getIsProduction());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getCriticalityLevel() != null) {
|
||||||
|
environment.setCriticalityLevel(request.getCriticalityLevel());
|
||||||
|
}
|
||||||
|
|
||||||
|
environment = environmentRepository.save(environment);
|
||||||
|
return mapToResponse(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnvironmentResponse findById(UUID id) {
|
||||||
|
Environment environment = environmentRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Environment not found with id: " + id));
|
||||||
|
return mapToResponse(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<EnvironmentResponse> findAll(Pageable pageable) {
|
||||||
|
return environmentRepository.findAll(pageable).map(this::mapToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<EnvironmentResponse> search(String query, Pageable pageable) {
|
||||||
|
return environmentRepository.findByNameContainingIgnoreCase(query, pageable)
|
||||||
|
.map(this::mapToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(UUID id) {
|
||||||
|
if (!environmentRepository.existsById(id)) {
|
||||||
|
throw new ResourceNotFoundException("Environment not found with id: " + id);
|
||||||
|
}
|
||||||
|
environmentRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnvironmentResponse mapToResponse(Environment environment) {
|
||||||
|
return new EnvironmentResponse(
|
||||||
|
environment.getId(),
|
||||||
|
environment.getName(),
|
||||||
|
environment.getDescription(),
|
||||||
|
environment.getIsProduction(),
|
||||||
|
environment.getCriticalityLevel(),
|
||||||
|
environment.getCreatedAt(),
|
||||||
|
environment.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JAVA
|
||||||
|
|
||||||
|
echo "Services created"
|
||||||
|
|
||||||
|
# ============= CONTROLLERS =============
|
||||||
|
|
||||||
|
cat > "$BASE/controller/AuthController.java" << 'JAVA'
|
||||||
|
package com.ldpv2.controller;
|
||||||
|
|
||||||
|
import com.ldpv2.dto.request.LoginRequest;
|
||||||
|
import com.ldpv2.dto.request.RegisterRequest;
|
||||||
|
import com.ldpv2.dto.response.AuthResponse;
|
||||||
|
import com.ldpv2.service.AuthService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/auth")
|
||||||
|
@Tag(name = "Authentication", description = "Authentication and registration endpoints")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthService authService;
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
@Operation(summary = "Register new user", description = "Create a new user account")
|
||||||
|
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||||
|
AuthResponse response = authService.register(request);
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
@Operation(summary = "Login", description = "Authenticate and receive JWT token")
|
||||||
|
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||||
|
AuthResponse response = authService.login(request);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JAVA
|
||||||
|
|
||||||
|
cat > "$BASE/controller/EnvironmentController.java" << 'JAVA'
|
||||||
|
package com.ldpv2.controller;
|
||||||
|
|
||||||
|
import com.ldpv2.dto.request.CreateEnvironmentRequest;
|
||||||
|
import com.ldpv2.dto.request.UpdateEnvironmentRequest;
|
||||||
|
import com.ldpv2.dto.response.EnvironmentResponse;
|
||||||
|
import com.ldpv2.service.EnvironmentService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/environments")
|
||||||
|
@Tag(name = "Environments", description = "Environment management endpoints")
|
||||||
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
|
public class EnvironmentController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EnvironmentService environmentService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "Create environment", description = "Create a new environment")
|
||||||
|
public ResponseEntity<EnvironmentResponse> create(@Valid @RequestBody CreateEnvironmentRequest request) {
|
||||||
|
EnvironmentResponse response = environmentService.create(request);
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "Update environment", description = "Update an existing environment")
|
||||||
|
public ResponseEntity<EnvironmentResponse> update(
|
||||||
|
@PathVariable UUID id,
|
||||||
|
@Valid @RequestBody UpdateEnvironmentRequest request) {
|
||||||
|
EnvironmentResponse response = environmentService.update(id, request);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "Get environment", description = "Get environment by ID")
|
||||||
|
public ResponseEntity<EnvironmentResponse> getById(@PathVariable UUID id) {
|
||||||
|
EnvironmentResponse response = environmentService.findById(id);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "List environments", description = "Get paginated list of environments")
|
||||||
|
public ResponseEntity<Page<EnvironmentResponse>> getAll(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size,
|
||||||
|
@RequestParam(defaultValue = "name") String sortBy,
|
||||||
|
@RequestParam(defaultValue = "asc") String sortDirection) {
|
||||||
|
|
||||||
|
Sort sort = sortDirection.equalsIgnoreCase("desc")
|
||||||
|
? Sort.by(sortBy).descending()
|
||||||
|
: Sort.by(sortBy).ascending();
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(page, size, sort);
|
||||||
|
Page<EnvironmentResponse> response = environmentService.findAll(pageable);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
@Operation(summary = "Search environments", description = "Search environments by name")
|
||||||
|
public ResponseEntity<Page<EnvironmentResponse>> search(
|
||||||
|
@RequestParam String query,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(page, size);
|
||||||
|
Page<EnvironmentResponse> response = environmentService.search(query, pageable);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "Delete environment", description = "Delete an environment")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||||
|
environmentService.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JAVA
|
||||||
|
|
||||||
|
echo "Controllers created"
|
||||||
|
echo "✓ All backend Java files created successfully!"
|
||||||
|
|
||||||
+177
@@ -0,0 +1,177 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.2</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.ldpv2</groupId>
|
||||||
|
<artifactId>ldpv2-backend</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
<name>LDPv2 Backend</name>
|
||||||
|
<description>Lifecycle Data Platform v2 - Backend API</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<jjwt.version>0.12.3</jjwt.version>
|
||||||
|
<springdoc.version>2.3.0</springdoc.version>
|
||||||
|
<testcontainers.version>1.19.3</testcontainers.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot Starters -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Database -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Liquibase -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.liquibase</groupId>
|
||||||
|
<artifactId>liquibase-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JWT -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>${jjwt.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>${jjwt.version}</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>${jjwt.version}</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SpringDoc OpenAPI (Swagger) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>${springdoc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Testing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Testcontainers -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>testcontainers</artifactId>
|
||||||
|
<version>${testcontainers.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>${testcontainers.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>${testcontainers.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Maven Surefire for unit tests -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.0.0</version>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- JaCoCo for code coverage -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.11</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>report</id>
|
||||||
|
<phase>test</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ldpv2;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for LDPv2 Backend Application
|
||||||
|
*
|
||||||
|
* @author LDPv2 Team
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableJpaAuditing
|
||||||
|
public class LdpV2Application {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(LdpV2Application.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ldpv2.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.Components;
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.info.License;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class OpenApiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI ldpV2OpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("LDPv2 API")
|
||||||
|
.description("Lifecycle Data Platform v2 - Application Management API")
|
||||||
|
.version("1.0.0")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("LDPv2 Team")
|
||||||
|
.email("team@ldpv2.com"))
|
||||||
|
.license(new License()
|
||||||
|
.name("Proprietary")
|
||||||
|
.url("https://ldpv2.com/license")))
|
||||||
|
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
|
||||||
|
.components(new Components()
|
||||||
|
.addSecuritySchemes("bearerAuth",
|
||||||
|
new SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.HTTP)
|
||||||
|
.scheme("bearer")
|
||||||
|
.bearerFormat("JWT")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ldpv2.config;
|
||||||
|
|
||||||
|
import com.ldpv2.security.JwtAuthenticationFilter;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
|
.sessionManagement(session ->
|
||||||
|
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
|
.requestMatchers("/api/public/**").permitAll()
|
||||||
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.authenticationProvider(authenticationProvider())
|
||||||
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
|
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200", "http://localhost:3000"));
|
||||||
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
||||||
|
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||||
|
configuration.setAllowCredentials(true);
|
||||||
|
configuration.setMaxAge(3600L);
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DaoAuthenticationProvider authenticationProvider() {
|
||||||
|
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||||
|
authProvider.setUserDetailsService(userDetailsService);
|
||||||
|
authProvider.setPasswordEncoder(passwordEncoder());
|
||||||
|
return authProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
|
||||||
|
return authConfig.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ldpv2.controller;
|
||||||
|
|
||||||
|
import com.ldpv2.dto.request.LoginRequest;
|
||||||
|
import com.ldpv2.dto.request.RegisterRequest;
|
||||||
|
import com.ldpv2.dto.response.AuthResponse;
|
||||||
|
import com.ldpv2.service.AuthService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/auth")
|
||||||
|
@Tag(name = "Authentication", description = "Authentication and registration endpoints")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthService authService;
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
@Operation(summary = "Register new user", description = "Create a new user account")
|
||||||
|
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||||
|
AuthResponse response = authService.register(request);
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
@Operation(summary = "Login", description = "Authenticate and receive JWT token")
|
||||||
|
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||||
|
AuthResponse response = authService.login(request);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.ldpv2.controller;
|
||||||
|
|
||||||
|
import com.ldpv2.dto.request.CreateEnvironmentRequest;
|
||||||
|
import com.ldpv2.dto.request.UpdateEnvironmentRequest;
|
||||||
|
import com.ldpv2.dto.response.EnvironmentResponse;
|
||||||
|
import com.ldpv2.service.EnvironmentService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/environments")
|
||||||
|
@Tag(name = "Environments", description = "Environment management endpoints")
|
||||||
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
|
public class EnvironmentController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EnvironmentService environmentService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "Create environment", description = "Create a new environment")
|
||||||
|
public ResponseEntity<EnvironmentResponse> create(@Valid @RequestBody CreateEnvironmentRequest request) {
|
||||||
|
EnvironmentResponse response = environmentService.create(request);
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "Update environment", description = "Update an existing environment")
|
||||||
|
public ResponseEntity<EnvironmentResponse> update(
|
||||||
|
@PathVariable UUID id,
|
||||||
|
@Valid @RequestBody UpdateEnvironmentRequest request) {
|
||||||
|
EnvironmentResponse response = environmentService.update(id, request);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "Get environment", description = "Get environment by ID")
|
||||||
|
public ResponseEntity<EnvironmentResponse> getById(@PathVariable UUID id) {
|
||||||
|
EnvironmentResponse response = environmentService.findById(id);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "List environments", description = "Get paginated list of environments")
|
||||||
|
public ResponseEntity<Page<EnvironmentResponse>> getAll(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size,
|
||||||
|
@RequestParam(defaultValue = "name") String sortBy,
|
||||||
|
@RequestParam(defaultValue = "asc") String sortDirection) {
|
||||||
|
|
||||||
|
Sort sort = sortDirection.equalsIgnoreCase("desc")
|
||||||
|
? Sort.by(sortBy).descending()
|
||||||
|
: Sort.by(sortBy).ascending();
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(page, size, sort);
|
||||||
|
Page<EnvironmentResponse> response = environmentService.findAll(pageable);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
@Operation(summary = "Search environments", description = "Search environments by name")
|
||||||
|
public ResponseEntity<Page<EnvironmentResponse>> search(
|
||||||
|
@RequestParam String query,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(page, size);
|
||||||
|
Page<EnvironmentResponse> response = environmentService.search(query, pageable);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "Delete environment", description = "Delete an environment")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||||
|
environmentService.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ldpv2.domain.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base entity class with common fields for all entities
|
||||||
|
* Provides automatic UUID generation and audit timestamps
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@MappedSuperclass
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public abstract class BaseEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
@Column(name = "id", updatable = false, nullable = false)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ldpv2.domain.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment entity representing deployment targets
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "environment")
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class Environment extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true, length = 100)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "is_production", nullable = false)
|
||||||
|
private Boolean isProduction = false;
|
||||||
|
|
||||||
|
@Column(name = "criticality_level")
|
||||||
|
private Integer criticalityLevel;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ldpv2.domain.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entity for authentication and authorization
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users")
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class User extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true, length = 50)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true, length = 255)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String role; // ADMIN, USER
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ldpv2.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CreateEnvironmentRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "Name is required")
|
||||||
|
@Size(max = 100, message = "Name must not exceed 100 characters")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private Boolean isProduction = false;
|
||||||
|
|
||||||
|
@Min(value = 1, message = "Criticality level must be between 1 and 5")
|
||||||
|
@Max(value = 5, message = "Criticality level must be between 1 and 5")
|
||||||
|
private Integer criticalityLevel;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ldpv2.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class LoginRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "Username is required")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank(message = "Password is required")
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ldpv2.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class RegisterRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "Username is required")
|
||||||
|
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank(message = "Email is required")
|
||||||
|
@Email(message = "Email must be valid")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@NotBlank(message = "Password is required")
|
||||||
|
@Size(min = 6, message = "Password must be at least 6 characters")
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ldpv2.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UpdateEnvironmentRequest {
|
||||||
|
|
||||||
|
@Size(max = 100, message = "Name must not exceed 100 characters")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private Boolean isProduction;
|
||||||
|
|
||||||
|
@Min(value = 1, message = "Criticality level must be between 1 and 5")
|
||||||
|
@Max(value = 5, message = "Criticality level must be between 1 and 5")
|
||||||
|
private Integer criticalityLevel;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ldpv2.dto.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AuthResponse {
|
||||||
|
private String token;
|
||||||
|
private String type = "Bearer";
|
||||||
|
private UserResponse user;
|
||||||
|
|
||||||
|
public AuthResponse(String token, UserResponse user) {
|
||||||
|
this.token = token;
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ldpv2.dto.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class EnvironmentResponse {
|
||||||
|
private UUID id;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private Boolean isProduction;
|
||||||
|
private Integer criticalityLevel;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ldpv2.dto.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserResponse {
|
||||||
|
private UUID id;
|
||||||
|
private String username;
|
||||||
|
private String email;
|
||||||
|
private String role;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ldpv2.exception;
|
||||||
|
|
||||||
|
public class BadRequestException extends RuntimeException {
|
||||||
|
public BadRequestException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.ldpv2.exception;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(ResourceNotFoundException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleResourceNotFound(ResourceNotFoundException ex) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("timestamp", LocalDateTime.now());
|
||||||
|
error.put("status", HttpStatus.NOT_FOUND.value());
|
||||||
|
error.put("message", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(BadRequestException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleBadRequest(BadRequestException ex) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("timestamp", LocalDateTime.now());
|
||||||
|
error.put("status", HttpStatus.BAD_REQUEST.value());
|
||||||
|
error.put("message", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex) {
|
||||||
|
Map<String, String> errors = new HashMap<>();
|
||||||
|
ex.getBindingResult().getAllErrors().forEach((error) -> {
|
||||||
|
String fieldName = ((FieldError) error).getField();
|
||||||
|
String errorMessage = error.getDefaultMessage();
|
||||||
|
errors.put(fieldName, errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("timestamp", LocalDateTime.now());
|
||||||
|
response.put("status", HttpStatus.BAD_REQUEST.value());
|
||||||
|
response.put("message", "Validation failed");
|
||||||
|
response.put("errors", errors);
|
||||||
|
|
||||||
|
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(BadCredentialsException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleBadCredentials(BadCredentialsException ex) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("timestamp", LocalDateTime.now());
|
||||||
|
error.put("status", HttpStatus.UNAUTHORIZED.value());
|
||||||
|
error.put("message", "Invalid username or password");
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleGlobalException(Exception ex) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("timestamp", LocalDateTime.now());
|
||||||
|
error.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||||
|
error.put("message", "An unexpected error occurred");
|
||||||
|
error.put("details", ex.getMessage());
|
||||||
|
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ldpv2.exception;
|
||||||
|
|
||||||
|
public class ResourceNotFoundException extends RuntimeException {
|
||||||
|
public ResourceNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ldpv2.repository;
|
||||||
|
|
||||||
|
import com.ldpv2.domain.entity.Environment;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface EnvironmentRepository extends JpaRepository<Environment, UUID> {
|
||||||
|
Optional<Environment> findByName(String name);
|
||||||
|
boolean existsByName(String name);
|
||||||
|
Page<Environment> findByNameContainingIgnoreCase(String name, Pageable pageable);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ldpv2.repository;
|
||||||
|
|
||||||
|
import com.ldpv2.domain.entity.User;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserRepository extends JpaRepository<User, UUID> {
|
||||||
|
Optional<User> findByUsername(String username);
|
||||||
|
Optional<User> findByEmail(String email);
|
||||||
|
boolean existsByUsername(String username);
|
||||||
|
boolean existsByEmail(String email);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.ldpv2.security;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtTokenProvider tokenProvider;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserDetailsServiceImpl userDetailsService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
try {
|
||||||
|
String jwt = getJwtFromRequest(request);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
|
||||||
|
String username = tokenProvider.getUsernameFromToken(jwt);
|
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Could not set user authentication in security context", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getJwtFromRequest(HttpServletRequest request) {
|
||||||
|
String bearerToken = request.getHeader("Authorization");
|
||||||
|
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||||
|
return bearerToken.substring(7);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.ldpv2.security;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtTokenProvider {
|
||||||
|
|
||||||
|
@Value("${jwt.secret}")
|
||||||
|
private String jwtSecret;
|
||||||
|
|
||||||
|
@Value("${jwt.expiration}")
|
||||||
|
private long jwtExpiration;
|
||||||
|
|
||||||
|
private SecretKey getSigningKey() {
|
||||||
|
return Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(Authentication authentication) {
|
||||||
|
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||||
|
Date now = new Date();
|
||||||
|
Date expiryDate = new Date(now.getTime() + jwtExpiration);
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(userDetails.getUsername())
|
||||||
|
.setIssuedAt(now)
|
||||||
|
.setExpiration(expiryDate)
|
||||||
|
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsernameFromToken(String token) {
|
||||||
|
Claims claims = Jwts.parserBuilder()
|
||||||
|
.setSigningKey(getSigningKey())
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
return claims.getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
try {
|
||||||
|
Jwts.parserBuilder()
|
||||||
|
.setSigningKey(getSigningKey())
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token);
|
||||||
|
return true;
|
||||||
|
} catch (JwtException | IllegalArgumentException ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ldpv2.security;
|
||||||
|
|
||||||
|
import com.ldpv2.domain.entity.User;
|
||||||
|
import com.ldpv2.repository.UserRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
User user = userRepository.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||||
|
|
||||||
|
return org.springframework.security.core.userdetails.User.builder()
|
||||||
|
.username(user.getUsername())
|
||||||
|
.password(user.getPassword())
|
||||||
|
.authorities(Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.ldpv2.service;
|
||||||
|
|
||||||
|
import com.ldpv2.domain.entity.User;
|
||||||
|
import com.ldpv2.dto.request.LoginRequest;
|
||||||
|
import com.ldpv2.dto.request.RegisterRequest;
|
||||||
|
import com.ldpv2.dto.response.AuthResponse;
|
||||||
|
import com.ldpv2.dto.response.UserResponse;
|
||||||
|
import com.ldpv2.exception.BadRequestException;
|
||||||
|
import com.ldpv2.repository.UserRepository;
|
||||||
|
import com.ldpv2.security.JwtTokenProvider;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtTokenProvider tokenProvider;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public AuthResponse register(RegisterRequest request) {
|
||||||
|
// Check if username exists
|
||||||
|
if (userRepository.existsByUsername(request.getUsername())) {
|
||||||
|
throw new BadRequestException("Username already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email exists
|
||||||
|
if (userRepository.existsByEmail(request.getEmail())) {
|
||||||
|
throw new BadRequestException("Email already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(request.getUsername());
|
||||||
|
user.setEmail(request.getEmail());
|
||||||
|
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||||
|
user.setRole("USER");
|
||||||
|
|
||||||
|
user = userRepository.save(user);
|
||||||
|
|
||||||
|
// Authenticate the user
|
||||||
|
Authentication authentication = authenticationManager.authenticate(
|
||||||
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
||||||
|
);
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
String token = tokenProvider.generateToken(authentication);
|
||||||
|
|
||||||
|
return new AuthResponse(token, mapToUserResponse(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthResponse login(LoginRequest request) {
|
||||||
|
Authentication authentication = authenticationManager.authenticate(
|
||||||
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
||||||
|
);
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
String token = tokenProvider.generateToken(authentication);
|
||||||
|
|
||||||
|
User user = userRepository.findByUsername(request.getUsername())
|
||||||
|
.orElseThrow(() -> new BadRequestException("User not found"));
|
||||||
|
|
||||||
|
return new AuthResponse(token, mapToUserResponse(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserResponse mapToUserResponse(User user) {
|
||||||
|
return new UserResponse(
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getRole(),
|
||||||
|
user.getCreatedAt(),
|
||||||
|
user.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.ldpv2.service;
|
||||||
|
|
||||||
|
import com.ldpv2.domain.entity.Environment;
|
||||||
|
import com.ldpv2.dto.request.CreateEnvironmentRequest;
|
||||||
|
import com.ldpv2.dto.request.UpdateEnvironmentRequest;
|
||||||
|
import com.ldpv2.dto.response.EnvironmentResponse;
|
||||||
|
import com.ldpv2.exception.BadRequestException;
|
||||||
|
import com.ldpv2.exception.ResourceNotFoundException;
|
||||||
|
import com.ldpv2.repository.EnvironmentRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EnvironmentService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EnvironmentRepository environmentRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public EnvironmentResponse create(CreateEnvironmentRequest request) {
|
||||||
|
// Check if name already exists
|
||||||
|
if (environmentRepository.existsByName(request.getName())) {
|
||||||
|
throw new BadRequestException("Environment with name '" + request.getName() + "' already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
Environment environment = new Environment();
|
||||||
|
environment.setName(request.getName());
|
||||||
|
environment.setDescription(request.getDescription());
|
||||||
|
environment.setIsProduction(request.getIsProduction() != null ? request.getIsProduction() : false);
|
||||||
|
environment.setCriticalityLevel(request.getCriticalityLevel());
|
||||||
|
|
||||||
|
environment = environmentRepository.save(environment);
|
||||||
|
return mapToResponse(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public EnvironmentResponse update(UUID id, UpdateEnvironmentRequest request) {
|
||||||
|
Environment environment = environmentRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Environment not found with id: " + id));
|
||||||
|
|
||||||
|
// Check if new name already exists (excluding current environment)
|
||||||
|
if (request.getName() != null && !request.getName().equals(environment.getName())) {
|
||||||
|
if (environmentRepository.existsByName(request.getName())) {
|
||||||
|
throw new BadRequestException("Environment with name '" + request.getName() + "' already exists");
|
||||||
|
}
|
||||||
|
environment.setName(request.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getDescription() != null) {
|
||||||
|
environment.setDescription(request.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getIsProduction() != null) {
|
||||||
|
environment.setIsProduction(request.getIsProduction());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getCriticalityLevel() != null) {
|
||||||
|
environment.setCriticalityLevel(request.getCriticalityLevel());
|
||||||
|
}
|
||||||
|
|
||||||
|
environment = environmentRepository.save(environment);
|
||||||
|
return mapToResponse(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnvironmentResponse findById(UUID id) {
|
||||||
|
Environment environment = environmentRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Environment not found with id: " + id));
|
||||||
|
return mapToResponse(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<EnvironmentResponse> findAll(Pageable pageable) {
|
||||||
|
return environmentRepository.findAll(pageable).map(this::mapToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<EnvironmentResponse> search(String query, Pageable pageable) {
|
||||||
|
return environmentRepository.findByNameContainingIgnoreCase(query, pageable)
|
||||||
|
.map(this::mapToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(UUID id) {
|
||||||
|
if (!environmentRepository.existsById(id)) {
|
||||||
|
throw new ResourceNotFoundException("Environment not found with id: " + id);
|
||||||
|
}
|
||||||
|
environmentRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnvironmentResponse mapToResponse(Environment environment) {
|
||||||
|
return new EnvironmentResponse(
|
||||||
|
environment.getId(),
|
||||||
|
environment.getName(),
|
||||||
|
environment.getDescription(),
|
||||||
|
environment.getIsProduction(),
|
||||||
|
environment.getCriticalityLevel(),
|
||||||
|
environment.getCreatedAt(),
|
||||||
|
environment.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: ldpv2-backend
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://${DB_HOST:localhost}:5432/ldpv2
|
||||||
|
username: ${DB_USERNAME:ldpv2_user}
|
||||||
|
password: ${DB_PASSWORD:ldpv2_password}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 10
|
||||||
|
minimum-idle: 5
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: validate
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
|
||||||
|
liquibase:
|
||||||
|
change-log: classpath:db/changelog/db.changelog-master.xml
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: ${JWT_SECRET:your-secret-key-change-in-production-minimum-512-bits-for-hs512-algorithm}
|
||||||
|
expiration: 3600000
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
servlet:
|
||||||
|
context-path: /api
|
||||||
|
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
path: /v3/api-docs
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.ldpv2: DEBUG
|
||||||
|
org.springframework.security: DEBUG
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||||
|
|
||||||
|
<changeSet id="initial-data" author="ldpv2-team">
|
||||||
|
|
||||||
|
<!-- Insert default admin user -->
|
||||||
|
<!-- Password: admin123 (hashed with BCrypt) -->
|
||||||
|
<insert tableName="users">
|
||||||
|
<column name="username" value="admin"/>
|
||||||
|
<column name="password" value="$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"/>
|
||||||
|
<column name="email" value="admin@ldpv2.com"/>
|
||||||
|
<column name="role" value="ADMIN"/>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- Insert sample environments -->
|
||||||
|
<insert tableName="environment">
|
||||||
|
<column name="name" value="PROD-EU"/>
|
||||||
|
<column name="description" value="Production environment for Europe"/>
|
||||||
|
<column name="is_production" valueBoolean="true"/>
|
||||||
|
<column name="criticality_level" valueNumeric="5"/>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<insert tableName="environment">
|
||||||
|
<column name="name" value="PROD-US"/>
|
||||||
|
<column name="description" value="Production environment for United States"/>
|
||||||
|
<column name="is_production" valueBoolean="true"/>
|
||||||
|
<column name="criticality_level" valueNumeric="5"/>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<insert tableName="environment">
|
||||||
|
<column name="name" value="INT"/>
|
||||||
|
<column name="description" value="Integration testing environment"/>
|
||||||
|
<column name="is_production" valueBoolean="false"/>
|
||||||
|
<column name="criticality_level" valueNumeric="3"/>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<insert tableName="environment">
|
||||||
|
<column name="name" value="DEV"/>
|
||||||
|
<column name="description" value="Development environment"/>
|
||||||
|
<column name="is_production" valueBoolean="false"/>
|
||||||
|
<column name="criticality_level" valueNumeric="1"/>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||||
|
|
||||||
|
<!-- Story 0: Foundation -->
|
||||||
|
<include file="db/changelog/v1.0/001-create-user-table.xml"/>
|
||||||
|
<include file="db/changelog/v1.0/002-create-environment-table.xml"/>
|
||||||
|
<include file="db/changelog/data/initial-data.xml"/>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||||
|
|
||||||
|
<changeSet id="001-create-user-table" author="ldpv2-team">
|
||||||
|
|
||||||
|
<!-- Enable UUID extension -->
|
||||||
|
<sql>CREATE EXTENSION IF NOT EXISTS "uuid-ossp";</sql>
|
||||||
|
|
||||||
|
<!-- Create users table -->
|
||||||
|
<createTable tableName="users">
|
||||||
|
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="username" type="VARCHAR(50)">
|
||||||
|
<constraints nullable="false" unique="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="password" type="VARCHAR(255)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="email" type="VARCHAR(255)">
|
||||||
|
<constraints nullable="false" unique="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="role" type="VARCHAR(20)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<!-- Create indexes -->
|
||||||
|
<createIndex tableName="users" indexName="idx_users_username">
|
||||||
|
<column name="username"/>
|
||||||
|
</createIndex>
|
||||||
|
|
||||||
|
<createIndex tableName="users" indexName="idx_users_email">
|
||||||
|
<column name="email"/>
|
||||||
|
</createIndex>
|
||||||
|
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||||
|
|
||||||
|
<changeSet id="002-create-environment-table" author="ldpv2-team">
|
||||||
|
|
||||||
|
<!-- Create environment table -->
|
||||||
|
<createTable tableName="environment">
|
||||||
|
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="name" type="VARCHAR(100)">
|
||||||
|
<constraints nullable="false" unique="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="description" type="TEXT"/>
|
||||||
|
<column name="is_production" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="criticality_level" type="INTEGER"/>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<!-- Create indexes -->
|
||||||
|
<createIndex tableName="environment" indexName="idx_environment_name">
|
||||||
|
<column name="name"/>
|
||||||
|
</createIndex>
|
||||||
|
|
||||||
|
<createIndex tableName="environment" indexName="idx_environment_is_production">
|
||||||
|
<column name="is_production"/>
|
||||||
|
</createIndex>
|
||||||
|
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script creates all backend files for Story 0
|
||||||
|
|
||||||
|
BASE_DIR="/home/feeling/Documents/Code/ldpv2/backend/src/main/java/com/ldpv2"
|
||||||
|
|
||||||
|
# Create all necessary directories
|
||||||
|
mkdir -p "$BASE_DIR"/{config,domain/{entity,enums},repository,service,dto/{request,response},controller,security,exception}
|
||||||
|
mkdir -p /home/feeling/Documents/Code/ldpv2/backend/src/main/resources/db/changelog/{v1.0,data}
|
||||||
|
mkdir -p /home/feeling/Documents/Code/ldpv2/backend/src/test/java/com/ldpv2/{service,controller,integration}
|
||||||
|
|
||||||
|
echo "Backend directory structure created successfully"
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# LDPv2 - MVP Overview
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This MVP documentation package contains a complete breakdown of the LDPv2 project into iterative development stories following a "Walking Skeleton + Vertical Slices" approach.
|
||||||
|
|
||||||
|
## MVP Scope
|
||||||
|
|
||||||
|
The Minimum Viable Product focuses on delivering the core value proposition:
|
||||||
|
**"Manage applications, track their deployments across environments, and maintain deployment history"**
|
||||||
|
|
||||||
|
### What's IN the MVP
|
||||||
|
✅ Application management with lifecycle status
|
||||||
|
✅ Environment management (flexible, production-ready)
|
||||||
|
✅ Version tracking
|
||||||
|
✅ Deployment recording and history
|
||||||
|
✅ Current deployment state queries
|
||||||
|
✅ Basic contact management
|
||||||
|
✅ Business unit management
|
||||||
|
✅ User authentication (local, JWT-based)
|
||||||
|
✅ Basic role-based access control
|
||||||
|
|
||||||
|
### What's OUT of the MVP (Phase 2+)
|
||||||
|
❌ External dependencies tracking
|
||||||
|
❌ Data usage agreements
|
||||||
|
❌ SLA management
|
||||||
|
❌ Technical documentation links
|
||||||
|
❌ Advanced reporting and dashboards
|
||||||
|
❌ OAuth integration
|
||||||
|
❌ Notification system
|
||||||
|
❌ Advanced search and filtering
|
||||||
|
|
||||||
|
## Development Approach
|
||||||
|
|
||||||
|
### Phase 0: Foundation (Walking Skeleton)
|
||||||
|
Establish the technical foundation and development patterns that will be replicated across all features.
|
||||||
|
|
||||||
|
**Duration**: 2-3 weeks
|
||||||
|
**Goal**: Working authentication + 1 complete CRUD example
|
||||||
|
|
||||||
|
### Phase 1: Core Domain (Vertical Slices)
|
||||||
|
Deliver business value incrementally with complete end-to-end features.
|
||||||
|
|
||||||
|
**Duration**: 6-8 weeks
|
||||||
|
**Goal**: Fully functional application and deployment tracking
|
||||||
|
|
||||||
|
### Phase 2: Enrichment (Future)
|
||||||
|
Add secondary features based on user feedback and business priorities.
|
||||||
|
|
||||||
|
## Story Structure
|
||||||
|
|
||||||
|
Each story follows this structure:
|
||||||
|
```
|
||||||
|
Story X: [Business Title]
|
||||||
|
├── Backend Development
|
||||||
|
│ ├── Database migration (Liquibase)
|
||||||
|
│ ├── JPA Entities
|
||||||
|
│ ├── Repository layer
|
||||||
|
│ ├── Service layer (business logic)
|
||||||
|
│ ├── DTOs (request/response)
|
||||||
|
│ ├── Controller (REST endpoints)
|
||||||
|
│ └── Tests (unit + integration)
|
||||||
|
├── Frontend Development
|
||||||
|
│ ├── TypeScript models/interfaces
|
||||||
|
│ ├── Angular service (HTTP client)
|
||||||
|
│ ├── Components (list, detail, form)
|
||||||
|
│ ├── Routing configuration
|
||||||
|
│ └── Tests (unit + e2e)
|
||||||
|
└── Acceptance Criteria
|
||||||
|
└── Testable user scenarios
|
||||||
|
```
|
||||||
|
|
||||||
|
## Story Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
Story 0 (Foundation)
|
||||||
|
├── Story 1 (Business Units) - Independent
|
||||||
|
├── Story 2 (Applications) - Depends on Story 1
|
||||||
|
├── Story 3 (Contacts) - Independent
|
||||||
|
└── Story 4 (Environments) - Independent
|
||||||
|
├── Story 5 (Versions) - Depends on Story 2
|
||||||
|
└── Story 6 (Deployments) - Depends on Stories 2, 4, 5
|
||||||
|
└── Story 7 (Current State & History) - Depends on Story 6
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ldpv2-mvp/
|
||||||
|
├── 00-MVP-OVERVIEW.md (this file)
|
||||||
|
├── 01-TECHNICAL-SETUP.md
|
||||||
|
├── stories/
|
||||||
|
│ ├── STORY-0-Foundation.md
|
||||||
|
│ ├── STORY-1-Business-Units.md
|
||||||
|
│ ├── STORY-2-Applications.md
|
||||||
|
│ ├── STORY-3-Contacts.md
|
||||||
|
│ ├── STORY-4-Environments.md
|
||||||
|
│ ├── STORY-5-Versions.md
|
||||||
|
│ ├── STORY-6-Deployments.md
|
||||||
|
│ └── STORY-7-Current-State-History.md
|
||||||
|
├── data-model/
|
||||||
|
│ ├── complete-data-model.ts
|
||||||
|
│ ├── mvp-entities-only.ts
|
||||||
|
│ └── database-schema.sql
|
||||||
|
└── api-specs/
|
||||||
|
├── openapi-mvp.yaml
|
||||||
|
└── endpoint-summary.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
The MVP will be considered successful when:
|
||||||
|
|
||||||
|
1. **Functional Completeness**
|
||||||
|
- All 8 stories are delivered and accepted
|
||||||
|
- All acceptance criteria are met
|
||||||
|
- Zero critical bugs
|
||||||
|
|
||||||
|
2. **Technical Quality**
|
||||||
|
- Backend test coverage > 80%
|
||||||
|
- Frontend test coverage > 70%
|
||||||
|
- All APIs documented in Swagger
|
||||||
|
- Code passes security audit
|
||||||
|
|
||||||
|
3. **User Satisfaction**
|
||||||
|
- Users can perform core workflows without assistance
|
||||||
|
- System performance meets requirements (<500ms API response)
|
||||||
|
- Positive feedback from pilot users
|
||||||
|
|
||||||
|
4. **Business Value**
|
||||||
|
- All applications are registered in the system
|
||||||
|
- Deployment history is accurate and complete
|
||||||
|
- Users prefer LDPv2 over previous tools/spreadsheets
|
||||||
|
|
||||||
|
## Timeline Estimate
|
||||||
|
|
||||||
|
| Phase | Duration | Stories |
|
||||||
|
|-------|----------|---------|
|
||||||
|
| Phase 0: Foundation | 2-3 weeks | Story 0 |
|
||||||
|
| Phase 1: Core (Batch 1) | 3-4 weeks | Stories 1-4 |
|
||||||
|
| Phase 1: Core (Batch 2) | 3-4 weeks | Stories 5-7 |
|
||||||
|
| **Total MVP** | **8-11 weeks** | **8 stories** |
|
||||||
|
|
||||||
|
*Note: Timeline assumes 1 full-time developer or 2 developers working part-time*
|
||||||
|
|
||||||
|
## Risk Mitigation
|
||||||
|
|
||||||
|
### Technical Risks
|
||||||
|
- **Database performance**: Addressed with indexing strategy in Story 0
|
||||||
|
- **JWT security**: Implemented following best practices in Story 0
|
||||||
|
- **Complex queries**: Deployment history queries optimized in Story 7
|
||||||
|
|
||||||
|
### Process Risks
|
||||||
|
- **Scope creep**: Strict adherence to MVP scope, Phase 2 features documented separately
|
||||||
|
- **Incomplete data**: Import tools and validation built into each entity story
|
||||||
|
- **User adoption**: Regular demos after each story completion
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Review this MVP overview
|
||||||
|
2. Read the Technical Setup guide
|
||||||
|
3. Begin with Story 0 (Foundation)
|
||||||
|
4. Follow stories in dependency order
|
||||||
|
5. Demo and gather feedback after every 2 stories
|
||||||
|
|
||||||
|
## Contact & Feedback
|
||||||
|
|
||||||
|
For questions or suggestions about this MVP plan, please contact the project team.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Last Updated**: February 2026
|
||||||
|
**Status**: Ready for Development
|
||||||
@@ -0,0 +1,702 @@
|
|||||||
|
# LDPv2 - Technical Setup Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide provides the technical foundation for the LDPv2 project, including technology stack details, project structure, and initial setup instructions.
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Framework**: Spring Boot 3.2.x
|
||||||
|
- **Language**: Java 17 or 21
|
||||||
|
- **Security**: Spring Security 6.x with JWT
|
||||||
|
- **Persistence**:
|
||||||
|
- PostgreSQL 16
|
||||||
|
- Hibernate 6.x (JPA)
|
||||||
|
- HikariCP (connection pooling)
|
||||||
|
- **Migration**: Liquibase 4.x
|
||||||
|
- **Build Tool**: Maven 3.9.x
|
||||||
|
- **Testing**:
|
||||||
|
- JUnit 5
|
||||||
|
- Mockito
|
||||||
|
- Testcontainers (for integration tests)
|
||||||
|
- REST Assured (for API tests)
|
||||||
|
- **Documentation**: SpringDoc OpenAPI (Swagger)
|
||||||
|
- **Validation**: Jakarta Bean Validation
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Framework**: Angular 18
|
||||||
|
- **Language**: TypeScript 5.x
|
||||||
|
- **UI Library**: Angular Material or PrimeNG (to be decided)
|
||||||
|
- **HTTP Client**: Angular HttpClient
|
||||||
|
- **State Management**: Service-based (or NgRx for complex state)
|
||||||
|
- **Forms**: Reactive Forms
|
||||||
|
- **Routing**: Angular Router
|
||||||
|
- **Testing**:
|
||||||
|
- Jasmine/Karma (unit tests)
|
||||||
|
- Cypress or Playwright (E2E tests)
|
||||||
|
- **Build Tool**: Angular CLI
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- **Containerization**: Docker
|
||||||
|
- **Container Orchestration**: Docker Compose (dev), Kubernetes (prod)
|
||||||
|
- **CI/CD**: Jenkins, GitLab CI, or GitHub Actions
|
||||||
|
- **Version Control**: Git
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Backend Structure
|
||||||
|
```
|
||||||
|
ldpv2-backend/
|
||||||
|
├── src/
|
||||||
|
│ ├── main/
|
||||||
|
│ │ ├── java/
|
||||||
|
│ │ │ └── com/ldpv2/
|
||||||
|
│ │ │ ├── LdpV2Application.java
|
||||||
|
│ │ │ ├── config/
|
||||||
|
│ │ │ │ ├── SecurityConfig.java
|
||||||
|
│ │ │ │ ├── JwtConfig.java
|
||||||
|
│ │ │ │ └── OpenApiConfig.java
|
||||||
|
│ │ │ ├── domain/
|
||||||
|
│ │ │ │ ├── entity/
|
||||||
|
│ │ │ │ │ ├── BaseEntity.java
|
||||||
|
│ │ │ │ │ ├── Application.java
|
||||||
|
│ │ │ │ │ ├── Environment.java
|
||||||
|
│ │ │ │ │ └── ...
|
||||||
|
│ │ │ │ └── enums/
|
||||||
|
│ │ │ │ ├── ApplicationStatus.java
|
||||||
|
│ │ │ │ └── ExternalDependencyType.java
|
||||||
|
│ │ │ ├── repository/
|
||||||
|
│ │ │ │ ├── ApplicationRepository.java
|
||||||
|
│ │ │ │ ├── EnvironmentRepository.java
|
||||||
|
│ │ │ │ └── ...
|
||||||
|
│ │ │ ├── service/
|
||||||
|
│ │ │ │ ├── ApplicationService.java
|
||||||
|
│ │ │ │ ├── EnvironmentService.java
|
||||||
|
│ │ │ │ └── ...
|
||||||
|
│ │ │ ├── dto/
|
||||||
|
│ │ │ │ ├── request/
|
||||||
|
│ │ │ │ │ ├── CreateApplicationRequest.java
|
||||||
|
│ │ │ │ │ └── ...
|
||||||
|
│ │ │ │ └── response/
|
||||||
|
│ │ │ │ ├── ApplicationResponse.java
|
||||||
|
│ │ │ │ └── ...
|
||||||
|
│ │ │ ├── controller/
|
||||||
|
│ │ │ │ ├── ApplicationController.java
|
||||||
|
│ │ │ │ ├── EnvironmentController.java
|
||||||
|
│ │ │ │ └── ...
|
||||||
|
│ │ │ ├── security/
|
||||||
|
│ │ │ │ ├── JwtTokenProvider.java
|
||||||
|
│ │ │ │ ├── JwtAuthenticationFilter.java
|
||||||
|
│ │ │ │ └── UserDetailsServiceImpl.java
|
||||||
|
│ │ │ └── exception/
|
||||||
|
│ │ │ ├── GlobalExceptionHandler.java
|
||||||
|
│ │ │ ├── ResourceNotFoundException.java
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── resources/
|
||||||
|
│ │ ├── application.yml
|
||||||
|
│ │ ├── application-dev.yml
|
||||||
|
│ │ ├── application-prod.yml
|
||||||
|
│ │ └── db/
|
||||||
|
│ │ └── changelog/
|
||||||
|
│ │ ├── db.changelog-master.xml
|
||||||
|
│ │ ├── v1.0/
|
||||||
|
│ │ │ ├── 001-create-base-tables.xml
|
||||||
|
│ │ │ ├── 002-create-application-tables.xml
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── data/
|
||||||
|
│ │ └── initial-data.xml
|
||||||
|
│ └── test/
|
||||||
|
│ └── java/
|
||||||
|
│ └── com/ldpv2/
|
||||||
|
│ ├── integration/
|
||||||
|
│ │ ├── ApplicationIntegrationTest.java
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── service/
|
||||||
|
│ │ ├── ApplicationServiceTest.java
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── controller/
|
||||||
|
│ ├── ApplicationControllerTest.java
|
||||||
|
│ └── ...
|
||||||
|
├── pom.xml
|
||||||
|
├── Dockerfile
|
||||||
|
└── docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Structure
|
||||||
|
```
|
||||||
|
ldpv2-frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── core/
|
||||||
|
│ │ │ ├── auth/
|
||||||
|
│ │ │ │ ├── auth.service.ts
|
||||||
|
│ │ │ │ ├── auth.guard.ts
|
||||||
|
│ │ │ │ ├── jwt.interceptor.ts
|
||||||
|
│ │ │ │ └── login/
|
||||||
|
│ │ │ │ ├── login.component.ts
|
||||||
|
│ │ │ │ ├── login.component.html
|
||||||
|
│ │ │ │ └── login.component.scss
|
||||||
|
│ │ │ ├── services/
|
||||||
|
│ │ │ │ ├── api.service.ts
|
||||||
|
│ │ │ │ └── error-handler.service.ts
|
||||||
|
│ │ │ └── interceptors/
|
||||||
|
│ │ │ ├── jwt.interceptor.ts
|
||||||
|
│ │ │ └── error.interceptor.ts
|
||||||
|
│ │ ├── shared/
|
||||||
|
│ │ │ ├── models/
|
||||||
|
│ │ │ │ ├── application.model.ts
|
||||||
|
│ │ │ │ ├── environment.model.ts
|
||||||
|
│ │ │ │ └── ...
|
||||||
|
│ │ │ ├── components/
|
||||||
|
│ │ │ │ ├── header/
|
||||||
|
│ │ │ │ ├── footer/
|
||||||
|
│ │ │ │ └── loading-spinner/
|
||||||
|
│ │ │ └── pipes/
|
||||||
|
│ │ │ └── date-format.pipe.ts
|
||||||
|
│ │ ├── features/
|
||||||
|
│ │ │ ├── applications/
|
||||||
|
│ │ │ │ ├── application.service.ts
|
||||||
|
│ │ │ │ ├── application-list/
|
||||||
|
│ │ │ │ │ ├── application-list.component.ts
|
||||||
|
│ │ │ │ │ ├── application-list.component.html
|
||||||
|
│ │ │ │ │ └── application-list.component.scss
|
||||||
|
│ │ │ │ ├── application-detail/
|
||||||
|
│ │ │ │ └── application-form/
|
||||||
|
│ │ │ ├── environments/
|
||||||
|
│ │ │ │ ├── environment.service.ts
|
||||||
|
│ │ │ │ └── ...
|
||||||
|
│ │ │ ├── deployments/
|
||||||
|
│ │ │ └── business-units/
|
||||||
|
│ │ ├── app.component.ts
|
||||||
|
│ │ ├── app.component.html
|
||||||
|
│ │ ├── app.routes.ts
|
||||||
|
│ │ └── app.config.ts
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ ├── images/
|
||||||
|
│ │ └── i18n/
|
||||||
|
│ ├── environments/
|
||||||
|
│ │ ├── environment.ts
|
||||||
|
│ │ └── environment.prod.ts
|
||||||
|
│ ├── styles.scss
|
||||||
|
│ └── index.html
|
||||||
|
├── angular.json
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
└── Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- JDK 17 or 21
|
||||||
|
- Node.js 18+ and npm
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- PostgreSQL 16 (or use Docker)
|
||||||
|
- Git
|
||||||
|
- IDE (IntelliJ IDEA, VS Code, or similar)
|
||||||
|
|
||||||
|
### Backend Setup
|
||||||
|
|
||||||
|
#### 1. Create Spring Boot Project
|
||||||
|
```bash
|
||||||
|
# Using Spring Initializr or
|
||||||
|
# Download from https://start.spring.io with:
|
||||||
|
# - Spring Boot 3.2.x
|
||||||
|
# - Dependencies: Web, Security, JPA, PostgreSQL, Liquibase, Validation
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Configure application.yml
|
||||||
|
```yaml
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: ldpv2-backend
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/ldpv2
|
||||||
|
username: ldpv2_user
|
||||||
|
password: ldpv2_password
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 10
|
||||||
|
minimum-idle: 5
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: validate # Let Liquibase handle schema
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
|
||||||
|
liquibase:
|
||||||
|
change-log: classpath:db/changelog/db.changelog-master.xml
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: ${JWT_SECRET:your-secret-key-change-in-production}
|
||||||
|
expiration: 3600000 # 1 hour in milliseconds
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
servlet:
|
||||||
|
context-path: /api
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.ldpv2: DEBUG
|
||||||
|
org.springframework.security: DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Docker Compose for Development
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: ldpv2-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ldpv2
|
||||||
|
POSTGRES_USER: ldpv2_user
|
||||||
|
POSTGRES_PASSWORD: ldpv2_password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- ldpv2-network
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build: ./ldpv2-backend
|
||||||
|
container_name: ldpv2-backend
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/ldpv2
|
||||||
|
SPRING_DATASOURCE_USERNAME: ldpv2_user
|
||||||
|
SPRING_DATASOURCE_PASSWORD: ldpv2_password
|
||||||
|
JWT_SECRET: development-secret-key
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
networks:
|
||||||
|
- ldpv2-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ldpv2-network:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Setup
|
||||||
|
|
||||||
|
#### 1. Create Angular Project
|
||||||
|
```bash
|
||||||
|
npm install -g @angular/cli@18
|
||||||
|
ng new ldpv2-frontend
|
||||||
|
cd ldpv2-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Install Dependencies
|
||||||
|
```bash
|
||||||
|
# Angular Material (or PrimeNG)
|
||||||
|
ng add @angular/material
|
||||||
|
|
||||||
|
# Additional dependencies
|
||||||
|
npm install --save \
|
||||||
|
@angular/common \
|
||||||
|
@angular/forms \
|
||||||
|
rxjs
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Configure Environment
|
||||||
|
```typescript
|
||||||
|
// src/environments/environment.ts
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
apiUrl: 'http://localhost:8080/api'
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/environments/environment.prod.ts
|
||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
apiUrl: '/api'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Proxy Configuration for Development
|
||||||
|
```json
|
||||||
|
// proxy.conf.json
|
||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:8080",
|
||||||
|
"secure": false,
|
||||||
|
"changeOrigin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `angular.json`:
|
||||||
|
```json
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"proxyConfig": "proxy.conf.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema Management
|
||||||
|
|
||||||
|
### Liquibase Changelog Structure
|
||||||
|
```xml
|
||||||
|
<!-- db.changelog-master.xml -->
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||||
|
|
||||||
|
<include file="db/changelog/v1.0/001-create-base-tables.xml"/>
|
||||||
|
<include file="db/changelog/v1.0/002-create-application-tables.xml"/>
|
||||||
|
<include file="db/changelog/v1.0/003-create-deployment-tables.xml"/>
|
||||||
|
<!-- Add more as stories are developed -->
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Migration File
|
||||||
|
```xml
|
||||||
|
<!-- 001-create-base-tables.xml -->
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||||
|
|
||||||
|
<changeSet id="001-create-base-entities" author="ldpv2-team">
|
||||||
|
|
||||||
|
<!-- Enable UUID extension -->
|
||||||
|
<sql>CREATE EXTENSION IF NOT EXISTS "uuid-ossp";</sql>
|
||||||
|
|
||||||
|
<!-- Create Business Unit table -->
|
||||||
|
<createTable tableName="business_unit">
|
||||||
|
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="name" type="VARCHAR(255)">
|
||||||
|
<constraints nullable="false" unique="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<!-- Create Environment table -->
|
||||||
|
<createTable tableName="environment">
|
||||||
|
<column name="id" type="UUID" defaultValueComputed="uuid_generate_v4()">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="name" type="VARCHAR(100)">
|
||||||
|
<constraints nullable="false" unique="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="description" type="TEXT"/>
|
||||||
|
<column name="is_production" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="criticality_level" type="INTEGER"/>
|
||||||
|
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<!-- Indexes -->
|
||||||
|
<createIndex tableName="environment" indexName="idx_env_is_production">
|
||||||
|
<column name="is_production"/>
|
||||||
|
</createIndex>
|
||||||
|
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Configuration
|
||||||
|
|
||||||
|
### JWT Token Provider Example
|
||||||
|
```java
|
||||||
|
@Component
|
||||||
|
public class JwtTokenProvider {
|
||||||
|
|
||||||
|
@Value("${jwt.secret}")
|
||||||
|
private String jwtSecret;
|
||||||
|
|
||||||
|
@Value("${jwt.expiration}")
|
||||||
|
private long jwtExpiration;
|
||||||
|
|
||||||
|
public String generateToken(Authentication authentication) {
|
||||||
|
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||||
|
Date now = new Date();
|
||||||
|
Date expiryDate = new Date(now.getTime() + jwtExpiration);
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(userDetails.getUsername())
|
||||||
|
.setIssuedAt(now)
|
||||||
|
.setExpiration(expiryDate)
|
||||||
|
.signWith(SignatureAlgorithm.HS512, jwtSecret)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsernameFromToken(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(jwtSecret)
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
return claims.getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
try {
|
||||||
|
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
|
||||||
|
return true;
|
||||||
|
} catch (SignatureException | MalformedJwtException | ExpiredJwtException |
|
||||||
|
UnsupportedJwtException | IllegalArgumentException ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Configuration Example
|
||||||
|
```java
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
|
.sessionManagement(session ->
|
||||||
|
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
|
.requestMatchers("/api/public/**").permitAll()
|
||||||
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.addFilterBefore(jwtAuthenticationFilter(),
|
||||||
|
UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
|
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
|
||||||
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||||
|
configuration.setAllowCredentials(true);
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Documentation (OpenAPI/Swagger)
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
```java
|
||||||
|
@Configuration
|
||||||
|
public class OpenApiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI ldpV2OpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("LDPv2 API")
|
||||||
|
.description("Lifecycle Data Platform v2 - Application Management API")
|
||||||
|
.version("1.0.0")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("LDPv2 Team")
|
||||||
|
.email("team@ldpv2.com"))
|
||||||
|
.license(new License()
|
||||||
|
.name("Proprietary")
|
||||||
|
.url("https://ldpv2.com/license")))
|
||||||
|
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
|
||||||
|
.components(new Components()
|
||||||
|
.addSecuritySchemes("bearerAuth",
|
||||||
|
new SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.HTTP)
|
||||||
|
.scheme("bearer")
|
||||||
|
.bearerFormat("JWT")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Access Swagger UI at: `http://localhost:8080/swagger-ui/index.html`
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Backend Development Cycle
|
||||||
|
1. Create Liquibase migration
|
||||||
|
2. Run migration: `mvn liquibase:update`
|
||||||
|
3. Generate JPA entities
|
||||||
|
4. Create repository interfaces
|
||||||
|
5. Implement service layer
|
||||||
|
6. Create DTOs
|
||||||
|
7. Implement controller
|
||||||
|
8. Write tests
|
||||||
|
9. Document API with Swagger annotations
|
||||||
|
10. Commit and push
|
||||||
|
|
||||||
|
### Frontend Development Cycle
|
||||||
|
1. Create TypeScript models
|
||||||
|
2. Implement Angular service
|
||||||
|
3. Create components (list, detail, form)
|
||||||
|
4. Add routing
|
||||||
|
5. Style with CSS/SCSS
|
||||||
|
6. Write tests
|
||||||
|
7. Commit and push
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Backend Testing
|
||||||
|
```java
|
||||||
|
// Unit Test Example
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ApplicationServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationRepository applicationRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ApplicationService applicationService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateApplication() {
|
||||||
|
// Test implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration Test Example with Testcontainers
|
||||||
|
@SpringBootTest
|
||||||
|
@Testcontainers
|
||||||
|
class ApplicationIntegrationTest {
|
||||||
|
|
||||||
|
@Container
|
||||||
|
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
|
||||||
|
|
||||||
|
@DynamicPropertySource
|
||||||
|
static void configureProperties(DynamicPropertyRegistry registry) {
|
||||||
|
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
||||||
|
registry.add("spring.datasource.username", postgres::getUsername);
|
||||||
|
registry.add("spring.datasource.password", postgres::getPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSaveAndRetrieveApplication() {
|
||||||
|
// Test implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Testing
|
||||||
|
```typescript
|
||||||
|
// Component Unit Test
|
||||||
|
describe('ApplicationListComponent', () => {
|
||||||
|
let component: ApplicationListComponent;
|
||||||
|
let fixture: ComponentFixture<ApplicationListComponent>;
|
||||||
|
let mockApplicationService: jasmine.SpyObj<ApplicationService>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const spy = jasmine.createSpyObj('ApplicationService', ['getApplications']);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ApplicationListComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ApplicationService, useValue: spy }
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
mockApplicationService = TestBed.inject(ApplicationService) as jasmine.SpyObj<ApplicationService>;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
```bash
|
||||||
|
# Using Maven
|
||||||
|
mvn spring-boot:run
|
||||||
|
|
||||||
|
# Using Docker Compose
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
```bash
|
||||||
|
# Development server
|
||||||
|
ng serve
|
||||||
|
|
||||||
|
# With proxy
|
||||||
|
ng serve --proxy-config proxy.conf.json
|
||||||
|
|
||||||
|
# Access at http://localhost:4200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
java -jar target/ldpv2-backend-1.0.0.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
```bash
|
||||||
|
ng build --configuration production
|
||||||
|
# Output in dist/ directory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After completing this technical setup:
|
||||||
|
1. Proceed to Story 0 (Foundation) to build the Walking Skeleton
|
||||||
|
2. Follow the story sequence for iterative development
|
||||||
|
3. Maintain test coverage throughout
|
||||||
|
4. Document APIs as you build
|
||||||
|
5. Commit frequently with meaningful messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Last Updated**: February 2026
|
||||||
|
**Status**: Ready for Development
|
||||||
+819
@@ -0,0 +1,819 @@
|
|||||||
|
# LDPv2 - Requirements Analysis Document
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
LDPv2 (Lifecycle Data Platform version 2) is a comprehensive application lifecycle management tool designed to manage and document all applications within a business unit. It serves as a central repository for application information, tracking deployments, dependencies, SLAs, and stakeholder relationships throughout the complete application lifecycle.
|
||||||
|
|
||||||
|
## 2. Purpose and Scope
|
||||||
|
|
||||||
|
### 2.1 Primary Objectives
|
||||||
|
|
||||||
|
- Provide a centralized documentation platform for all managed applications
|
||||||
|
- Track application lifecycle from ideation to decommissioning
|
||||||
|
- Manage deployment history across multiple environments
|
||||||
|
- Document and monitor external dependencies and data usage agreements
|
||||||
|
- Maintain SLA definitions and compliance tracking
|
||||||
|
- Facilitate stakeholder communication and responsibility mapping
|
||||||
|
|
||||||
|
### 2.2 Target Users
|
||||||
|
|
||||||
|
- Business Unit Directors and Program Managers
|
||||||
|
- Application Product Owners
|
||||||
|
- Development Teams
|
||||||
|
- Infrastructure and Operations Teams
|
||||||
|
- Compliance and Data Governance Officers
|
||||||
|
|
||||||
|
## 3. Technical Architecture
|
||||||
|
|
||||||
|
### 3.1 Technology Stack
|
||||||
|
|
||||||
|
#### 3.1.1 Backend
|
||||||
|
|
||||||
|
- **Framework**: Spring Boot 3.x
|
||||||
|
- **Security**: Spring Security with JWT-based authentication
|
||||||
|
- **Authentication Strategy**:
|
||||||
|
- Initial implementation: Local authentication (username/password)
|
||||||
|
- Future evolution: OAuth 2.0 / OpenID Connect integration
|
||||||
|
- **Authorization**: Role-based access control (RBAC) with environment-specific permissions
|
||||||
|
- **Persistence**: PostgreSQL 16
|
||||||
|
- **ORM**: Hibernate 6.x (JPA implementation)
|
||||||
|
- **API Style**: RESTful API with JSON payloads
|
||||||
|
|
||||||
|
#### 3.1.2 Frontend
|
||||||
|
|
||||||
|
- **Framework**: Angular 18
|
||||||
|
- **Authentication**: JWT token-based authentication
|
||||||
|
- **HTTP Client**: Angular HttpClient with interceptors for token management
|
||||||
|
- **State Management**: To be determined (NgRx, Signals, or service-based)
|
||||||
|
- **UI Components**: To be determined (Angular Material, PrimeNG, or custom)
|
||||||
|
|
||||||
|
#### 3.1.3 Database
|
||||||
|
|
||||||
|
- **RDBMS**: PostgreSQL 16
|
||||||
|
- **Schema Management**: Liquibase or Flyway for database migrations
|
||||||
|
- **Connection Pooling**: HikariCP (Spring Boot default)
|
||||||
|
|
||||||
|
### 3.2 Security Architecture
|
||||||
|
|
||||||
|
#### 3.2.1 Authentication Flow
|
||||||
|
|
||||||
|
1. User submits credentials to `/api/auth/login`
|
||||||
|
2. Backend validates credentials against database
|
||||||
|
3. Upon successful authentication, JWT token is generated and returned
|
||||||
|
4. Frontend stores JWT token (localStorage or sessionStorage)
|
||||||
|
5. Subsequent requests include JWT token in Authorization header
|
||||||
|
6. Backend validates token on each request via Spring Security filters
|
||||||
|
|
||||||
|
#### 3.2.2 Authorization Model
|
||||||
|
|
||||||
|
- **Environment-Based Roles**: Users can have different permissions per environment
|
||||||
|
- Example: User may have READ access to PROD but WRITE access to DEV
|
||||||
|
- **Role Hierarchy**: To be defined (e.g., ADMIN > MANAGER > USER > VIEWER)
|
||||||
|
- **Resource-Level Permissions**: Fine-grained access control on applications and business units
|
||||||
|
|
||||||
|
#### 3.2.3 Future OAuth Integration
|
||||||
|
|
||||||
|
- Support for enterprise SSO (Single Sign-On)
|
||||||
|
- Integration with identity providers (Azure AD, Okta, Keycloak)
|
||||||
|
- Backward compatibility with local authentication during migration
|
||||||
|
|
||||||
|
### 3.3 API Design Principles
|
||||||
|
|
||||||
|
- RESTful resource-based endpoints (e.g., `/api/applications`, `/api/deployments`)
|
||||||
|
- Consistent HTTP verb usage (GET, POST, PUT, DELETE, PATCH)
|
||||||
|
- Pagination for list endpoints
|
||||||
|
- Filtering and sorting capabilities
|
||||||
|
- HATEOAS support for discoverability (optional)
|
||||||
|
- Versioned API (e.g., `/api/v1/...`)
|
||||||
|
|
||||||
|
### 3.4 Data Persistence Strategy
|
||||||
|
|
||||||
|
- **Entity Mapping**: JPA entities with Hibernate annotations
|
||||||
|
- **Relationship Management**: Appropriate use of @OneToMany, @ManyToOne, @ManyToMany
|
||||||
|
- **Cascade Operations**: Carefully configured cascade types to maintain referential integrity
|
||||||
|
- **Lazy/Eager Loading**: Optimized fetch strategies to prevent N+1 queries
|
||||||
|
- **Auditing**: Automatic timestamp management using `@CreatedDate` and `@LastModifiedDate`
|
||||||
|
|
||||||
|
## 4. Functional Requirements
|
||||||
|
|
||||||
|
### 4.1 Business Unit Management
|
||||||
|
|
||||||
|
**Description**: Organizations are structured around business units that own and manage applications.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
|
||||||
|
- Each business unit must have a unique name
|
||||||
|
- Business units must maintain a list of associated contacts with specific roles (Director, Program Manager, etc.)
|
||||||
|
- Support for hierarchical contact management with role-based assignments
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- Business Unit entity with name and metadata
|
||||||
|
- Many-to-many relationship with Contact entities
|
||||||
|
- Role specification for each business unit contact
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/business-units` - List all business units
|
||||||
|
- `GET /api/business-units/{id}` - Get business unit details
|
||||||
|
- `POST /api/business-units` - Create new business unit
|
||||||
|
- `PUT /api/business-units/{id}` - Update business unit
|
||||||
|
- `DELETE /api/business-units/{id}` - Delete business unit
|
||||||
|
- `GET /api/business-units/{id}/contacts` - Get contacts for business unit
|
||||||
|
|
||||||
|
### 4.2 Application Management
|
||||||
|
|
||||||
|
**Description**: Applications are the core entities tracked within the system, representing software systems deployed across various environments.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
|
||||||
|
#### 4.2.1 Application Lifecycle Status
|
||||||
|
|
||||||
|
Applications must track their current status through the following states:
|
||||||
|
|
||||||
|
- **IDEA**: Initial concept phase, not yet in development
|
||||||
|
- **IN_DEVELOPMENT**: Active development in progress
|
||||||
|
- **IN_SERVICE**: Deployed and operational in production
|
||||||
|
- **MAINTENANCE**: Undergoing maintenance or limited support
|
||||||
|
- **DECOMMISSIONED**: Retired and no longer in use
|
||||||
|
|
||||||
|
Status transitions should be tracked with timestamps for audit purposes.
|
||||||
|
|
||||||
|
#### 4.2.2 Multi-Environment Deployment
|
||||||
|
|
||||||
|
- Applications must support deployment across multiple environments
|
||||||
|
- Each deployment must be linked to a specific version of the application
|
||||||
|
- Complete deployment history must be maintained with timestamps
|
||||||
|
- Deployment tracking must record who deployed and when
|
||||||
|
|
||||||
|
#### 4.2.3 Stakeholder Management
|
||||||
|
|
||||||
|
- Applications must maintain a list of contacts
|
||||||
|
- Each contact must be associated with a specific role (Product Owner, Functional Authority, Developer, Maintainer, etc.)
|
||||||
|
- Support for multiple stakeholders per role
|
||||||
|
- Contact information must include name, email, and phone number
|
||||||
|
|
||||||
|
#### 4.2.4 Service Level Agreements
|
||||||
|
|
||||||
|
- Each application must be associated with an SLA definition
|
||||||
|
- SLA compliance must be trackable
|
||||||
|
|
||||||
|
#### 4.2.5 Technical Documentation
|
||||||
|
|
||||||
|
- Applications must support multiple technical documentation references
|
||||||
|
- Each documentation entry must include:
|
||||||
|
- Title
|
||||||
|
- URL reference
|
||||||
|
- Creation and update timestamps
|
||||||
|
|
||||||
|
#### 4.2.6 Lifecycle Management
|
||||||
|
|
||||||
|
- End-of-life date tracking
|
||||||
|
- End-of-support date tracking
|
||||||
|
- Version-specific lifecycle information
|
||||||
|
|
||||||
|
#### 4.2.7 External Dependencies
|
||||||
|
|
||||||
|
- Applications must be able to reference multiple external dependencies
|
||||||
|
- Dependencies can be of various types (detailed in section 4.4)
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- Application entity with status tracking (enum: IDEA, IN_DEVELOPMENT, IN_SERVICE, MAINTENANCE, DECOMMISSIONED)
|
||||||
|
- Relationship to Business Unit (many-to-one)
|
||||||
|
- Relationship to SLA (optional, one-to-one)
|
||||||
|
- Relationship to Versions (one-to-many)
|
||||||
|
- Relationship to Contacts (many-to-many)
|
||||||
|
- Relationship to Technical Documentation (one-to-many)
|
||||||
|
- Relationship to External Dependencies (one-to-many)
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/applications` - List applications (with filtering by status, business unit)
|
||||||
|
- `GET /api/applications/{id}` - Get application details
|
||||||
|
- `POST /api/applications` - Create new application
|
||||||
|
- `PUT /api/applications/{id}` - Update application
|
||||||
|
- `PATCH /api/applications/{id}/status` - Update application status
|
||||||
|
- `DELETE /api/applications/{id}` - Delete application
|
||||||
|
- `GET /api/applications/{id}/versions` - Get versions for application
|
||||||
|
- `GET /api/applications/{id}/deployments` - Get deployment history
|
||||||
|
- `GET /api/applications/{id}/contacts` - Get stakeholders
|
||||||
|
- `GET /api/applications/{id}/dependencies` - Get external dependencies
|
||||||
|
|
||||||
|
### 4.3 Version Management
|
||||||
|
|
||||||
|
**Description**: Each application release is tracked as a distinct version with specific attributes.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
|
||||||
|
- Every deployed application must be linked to a specific version
|
||||||
|
- Version tracking must include:
|
||||||
|
- **Version Identifier**: Semantic version or custom identifier (e.g., "1.2.3", "2024.Q1")
|
||||||
|
- **External Reference**: Link to source control, ticketing system, or release notes
|
||||||
|
- **Release Date**: When the version was officially released
|
||||||
|
- **End-of-Life Date**: When support for this version ends
|
||||||
|
- **Creation Timestamp**: When the version record was created
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- Version entity linked to Application (many-to-one)
|
||||||
|
- Unique identifier and version metadata
|
||||||
|
- Lifecycle dates specific to the version
|
||||||
|
- Constraint: Version identifier must be unique within an application
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/applications/{appId}/versions` - List versions for an application
|
||||||
|
- `GET /api/versions/{id}` - Get version details
|
||||||
|
- `POST /api/applications/{appId}/versions` - Create new version
|
||||||
|
- `PUT /api/versions/{id}` - Update version
|
||||||
|
- `DELETE /api/versions/{id}` - Delete version
|
||||||
|
|
||||||
|
### 4.4 External Dependencies Management
|
||||||
|
|
||||||
|
**Description**: Applications often rely on external data sources and services. These dependencies must be documented and tracked.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
|
||||||
|
#### 4.4.1 Dependency Types
|
||||||
|
|
||||||
|
External dependencies can be classified into the following types:
|
||||||
|
|
||||||
|
- **WEB_SERVICE**: REST APIs, SOAP services, microservices
|
||||||
|
- **DATABASE**: External database connections
|
||||||
|
- **CERTIFICATE**: SSL/TLS certificates, authentication certificates
|
||||||
|
- **NETWORK_FLOW**: Network connections, firewall rules, VPN tunnels
|
||||||
|
|
||||||
|
#### 4.4.2 Dependency Documentation
|
||||||
|
|
||||||
|
Each external dependency must include:
|
||||||
|
|
||||||
|
- Name and type classification
|
||||||
|
- Descriptive documentation (free text)
|
||||||
|
- Technical documentation specific to the dependency
|
||||||
|
- Creation and update timestamps
|
||||||
|
|
||||||
|
#### 4.4.3 Data Usage Agreements
|
||||||
|
|
||||||
|
- External dependencies must be linkable to one or more Data Usage Agreements
|
||||||
|
- This enables tracking of data governance and compliance requirements
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- External Dependency entity with type enumeration
|
||||||
|
- Relationship to Application (many-to-one)
|
||||||
|
- Relationship to Data Usage Agreements (many-to-many)
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/applications/{appId}/dependencies` - List dependencies for application
|
||||||
|
- `GET /api/dependencies/{id}` - Get dependency details
|
||||||
|
- `POST /api/applications/{appId}/dependencies` - Create new dependency
|
||||||
|
- `PUT /api/dependencies/{id}` - Update dependency
|
||||||
|
- `DELETE /api/dependencies/{id}` - Delete dependency
|
||||||
|
- `GET /api/dependencies/{id}/agreements` - Get data usage agreements for dependency
|
||||||
|
|
||||||
|
### 4.5 Data Usage Agreements
|
||||||
|
|
||||||
|
**Description**: Data Usage Agreements formalize the authorization to use external data sources and must be tracked for compliance purposes.
|
||||||
|
|
||||||
|
**Requirements**: Each Data Usage Agreement must contain:
|
||||||
|
|
||||||
|
- **Data Nature**: Description of the type of data covered by the agreement
|
||||||
|
- **Documentation Link**: Reference to the legal or formal agreement document
|
||||||
|
- **Validity Start Date**: When the agreement becomes effective
|
||||||
|
- **Validity End Date**: When the agreement expires (optional for indefinite agreements)
|
||||||
|
- **Creation Timestamp**: When the agreement record was created
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- Data Usage Agreement entity with validity period
|
||||||
|
- Many-to-many relationship with External Dependencies
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/data-usage-agreements` - List all agreements (with expiration filtering)
|
||||||
|
- `GET /api/data-usage-agreements/{id}` - Get agreement details
|
||||||
|
- `POST /api/data-usage-agreements` - Create new agreement
|
||||||
|
- `PUT /api/data-usage-agreements/{id}` - Update agreement
|
||||||
|
- `DELETE /api/data-usage-agreements/{id}` - Delete agreement
|
||||||
|
|
||||||
|
### 4.6 Environment Management
|
||||||
|
|
||||||
|
**Description**: Environments represent distinct deployment targets where applications can be installed. Unlike traditional fixed environment types, LDPv2 treats environments as flexible entities that can be created and configured as needed.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
|
||||||
|
#### 4.6.1 Environment as First-Class Entity
|
||||||
|
|
||||||
|
Each environment is a distinct entity with its own attributes and lifecycle. This allows for:
|
||||||
|
|
||||||
|
- Multiple production environments (e.g., PROD-EU, PROD-US, PROD-ASIA)
|
||||||
|
- Team-specific development environments (e.g., DEV-TeamAlpha, DEV-TeamBeta)
|
||||||
|
- Customer-specific environments
|
||||||
|
- Temporary or experimental environments
|
||||||
|
|
||||||
|
#### 4.6.2 Environment Attributes
|
||||||
|
|
||||||
|
Each environment must be tracked with the following attributes:
|
||||||
|
|
||||||
|
- **Name**: Unique identifier for the environment (e.g., "PROD-EU", "INT-QA", "DEV-Mobile")
|
||||||
|
- **Description**: Optional free-text description of the environment's purpose
|
||||||
|
- **Production Flag**: Boolean indicator to identify production environments
|
||||||
|
- **Criticality Level**: Numeric rating (e.g., 1-5) to prioritize monitoring, incident response, and deployment approvals
|
||||||
|
- Level 5: Critical production environments
|
||||||
|
- Level 4: Important pre-production environments
|
||||||
|
- Level 3: Integration and testing environments
|
||||||
|
- Level 2: Development environments
|
||||||
|
- Level 1: Experimental or temporary environments
|
||||||
|
- **Timestamps**: Creation and update tracking for audit purposes
|
||||||
|
|
||||||
|
#### 4.6.3 Environment Flexibility
|
||||||
|
|
||||||
|
- Environments can be created, updated, and deactivated as needed
|
||||||
|
- No hard-coded list of environments in the application code
|
||||||
|
- Support for environment-specific security roles and permissions
|
||||||
|
- Environments can be organized or tagged for better management (future enhancement)
|
||||||
|
|
||||||
|
#### 4.6.4 Environment-Based Access Control
|
||||||
|
|
||||||
|
- Users can have different permissions per environment
|
||||||
|
- Critical environments (high criticality level or production flag) may require additional approval workflows
|
||||||
|
- Deployment permissions should be configurable per environment
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- Environment entity with flexible attributes
|
||||||
|
- No enumeration constraints on environment names
|
||||||
|
- Referenced by Deployment entity for tracking application versions
|
||||||
|
- Potential for environment grouping or categorization (future)
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/environments` - List all environments (with filtering by production flag, criticality)
|
||||||
|
- `GET /api/environments/{id}` - Get environment details
|
||||||
|
- `POST /api/environments` - Create new environment
|
||||||
|
- `PUT /api/environments/{id}` - Update environment
|
||||||
|
- `DELETE /api/environments/{id}` - Deactivate/delete environment
|
||||||
|
- `GET /api/environments/{id}/deployments` - Get all deployments in this environment
|
||||||
|
|
||||||
|
**Security Considerations**:
|
||||||
|
|
||||||
|
- Creating or modifying production environments should require elevated privileges
|
||||||
|
- Environment deletion should be soft delete to preserve deployment history
|
||||||
|
- Audit logging for all environment changes
|
||||||
|
|
||||||
|
### 4.7 Deployment Tracking
|
||||||
|
|
||||||
|
**Description**: Deployments represent the installation of a specific application version to a specific environment.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
|
||||||
|
#### 4.7.1 Deployment Definition
|
||||||
|
|
||||||
|
A deployment is uniquely defined by the combination of:
|
||||||
|
|
||||||
|
- Application (which application)
|
||||||
|
- Version (which release)
|
||||||
|
- Environment (where it's deployed)
|
||||||
|
|
||||||
|
#### 4.7.2 Deployment History
|
||||||
|
|
||||||
|
- Complete deployment history must be maintained (immutable records)
|
||||||
|
- Each deployment record must include:
|
||||||
|
- Deployment timestamp
|
||||||
|
- User or system that performed the deployment
|
||||||
|
- Optional notes or comments
|
||||||
|
- Creation timestamp for audit purposes
|
||||||
|
|
||||||
|
#### 4.7.3 Current State Tracking
|
||||||
|
|
||||||
|
- The system must be able to identify the currently active version in each environment
|
||||||
|
- This is determined by the most recent deployment for a given application-environment combination
|
||||||
|
- A dedicated API endpoint should provide current deployment state
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- Deployment entity with foreign keys to Application, Version, and Environment
|
||||||
|
- Timestamp-based ordering for historical tracking
|
||||||
|
- Business logic to determine "current" deployment (most recent by deployment date)
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/deployments` - List all deployments (with filtering)
|
||||||
|
- `GET /api/deployments/{id}` - Get deployment details
|
||||||
|
- `POST /api/deployments` - Record new deployment
|
||||||
|
- `GET /api/applications/{appId}/deployments/current` - Get current deployments per environment
|
||||||
|
- `GET /api/environments/{envId}/deployments/current` - Get all current deployments in environment
|
||||||
|
- `GET /api/deployments/history?applicationId={appId}&environmentId={envId}` - Deployment history for app in env
|
||||||
|
|
||||||
|
**Business Rules**:
|
||||||
|
|
||||||
|
- Deployments are immutable once created (no updates, only new deployments)
|
||||||
|
- Deployment dates cannot be in the future
|
||||||
|
- Version must belong to the specified application
|
||||||
|
|
||||||
|
### 4.8 Contact and Stakeholder Management
|
||||||
|
|
||||||
|
**Description**: Contacts represent roles and responsibilities associated with applications, business units, and SLAs.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
|
||||||
|
#### 4.8.1 Contact Roles
|
||||||
|
|
||||||
|
The system must support various contact roles including but not limited to:
|
||||||
|
|
||||||
|
- Product Owner
|
||||||
|
- Functional Authority
|
||||||
|
- Developer
|
||||||
|
- Maintainer
|
||||||
|
- Technical Lead
|
||||||
|
- Business Analyst
|
||||||
|
- Director
|
||||||
|
- Program Manager
|
||||||
|
|
||||||
|
Roles should be configurable and extensible.
|
||||||
|
|
||||||
|
#### 4.8.2 Contact Structure
|
||||||
|
|
||||||
|
A contact represents a functional role that can be associated with:
|
||||||
|
|
||||||
|
- One or more named individuals (persons)
|
||||||
|
- Multiple applications
|
||||||
|
- Business units
|
||||||
|
- SLAs
|
||||||
|
|
||||||
|
#### 4.8.3 Person Information
|
||||||
|
|
||||||
|
Each person assigned to a contact must have:
|
||||||
|
|
||||||
|
- First name and last name
|
||||||
|
- Email address (required, unique)
|
||||||
|
- Phone number (optional)
|
||||||
|
- Primary contact designation (one person per contact should be marked as primary)
|
||||||
|
|
||||||
|
#### 4.8.4 Contact Relationships
|
||||||
|
|
||||||
|
Contacts must be linkable to:
|
||||||
|
|
||||||
|
- Applications (with application-specific roles)
|
||||||
|
- Business Units (with organizational roles)
|
||||||
|
- SLAs (as responsible parties for service delivery)
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- ContactRole entity defining available roles
|
||||||
|
- Contact entity representing the functional position
|
||||||
|
- Person entity with individual contact details
|
||||||
|
- Junction tables for Contact-Person, Application-Contact, BusinessUnit-Contact, and SLA-Contact relationships
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/contact-roles` - List available contact roles
|
||||||
|
- `GET /api/contacts` - List all contacts
|
||||||
|
- `GET /api/contacts/{id}` - Get contact details with persons
|
||||||
|
- `POST /api/contacts` - Create new contact
|
||||||
|
- `PUT /api/contacts/{id}` - Update contact
|
||||||
|
- `DELETE /api/contacts/{id}` - Delete contact
|
||||||
|
- `GET /api/persons` - List all persons
|
||||||
|
- `GET /api/persons/{id}` - Get person details
|
||||||
|
- `POST /api/persons` - Create new person
|
||||||
|
- `PUT /api/persons/{id}` - Update person
|
||||||
|
|
||||||
|
### 4.9 Service Level Agreement (SLA) Management
|
||||||
|
|
||||||
|
**Description**: SLAs define the expected service levels, maintenance windows, and responsibilities for applications.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
|
||||||
|
#### 4.9.1 Core SLA Metrics
|
||||||
|
|
||||||
|
Each SLA must define:
|
||||||
|
|
||||||
|
- **Availability Percentage**: Expected uptime (e.g., 99.9%)
|
||||||
|
- **Minimum Average Response Time**: Expected response time in seconds
|
||||||
|
- **Reaction Time**: Time to respond to incidents (as text description)
|
||||||
|
|
||||||
|
#### 4.9.2 Maintenance Windows
|
||||||
|
|
||||||
|
- SLAs must support multiple maintenance windows
|
||||||
|
- Each maintenance window must specify:
|
||||||
|
- Day of the week (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY)
|
||||||
|
- Start time (HH:MM format)
|
||||||
|
- End time (HH:MM format)
|
||||||
|
- Multiple maintenance windows can be defined for the same SLA
|
||||||
|
|
||||||
|
#### 4.9.3 Critical Periods
|
||||||
|
|
||||||
|
- SLAs must track critical periods when higher service levels are required
|
||||||
|
- Each critical period must include:
|
||||||
|
- Start datetime
|
||||||
|
- End datetime
|
||||||
|
- Duration in minutes
|
||||||
|
- Optional description
|
||||||
|
|
||||||
|
#### 4.9.4 SLA Documentation
|
||||||
|
|
||||||
|
- **Diagram**: Support for storing draw.io diagrams in JSON format showing system architecture and dependencies
|
||||||
|
- **Dependencies Description**: Free-text description of system dependencies
|
||||||
|
- **Responsibility Scope**: Clear definition of what is and isn't covered by the SLA
|
||||||
|
|
||||||
|
#### 4.9.5 SLA Contacts
|
||||||
|
|
||||||
|
- Multiple contacts must be assignable to each SLA
|
||||||
|
- These represent the responsible parties for maintaining service levels
|
||||||
|
|
||||||
|
**Data Model**:
|
||||||
|
|
||||||
|
- SLA entity with metrics and documentation
|
||||||
|
- MaintenanceWindow entity (one-to-many from SLA)
|
||||||
|
- CriticalPeriod entity (one-to-many from SLA)
|
||||||
|
- SLA-Contact junction table (many-to-many)
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
|
||||||
|
- `GET /api/slas` - List all SLAs
|
||||||
|
- `GET /api/slas/{id}` - Get SLA details
|
||||||
|
- `POST /api/slas` - Create new SLA
|
||||||
|
- `PUT /api/slas/{id}` - Update SLA
|
||||||
|
- `DELETE /api/slas/{id}` - Delete SLA
|
||||||
|
- `GET /api/slas/{id}/maintenance-windows` - Get maintenance windows
|
||||||
|
- `POST /api/slas/{id}/maintenance-windows` - Add maintenance window
|
||||||
|
- `GET /api/slas/{id}/critical-periods` - Get critical periods
|
||||||
|
- `POST /api/slas/{id}/critical-periods` - Add critical period
|
||||||
|
|
||||||
|
## 5. Data Model Summary
|
||||||
|
|
||||||
|
### 5.1 Core Entities
|
||||||
|
|
||||||
|
1. **BusinessUnit**: Organizational container for applications
|
||||||
|
2. **Application**: Central entity representing software systems
|
||||||
|
3. **Version**: Releases of applications
|
||||||
|
4. **Environment**: Flexible deployment targets with custom names and attributes
|
||||||
|
5. **Deployment**: Records of version deployments to environments
|
||||||
|
6. **Person**: Individual stakeholders
|
||||||
|
7. **ContactRole**: Predefined roles for stakeholders
|
||||||
|
8. **Contact**: Functional positions held by persons
|
||||||
|
9. **TechnicalDocumentation**: Links to technical resources
|
||||||
|
10. **ExternalDependency**: External services and data sources
|
||||||
|
11. **DataUsageAgreement**: Legal agreements for data usage
|
||||||
|
12. **SLA**: Service level agreement definitions
|
||||||
|
13. **MaintenanceWindow**: Scheduled maintenance periods
|
||||||
|
14. **CriticalPeriod**: High-priority time windows
|
||||||
|
|
||||||
|
### 5.2 Relationship Summary
|
||||||
|
|
||||||
|
- BusinessUnit → Application (1:N)
|
||||||
|
- Application → Version (1:N)
|
||||||
|
- Application + Version + Environment → Deployment (unique combination)
|
||||||
|
- Application ↔ Contact (N:N)
|
||||||
|
- Application → ExternalDependency (1:N)
|
||||||
|
- Application → TechnicalDocumentation (1:N)
|
||||||
|
- Application → SLA (1:1, optional)
|
||||||
|
- ExternalDependency ↔ DataUsageAgreement (N:N)
|
||||||
|
- Contact ↔ Person (N:N)
|
||||||
|
- SLA → MaintenanceWindow (1:N)
|
||||||
|
- SLA → CriticalPeriod (1:N)
|
||||||
|
- SLA ↔ Contact (N:N)
|
||||||
|
|
||||||
|
### 5.3 Database Schema Considerations
|
||||||
|
|
||||||
|
- All primary keys: UUID type
|
||||||
|
- Foreign keys with appropriate CASCADE/RESTRICT rules
|
||||||
|
- Indexes on frequently queried fields (application status, environment production flag, deployment dates)
|
||||||
|
- Unique constraints where applicable (email in Person, environment name)
|
||||||
|
- Audit columns (createdAt, updatedAt) on all entities using JPA auditing
|
||||||
|
|
||||||
|
## 6. Non-Functional Requirements
|
||||||
|
|
||||||
|
### 6.1 Performance
|
||||||
|
|
||||||
|
- API response time < 500ms for 95% of requests
|
||||||
|
- Support for pagination on all list endpoints (default page size: 20)
|
||||||
|
- Efficient query optimization to prevent N+1 problems
|
||||||
|
- Database connection pooling (HikariCP)
|
||||||
|
- Caching strategy for reference data (contact roles, environments)
|
||||||
|
|
||||||
|
### 6.2 Security
|
||||||
|
|
||||||
|
- JWT token expiration: 1 hour (configurable)
|
||||||
|
- Refresh token mechanism for session extension
|
||||||
|
- Password hashing: BCrypt with appropriate work factor
|
||||||
|
- HTTPS only in production
|
||||||
|
- CORS configuration for frontend origin
|
||||||
|
- SQL injection prevention via parameterized queries (JPA)
|
||||||
|
- XSS protection via Angular sanitization
|
||||||
|
- CSRF protection via Spring Security
|
||||||
|
|
||||||
|
### 6.3 Audit and Compliance
|
||||||
|
|
||||||
|
- All entities must track creation and update timestamps
|
||||||
|
- Deployment history must be immutable for audit purposes
|
||||||
|
- Data Usage Agreement expiration must be trackable
|
||||||
|
- User actions should be logged (who did what, when)
|
||||||
|
- Database audit trail for sensitive operations
|
||||||
|
|
||||||
|
### 6.4 Data Integrity
|
||||||
|
|
||||||
|
- Foreign key constraints enforced at database level
|
||||||
|
- Validation annotations on JPA entities (@NotNull, @Size, @Email, etc.)
|
||||||
|
- Business rule validation in service layer
|
||||||
|
- Transaction management for complex operations
|
||||||
|
- Soft delete for critical entities to preserve referential integrity
|
||||||
|
|
||||||
|
### 6.5 Scalability
|
||||||
|
|
||||||
|
- The system must support hundreds of applications
|
||||||
|
- Deployment history may grow to thousands of records per application
|
||||||
|
- Efficient querying of current deployment states is critical
|
||||||
|
- Database indexing strategy for performance
|
||||||
|
- Potential for read replicas if query load increases
|
||||||
|
|
||||||
|
### 6.6 Maintainability
|
||||||
|
|
||||||
|
- Clean separation of concerns (Controller → Service → Repository)
|
||||||
|
- DTOs for API layer to decouple from entity model
|
||||||
|
- MapStruct or ModelMapper for DTO conversion
|
||||||
|
- Comprehensive API documentation (Swagger/OpenAPI)
|
||||||
|
- Unit tests (JUnit 5, Mockito) with >80% code coverage
|
||||||
|
- Integration tests for critical workflows
|
||||||
|
- Docker containerization for consistent deployments
|
||||||
|
|
||||||
|
### 6.7 Usability
|
||||||
|
|
||||||
|
- Intuitive Angular UI with responsive design
|
||||||
|
- Clear error messages and validation feedback
|
||||||
|
- Loading indicators for async operations
|
||||||
|
- Confirmation dialogs for destructive actions
|
||||||
|
- Keyboard navigation support
|
||||||
|
- Accessibility compliance (WCAG 2.1 Level AA)
|
||||||
|
|
||||||
|
## 7. Implementation Phases
|
||||||
|
|
||||||
|
### 7.1 Phase 1: Core Foundation (MVP)
|
||||||
|
|
||||||
|
- Database schema creation with Liquibase/Flyway
|
||||||
|
- JPA entity model implementation
|
||||||
|
- Spring Security with local authentication
|
||||||
|
- JWT token generation and validation
|
||||||
|
- Basic CRUD APIs for:
|
||||||
|
- BusinessUnit
|
||||||
|
- Application (with status tracking)
|
||||||
|
- Environment
|
||||||
|
- Contact and Person
|
||||||
|
- Angular authentication module
|
||||||
|
- Basic application listing and detail views
|
||||||
|
|
||||||
|
### 7.2 Phase 2: Deployment Tracking
|
||||||
|
|
||||||
|
- Version entity and APIs
|
||||||
|
- Deployment entity and APIs
|
||||||
|
- Deployment history tracking
|
||||||
|
- Current deployment state queries
|
||||||
|
- Angular deployment management interface
|
||||||
|
- Deployment timeline visualization
|
||||||
|
|
||||||
|
### 7.3 Phase 3: Dependencies and Compliance
|
||||||
|
|
||||||
|
- External Dependency entity and APIs
|
||||||
|
- Data Usage Agreement entity and APIs
|
||||||
|
- Dependency-Agreement linkage
|
||||||
|
- Technical Documentation management
|
||||||
|
- Angular forms for dependency tracking
|
||||||
|
- Expiration alerts for agreements
|
||||||
|
|
||||||
|
### 7.4 Phase 4: SLA Management
|
||||||
|
|
||||||
|
- SLA entity and APIs
|
||||||
|
- Maintenance Window management
|
||||||
|
- Critical Period tracking
|
||||||
|
- SLA-Contact relationships
|
||||||
|
- Draw.io diagram storage and display
|
||||||
|
- Angular SLA management interface
|
||||||
|
|
||||||
|
### 7.5 Phase 5: Advanced Features
|
||||||
|
|
||||||
|
- Environment-based role permissions
|
||||||
|
- OAuth 2.0 integration
|
||||||
|
- Advanced search and filtering
|
||||||
|
- Dashboard and analytics
|
||||||
|
- Reporting capabilities
|
||||||
|
- Notification system
|
||||||
|
- API rate limiting
|
||||||
|
|
||||||
|
## 8. Testing Strategy
|
||||||
|
|
||||||
|
### 8.1 Backend Testing
|
||||||
|
|
||||||
|
- **Unit Tests**: Service layer business logic (JUnit 5, Mockito)
|
||||||
|
- **Integration Tests**: Repository layer with test database (Testcontainers)
|
||||||
|
- **API Tests**: Controller endpoints (MockMvc, REST Assured)
|
||||||
|
- **Security Tests**: Authentication and authorization flows
|
||||||
|
- **Performance Tests**: Load testing for critical endpoints (JMeter, Gatling)
|
||||||
|
|
||||||
|
### 8.2 Frontend Testing
|
||||||
|
|
||||||
|
- **Unit Tests**: Angular components and services (Jasmine, Karma)
|
||||||
|
- **Integration Tests**: Component interaction and routing
|
||||||
|
- **E2E Tests**: Critical user workflows (Cypress, Playwright)
|
||||||
|
- **Accessibility Tests**: WCAG compliance checking
|
||||||
|
|
||||||
|
### 8.3 Test Coverage Goals
|
||||||
|
|
||||||
|
- Backend code coverage: >80%
|
||||||
|
- Frontend code coverage: >70%
|
||||||
|
- Critical path E2E coverage: 100%
|
||||||
|
|
||||||
|
## 9. Deployment Architecture
|
||||||
|
|
||||||
|
### 9.1 Development Environment
|
||||||
|
|
||||||
|
- Local PostgreSQL instance via Docker
|
||||||
|
- Spring Boot running on port 8080
|
||||||
|
- Angular dev server on port 4200
|
||||||
|
- Hot reload enabled for both frontend and backend
|
||||||
|
|
||||||
|
### 9.2 Production Environment
|
||||||
|
|
||||||
|
- Containerized deployment (Docker/Kubernetes)
|
||||||
|
- PostgreSQL cluster with replication
|
||||||
|
- Load balancer for backend instances
|
||||||
|
- CDN for frontend static assets
|
||||||
|
- Separate database for each environment (dev, test, prod)
|
||||||
|
- Automated backup strategy
|
||||||
|
|
||||||
|
### 9.3 CI/CD Pipeline
|
||||||
|
|
||||||
|
- Source control: Git (GitHub, GitLab, or Bitbucket)
|
||||||
|
- Build automation: Maven for backend, npm for frontend
|
||||||
|
- Continuous Integration: Jenkins, GitLab CI, or GitHub Actions
|
||||||
|
- Automated testing in pipeline
|
||||||
|
- Docker image building and publishing
|
||||||
|
- Automated deployment to test environments
|
||||||
|
- Manual approval for production deployment
|
||||||
|
|
||||||
|
## 10. Documentation Requirements
|
||||||
|
|
||||||
|
### 10.1 Technical Documentation
|
||||||
|
|
||||||
|
- API documentation (Swagger/OpenAPI specification)
|
||||||
|
- Database schema documentation (ER diagrams)
|
||||||
|
- Architecture decision records (ADRs)
|
||||||
|
- Deployment and configuration guides
|
||||||
|
- Security best practices guide
|
||||||
|
|
||||||
|
### 10.2 User Documentation
|
||||||
|
|
||||||
|
- User manual for each user role
|
||||||
|
- Quick start guide
|
||||||
|
- Video tutorials for common workflows
|
||||||
|
- FAQ section
|
||||||
|
- Troubleshooting guide
|
||||||
|
|
||||||
|
## 11. Future Considerations
|
||||||
|
|
||||||
|
### 11.1 Potential Enhancements
|
||||||
|
|
||||||
|
- Integration with CI/CD pipelines for automatic deployment recording
|
||||||
|
- Automated SLA compliance monitoring and alerting
|
||||||
|
- Dashboard views for application portfolio management
|
||||||
|
- Notification system for expiring Data Usage Agreements
|
||||||
|
- Advanced reporting and analytics
|
||||||
|
- Mobile application for on-the-go access
|
||||||
|
- Webhook support for external integrations
|
||||||
|
- GraphQL API as alternative to REST
|
||||||
|
- Elasticsearch integration for advanced search
|
||||||
|
- Audit log viewer with filtering
|
||||||
|
|
||||||
|
### 11.2 Reporting Requirements
|
||||||
|
|
||||||
|
- Application inventory reports by status
|
||||||
|
- Deployment frequency analytics
|
||||||
|
- Environment utilization reports
|
||||||
|
- SLA compliance reporting
|
||||||
|
- Dependency mapping and impact analysis
|
||||||
|
- End-of-life planning reports
|
||||||
|
- Data usage agreement expiration forecasts
|
||||||
|
- Stakeholder contact directory
|
||||||
|
|
||||||
|
## 12. Success Criteria
|
||||||
|
|
||||||
|
The LDPv2 system will be considered successful when:
|
||||||
|
|
||||||
|
1. All applications within the business unit are accurately documented
|
||||||
|
2. Deployment history is complete and traceable across all environments
|
||||||
|
3. Stakeholders can easily identify current application states and ownership
|
||||||
|
4. Compliance officers can track data usage agreements effectively
|
||||||
|
5. SLA definitions are clearly documented and accessible
|
||||||
|
6. The system serves as the single source of truth for application lifecycle data
|
||||||
|
7. Users can perform all core functions without training (intuitive UI)
|
||||||
|
8. System uptime > 99.5%
|
||||||
|
9. User satisfaction score > 4.0/5.0
|
||||||
|
|
||||||
|
----------
|
||||||
|
|
||||||
|
**Document Version**: 2.0
|
||||||
|
**Last Updated**: February 2026
|
||||||
|
**Status**: Technical Specification - Ready for Development
|
||||||
|
**Prepared By**: Laurent
|
||||||
|
**Review Date**: To be scheduled
|
||||||
+335
@@ -0,0 +1,335 @@
|
|||||||
|
# LDPv2 MVP - Development Package
|
||||||
|
|
||||||
|
Welcome to the LDPv2 (Lifecycle Data Platform v2) MVP development package!
|
||||||
|
|
||||||
|
This package contains complete documentation and specifications for building the LDPv2 application using an iterative, story-based approach.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Package Contents
|
||||||
|
|
||||||
|
```
|
||||||
|
ldpv2-mvp/
|
||||||
|
├── README.md (this file)
|
||||||
|
├── 00-MVP-OVERVIEW.md
|
||||||
|
├── 01-TECHNICAL-SETUP.md
|
||||||
|
├── stories/
|
||||||
|
│ ├── STORY-0-Foundation.md
|
||||||
|
│ ├── STORY-1-Business-Units.md
|
||||||
|
│ ├── STORY-2-Applications.md
|
||||||
|
│ ├── STORY-3-Contacts.md
|
||||||
|
│ ├── STORY-4-Environments.md
|
||||||
|
│ ├── STORY-5-Versions.md
|
||||||
|
│ ├── STORY-6-Deployments.md
|
||||||
|
│ └── STORY-7-Current-State-History.md
|
||||||
|
├── data-model/
|
||||||
|
│ └── complete-data-model.ts
|
||||||
|
└── api-specs/
|
||||||
|
└── endpoint-summary.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Step 1: Read the Overview
|
||||||
|
Start with **00-MVP-OVERVIEW.md** to understand:
|
||||||
|
- MVP scope and objectives
|
||||||
|
- Development approach (Walking Skeleton + Vertical Slices)
|
||||||
|
- Story dependencies
|
||||||
|
- Timeline estimates
|
||||||
|
|
||||||
|
### Step 2: Setup Your Environment
|
||||||
|
Follow **01-TECHNICAL-SETUP.md** to:
|
||||||
|
- Install prerequisites (Java 17+, Node.js 18+, PostgreSQL 16, Docker)
|
||||||
|
- Setup Spring Boot backend project
|
||||||
|
- Setup Angular 18 frontend project
|
||||||
|
- Configure Docker Compose for local development
|
||||||
|
- Understand project structure and conventions
|
||||||
|
|
||||||
|
### Step 3: Start Development
|
||||||
|
Begin with **Story 0** (Foundation) and follow the story sequence:
|
||||||
|
|
||||||
|
```
|
||||||
|
Story 0: Foundation (Walking Skeleton)
|
||||||
|
↓
|
||||||
|
Story 1: Business Units
|
||||||
|
↓
|
||||||
|
Story 2: Applications
|
||||||
|
↓
|
||||||
|
Story 3: Contacts (can be parallel with Story 4)
|
||||||
|
↓
|
||||||
|
Story 4: Environments (enhancement)
|
||||||
|
↓
|
||||||
|
Story 5: Versions
|
||||||
|
↓
|
||||||
|
Story 6: Deployments
|
||||||
|
↓
|
||||||
|
Story 7: Current State & History
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Story Structure
|
||||||
|
|
||||||
|
Each story follows a consistent structure:
|
||||||
|
|
||||||
|
- **Story Overview**: Business value and objectives
|
||||||
|
- **Scope**: What's in and out of scope
|
||||||
|
- **Technical Implementation**: Detailed backend and frontend tasks
|
||||||
|
- **Acceptance Criteria**: Testable requirements
|
||||||
|
- **Testing Scenarios**: Step-by-step test cases
|
||||||
|
- **Definition of Done**: Checklist before considering story complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 MVP Scope Summary
|
||||||
|
|
||||||
|
### What's Included in MVP
|
||||||
|
|
||||||
|
✅ **Core Domain**
|
||||||
|
- Application management with lifecycle tracking
|
||||||
|
- Business unit organization
|
||||||
|
- Environment management (flexible, production-ready)
|
||||||
|
- Version tracking
|
||||||
|
- Deployment recording and history
|
||||||
|
- Current deployment state queries
|
||||||
|
|
||||||
|
✅ **User Management**
|
||||||
|
- Local authentication (JWT-based)
|
||||||
|
- Basic role-based access control (ADMIN, USER)
|
||||||
|
- User registration and login
|
||||||
|
|
||||||
|
✅ **Contact Management**
|
||||||
|
- Contact roles (Product Owner, Developer, etc.)
|
||||||
|
- Person management
|
||||||
|
- Contact-Person associations
|
||||||
|
|
||||||
|
✅ **Reporting & Analytics**
|
||||||
|
- Deployment dashboard
|
||||||
|
- Deployment history and timeline
|
||||||
|
- Statistics and charts
|
||||||
|
- Export to CSV/Excel
|
||||||
|
|
||||||
|
### What's NOT in MVP (Phase 2)
|
||||||
|
|
||||||
|
❌ External dependencies tracking
|
||||||
|
❌ Data usage agreements
|
||||||
|
❌ SLA management
|
||||||
|
❌ Technical documentation links
|
||||||
|
❌ OAuth integration
|
||||||
|
❌ Advanced notifications
|
||||||
|
❌ Deployment approval workflows
|
||||||
|
❌ CI/CD integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Technology Stack
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Framework**: Spring Boot 3.2.x
|
||||||
|
- **Language**: Java 17/21
|
||||||
|
- **Security**: Spring Security + JWT
|
||||||
|
- **Database**: PostgreSQL 16
|
||||||
|
- **ORM**: Hibernate (JPA)
|
||||||
|
- **Migrations**: Liquibase
|
||||||
|
- **Testing**: JUnit 5, Mockito, Testcontainers
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Framework**: Angular 18
|
||||||
|
- **Language**: TypeScript 5.x
|
||||||
|
- **Auth**: JWT interceptors
|
||||||
|
- **UI Library**: Angular Material or PrimeNG
|
||||||
|
- **Testing**: Jasmine/Karma, Cypress
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- **Containerization**: Docker
|
||||||
|
- **Orchestration**: Docker Compose
|
||||||
|
- **CI/CD**: Jenkins/GitLab CI/GitHub Actions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏱️ Timeline Estimate
|
||||||
|
|
||||||
|
| Phase | Duration | Stories |
|
||||||
|
|-------|----------|---------|
|
||||||
|
| **Phase 0: Foundation** | 2-3 weeks | Story 0 |
|
||||||
|
| **Phase 1: Core (Batch 1)** | 3-4 weeks | Stories 1-4 |
|
||||||
|
| **Phase 1: Core (Batch 2)** | 3-4 weeks | Stories 5-7 |
|
||||||
|
| **Total MVP** | **8-11 weeks** | **8 stories** |
|
||||||
|
|
||||||
|
*Based on 1 full-time developer or 2 part-time developers*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Story Dependencies Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
Story 0 (Foundation)
|
||||||
|
├── Story 1 (Business Units) ────────┐
|
||||||
|
│ └── Story 2 (Applications) ──────┤
|
||||||
|
├── Story 3 (Contacts) ──────────────┤── Can be developed in parallel
|
||||||
|
├── Story 4 (Environments) ──────────┘
|
||||||
|
└── Story 5 (Versions)
|
||||||
|
└── Story 6 (Deployments)
|
||||||
|
└── Story 7 (Current State & History)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Success Criteria
|
||||||
|
|
||||||
|
The MVP will be considered successful when:
|
||||||
|
|
||||||
|
1. **Functional Completeness**
|
||||||
|
- All 8 stories delivered and accepted
|
||||||
|
- All acceptance criteria met
|
||||||
|
- Zero critical bugs
|
||||||
|
|
||||||
|
2. **Technical Quality**
|
||||||
|
- Backend test coverage > 80%
|
||||||
|
- Frontend test coverage > 70%
|
||||||
|
- All APIs documented in Swagger
|
||||||
|
- Code passes security review
|
||||||
|
|
||||||
|
3. **User Satisfaction**
|
||||||
|
- Users can perform core workflows without training
|
||||||
|
- System performance < 500ms API response
|
||||||
|
- Positive feedback from pilot users
|
||||||
|
|
||||||
|
4. **Business Value**
|
||||||
|
- All applications registered in system
|
||||||
|
- Deployment history accurate and complete
|
||||||
|
- Users prefer LDPv2 over previous tools
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing Strategy
|
||||||
|
|
||||||
|
### Backend Testing
|
||||||
|
- **Unit Tests**: Service layer with Mockito (>80% coverage)
|
||||||
|
- **Integration Tests**: Controllers with Testcontainers
|
||||||
|
- **API Tests**: REST Assured or MockMvc
|
||||||
|
|
||||||
|
### Frontend Testing
|
||||||
|
- **Unit Tests**: Components and services (>70% coverage)
|
||||||
|
- **Integration Tests**: Component interactions
|
||||||
|
- **E2E Tests**: Critical user flows with Cypress
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Key Documents
|
||||||
|
|
||||||
|
### For Project Managers
|
||||||
|
- **00-MVP-OVERVIEW.md**: Scope, timeline, success criteria
|
||||||
|
- **Stories/*.md**: Detailed requirements and acceptance criteria
|
||||||
|
|
||||||
|
### For Architects
|
||||||
|
- **01-TECHNICAL-SETUP.md**: Architecture and tech stack
|
||||||
|
- **data-model/complete-data-model.ts**: Complete data model
|
||||||
|
- **api-specs/endpoint-summary.md**: API design
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
- **stories/STORY-*.md**: Detailed implementation tasks
|
||||||
|
- **01-TECHNICAL-SETUP.md**: Setup instructions
|
||||||
|
- **api-specs/endpoint-summary.md**: API reference
|
||||||
|
|
||||||
|
### For QA/Testers
|
||||||
|
- **stories/STORY-*.md**: Acceptance criteria and test scenarios
|
||||||
|
- Each story contains detailed testing scenarios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Development Workflow
|
||||||
|
|
||||||
|
### For Each Story
|
||||||
|
|
||||||
|
1. **Read the story documentation** thoroughly
|
||||||
|
2. **Setup environment** (if Story 0)
|
||||||
|
3. **Backend Development**:
|
||||||
|
- Create Liquibase migration
|
||||||
|
- Implement JPA entities
|
||||||
|
- Create repositories
|
||||||
|
- Implement service layer
|
||||||
|
- Create DTOs
|
||||||
|
- Implement controllers
|
||||||
|
- Write tests (unit + integration)
|
||||||
|
4. **Frontend Development**:
|
||||||
|
- Create TypeScript models
|
||||||
|
- Implement Angular service
|
||||||
|
- Create components (list, detail, form)
|
||||||
|
- Add routing
|
||||||
|
- Write tests (unit + E2E)
|
||||||
|
5. **Review**: Code review, test review
|
||||||
|
6. **Demo**: Demonstrate to stakeholders
|
||||||
|
7. **Merge**: Merge to main branch
|
||||||
|
8. **Document**: Update API docs, user docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Known Limitations & Technical Debt
|
||||||
|
|
||||||
|
### MVP Limitations
|
||||||
|
- Simple role-based authorization (will need environment-specific roles later)
|
||||||
|
- Local authentication only (OAuth planned for Phase 2)
|
||||||
|
- No deployment approval workflows
|
||||||
|
- No automated notifications
|
||||||
|
- No CI/CD integration
|
||||||
|
|
||||||
|
### Planned Improvements (Phase 2)
|
||||||
|
- OAuth 2.0 integration (Azure AD, Okta)
|
||||||
|
- Environment-specific user permissions
|
||||||
|
- External dependencies tracking
|
||||||
|
- SLA management
|
||||||
|
- Automated deployment notifications
|
||||||
|
- Integration with CI/CD pipelines
|
||||||
|
- Advanced analytics and reporting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
### Code Standards
|
||||||
|
- Follow Spring Boot conventions for backend
|
||||||
|
- Follow Angular style guide for frontend
|
||||||
|
- Write meaningful commit messages
|
||||||
|
- Maintain test coverage thresholds
|
||||||
|
- Document all public APIs
|
||||||
|
|
||||||
|
### Git Workflow
|
||||||
|
1. Create feature branch: `feature/story-N-description`
|
||||||
|
2. Commit frequently with clear messages
|
||||||
|
3. Write/update tests
|
||||||
|
4. Create pull request
|
||||||
|
5. Address review comments
|
||||||
|
6. Merge to main after approval
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support & Questions
|
||||||
|
|
||||||
|
For questions about this package:
|
||||||
|
- Review the relevant story documentation
|
||||||
|
- Check the technical setup guide
|
||||||
|
- Consult the API specification
|
||||||
|
- Refer to the data model
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
Proprietary - Internal use only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Let's Build!
|
||||||
|
|
||||||
|
You now have everything you need to build LDPv2 MVP successfully!
|
||||||
|
|
||||||
|
Start with Story 0 and follow the iterative approach. Good luck! 🚀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Package Version**: 1.0
|
||||||
|
**Last Updated**: February 2026
|
||||||
|
**Status**: Ready for Development
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
# LDPv2 MVP - API Endpoints Summary
|
||||||
|
|
||||||
|
Complete list of RESTful API endpoints for the MVP.
|
||||||
|
|
||||||
|
**Base URL**: `http://localhost:8080/api`
|
||||||
|
**Authentication**: Bearer JWT token (except /auth endpoints)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| POST | `/auth/register` | Register new user | No |
|
||||||
|
| POST | `/auth/login` | Login and get JWT token | No |
|
||||||
|
| GET | `/auth/me` | Get current user info | Yes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Unit Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/business-units` | List all business units | Yes |
|
||||||
|
| GET | `/business-units/search?q={query}` | Search business units | Yes |
|
||||||
|
| GET | `/business-units/{id}` | Get business unit by ID | Yes |
|
||||||
|
| POST | `/business-units` | Create business unit | Yes |
|
||||||
|
| PUT | `/business-units/{id}` | Update business unit | Yes |
|
||||||
|
| DELETE | `/business-units/{id}` | Delete business unit | Yes |
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `page`: Page number (default: 0)
|
||||||
|
- `size`: Page size (default: 20)
|
||||||
|
- `sort`: Sort criteria (e.g., `name,asc`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Application Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/applications` | List all applications | Yes |
|
||||||
|
| GET | `/applications/search` | Advanced search | Yes |
|
||||||
|
| GET | `/applications/{id}` | Get application by ID | Yes |
|
||||||
|
| POST | `/applications` | Create application | Yes |
|
||||||
|
| PUT | `/applications/{id}` | Update application | Yes |
|
||||||
|
| PATCH | `/applications/{id}/status` | Update status only | Yes |
|
||||||
|
| DELETE | `/applications/{id}` | Delete application | Yes |
|
||||||
|
| GET | `/applications/by-status/{status}` | Filter by status | Yes |
|
||||||
|
| GET | `/applications/by-business-unit/{businessUnitId}` | Filter by BU | Yes |
|
||||||
|
|
||||||
|
**Query Parameters for `/applications` and `/applications/search`**:
|
||||||
|
- `status`: Filter by ApplicationStatus
|
||||||
|
- `businessUnitId`: Filter by business unit ID
|
||||||
|
- `name`: Search by name (partial match)
|
||||||
|
- `page`, `size`, `sort`: Pagination
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/environments` | List all environments | Yes |
|
||||||
|
| GET | `/environments/{id}` | Get environment by ID | Yes |
|
||||||
|
| POST | `/environments` | Create environment | Yes |
|
||||||
|
| PUT | `/environments/{id}` | Update environment | Yes |
|
||||||
|
| DELETE | `/environments/{id}` | Delete environment | Yes |
|
||||||
|
| PATCH | `/environments/{id}/deactivate` | Deactivate environment | Yes |
|
||||||
|
| PATCH | `/environments/{id}/reactivate` | Reactivate environment | Yes |
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `isActive`: Filter by active status (default: true)
|
||||||
|
- `isProduction`: Filter production environments
|
||||||
|
- `environmentGroup`: Filter by group
|
||||||
|
- `page`, `size`, `sort`: Pagination
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/applications/{appId}/versions` | List versions for application | Yes |
|
||||||
|
| GET | `/versions/{id}` | Get version by ID | Yes |
|
||||||
|
| POST | `/applications/{appId}/versions` | Create version | Yes |
|
||||||
|
| PUT | `/versions/{id}` | Update version | Yes |
|
||||||
|
| DELETE | `/versions/{id}` | Delete version | Yes |
|
||||||
|
| GET | `/versions/latest?applicationId={appId}` | Get latest version | Yes |
|
||||||
|
|
||||||
|
**Query Parameters for `/applications/{appId}/versions`**:
|
||||||
|
- `page`, `size`, `sort`: Pagination (default sort: releaseDate,desc)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Endpoints
|
||||||
|
|
||||||
|
### Recording & Retrieval
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| POST | `/deployments` | Record new deployment | Yes |
|
||||||
|
| GET | `/deployments` | List all deployments | Yes |
|
||||||
|
| GET | `/deployments/{id}` | Get deployment by ID | Yes |
|
||||||
|
| GET | `/deployments/search` | Advanced search | Yes |
|
||||||
|
|
||||||
|
### Current State Queries
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/deployments/current` | Get current state (all apps/envs) | Yes |
|
||||||
|
| GET | `/applications/{appId}/deployments` | Deployment history for app | Yes |
|
||||||
|
| GET | `/applications/{appId}/deployments/current` | Current state per env for app | Yes |
|
||||||
|
| GET | `/environments/{envId}/deployments/current` | Current deployments in env | Yes |
|
||||||
|
|
||||||
|
### Statistics & Analytics
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/deployments/stats/summary` | Summary statistics | Yes |
|
||||||
|
| GET | `/deployments/stats/frequency?range={days}` | Deployment frequency | Yes |
|
||||||
|
| GET | `/deployments/stats/by-environment` | Deployments by environment | Yes |
|
||||||
|
| GET | `/deployments/stats/by-application` | Top deployed applications | Yes |
|
||||||
|
| GET | `/deployments/stats/version-distribution` | Version distribution | Yes |
|
||||||
|
| GET | `/deployments/calendar?year={year}&month={month}` | Calendar data | Yes |
|
||||||
|
|
||||||
|
### Export
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/deployments/export?format={csv\|excel}` | Export deployments | Yes |
|
||||||
|
|
||||||
|
**Query Parameters for `/deployments` and `/deployments/search`**:
|
||||||
|
- `applicationId`: Filter by application
|
||||||
|
- `versionId`: Filter by version
|
||||||
|
- `environmentId`: Filter by environment
|
||||||
|
- `dateFrom`: Filter by start date
|
||||||
|
- `dateTo`: Filter by end date
|
||||||
|
- `deployedBy`: Filter by user
|
||||||
|
- `page`, `size`, `sort`: Pagination
|
||||||
|
|
||||||
|
**Query Parameters for `/deployments/current`**:
|
||||||
|
- `applicationId`: Optional filter by application
|
||||||
|
- `environmentId`: Optional filter by environment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contact Endpoints
|
||||||
|
|
||||||
|
### Contact Roles
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/contact-roles` | List all contact roles | Yes |
|
||||||
|
| POST | `/contact-roles` | Create contact role | Yes (Admin) |
|
||||||
|
|
||||||
|
### Persons
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/persons` | List all persons | Yes |
|
||||||
|
| GET | `/persons/{id}` | Get person by ID | Yes |
|
||||||
|
| POST | `/persons` | Create person | Yes |
|
||||||
|
| PUT | `/persons/{id}` | Update person | Yes |
|
||||||
|
| DELETE | `/persons/{id}` | Delete person | Yes |
|
||||||
|
|
||||||
|
**Query Parameters for `/persons`**:
|
||||||
|
- `email`: Search by email
|
||||||
|
- `name`: Search by name (first or last)
|
||||||
|
- `page`, `size`, `sort`: Pagination
|
||||||
|
|
||||||
|
### Contacts
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Auth Required |
|
||||||
|
|--------|----------|-------------|---------------|
|
||||||
|
| GET | `/contacts` | List all contacts | Yes |
|
||||||
|
| GET | `/contacts/{id}` | Get contact with persons | Yes |
|
||||||
|
| POST | `/contacts` | Create contact | Yes |
|
||||||
|
| PUT | `/contacts/{id}` | Update contact | Yes |
|
||||||
|
| DELETE | `/contacts/{id}` | Delete contact | Yes |
|
||||||
|
| POST | `/contacts/{id}/persons/{personId}` | Add person to contact | Yes |
|
||||||
|
| DELETE | `/contacts/{id}/persons/{personId}` | Remove person from contact | Yes |
|
||||||
|
| PATCH | `/contacts/{id}/persons/{personId}/primary` | Set as primary | Yes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Response Formats
|
||||||
|
|
||||||
|
### Success Response (Single Entity)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Entity Name",
|
||||||
|
...
|
||||||
|
"createdAt": "2026-02-07T10:00:00Z",
|
||||||
|
"updatedAt": "2026-02-07T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Success Response (Paginated List)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": [ /* array of entities */ ],
|
||||||
|
"pageable": {
|
||||||
|
"pageNumber": 0,
|
||||||
|
"pageSize": 20,
|
||||||
|
"sort": { "sorted": true, "unsorted": false }
|
||||||
|
},
|
||||||
|
"totalElements": 100,
|
||||||
|
"totalPages": 5,
|
||||||
|
"last": false,
|
||||||
|
"first": true,
|
||||||
|
"size": 20,
|
||||||
|
"number": 0,
|
||||||
|
"numberOfElements": 20,
|
||||||
|
"empty": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 400,
|
||||||
|
"message": "Validation failed",
|
||||||
|
"timestamp": "2026-02-07T10:00:00Z",
|
||||||
|
"path": "/api/applications",
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"field": "name",
|
||||||
|
"message": "Name is required",
|
||||||
|
"rejectedValue": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTTP Status Codes
|
||||||
|
|
||||||
|
| Code | Meaning | When Used |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| 200 | OK | Successful GET, PUT, PATCH |
|
||||||
|
| 201 | Created | Successful POST |
|
||||||
|
| 204 | No Content | Successful DELETE |
|
||||||
|
| 400 | Bad Request | Validation error, business rule violation |
|
||||||
|
| 401 | Unauthorized | Missing or invalid JWT token |
|
||||||
|
| 403 | Forbidden | Valid token but insufficient permissions |
|
||||||
|
| 404 | Not Found | Resource doesn't exist |
|
||||||
|
| 409 | Conflict | Duplicate resource (e.g., unique constraint violation) |
|
||||||
|
| 500 | Internal Server Error | Unexpected server error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication Header
|
||||||
|
|
||||||
|
All authenticated endpoints require:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
GET /api/applications
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CORS Configuration
|
||||||
|
|
||||||
|
Frontend origin allowed: `http://localhost:4200` (development)
|
||||||
|
|
||||||
|
Allowed methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`
|
||||||
|
|
||||||
|
Allowed headers: `*`
|
||||||
|
|
||||||
|
Credentials: `true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limiting (Future)
|
||||||
|
|
||||||
|
Not implemented in MVP, but planned for production:
|
||||||
|
- 100 requests per minute per user
|
||||||
|
- 1000 requests per hour per user
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Versioning
|
||||||
|
|
||||||
|
Current version: `v1` (implicit)
|
||||||
|
|
||||||
|
Future versions will use URL path: `/api/v2/...`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OpenAPI/Swagger Documentation
|
||||||
|
|
||||||
|
Interactive API documentation available at:
|
||||||
|
- **Swagger UI**: `http://localhost:8080/swagger-ui/index.html`
|
||||||
|
- **OpenAPI Spec**: `http://localhost:8080/v3/api-docs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Last Updated**: February 2026
|
||||||
|
**Status**: Complete for MVP
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
# LDPv2 - Complete Data Model (TypeScript)
|
||||||
|
|
||||||
|
This file contains all TypeScript interfaces and enums for the LDPv2 MVP.
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Application lifecycle status
|
||||||
|
export enum ApplicationStatus {
|
||||||
|
IDEA = 'IDEA',
|
||||||
|
IN_DEVELOPMENT = 'IN_DEVELOPMENT',
|
||||||
|
IN_SERVICE = 'IN_SERVICE',
|
||||||
|
MAINTENANCE = 'MAINTENANCE',
|
||||||
|
DECOMMISSIONED = 'DECOMMISSIONED'
|
||||||
|
}
|
||||||
|
|
||||||
|
// External dependency types (Phase 2)
|
||||||
|
export enum ExternalDependencyType {
|
||||||
|
WEB_SERVICE = 'WEB_SERVICE',
|
||||||
|
DATABASE = 'DATABASE',
|
||||||
|
CERTIFICATE = 'CERTIFICATE',
|
||||||
|
NETWORK_FLOW = 'NETWORK_FLOW'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day of week for maintenance windows (Phase 2)
|
||||||
|
export enum DayOfWeek {
|
||||||
|
MONDAY = 'MONDAY',
|
||||||
|
TUESDAY = 'TUESDAY',
|
||||||
|
WEDNESDAY = 'WEDNESDAY',
|
||||||
|
THURSDAY = 'THURSDAY',
|
||||||
|
FRIDAY = 'FRIDAY',
|
||||||
|
SATURDAY = 'SATURDAY',
|
||||||
|
SUNDAY = 'SUNDAY'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Entities
|
||||||
|
|
||||||
|
### Business Unit
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface BusinessUnit {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateBusinessUnitRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateBusinessUnitRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BusinessUnitSummary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Application {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
status: ApplicationStatus;
|
||||||
|
businessUnit: BusinessUnitSummary;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
endOfSupportDate?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateApplicationRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
status: ApplicationStatus;
|
||||||
|
businessUnitId: string;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
endOfSupportDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateApplicationRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
status?: ApplicationStatus;
|
||||||
|
businessUnitId?: string;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
endOfSupportDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationSummary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: ApplicationStatus;
|
||||||
|
businessUnitName: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Environment {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isProduction: boolean;
|
||||||
|
criticalityLevel?: number; // 1-5
|
||||||
|
isActive: boolean;
|
||||||
|
environmentGroup?: string;
|
||||||
|
tags?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEnvironmentRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isProduction: boolean;
|
||||||
|
criticalityLevel?: number;
|
||||||
|
environmentGroup?: string;
|
||||||
|
tags?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateEnvironmentRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
isProduction?: boolean;
|
||||||
|
criticalityLevel?: number;
|
||||||
|
isActive?: boolean;
|
||||||
|
environmentGroup?: string;
|
||||||
|
tags?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvironmentSummary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isProduction: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Version
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Version {
|
||||||
|
id: string;
|
||||||
|
applicationId: string;
|
||||||
|
versionIdentifier: string; // "1.2.3", "2024.Q1"
|
||||||
|
externalReference?: string; // Git tag, JIRA link
|
||||||
|
releaseDate: Date;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateVersionRequest {
|
||||||
|
versionIdentifier: string;
|
||||||
|
externalReference?: string;
|
||||||
|
releaseDate: Date;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateVersionRequest {
|
||||||
|
versionIdentifier?: string;
|
||||||
|
externalReference?: string;
|
||||||
|
releaseDate?: Date;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionSummary {
|
||||||
|
id: string;
|
||||||
|
versionIdentifier: string;
|
||||||
|
releaseDate: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Deployment {
|
||||||
|
id: string;
|
||||||
|
application: ApplicationSummary;
|
||||||
|
version: VersionSummary;
|
||||||
|
environment: EnvironmentSummary;
|
||||||
|
deploymentDate: Date;
|
||||||
|
deployedBy?: string;
|
||||||
|
notes?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordDeploymentRequest {
|
||||||
|
applicationId: string;
|
||||||
|
versionId: string;
|
||||||
|
environmentId: string;
|
||||||
|
deploymentDate: Date;
|
||||||
|
deployedBy?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentDeploymentState {
|
||||||
|
application: ApplicationSummary;
|
||||||
|
environment: EnvironmentSummary;
|
||||||
|
version: VersionSummary;
|
||||||
|
deploymentDate: Date;
|
||||||
|
deployedBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeploymentSearchCriteria {
|
||||||
|
applicationId?: string;
|
||||||
|
applicationName?: string;
|
||||||
|
versionId?: string;
|
||||||
|
environmentIds?: string[];
|
||||||
|
dateFrom?: Date;
|
||||||
|
dateTo?: Date;
|
||||||
|
deployedBy?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Contact Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ContactRole {
|
||||||
|
id: string;
|
||||||
|
roleName: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Person {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePersonRequest {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePersonRequest {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Contact {
|
||||||
|
id: string;
|
||||||
|
contactRole: ContactRole;
|
||||||
|
persons: PersonInContact[];
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonInContact {
|
||||||
|
person: Person;
|
||||||
|
isPrimary: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateContactRequest {
|
||||||
|
contactRoleId: string;
|
||||||
|
personIds: string[];
|
||||||
|
primaryPersonId: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User & Authentication
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
role: string; // "ADMIN", "USER"
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Statistics & Analytics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface DeploymentSummary {
|
||||||
|
totalApplications: number;
|
||||||
|
totalDeployments: number;
|
||||||
|
deploymentsThisWeek: number;
|
||||||
|
deploymentsThisMonth: number;
|
||||||
|
productionDeployments: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeploymentFrequency {
|
||||||
|
date: string; // "2026-02-01"
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvironmentStats {
|
||||||
|
environmentName: string;
|
||||||
|
deploymentCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationStats {
|
||||||
|
applicationName: string;
|
||||||
|
deploymentCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionDistribution {
|
||||||
|
versionIdentifier: string;
|
||||||
|
environmentCount: number;
|
||||||
|
environments: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendarData {
|
||||||
|
date: string; // "2026-02-01"
|
||||||
|
deploymentCount: number;
|
||||||
|
activityLevel: 'low' | 'normal' | 'high' | 'none';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pagination & Sorting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Pageable {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
sort: Sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Sort {
|
||||||
|
sorted: boolean;
|
||||||
|
unsorted: boolean;
|
||||||
|
orders?: SortOrder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortOrder {
|
||||||
|
property: string;
|
||||||
|
direction: 'ASC' | 'DESC';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Page<T> {
|
||||||
|
content: T[];
|
||||||
|
pageable: Pageable;
|
||||||
|
totalElements: number;
|
||||||
|
totalPages: number;
|
||||||
|
last: boolean;
|
||||||
|
first: boolean;
|
||||||
|
size: number;
|
||||||
|
number: number;
|
||||||
|
numberOfElements: number;
|
||||||
|
empty: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ApiError {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
timestamp: Date;
|
||||||
|
path?: string;
|
||||||
|
errors?: FieldError[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldError {
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
rejectedValue?: any;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Entities (Phase 2)
|
||||||
|
|
||||||
|
These are not part of the MVP but are included for completeness:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// External Dependencies (Phase 2)
|
||||||
|
export interface ExternalDependency {
|
||||||
|
id: string;
|
||||||
|
applicationId: string;
|
||||||
|
dependencyType: ExternalDependencyType;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
documentation?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data Usage Agreements (Phase 2)
|
||||||
|
export interface DataUsageAgreement {
|
||||||
|
id: string;
|
||||||
|
dataNature: string;
|
||||||
|
documentationUrl: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SLA (Phase 2)
|
||||||
|
export interface SLA {
|
||||||
|
id: string;
|
||||||
|
availabilityPercentage: number;
|
||||||
|
minAvgResponseTimeSeconds: number;
|
||||||
|
reactionTime: string;
|
||||||
|
diagramJson?: object;
|
||||||
|
dependenciesDescription?: string;
|
||||||
|
responsibilityScope?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Technical Documentation (Phase 2)
|
||||||
|
export interface TechnicalDocumentation {
|
||||||
|
id: string;
|
||||||
|
applicationId: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**File Version**: 1.0
|
||||||
|
**Last Updated**: February 2026
|
||||||
|
**Status**: Complete for MVP
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
# Story 0: Foundation - Walking Skeleton
|
||||||
|
|
||||||
|
## Story Overview
|
||||||
|
|
||||||
|
**As a** development team
|
||||||
|
**I want** to establish the technical foundation and development patterns
|
||||||
|
**So that** all future features can be built consistently and efficiently
|
||||||
|
|
||||||
|
**Story Type**: Technical Foundation
|
||||||
|
**Priority**: Highest
|
||||||
|
**Estimated Effort**: 2-3 weeks
|
||||||
|
**Dependencies**: None (first story)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Value
|
||||||
|
|
||||||
|
This story establishes:
|
||||||
|
- Complete tech stack setup (Spring Boot + Angular + PostgreSQL)
|
||||||
|
- Authentication and authorization framework
|
||||||
|
- One complete CRUD example (Environment entity) as a template
|
||||||
|
- CI/CD pipeline foundation
|
||||||
|
- Testing infrastructure
|
||||||
|
- Development patterns for all future stories
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
✅ Spring Boot project setup with security
|
||||||
|
✅ PostgreSQL database with Liquibase
|
||||||
|
✅ JWT authentication implementation
|
||||||
|
✅ Angular project setup
|
||||||
|
✅ Authentication UI (login page)
|
||||||
|
✅ Environment entity (complete CRUD) as pattern example
|
||||||
|
✅ API documentation (Swagger)
|
||||||
|
✅ Docker setup for local development
|
||||||
|
✅ Basic CI/CD configuration
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
❌ All business entities except Environment
|
||||||
|
❌ Complex authorization rules (just basic role check)
|
||||||
|
❌ OAuth integration (local auth only)
|
||||||
|
❌ Advanced UI features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Backend Tasks
|
||||||
|
|
||||||
|
#### 1. Project Setup
|
||||||
|
- [ ] Create Spring Boot project via Spring Initializr
|
||||||
|
- Spring Boot 3.2.x
|
||||||
|
- Dependencies: Web, Security, JPA, PostgreSQL, Liquibase, Validation
|
||||||
|
- [ ] Configure `pom.xml` with additional dependencies
|
||||||
|
- JJWT for JWT handling
|
||||||
|
- Lombok
|
||||||
|
- SpringDoc OpenAPI
|
||||||
|
- Testcontainers
|
||||||
|
- [ ] Setup project structure (packages: config, domain, repository, service, dto, controller, security, exception)
|
||||||
|
|
||||||
|
#### 2. Database Setup
|
||||||
|
- [ ] Create Liquibase changelog master file
|
||||||
|
- [ ] Migration 001: Create `user` table with columns:
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `username` (VARCHAR, unique, not null)
|
||||||
|
- `password` (VARCHAR, hashed, not null)
|
||||||
|
- `email` (VARCHAR, unique, not null)
|
||||||
|
- `role` (VARCHAR, not null) - Simple role: ADMIN, USER
|
||||||
|
- `created_at`, `updated_at` (TIMESTAMP)
|
||||||
|
- [ ] Migration 002: Create `environment` table with columns:
|
||||||
|
- `id` (UUID, PK)
|
||||||
|
- `name` (VARCHAR, unique, not null)
|
||||||
|
- `description` (TEXT, nullable)
|
||||||
|
- `is_production` (BOOLEAN, default false)
|
||||||
|
- `criticality_level` (INTEGER, nullable)
|
||||||
|
- `created_at`, `updated_at` (TIMESTAMP)
|
||||||
|
- [ ] Add indexes on `is_production` and `name`
|
||||||
|
|
||||||
|
#### 3. JPA Entities
|
||||||
|
- [ ] Create `BaseEntity` abstract class with:
|
||||||
|
- `id` field with UUID generator
|
||||||
|
- `createdAt`, `updatedAt` with JPA auditing
|
||||||
|
- [ ] Create `User` entity extending `BaseEntity`
|
||||||
|
- [ ] Create `Environment` entity extending `BaseEntity`
|
||||||
|
- [ ] Configure JPA auditing in configuration class
|
||||||
|
|
||||||
|
#### 4. Security Implementation
|
||||||
|
- [ ] Create `JwtTokenProvider` class
|
||||||
|
- `generateToken(Authentication)`: Create JWT token
|
||||||
|
- `getUsernameFromToken(String)`: Extract username
|
||||||
|
- `validateToken(String)`: Validate token
|
||||||
|
- [ ] Create `JwtAuthenticationFilter` extends `OncePerRequestFilter`
|
||||||
|
- Extract JWT from Authorization header
|
||||||
|
- Validate token
|
||||||
|
- Set authentication in SecurityContext
|
||||||
|
- [ ] Create `UserDetailsServiceImpl` implements `UserDetailsService`
|
||||||
|
- Load user from database
|
||||||
|
- Map to Spring Security UserDetails
|
||||||
|
- [ ] Create `SecurityConfig`
|
||||||
|
- Configure HTTP security
|
||||||
|
- Disable CSRF (stateless API)
|
||||||
|
- Configure CORS
|
||||||
|
- Set session management to STATELESS
|
||||||
|
- Define public endpoints: `/api/auth/**`
|
||||||
|
- Require authentication for all other endpoints
|
||||||
|
- Add JWT filter before UsernamePasswordAuthenticationFilter
|
||||||
|
- [ ] Create `AuthController` with endpoints:
|
||||||
|
- `POST /api/auth/register`: Register new user
|
||||||
|
- `POST /api/auth/login`: Authenticate and return JWT
|
||||||
|
- `GET /api/auth/me`: Get current user info
|
||||||
|
|
||||||
|
#### 5. Environment CRUD (Pattern Example)
|
||||||
|
- [ ] Create `EnvironmentRepository` extends `JpaRepository<Environment, UUID>`
|
||||||
|
- [ ] Create DTOs:
|
||||||
|
- `CreateEnvironmentRequest` (name, description, isProduction, criticalityLevel)
|
||||||
|
- `UpdateEnvironmentRequest` (same as create, all optional)
|
||||||
|
- `EnvironmentResponse` (all fields + createdAt, updatedAt)
|
||||||
|
- [ ] Create `EnvironmentService` with methods:
|
||||||
|
- `create(CreateEnvironmentRequest)`: Create new environment
|
||||||
|
- `update(UUID, UpdateEnvironmentRequest)`: Update environment
|
||||||
|
- `findById(UUID)`: Get by ID
|
||||||
|
- `findAll(Pageable)`: List all with pagination
|
||||||
|
- `delete(UUID)`: Delete environment
|
||||||
|
- [ ] Create `EnvironmentController` with endpoints:
|
||||||
|
- `GET /api/environments`: List all (with pagination, sorting)
|
||||||
|
- `GET /api/environments/{id}`: Get by ID
|
||||||
|
- `POST /api/environments`: Create new
|
||||||
|
- `PUT /api/environments/{id}`: Update
|
||||||
|
- `DELETE /api/environments/{id}`: Delete
|
||||||
|
- [ ] Add validation annotations on DTOs
|
||||||
|
- [ ] Add Swagger annotations on controller methods
|
||||||
|
|
||||||
|
#### 6. Exception Handling
|
||||||
|
- [ ] Create custom exceptions:
|
||||||
|
- `ResourceNotFoundException`
|
||||||
|
- `BadRequestException`
|
||||||
|
- `UnauthorizedException`
|
||||||
|
- [ ] Create `GlobalExceptionHandler` with `@ControllerAdvice`
|
||||||
|
- Handle all custom exceptions
|
||||||
|
- Return standardized error response (status, message, timestamp)
|
||||||
|
|
||||||
|
#### 7. Configuration
|
||||||
|
- [ ] Configure `application.yml` for database, JPA, Liquibase
|
||||||
|
- [ ] Configure `application-dev.yml` for development
|
||||||
|
- [ ] Configure `application-prod.yml` for production
|
||||||
|
- [ ] Create `OpenApiConfig` for Swagger documentation
|
||||||
|
- [ ] Create `CorsConfig` for frontend integration
|
||||||
|
|
||||||
|
#### 8. Testing
|
||||||
|
- [ ] Unit tests for `EnvironmentService` (with Mockito)
|
||||||
|
- [ ] Integration tests for `EnvironmentController` (with Testcontainers)
|
||||||
|
- [ ] Security tests for JWT authentication flow
|
||||||
|
- [ ] Test coverage > 80%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend Tasks
|
||||||
|
|
||||||
|
#### 1. Project Setup
|
||||||
|
- [ ] Create Angular 18 project: `ng new ldpv2-frontend`
|
||||||
|
- [ ] Add Angular Material or PrimeNG
|
||||||
|
- [ ] Configure TypeScript strict mode
|
||||||
|
- [ ] Setup proxy configuration for API calls
|
||||||
|
- [ ] Configure environments (dev, prod)
|
||||||
|
|
||||||
|
#### 2. Project Structure
|
||||||
|
- [ ] Create folder structure:
|
||||||
|
- `core/` (auth, guards, interceptors, services)
|
||||||
|
- `shared/` (models, components, pipes)
|
||||||
|
- `features/` (feature modules)
|
||||||
|
- [ ] Setup routing module
|
||||||
|
|
||||||
|
#### 3. Authentication Module
|
||||||
|
- [ ] Create models:
|
||||||
|
- `User` interface (id, username, email, role)
|
||||||
|
- `LoginRequest` interface (username, password)
|
||||||
|
- `LoginResponse` interface (token, user)
|
||||||
|
- [ ] Create `AuthService`:
|
||||||
|
- `login(credentials)`: Call login API, store JWT in localStorage
|
||||||
|
- `logout()`: Clear JWT from localStorage
|
||||||
|
- `isAuthenticated()`: Check if JWT exists and is valid
|
||||||
|
- `getCurrentUser()`: Get current user from token
|
||||||
|
- [ ] Create `JwtInterceptor` (implements `HttpInterceptor`):
|
||||||
|
- Add Authorization header with JWT to all requests
|
||||||
|
- [ ] Create `ErrorInterceptor` (implements `HttpInterceptor`):
|
||||||
|
- Handle 401 (redirect to login)
|
||||||
|
- Handle other errors (show notification)
|
||||||
|
- [ ] Create `AuthGuard` (implements `CanActivate`):
|
||||||
|
- Protect routes requiring authentication
|
||||||
|
- Redirect to login if not authenticated
|
||||||
|
- [ ] Create `LoginComponent`:
|
||||||
|
- Reactive form with username and password
|
||||||
|
- Call AuthService.login()
|
||||||
|
- Redirect to dashboard on success
|
||||||
|
- Show error message on failure
|
||||||
|
|
||||||
|
#### 4. Environment Management (Pattern Example)
|
||||||
|
- [ ] Create `Environment` model (TypeScript interface)
|
||||||
|
- [ ] Create `EnvironmentService`:
|
||||||
|
- `getEnvironments(page, size, sort)`: GET /api/environments
|
||||||
|
- `getEnvironment(id)`: GET /api/environments/{id}
|
||||||
|
- `createEnvironment(data)`: POST /api/environments
|
||||||
|
- `updateEnvironment(id, data)`: PUT /api/environments/{id}
|
||||||
|
- `deleteEnvironment(id)`: DELETE /api/environments/{id}
|
||||||
|
- [ ] Create `EnvironmentListComponent`:
|
||||||
|
- Display environments in table/list
|
||||||
|
- Pagination controls
|
||||||
|
- Sort by name, criticality, etc.
|
||||||
|
- Actions: View, Edit, Delete
|
||||||
|
- Button: Create New
|
||||||
|
- [ ] Create `EnvironmentDetailComponent`:
|
||||||
|
- Display full environment details
|
||||||
|
- Edit and Delete buttons
|
||||||
|
- [ ] Create `EnvironmentFormComponent`:
|
||||||
|
- Reactive form for create/edit
|
||||||
|
- Validation (required fields, unique name)
|
||||||
|
- Submit to API
|
||||||
|
- Success/error notifications
|
||||||
|
- [ ] Add routing:
|
||||||
|
- `/environments`: EnvironmentListComponent
|
||||||
|
- `/environments/new`: EnvironmentFormComponent (create mode)
|
||||||
|
- `/environments/:id`: EnvironmentDetailComponent
|
||||||
|
- `/environments/:id/edit`: EnvironmentFormComponent (edit mode)
|
||||||
|
|
||||||
|
#### 5. Shared Components
|
||||||
|
- [ ] Create `HeaderComponent` (navigation, user menu, logout)
|
||||||
|
- [ ] Create `LoadingSpinnerComponent`
|
||||||
|
- [ ] Create `ConfirmDialogComponent` (for delete confirmations)
|
||||||
|
- [ ] Create `NotificationService` (toast notifications)
|
||||||
|
|
||||||
|
#### 6. Styling
|
||||||
|
- [ ] Setup global styles
|
||||||
|
- [ ] Create responsive layout
|
||||||
|
- [ ] Style login page
|
||||||
|
- [ ] Style environment management pages
|
||||||
|
|
||||||
|
#### 7. Testing
|
||||||
|
- [ ] Unit tests for `AuthService`
|
||||||
|
- [ ] Unit tests for `EnvironmentService`
|
||||||
|
- [ ] Component tests for `LoginComponent`
|
||||||
|
- [ ] Component tests for `EnvironmentListComponent`
|
||||||
|
- [ ] E2E test: Login flow
|
||||||
|
- [ ] E2E test: Create environment flow
|
||||||
|
- [ ] Test coverage > 70%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DevOps Tasks
|
||||||
|
|
||||||
|
#### 1. Docker Setup
|
||||||
|
- [ ] Create `Dockerfile` for backend
|
||||||
|
- [ ] Create `Dockerfile` for frontend
|
||||||
|
- [ ] Create `docker-compose.yml` with services:
|
||||||
|
- PostgreSQL
|
||||||
|
- Backend
|
||||||
|
- Frontend (optional for dev)
|
||||||
|
- [ ] Document how to run with Docker
|
||||||
|
|
||||||
|
#### 2. CI/CD Pipeline
|
||||||
|
- [ ] Create `.gitlab-ci.yml` or `Jenkinsfile` or GitHub Actions workflow
|
||||||
|
- [ ] Pipeline stages:
|
||||||
|
- Build (backend and frontend)
|
||||||
|
- Test (run all tests)
|
||||||
|
- Code quality check (SonarQube optional)
|
||||||
|
- Build Docker images
|
||||||
|
- Push to registry (optional)
|
||||||
|
- [ ] Configure environment variables
|
||||||
|
|
||||||
|
#### 3. Documentation
|
||||||
|
- [ ] README.md with:
|
||||||
|
- Project description
|
||||||
|
- Prerequisites
|
||||||
|
- How to run locally
|
||||||
|
- How to run with Docker
|
||||||
|
- How to run tests
|
||||||
|
- [ ] API documentation accessible via Swagger UI
|
||||||
|
- [ ] Contribution guidelines (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- [ ] User can register a new account
|
||||||
|
- [ ] User can login with username and password
|
||||||
|
- [ ] Upon successful login, JWT token is returned
|
||||||
|
- [ ] JWT token is stored in browser (localStorage)
|
||||||
|
- [ ] JWT token is automatically included in API requests
|
||||||
|
- [ ] User is redirected to login page when token is invalid/expired
|
||||||
|
- [ ] User can logout (token is cleared)
|
||||||
|
|
||||||
|
### Environment CRUD
|
||||||
|
- [ ] User can view a list of all environments
|
||||||
|
- [ ] List is paginated (20 items per page)
|
||||||
|
- [ ] User can sort environments by name or criticality
|
||||||
|
- [ ] User can view details of a specific environment
|
||||||
|
- [ ] User can create a new environment with:
|
||||||
|
- Name (required, unique)
|
||||||
|
- Description (optional)
|
||||||
|
- Production flag (default: false)
|
||||||
|
- Criticality level (optional, 1-5)
|
||||||
|
- [ ] Form validation works correctly (required fields, unique name)
|
||||||
|
- [ ] User can edit an existing environment
|
||||||
|
- [ ] User can delete an environment (with confirmation dialog)
|
||||||
|
- [ ] Success and error notifications are displayed appropriately
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
- [ ] Swagger UI is accessible at `/swagger-ui/index.html`
|
||||||
|
- [ ] All endpoints are documented with request/response schemas
|
||||||
|
- [ ] Authentication endpoints are documented
|
||||||
|
- [ ] Environment endpoints are documented
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Backend unit tests pass (>80% coverage)
|
||||||
|
- [ ] Backend integration tests pass
|
||||||
|
- [ ] Frontend unit tests pass (>70% coverage)
|
||||||
|
- [ ] E2E tests pass for critical flows
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- [ ] Application runs successfully with `docker-compose up`
|
||||||
|
- [ ] CI/CD pipeline builds and tests successfully
|
||||||
|
- [ ] README documentation is complete and accurate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: User Registration and Login
|
||||||
|
1. Navigate to registration page
|
||||||
|
2. Fill in username, email, password
|
||||||
|
3. Submit registration form
|
||||||
|
4. Verify success message
|
||||||
|
5. Navigate to login page
|
||||||
|
6. Enter username and password
|
||||||
|
7. Submit login form
|
||||||
|
8. Verify redirect to dashboard
|
||||||
|
9. Verify JWT token in localStorage
|
||||||
|
10. Verify Authorization header in subsequent requests
|
||||||
|
|
||||||
|
### Scenario 2: Create Environment
|
||||||
|
1. Login as authenticated user
|
||||||
|
2. Navigate to environments list
|
||||||
|
3. Click "Create New Environment"
|
||||||
|
4. Fill in form:
|
||||||
|
- Name: "PROD-EU"
|
||||||
|
- Description: "Production environment for Europe"
|
||||||
|
- Production: true
|
||||||
|
- Criticality: 5
|
||||||
|
5. Submit form
|
||||||
|
6. Verify success notification
|
||||||
|
7. Verify redirect to environment list
|
||||||
|
8. Verify new environment appears in list
|
||||||
|
|
||||||
|
### Scenario 3: Edit Environment
|
||||||
|
1. Navigate to environment list
|
||||||
|
2. Click "Edit" on an environment
|
||||||
|
3. Modify description
|
||||||
|
4. Submit form
|
||||||
|
5. Verify success notification
|
||||||
|
6. Verify changes are reflected
|
||||||
|
|
||||||
|
### Scenario 4: Delete Environment
|
||||||
|
1. Navigate to environment list
|
||||||
|
2. Click "Delete" on an environment
|
||||||
|
3. Confirm deletion in dialog
|
||||||
|
4. Verify success notification
|
||||||
|
5. Verify environment is removed from list
|
||||||
|
|
||||||
|
### Scenario 5: Unauthorized Access
|
||||||
|
1. Logout or clear JWT token
|
||||||
|
2. Attempt to access protected route (e.g., `/environments`)
|
||||||
|
3. Verify redirect to login page
|
||||||
|
4. Login again
|
||||||
|
5. Verify redirect to originally requested page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
- [ ] All backend tasks completed
|
||||||
|
- [ ] All frontend tasks completed
|
||||||
|
- [ ] All DevOps tasks completed
|
||||||
|
- [ ] All acceptance criteria met
|
||||||
|
- [ ] All tests passing (unit, integration, E2E)
|
||||||
|
- [ ] Code reviewed and approved
|
||||||
|
- [ ] API documented in Swagger
|
||||||
|
- [ ] User documentation updated
|
||||||
|
- [ ] Demo conducted successfully
|
||||||
|
- [ ] Code merged to main branch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Debt & Future Improvements
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
- Simple role-based authorization (ADMIN, USER) - will need environment-specific roles in future
|
||||||
|
- Local authentication only - OAuth integration planned for later
|
||||||
|
- Basic error handling - can be enhanced with more specific error codes
|
||||||
|
- No password reset functionality - to be added later
|
||||||
|
|
||||||
|
### Future Enhancements
|
||||||
|
- Implement refresh token mechanism
|
||||||
|
- Add password strength validation
|
||||||
|
- Implement account activation via email
|
||||||
|
- Add "Remember me" functionality
|
||||||
|
- Implement rate limiting on auth endpoints
|
||||||
|
- Add audit logging for security events
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Blockers
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
None - this is the first story
|
||||||
|
|
||||||
|
### Potential Blockers
|
||||||
|
- PostgreSQL setup issues → Use Docker Compose
|
||||||
|
- CORS configuration issues → Properly configure allowed origins
|
||||||
|
- JWT secret management → Use environment variables
|
||||||
|
- Network connectivity between services → Use Docker network
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources & References
|
||||||
|
|
||||||
|
### Spring Boot Documentation
|
||||||
|
- [Spring Security](https://docs.spring.io/spring-security/reference/index.html)
|
||||||
|
- [Spring Data JPA](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/)
|
||||||
|
- [Liquibase with Spring Boot](https://docs.liquibase.com/tools-integrations/springboot/springboot.html)
|
||||||
|
|
||||||
|
### Angular Documentation
|
||||||
|
- [Angular Authentication](https://angular.io/guide/security)
|
||||||
|
- [HTTP Interceptors](https://angular.io/guide/http-interceptor-use-cases)
|
||||||
|
- [Reactive Forms](https://angular.io/guide/reactive-forms)
|
||||||
|
|
||||||
|
### JWT
|
||||||
|
- [JJWT Library](https://github.com/jwtk/jjwt)
|
||||||
|
- [JWT Best Practices](https://tools.ietf.org/html/rfc8725)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Story Status**: Ready for Development
|
||||||
|
**Story Created**: February 2026
|
||||||
|
**Estimated Completion**: 2-3 weeks from start
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
# Story 1: Business Unit Management
|
||||||
|
|
||||||
|
## Story Overview
|
||||||
|
|
||||||
|
**As a** system administrator
|
||||||
|
**I want** to manage business units in the system
|
||||||
|
**So that** I can organize applications under their respective organizational units
|
||||||
|
|
||||||
|
**Story Type**: Feature
|
||||||
|
**Priority**: High
|
||||||
|
**Estimated Effort**: 3-5 days
|
||||||
|
**Dependencies**: Story 0 (Foundation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Value
|
||||||
|
|
||||||
|
Business units represent the organizational structure within which applications are managed. This story enables:
|
||||||
|
- Clear organizational hierarchy
|
||||||
|
- Application ownership mapping
|
||||||
|
- Contact management at the business unit level
|
||||||
|
- Foundation for multi-tenant capabilities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
✅ Business unit CRUD operations
|
||||||
|
✅ Business unit listing with search/filter
|
||||||
|
✅ Basic contact association (simplified for MVP)
|
||||||
|
✅ Validation (unique names, required fields)
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
❌ Complex hierarchical business units (parent/child relationships)
|
||||||
|
❌ Full contact management (covered in Story 3)
|
||||||
|
❌ Business unit-specific permissions
|
||||||
|
❌ Bulk import of business units
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Backend Tasks
|
||||||
|
|
||||||
|
#### 1. Database Migration
|
||||||
|
- [ ] Create Liquibase migration file: `003-create-business-unit-tables.xml`
|
||||||
|
- [ ] Create `business_unit` table:
|
||||||
|
```sql
|
||||||
|
- id (UUID, PK)
|
||||||
|
- name (VARCHAR(255), unique, not null)
|
||||||
|
- description (TEXT, nullable)
|
||||||
|
- created_at (TIMESTAMP, not null)
|
||||||
|
- updated_at (TIMESTAMP, not null)
|
||||||
|
```
|
||||||
|
- [ ] Add index on `name` for search performance
|
||||||
|
- [ ] Insert sample data (3-5 business units) for testing
|
||||||
|
|
||||||
|
#### 2. JPA Entity
|
||||||
|
- [ ] Create `BusinessUnit` entity extending `BaseEntity`
|
||||||
|
```java
|
||||||
|
@Entity
|
||||||
|
@Table(name = "business_unit")
|
||||||
|
public class BusinessUnit extends BaseEntity {
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
// Getters, setters, constructors
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Repository Layer
|
||||||
|
- [ ] Create `BusinessUnitRepository` interface
|
||||||
|
```java
|
||||||
|
public interface BusinessUnitRepository extends JpaRepository<BusinessUnit, UUID> {
|
||||||
|
Optional<BusinessUnit> findByName(String name);
|
||||||
|
boolean existsByName(String name);
|
||||||
|
Page<BusinessUnit> findByNameContainingIgnoreCase(String name, Pageable pageable);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. DTOs
|
||||||
|
- [ ] Create `CreateBusinessUnitRequest`:
|
||||||
|
- `name` (required, max 255 chars)
|
||||||
|
- `description` (optional)
|
||||||
|
- [ ] Create `UpdateBusinessUnitRequest`:
|
||||||
|
- `name` (optional, max 255 chars)
|
||||||
|
- `description` (optional)
|
||||||
|
- [ ] Create `BusinessUnitResponse`:
|
||||||
|
- `id`, `name`, `description`, `createdAt`, `updatedAt`
|
||||||
|
- [ ] Create `BusinessUnitSummaryResponse` (for lists):
|
||||||
|
- `id`, `name`
|
||||||
|
|
||||||
|
#### 5. Service Layer
|
||||||
|
- [ ] Create `BusinessUnitService` with methods:
|
||||||
|
```java
|
||||||
|
BusinessUnitResponse create(CreateBusinessUnitRequest request);
|
||||||
|
BusinessUnitResponse update(UUID id, UpdateBusinessUnitRequest request);
|
||||||
|
BusinessUnitResponse findById(UUID id);
|
||||||
|
Page<BusinessUnitResponse> findAll(Pageable pageable);
|
||||||
|
Page<BusinessUnitResponse> search(String query, Pageable pageable);
|
||||||
|
void delete(UUID id);
|
||||||
|
```
|
||||||
|
- [ ] Implement business logic:
|
||||||
|
- Check uniqueness of name on create/update
|
||||||
|
- Throw `ResourceNotFoundException` if not found
|
||||||
|
- Throw `BadRequestException` for duplicate names
|
||||||
|
- [ ] Add validation for required fields
|
||||||
|
|
||||||
|
#### 6. Controller Layer
|
||||||
|
- [ ] Create `BusinessUnitController` with endpoints:
|
||||||
|
```java
|
||||||
|
GET /api/business-units - List all (paginated)
|
||||||
|
GET /api/business-units/search?q={query} - Search by name
|
||||||
|
GET /api/business-units/{id} - Get by ID
|
||||||
|
POST /api/business-units - Create new
|
||||||
|
PUT /api/business-units/{id} - Update
|
||||||
|
DELETE /api/business-units/{id} - Delete
|
||||||
|
```
|
||||||
|
- [ ] Add validation annotations (`@Valid`, `@NotNull`, etc.)
|
||||||
|
- [ ] Add Swagger/OpenAPI annotations
|
||||||
|
- [ ] Add security: require authentication for all endpoints
|
||||||
|
|
||||||
|
#### 7. Testing
|
||||||
|
- [ ] Unit tests for `BusinessUnitService`:
|
||||||
|
- Test create with valid data
|
||||||
|
- Test create with duplicate name (should fail)
|
||||||
|
- Test update existing business unit
|
||||||
|
- Test delete business unit
|
||||||
|
- Test search functionality
|
||||||
|
- [ ] Integration tests for `BusinessUnitController`:
|
||||||
|
- Test full CRUD flow
|
||||||
|
- Test pagination and sorting
|
||||||
|
- Test search with various queries
|
||||||
|
- Test error cases (404, 400)
|
||||||
|
- [ ] Test coverage > 80%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend Tasks
|
||||||
|
|
||||||
|
#### 1. Models
|
||||||
|
- [ ] Create `business-unit.model.ts`:
|
||||||
|
```typescript
|
||||||
|
export interface BusinessUnit {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BusinessUnitSummary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateBusinessUnitRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateBusinessUnitRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Service
|
||||||
|
- [ ] Create `business-unit.service.ts`:
|
||||||
|
```typescript
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class BusinessUnitService {
|
||||||
|
getBusinessUnits(page, size, sort): Observable<Page<BusinessUnit>>
|
||||||
|
searchBusinessUnits(query, page, size): Observable<Page<BusinessUnit>>
|
||||||
|
getBusinessUnit(id): Observable<BusinessUnit>
|
||||||
|
createBusinessUnit(data): Observable<BusinessUnit>
|
||||||
|
updateBusinessUnit(id, data): Observable<BusinessUnit>
|
||||||
|
deleteBusinessUnit(id): Observable<void>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Components
|
||||||
|
|
||||||
|
##### BusinessUnitListComponent
|
||||||
|
- [ ] Create component with template and styles
|
||||||
|
- [ ] Features:
|
||||||
|
- Table/list view of business units
|
||||||
|
- Columns: Name, Description, Actions
|
||||||
|
- Pagination controls (page size: 20)
|
||||||
|
- Sort by name
|
||||||
|
- Search bar (filter by name)
|
||||||
|
- "Create New" button
|
||||||
|
- Actions per row: View, Edit, Delete
|
||||||
|
- [ ] Implement component logic:
|
||||||
|
- Load business units on init
|
||||||
|
- Handle pagination events
|
||||||
|
- Handle sort events
|
||||||
|
- Handle search with debounce (300ms)
|
||||||
|
- Navigate to detail/form pages
|
||||||
|
- Handle delete with confirmation
|
||||||
|
|
||||||
|
##### BusinessUnitDetailComponent
|
||||||
|
- [ ] Create component with template and styles
|
||||||
|
- [ ] Features:
|
||||||
|
- Display all business unit details
|
||||||
|
- "Edit" button
|
||||||
|
- "Delete" button
|
||||||
|
- "Back to List" button
|
||||||
|
- Show created/updated timestamps
|
||||||
|
- [ ] Load business unit by ID from route params
|
||||||
|
|
||||||
|
##### BusinessUnitFormComponent
|
||||||
|
- [ ] Create component with template and styles
|
||||||
|
- [ ] Reactive form with fields:
|
||||||
|
- Name (required, max 255 chars)
|
||||||
|
- Description (optional, textarea)
|
||||||
|
- [ ] Form validation:
|
||||||
|
- Name required
|
||||||
|
- Max length validation
|
||||||
|
- Show validation errors
|
||||||
|
- [ ] Support both create and edit modes (based on route)
|
||||||
|
- [ ] Handle form submission:
|
||||||
|
- Call appropriate service method
|
||||||
|
- Show loading indicator during save
|
||||||
|
- Navigate to list on success
|
||||||
|
- Show error notification on failure
|
||||||
|
- [ ] "Cancel" button (navigate back)
|
||||||
|
|
||||||
|
#### 4. Routing
|
||||||
|
- [ ] Add routes to `app.routes.ts`:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
path: 'business-units',
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
children: [
|
||||||
|
{ path: '', component: BusinessUnitListComponent },
|
||||||
|
{ path: 'new', component: BusinessUnitFormComponent },
|
||||||
|
{ path: ':id', component: BusinessUnitDetailComponent },
|
||||||
|
{ path: ':id/edit', component: BusinessUnitFormComponent }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Navigation
|
||||||
|
- [ ] Add "Business Units" link to header/sidebar navigation
|
||||||
|
|
||||||
|
#### 6. Testing
|
||||||
|
- [ ] Unit tests for `BusinessUnitService`
|
||||||
|
- [ ] Component tests for `BusinessUnitListComponent`:
|
||||||
|
- Test rendering of business units
|
||||||
|
- Test pagination
|
||||||
|
- Test search
|
||||||
|
- Test navigation to create/edit/detail
|
||||||
|
- [ ] Component tests for `BusinessUnitFormComponent`:
|
||||||
|
- Test form validation
|
||||||
|
- Test create mode
|
||||||
|
- Test edit mode
|
||||||
|
- Test form submission
|
||||||
|
- [ ] E2E test: Full CRUD flow
|
||||||
|
- [ ] Test coverage > 70%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] Business unit can be created with unique name
|
||||||
|
- [ ] Duplicate business unit names are rejected with 400 error
|
||||||
|
- [ ] Business unit can be updated
|
||||||
|
- [ ] Business unit can be deleted
|
||||||
|
- [ ] Business units can be listed with pagination
|
||||||
|
- [ ] Business units can be searched by name (case-insensitive)
|
||||||
|
- [ ] All endpoints are authenticated
|
||||||
|
- [ ] All endpoints return proper HTTP status codes
|
||||||
|
- [ ] API is documented in Swagger
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] User can view list of all business units
|
||||||
|
- [ ] List supports pagination (20 items per page)
|
||||||
|
- [ ] User can search business units by name
|
||||||
|
- [ ] Search has debounce (doesn't query on every keystroke)
|
||||||
|
- [ ] User can create new business unit
|
||||||
|
- [ ] Form validates required fields
|
||||||
|
- [ ] User cannot create duplicate business unit names
|
||||||
|
- [ ] User can view business unit details
|
||||||
|
- [ ] User can edit existing business unit
|
||||||
|
- [ ] User can delete business unit (with confirmation)
|
||||||
|
- [ ] Success/error notifications are displayed
|
||||||
|
- [ ] Navigation is intuitive and works correctly
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Backend unit tests pass (>80% coverage)
|
||||||
|
- [ ] Backend integration tests pass
|
||||||
|
- [ ] Frontend unit tests pass (>70% coverage)
|
||||||
|
- [ ] E2E tests pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Create Business Unit
|
||||||
|
1. Navigate to business units list
|
||||||
|
2. Click "Create New Business Unit"
|
||||||
|
3. Fill in form:
|
||||||
|
- Name: "Digital Services"
|
||||||
|
- Description: "Digital transformation initiatives"
|
||||||
|
4. Click "Save"
|
||||||
|
5. Verify success notification
|
||||||
|
6. Verify redirect to business unit list
|
||||||
|
7. Verify new business unit appears in list
|
||||||
|
|
||||||
|
### Scenario 2: Duplicate Name Prevention
|
||||||
|
1. Navigate to create business unit form
|
||||||
|
2. Enter name of existing business unit
|
||||||
|
3. Click "Save"
|
||||||
|
4. Verify error message: "Business unit with this name already exists"
|
||||||
|
5. Verify business unit is not created
|
||||||
|
|
||||||
|
### Scenario 3: Edit Business Unit
|
||||||
|
1. Navigate to business units list
|
||||||
|
2. Click "Edit" on a business unit
|
||||||
|
3. Modify description
|
||||||
|
4. Click "Save"
|
||||||
|
5. Verify success notification
|
||||||
|
6. Verify changes are reflected in detail view
|
||||||
|
|
||||||
|
### Scenario 4: Delete Business Unit
|
||||||
|
1. Navigate to business units list
|
||||||
|
2. Click "Delete" on a business unit
|
||||||
|
3. Confirm deletion in dialog
|
||||||
|
4. Verify success notification
|
||||||
|
5. Verify business unit is removed from list
|
||||||
|
|
||||||
|
### Scenario 5: Search Business Units
|
||||||
|
1. Navigate to business units list
|
||||||
|
2. Enter search query: "digital"
|
||||||
|
3. Wait for debounce (300ms)
|
||||||
|
4. Verify filtered results contain only matching business units
|
||||||
|
5. Clear search
|
||||||
|
6. Verify all business units are shown again
|
||||||
|
|
||||||
|
### Scenario 6: Pagination
|
||||||
|
1. Create 25+ business units (if not exists)
|
||||||
|
2. Navigate to business units list
|
||||||
|
3. Verify only 20 items shown on page 1
|
||||||
|
4. Click "Next page"
|
||||||
|
5. Verify remaining items shown on page 2
|
||||||
|
6. Click "Previous page"
|
||||||
|
7. Verify back to page 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
- [ ] All backend tasks completed
|
||||||
|
- [ ] All frontend tasks completed
|
||||||
|
- [ ] All acceptance criteria met
|
||||||
|
- [ ] All tests passing (unit, integration, E2E)
|
||||||
|
- [ ] Code reviewed and approved
|
||||||
|
- [ ] API documented in Swagger
|
||||||
|
- [ ] User can perform all CRUD operations via UI
|
||||||
|
- [ ] Demo conducted successfully
|
||||||
|
- [ ] Code merged to main branch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Debt & Future Improvements
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
- No hierarchical business units (flat structure only)
|
||||||
|
- No business unit deactivation (only hard delete)
|
||||||
|
- No audit trail for changes
|
||||||
|
- No bulk operations
|
||||||
|
|
||||||
|
### Future Enhancements
|
||||||
|
- Add parent-child relationships between business units
|
||||||
|
- Implement soft delete with deactivation flag
|
||||||
|
- Add audit logging for business unit changes
|
||||||
|
- Add bulk import from CSV/Excel
|
||||||
|
- Add business unit-specific settings
|
||||||
|
- Implement business unit archiving
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Blockers
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Story 0 (Foundation) must be complete
|
||||||
|
|
||||||
|
### Potential Blockers
|
||||||
|
- None identified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Specification
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
#### GET /api/business-units
|
||||||
|
**Description**: List all business units
|
||||||
|
**Query Parameters**:
|
||||||
|
- `page` (default: 0)
|
||||||
|
- `size` (default: 20)
|
||||||
|
- `sort` (default: name,asc)
|
||||||
|
|
||||||
|
**Response**: `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Digital Services",
|
||||||
|
"description": "Digital transformation initiatives",
|
||||||
|
"createdAt": "2026-02-01T10:00:00Z",
|
||||||
|
"updatedAt": "2026-02-01T10:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pageable": {
|
||||||
|
"pageNumber": 0,
|
||||||
|
"pageSize": 20,
|
||||||
|
"sort": { "sorted": true, "unsorted": false }
|
||||||
|
},
|
||||||
|
"totalElements": 5,
|
||||||
|
"totalPages": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/business-units/search
|
||||||
|
**Description**: Search business units by name
|
||||||
|
**Query Parameters**:
|
||||||
|
- `q` (search query)
|
||||||
|
- `page`, `size`, `sort`
|
||||||
|
|
||||||
|
**Response**: Same as GET /api/business-units
|
||||||
|
|
||||||
|
#### GET /api/business-units/{id}
|
||||||
|
**Description**: Get business unit by ID
|
||||||
|
**Response**: `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Digital Services",
|
||||||
|
"description": "Digital transformation initiatives",
|
||||||
|
"createdAt": "2026-02-01T10:00:00Z",
|
||||||
|
"updatedAt": "2026-02-01T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Error**: `404 Not Found` if business unit doesn't exist
|
||||||
|
|
||||||
|
#### POST /api/business-units
|
||||||
|
**Description**: Create new business unit
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Digital Services",
|
||||||
|
"description": "Digital transformation initiatives"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Response**: `201 Created`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Digital Services",
|
||||||
|
"description": "Digital transformation initiatives",
|
||||||
|
"createdAt": "2026-02-01T10:00:00Z",
|
||||||
|
"updatedAt": "2026-02-01T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Errors**:
|
||||||
|
- `400 Bad Request` if name is duplicate or validation fails
|
||||||
|
- `401 Unauthorized` if not authenticated
|
||||||
|
|
||||||
|
#### PUT /api/business-units/{id}
|
||||||
|
**Description**: Update business unit
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Digital Services",
|
||||||
|
"description": "Updated description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Response**: `200 OK` (same format as POST)
|
||||||
|
**Errors**:
|
||||||
|
- `404 Not Found` if business unit doesn't exist
|
||||||
|
- `400 Bad Request` if validation fails
|
||||||
|
|
||||||
|
#### DELETE /api/business-units/{id}
|
||||||
|
**Description**: Delete business unit
|
||||||
|
**Response**: `204 No Content`
|
||||||
|
**Error**: `404 Not Found` if business unit doesn't exist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Story Status**: Ready for Development
|
||||||
|
**Story Created**: February 2026
|
||||||
|
**Estimated Completion**: 3-5 days from start
|
||||||
@@ -0,0 +1,541 @@
|
|||||||
|
# Story 2: Application Management
|
||||||
|
|
||||||
|
## Story Overview
|
||||||
|
|
||||||
|
**As a** application manager
|
||||||
|
**I want** to manage applications in the system
|
||||||
|
**So that** I can track all applications and their lifecycle status
|
||||||
|
|
||||||
|
**Story Type**: Feature (Core Domain)
|
||||||
|
**Priority**: Highest
|
||||||
|
**Estimated Effort**: 5-7 days
|
||||||
|
**Dependencies**: Story 1 (Business Units)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Value
|
||||||
|
|
||||||
|
Applications are the central entity in LDPv2. This story enables:
|
||||||
|
- Complete application inventory
|
||||||
|
- Lifecycle status tracking (IDEA → IN_DEVELOPMENT → IN_SERVICE → MAINTENANCE → DECOMMISSIONED)
|
||||||
|
- Business unit ownership
|
||||||
|
- Foundation for deployment tracking (Story 6)
|
||||||
|
- End-of-life and support tracking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
✅ Application CRUD operations
|
||||||
|
✅ Lifecycle status management
|
||||||
|
✅ Business unit association
|
||||||
|
✅ End-of-life and end-of-support date tracking
|
||||||
|
✅ Application search and filtering
|
||||||
|
✅ Status-based filtering
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
❌ Version management (Story 5)
|
||||||
|
❌ Deployment tracking (Story 6)
|
||||||
|
❌ Contact/stakeholder management (Story 3)
|
||||||
|
❌ SLA management (Phase 2)
|
||||||
|
❌ Technical documentation (Phase 2)
|
||||||
|
❌ External dependencies (Phase 2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Backend Tasks
|
||||||
|
|
||||||
|
#### 1. Database Migration
|
||||||
|
- [ ] Create Liquibase migration: `004-create-application-tables.xml`
|
||||||
|
- [ ] Create `application` table:
|
||||||
|
```sql
|
||||||
|
- id (UUID, PK)
|
||||||
|
- name (VARCHAR(255), not null)
|
||||||
|
- description (TEXT, nullable)
|
||||||
|
- status (VARCHAR(50), not null) -- IDEA, IN_DEVELOPMENT, IN_SERVICE, MAINTENANCE, DECOMMISSIONED
|
||||||
|
- business_unit_id (UUID, FK to business_unit, not null)
|
||||||
|
- end_of_life_date (DATE, nullable)
|
||||||
|
- end_of_support_date (DATE, nullable)
|
||||||
|
- created_at (TIMESTAMP, not null)
|
||||||
|
- updated_at (TIMESTAMP, not null)
|
||||||
|
```
|
||||||
|
- [ ] Add indexes:
|
||||||
|
- `idx_application_status` on status
|
||||||
|
- `idx_application_business_unit` on business_unit_id
|
||||||
|
- `idx_application_name` on name (for search)
|
||||||
|
- [ ] Add foreign key constraint: `business_unit_id` → `business_unit(id)`
|
||||||
|
- [ ] Insert sample data (5-10 applications with various statuses)
|
||||||
|
|
||||||
|
#### 2. Enum
|
||||||
|
- [ ] Create `ApplicationStatus` enum:
|
||||||
|
```java
|
||||||
|
public enum ApplicationStatus {
|
||||||
|
IDEA("Idea"),
|
||||||
|
IN_DEVELOPMENT("In Development"),
|
||||||
|
IN_SERVICE("In Service"),
|
||||||
|
MAINTENANCE("Maintenance"),
|
||||||
|
DECOMMISSIONED("Decommissioned");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
// Constructor, getter
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. JPA Entity
|
||||||
|
- [ ] Create `Application` entity extending `BaseEntity`:
|
||||||
|
```java
|
||||||
|
@Entity
|
||||||
|
@Table(name = "application")
|
||||||
|
public class Application extends BaseEntity {
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false, length = 50)
|
||||||
|
private ApplicationStatus status;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "business_unit_id", nullable = false)
|
||||||
|
private BusinessUnit businessUnit;
|
||||||
|
|
||||||
|
@Column(name = "end_of_life_date")
|
||||||
|
private LocalDate endOfLifeDate;
|
||||||
|
|
||||||
|
@Column(name = "end_of_support_date")
|
||||||
|
private LocalDate endOfSupportDate;
|
||||||
|
|
||||||
|
// Getters, setters, constructors
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Repository Layer
|
||||||
|
- [ ] Create `ApplicationRepository`:
|
||||||
|
```java
|
||||||
|
public interface ApplicationRepository extends JpaRepository<Application, UUID> {
|
||||||
|
Page<Application> findByStatus(ApplicationStatus status, Pageable pageable);
|
||||||
|
Page<Application> findByBusinessUnitId(UUID businessUnitId, Pageable pageable);
|
||||||
|
Page<Application> findByNameContainingIgnoreCase(String name, Pageable pageable);
|
||||||
|
Page<Application> findByStatusAndBusinessUnitId(ApplicationStatus status, UUID businessUnitId, Pageable pageable);
|
||||||
|
|
||||||
|
@Query("SELECT a FROM Application a WHERE " +
|
||||||
|
"(:status IS NULL OR a.status = :status) AND " +
|
||||||
|
"(:businessUnitId IS NULL OR a.businessUnit.id = :businessUnitId) AND " +
|
||||||
|
"(:name IS NULL OR LOWER(a.name) LIKE LOWER(CONCAT('%', :name, '%')))")
|
||||||
|
Page<Application> search(
|
||||||
|
@Param("status") ApplicationStatus status,
|
||||||
|
@Param("businessUnitId") UUID businessUnitId,
|
||||||
|
@Param("name") String name,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. DTOs
|
||||||
|
- [ ] Create `CreateApplicationRequest`:
|
||||||
|
- `name` (required, max 255 chars)
|
||||||
|
- `description` (optional)
|
||||||
|
- `status` (required, default: IDEA)
|
||||||
|
- `businessUnitId` (required, UUID)
|
||||||
|
- `endOfLifeDate` (optional, LocalDate)
|
||||||
|
- `endOfSupportDate` (optional, LocalDate)
|
||||||
|
- [ ] Create `UpdateApplicationRequest`:
|
||||||
|
- All fields optional (partial update support)
|
||||||
|
- [ ] Create `ApplicationResponse`:
|
||||||
|
- `id`, `name`, `description`, `status`, `businessUnit` (summary), `endOfLifeDate`, `endOfSupportDate`, `createdAt`, `updatedAt`
|
||||||
|
- [ ] Create `ApplicationSummaryResponse` (for lists):
|
||||||
|
- `id`, `name`, `status`, `businessUnitName`
|
||||||
|
- [ ] Create `BusinessUnitSummaryDto`:
|
||||||
|
- `id`, `name`
|
||||||
|
|
||||||
|
#### 6. Service Layer
|
||||||
|
- [ ] Create `ApplicationService` with methods:
|
||||||
|
```java
|
||||||
|
ApplicationResponse create(CreateApplicationRequest request);
|
||||||
|
ApplicationResponse update(UUID id, UpdateApplicationRequest request);
|
||||||
|
ApplicationResponse findById(UUID id);
|
||||||
|
Page<ApplicationResponse> findAll(Pageable pageable);
|
||||||
|
Page<ApplicationResponse> search(ApplicationStatus status, UUID businessUnitId, String name, Pageable pageable);
|
||||||
|
Page<ApplicationResponse> findByStatus(ApplicationStatus status, Pageable pageable);
|
||||||
|
Page<ApplicationResponse> findByBusinessUnit(UUID businessUnitId, Pageable pageable);
|
||||||
|
void delete(UUID id);
|
||||||
|
ApplicationResponse updateStatus(UUID id, ApplicationStatus newStatus);
|
||||||
|
```
|
||||||
|
- [ ] Implement business logic:
|
||||||
|
- Validate business unit exists on create/update
|
||||||
|
- Validate end-of-support date is before end-of-life date
|
||||||
|
- Throw exceptions for invalid states
|
||||||
|
- Map entities to DTOs (consider using MapStruct or ModelMapper)
|
||||||
|
|
||||||
|
#### 7. Controller Layer
|
||||||
|
- [ ] Create `ApplicationController` with endpoints:
|
||||||
|
```java
|
||||||
|
GET /api/applications - List all (with filters)
|
||||||
|
GET /api/applications/search - Advanced search
|
||||||
|
GET /api/applications/{id} - Get by ID
|
||||||
|
POST /api/applications - Create new
|
||||||
|
PUT /api/applications/{id} - Update
|
||||||
|
PATCH /api/applications/{id}/status - Update status only
|
||||||
|
DELETE /api/applications/{id} - Delete
|
||||||
|
GET /api/applications/by-status/{status} - Filter by status
|
||||||
|
GET /api/applications/by-business-unit/{businessUnitId} - Filter by BU
|
||||||
|
```
|
||||||
|
- [ ] Add query parameters for filtering:
|
||||||
|
- `status`: Filter by status
|
||||||
|
- `businessUnitId`: Filter by business unit
|
||||||
|
- `name`: Search by name
|
||||||
|
- `page`, `size`, `sort`: Pagination
|
||||||
|
- [ ] Add validation annotations
|
||||||
|
- [ ] Add Swagger/OpenAPI annotations
|
||||||
|
- [ ] Add security: require authentication
|
||||||
|
|
||||||
|
#### 8. Testing
|
||||||
|
- [ ] Unit tests for `ApplicationService`:
|
||||||
|
- Test create application
|
||||||
|
- Test create with invalid business unit (should fail)
|
||||||
|
- Test update application
|
||||||
|
- Test update status
|
||||||
|
- Test search with various filters
|
||||||
|
- Test validation (end-of-support before end-of-life)
|
||||||
|
- [ ] Integration tests for `ApplicationController`:
|
||||||
|
- Test full CRUD flow
|
||||||
|
- Test filtering by status
|
||||||
|
- Test filtering by business unit
|
||||||
|
- Test search functionality
|
||||||
|
- Test pagination and sorting
|
||||||
|
- Test error cases (404, 400)
|
||||||
|
- [ ] Test coverage > 80%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend Tasks
|
||||||
|
|
||||||
|
#### 1. Models
|
||||||
|
- [ ] Create `application.model.ts`:
|
||||||
|
```typescript
|
||||||
|
export enum ApplicationStatus {
|
||||||
|
IDEA = 'IDEA',
|
||||||
|
IN_DEVELOPMENT = 'IN_DEVELOPMENT',
|
||||||
|
IN_SERVICE = 'IN_SERVICE',
|
||||||
|
MAINTENANCE = 'MAINTENANCE',
|
||||||
|
DECOMMISSIONED = 'DECOMMISSIONED'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Application {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
status: ApplicationStatus;
|
||||||
|
businessUnit: { id: string; name: string };
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
endOfSupportDate?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateApplicationRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
status: ApplicationStatus;
|
||||||
|
businessUnitId: string;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
endOfSupportDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateApplicationRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
status?: ApplicationStatus;
|
||||||
|
businessUnitId?: string;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
endOfSupportDate?: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Service
|
||||||
|
- [ ] Create `application.service.ts`:
|
||||||
|
```typescript
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ApplicationService {
|
||||||
|
getApplications(filters?, page?, size?, sort?): Observable<Page<Application>>
|
||||||
|
searchApplications(criteria): Observable<Page<Application>>
|
||||||
|
getApplication(id): Observable<Application>
|
||||||
|
createApplication(data): Observable<Application>
|
||||||
|
updateApplication(id, data): Observable<Application>
|
||||||
|
updateStatus(id, status): Observable<Application>
|
||||||
|
deleteApplication(id): Observable<void>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Components
|
||||||
|
|
||||||
|
##### ApplicationListComponent
|
||||||
|
- [ ] Create component with template and styles
|
||||||
|
- [ ] Features:
|
||||||
|
- Table view with columns: Name, Status, Business Unit, End of Life, Actions
|
||||||
|
- Status badge with color coding:
|
||||||
|
- IDEA: blue
|
||||||
|
- IN_DEVELOPMENT: yellow
|
||||||
|
- IN_SERVICE: green
|
||||||
|
- MAINTENANCE: orange
|
||||||
|
- DECOMMISSIONED: gray
|
||||||
|
- Filters:
|
||||||
|
- Status dropdown (All, IDEA, IN_DEVELOPMENT, IN_SERVICE, MAINTENANCE, DECOMMISSIONED)
|
||||||
|
- Business unit dropdown (All + list of BUs)
|
||||||
|
- Search by name
|
||||||
|
- Pagination controls (20 items per page)
|
||||||
|
- Sort by name, status, created date
|
||||||
|
- "Create New Application" button
|
||||||
|
- Actions per row: View, Edit, Delete, Change Status
|
||||||
|
- [ ] Implement logic:
|
||||||
|
- Load applications on init
|
||||||
|
- Apply filters reactively
|
||||||
|
- Handle pagination and sorting
|
||||||
|
- Handle search with debounce
|
||||||
|
- Quick status change dropdown
|
||||||
|
- Navigate to detail/form pages
|
||||||
|
- Handle delete with confirmation
|
||||||
|
|
||||||
|
##### ApplicationDetailComponent
|
||||||
|
- [ ] Create component with template and styles
|
||||||
|
- [ ] Features:
|
||||||
|
- Display all application details
|
||||||
|
- Status badge (colored)
|
||||||
|
- Business unit link (navigate to BU detail)
|
||||||
|
- Show end-of-life and end-of-support dates (highlight if approaching)
|
||||||
|
- "Edit" button
|
||||||
|
- "Change Status" dropdown
|
||||||
|
- "Delete" button
|
||||||
|
- "Back to List" button
|
||||||
|
- Tabs (for future: Versions, Deployments, Contacts)
|
||||||
|
- [ ] Load application by ID from route params
|
||||||
|
- [ ] Reload on status change
|
||||||
|
|
||||||
|
##### ApplicationFormComponent
|
||||||
|
- [ ] Create component with template and styles
|
||||||
|
- [ ] Reactive form with fields:
|
||||||
|
- Name (required, max 255 chars)
|
||||||
|
- Description (optional, textarea)
|
||||||
|
- Status (required, dropdown)
|
||||||
|
- Business Unit (required, dropdown - load from API)
|
||||||
|
- End of Life Date (optional, date picker)
|
||||||
|
- End of Support Date (optional, date picker)
|
||||||
|
- [ ] Form validation:
|
||||||
|
- Name required
|
||||||
|
- Status required
|
||||||
|
- Business unit required
|
||||||
|
- End of support date must be before end of life date
|
||||||
|
- Show validation errors
|
||||||
|
- [ ] Support both create and edit modes
|
||||||
|
- [ ] Handle form submission
|
||||||
|
- [ ] "Cancel" button
|
||||||
|
|
||||||
|
#### 4. Shared Components
|
||||||
|
- [ ] Create `StatusBadgeComponent`:
|
||||||
|
- Input: status (ApplicationStatus)
|
||||||
|
- Display colored badge based on status
|
||||||
|
- Reusable across application views
|
||||||
|
|
||||||
|
#### 5. Routing
|
||||||
|
- [ ] Add routes:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
path: 'applications',
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
children: [
|
||||||
|
{ path: '', component: ApplicationListComponent },
|
||||||
|
{ path: 'new', component: ApplicationFormComponent },
|
||||||
|
{ path: ':id', component: ApplicationDetailComponent },
|
||||||
|
{ path: ':id/edit', component: ApplicationFormComponent }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Navigation
|
||||||
|
- [ ] Add "Applications" link to header/sidebar (prominent position)
|
||||||
|
|
||||||
|
#### 7. Testing
|
||||||
|
- [ ] Unit tests for `ApplicationService`
|
||||||
|
- [ ] Component tests for `ApplicationListComponent`:
|
||||||
|
- Test rendering
|
||||||
|
- Test filters (status, business unit, search)
|
||||||
|
- Test pagination
|
||||||
|
- Test status change
|
||||||
|
- [ ] Component tests for `ApplicationFormComponent`:
|
||||||
|
- Test form validation
|
||||||
|
- Test date validation (end-of-support before end-of-life)
|
||||||
|
- Test create and edit modes
|
||||||
|
- [ ] E2E tests:
|
||||||
|
- Create application flow
|
||||||
|
- Edit application flow
|
||||||
|
- Change status flow
|
||||||
|
- Filter and search flow
|
||||||
|
- [ ] Test coverage > 70%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] Application can be created with all required fields
|
||||||
|
- [ ] Application creation fails if business unit doesn't exist
|
||||||
|
- [ ] Application can be updated (partial updates supported)
|
||||||
|
- [ ] Application status can be updated independently
|
||||||
|
- [ ] Application can be deleted
|
||||||
|
- [ ] Applications can be listed with pagination
|
||||||
|
- [ ] Applications can be filtered by status
|
||||||
|
- [ ] Applications can be filtered by business unit
|
||||||
|
- [ ] Applications can be searched by name
|
||||||
|
- [ ] Advanced search combines multiple filters
|
||||||
|
- [ ] End-of-support date validation (must be before end-of-life)
|
||||||
|
- [ ] All endpoints authenticated
|
||||||
|
- [ ] API documented in Swagger
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] User can view list of all applications
|
||||||
|
- [ ] List shows application name, status, business unit
|
||||||
|
- [ ] Status is displayed with colored badge
|
||||||
|
- [ ] User can filter applications by status
|
||||||
|
- [ ] User can filter applications by business unit
|
||||||
|
- [ ] User can search applications by name
|
||||||
|
- [ ] Filters work together (combined)
|
||||||
|
- [ ] List supports pagination and sorting
|
||||||
|
- [ ] User can create new application
|
||||||
|
- [ ] Form validates all required fields
|
||||||
|
- [ ] Form validates date logic (end-of-support before end-of-life)
|
||||||
|
- [ ] User can view application details
|
||||||
|
- [ ] User can edit existing application
|
||||||
|
- [ ] User can change application status from list or detail view
|
||||||
|
- [ ] User can delete application (with confirmation)
|
||||||
|
- [ ] Approaching end-of-life dates are highlighted
|
||||||
|
- [ ] Success/error notifications displayed
|
||||||
|
- [ ] Navigation is intuitive
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Backend tests pass (>80% coverage)
|
||||||
|
- [ ] Frontend tests pass (>70% coverage)
|
||||||
|
- [ ] E2E tests pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Create Application
|
||||||
|
1. Navigate to applications list
|
||||||
|
2. Click "Create New Application"
|
||||||
|
3. Fill in form:
|
||||||
|
- Name: "Customer Portal"
|
||||||
|
- Description: "External customer-facing portal"
|
||||||
|
- Status: IN_DEVELOPMENT
|
||||||
|
- Business Unit: Select "Digital Services"
|
||||||
|
- End of Support: 2028-12-31
|
||||||
|
- End of Life: 2030-12-31
|
||||||
|
4. Click "Save"
|
||||||
|
5. Verify success notification
|
||||||
|
6. Verify redirect to application list
|
||||||
|
7. Verify new application appears with IN_DEVELOPMENT badge (yellow)
|
||||||
|
|
||||||
|
### Scenario 2: Filter Applications
|
||||||
|
1. Navigate to applications list
|
||||||
|
2. Select status filter: "IN_SERVICE"
|
||||||
|
3. Verify only IN_SERVICE applications shown
|
||||||
|
4. Select business unit filter: "Digital Services"
|
||||||
|
5. Verify only IN_SERVICE applications from Digital Services shown
|
||||||
|
6. Clear filters
|
||||||
|
7. Verify all applications shown
|
||||||
|
|
||||||
|
### Scenario 3: Search Applications
|
||||||
|
1. Navigate to applications list
|
||||||
|
2. Enter search query: "portal"
|
||||||
|
3. Wait for debounce
|
||||||
|
4. Verify results contain only matching applications
|
||||||
|
5. Clear search
|
||||||
|
6. Verify all applications shown
|
||||||
|
|
||||||
|
### Scenario 4: Change Application Status
|
||||||
|
1. Navigate to applications list
|
||||||
|
2. Click "Change Status" dropdown on an application
|
||||||
|
3. Select new status: "IN_SERVICE"
|
||||||
|
4. Verify success notification
|
||||||
|
5. Verify status badge updated to green
|
||||||
|
6. Refresh page
|
||||||
|
7. Verify status persisted
|
||||||
|
|
||||||
|
### Scenario 5: Edit Application
|
||||||
|
1. Navigate to application detail page
|
||||||
|
2. Click "Edit"
|
||||||
|
3. Modify description and end-of-life date
|
||||||
|
4. Click "Save"
|
||||||
|
5. Verify success notification
|
||||||
|
6. Verify changes reflected in detail view
|
||||||
|
|
||||||
|
### Scenario 6: Date Validation
|
||||||
|
1. Navigate to create/edit application form
|
||||||
|
2. Set End of Support: 2030-12-31
|
||||||
|
3. Set End of Life: 2028-12-31 (before end-of-support)
|
||||||
|
4. Attempt to save
|
||||||
|
5. Verify validation error: "End of support must be before end of life"
|
||||||
|
6. Correct dates
|
||||||
|
7. Verify form can be saved
|
||||||
|
|
||||||
|
### Scenario 7: Delete Application
|
||||||
|
1. Navigate to application detail page
|
||||||
|
2. Click "Delete"
|
||||||
|
3. Confirm deletion
|
||||||
|
4. Verify success notification
|
||||||
|
5. Verify redirect to application list
|
||||||
|
6. Verify application removed from list
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
- [ ] All backend tasks completed
|
||||||
|
- [ ] All frontend tasks completed
|
||||||
|
- [ ] All acceptance criteria met
|
||||||
|
- [ ] All tests passing (unit, integration, E2E)
|
||||||
|
- [ ] Code reviewed and approved
|
||||||
|
- [ ] API documented in Swagger
|
||||||
|
- [ ] User can perform all operations via UI
|
||||||
|
- [ ] Demo conducted successfully
|
||||||
|
- [ ] Code merged to main branch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Debt & Future Improvements
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
- No application archiving (only hard delete)
|
||||||
|
- No status change audit trail
|
||||||
|
- No automated end-of-life notifications
|
||||||
|
- No application tags/categories
|
||||||
|
|
||||||
|
### Future Enhancements
|
||||||
|
- Implement soft delete with archiving
|
||||||
|
- Add status change history/audit log
|
||||||
|
- Add automated alerts for approaching end-of-life
|
||||||
|
- Add tagging system for categorization
|
||||||
|
- Add application relationships (depends on, integrates with)
|
||||||
|
- Add bulk status updates
|
||||||
|
- Add export to Excel/CSV
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Blockers
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Story 1 (Business Units) must be complete
|
||||||
|
|
||||||
|
### Potential Blockers
|
||||||
|
- None identified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Story Status**: Ready for Development
|
||||||
|
**Story Created**: February 2026
|
||||||
|
**Estimated Completion**: 5-7 days from start
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Story 3: Contact Management
|
||||||
|
|
||||||
|
## Story Overview
|
||||||
|
|
||||||
|
**As a** system user
|
||||||
|
**I want** to manage contacts and their roles
|
||||||
|
**So that** I can associate stakeholders with applications and business units
|
||||||
|
|
||||||
|
**Story Type**: Feature
|
||||||
|
**Priority**: Medium
|
||||||
|
**Estimated Effort**: 3-5 days
|
||||||
|
**Dependencies**: Story 0 (Foundation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Value
|
||||||
|
|
||||||
|
Contacts represent stakeholders (Product Owners, Developers, etc.) and their roles. This enables:
|
||||||
|
- Tracking who is responsible for what
|
||||||
|
- Contact information management
|
||||||
|
- Foundation for application stakeholder mapping (used in later stories)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
✅ Contact role management (predefined roles)
|
||||||
|
✅ Person management (individuals)
|
||||||
|
✅ Contact-Person associations
|
||||||
|
✅ Basic CRUD for all entities
|
||||||
|
|
||||||
|
### Out of Scope (Story 2, 6)
|
||||||
|
❌ Application-Contact associations (added when Story 2 needs it)
|
||||||
|
❌ Business Unit-Contact associations
|
||||||
|
❌ SLA-Contact associations (Phase 2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Contact Roles (predefined)
|
||||||
|
CREATE TABLE contact_role (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
role_name VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Persons (individuals)
|
||||||
|
CREATE TABLE person (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
first_name VARCHAR(100) NOT NULL,
|
||||||
|
last_name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
phone VARCHAR(50),
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Contacts (functional roles)
|
||||||
|
CREATE TABLE contact (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
contact_role_id UUID NOT NULL REFERENCES contact_role(id),
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Contact-Person junction (many-to-many)
|
||||||
|
CREATE TABLE contact_person (
|
||||||
|
contact_id UUID NOT NULL REFERENCES contact(id) ON DELETE CASCADE,
|
||||||
|
person_id UUID NOT NULL REFERENCES person(id) ON DELETE CASCADE,
|
||||||
|
is_primary BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
PRIMARY KEY (contact_id, person_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_person_email ON person(email);
|
||||||
|
CREATE INDEX idx_contact_role ON contact(contact_role_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Endpoints
|
||||||
|
|
||||||
|
### Contact Roles
|
||||||
|
- `GET /api/contact-roles` - List all roles
|
||||||
|
- `POST /api/contact-roles` - Create role (admin only)
|
||||||
|
|
||||||
|
### Persons
|
||||||
|
- `GET /api/persons` - List all persons
|
||||||
|
- `GET /api/persons/{id}` - Get person details
|
||||||
|
- `POST /api/persons` - Create person
|
||||||
|
- `PUT /api/persons/{id}` - Update person
|
||||||
|
- `DELETE /api/persons/{id}` - Delete person
|
||||||
|
|
||||||
|
### Contacts
|
||||||
|
- `GET /api/contacts` - List all contacts
|
||||||
|
- `GET /api/contacts/{id}` - Get contact with persons
|
||||||
|
- `POST /api/contacts` - Create contact (with person IDs)
|
||||||
|
- `PUT /api/contacts/{id}` - Update contact
|
||||||
|
- `POST /api/contacts/{id}/persons/{personId}` - Add person to contact
|
||||||
|
- `DELETE /api/contacts/{id}/persons/{personId}` - Remove person from contact
|
||||||
|
- `PATCH /api/contacts/{id}/persons/{personId}/primary` - Set as primary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Components
|
||||||
|
|
||||||
|
### PersonListComponent
|
||||||
|
- Table: First Name, Last Name, Email, Phone, Actions
|
||||||
|
- CRUD operations
|
||||||
|
|
||||||
|
### ContactRoleListComponent
|
||||||
|
- Simple list of roles (mostly read-only for regular users)
|
||||||
|
|
||||||
|
### ContactFormComponent
|
||||||
|
- Select contact role (dropdown)
|
||||||
|
- Multi-select persons (with primary designation)
|
||||||
|
- Add/remove persons dynamically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Contact roles can be created and listed
|
||||||
|
- [ ] Persons can be created with unique email
|
||||||
|
- [ ] Contacts can be created with role and persons
|
||||||
|
- [ ] One person per contact can be marked as primary
|
||||||
|
- [ ] Person can be associated with multiple contacts
|
||||||
|
- [ ] All CRUD operations work via UI
|
||||||
|
- [ ] Email uniqueness enforced
|
||||||
|
- [ ] Tests pass (>80% backend, >70% frontend)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
1. **Create Person**: Add new person with unique email
|
||||||
|
2. **Duplicate Email**: Attempt to create person with existing email (should fail)
|
||||||
|
3. **Create Contact**: Create contact with role and multiple persons
|
||||||
|
4. **Set Primary**: Mark one person as primary contact
|
||||||
|
5. **Remove Person**: Remove person from contact
|
||||||
|
6. **Delete Person**: Delete person (should remove from all contacts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Story Status**: Ready for Development
|
||||||
|
**Estimated Completion**: 3-5 days
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Story 4: Environment Management (Enhancement)
|
||||||
|
|
||||||
|
## Story Overview
|
||||||
|
|
||||||
|
**As a** system administrator
|
||||||
|
**I want** to enhance environment management
|
||||||
|
**So that** I can better organize and categorize deployment targets
|
||||||
|
|
||||||
|
**Story Type**: Enhancement
|
||||||
|
**Priority**: Medium
|
||||||
|
**Estimated Effort**: 2-3 days
|
||||||
|
**Dependencies**: Story 0 (Foundation - basic Environment CRUD exists)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Value
|
||||||
|
|
||||||
|
Story 0 created basic Environment CRUD. This story enhances it with:
|
||||||
|
- Better organization and categorization
|
||||||
|
- Production environment safety controls
|
||||||
|
- Criticality-based filtering and alerts
|
||||||
|
- Foundation for environment-specific permissions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope (Enhancements to Story 0)
|
||||||
|
✅ Add environment categorization/tags
|
||||||
|
✅ Add environment deactivation (soft delete)
|
||||||
|
✅ Add production environment warnings
|
||||||
|
✅ Add environment groups (e.g., "Production EU", "Development")
|
||||||
|
✅ Enhanced search and filtering
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
❌ Environment-specific user permissions (future)
|
||||||
|
❌ Environment health monitoring (future)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Migration
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Add new columns to existing environment table
|
||||||
|
ALTER TABLE environment
|
||||||
|
ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN environment_group VARCHAR(100),
|
||||||
|
ADD COLUMN tags VARCHAR(255);
|
||||||
|
|
||||||
|
-- Index for filtering
|
||||||
|
CREATE INDEX idx_environment_active ON environment(is_active);
|
||||||
|
CREATE INDEX idx_environment_group ON environment(environment_group);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Enhancements
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] Add `isActive` field to Environment entity (soft delete)
|
||||||
|
- [ ] Add `environmentGroup` field (e.g., "Production", "Non-Production")
|
||||||
|
- [ ] Add `tags` field (comma-separated or JSON array)
|
||||||
|
- [ ] Update `EnvironmentRepository` with new queries:
|
||||||
|
- `findByIsActive(boolean)`
|
||||||
|
- `findByEnvironmentGroup(String)`
|
||||||
|
- `findByIsProductionAndIsActive(boolean, boolean)`
|
||||||
|
- [ ] Add deactivate/reactivate methods to service
|
||||||
|
- [ ] Update DTOs with new fields
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] Add "Active/Inactive" filter toggle
|
||||||
|
- [ ] Add "Group" filter dropdown
|
||||||
|
- [ ] Show warning dialog when creating/editing production environments
|
||||||
|
- [ ] Show "Deactivate" instead of "Delete" (with reactivate option)
|
||||||
|
- [ ] Add visual indicators for inactive environments (grayed out)
|
||||||
|
- [ ] Add environment group badges
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Environments can be deactivated (soft delete)
|
||||||
|
- [ ] Inactive environments are hidden by default
|
||||||
|
- [ ] User can view inactive environments via filter
|
||||||
|
- [ ] Inactive environments can be reactivated
|
||||||
|
- [ ] Production environments show warning before modifications
|
||||||
|
- [ ] Environments can be organized into groups
|
||||||
|
- [ ] Environments can be tagged
|
||||||
|
- [ ] Search includes tags and groups
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
1. **Deactivate Environment**: Deactivate an environment, verify it's hidden from default list
|
||||||
|
2. **Reactivate Environment**: Reactivate an environment, verify it appears again
|
||||||
|
3. **Production Warning**: Attempt to edit production environment, verify warning shown
|
||||||
|
4. **Group Filtering**: Filter by environment group, verify correct results
|
||||||
|
5. **Tag Search**: Search by tag, verify matching environments shown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Story Status**: Ready for Development
|
||||||
|
**Estimated Completion**: 2-3 days
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# Story 5: Version Management
|
||||||
|
|
||||||
|
## Story Overview
|
||||||
|
|
||||||
|
**As a** application manager
|
||||||
|
**I want** to track application versions
|
||||||
|
**So that** I can manage releases and their lifecycle
|
||||||
|
|
||||||
|
**Story Type**: Feature (Core Domain)
|
||||||
|
**Priority**: High
|
||||||
|
**Estimated Effort**: 3-4 days
|
||||||
|
**Dependencies**: Story 2 (Applications)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Value
|
||||||
|
|
||||||
|
Versions represent application releases. This enables:
|
||||||
|
- Tracking what versions exist for each application
|
||||||
|
- Version lifecycle management (release date, end-of-life)
|
||||||
|
- Foundation for deployment tracking (Story 6)
|
||||||
|
- Link to external references (Git tags, JIRA releases)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
✅ Version CRUD operations
|
||||||
|
✅ Version-Application association
|
||||||
|
✅ Version lifecycle (release date, end-of-life)
|
||||||
|
✅ External reference tracking (Git, JIRA, etc.)
|
||||||
|
✅ Version listing per application
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
❌ Deployment tracking (Story 6)
|
||||||
|
❌ Version comparison/diff
|
||||||
|
❌ Automated version detection from CI/CD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE version (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
application_id UUID NOT NULL REFERENCES application(id) ON DELETE CASCADE,
|
||||||
|
version_identifier VARCHAR(100) NOT NULL, -- "1.2.3", "2024.Q1"
|
||||||
|
external_reference VARCHAR(500), -- Git tag, JIRA link, etc.
|
||||||
|
release_date DATE NOT NULL,
|
||||||
|
end_of_life_date DATE,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
UNIQUE(application_id, version_identifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_version_application ON version(application_id);
|
||||||
|
CREATE INDEX idx_version_release_date ON version(release_date);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Endpoints
|
||||||
|
|
||||||
|
- `GET /api/applications/{appId}/versions` - List versions for application
|
||||||
|
- `GET /api/versions/{id}` - Get version details
|
||||||
|
- `POST /api/applications/{appId}/versions` - Create version
|
||||||
|
- `PUT /api/versions/{id}` - Update version
|
||||||
|
- `DELETE /api/versions/{id}` - Delete version
|
||||||
|
- `GET /api/versions/latest?applicationId={appId}` - Get latest version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DTOs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Version {
|
||||||
|
id: string;
|
||||||
|
applicationId: string;
|
||||||
|
versionIdentifier: string; // "1.2.3"
|
||||||
|
externalReference?: string; // "https://github.com/org/repo/releases/tag/v1.2.3"
|
||||||
|
releaseDate: Date;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateVersionRequest {
|
||||||
|
versionIdentifier: string;
|
||||||
|
externalReference?: string;
|
||||||
|
releaseDate: Date;
|
||||||
|
endOfLifeDate?: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Components
|
||||||
|
|
||||||
|
### VersionListComponent (sub-component of ApplicationDetailComponent)
|
||||||
|
- Displayed as tab in Application Detail view
|
||||||
|
- Table: Version, Release Date, End of Life, External Reference, Actions
|
||||||
|
- "Add New Version" button
|
||||||
|
- Sort by release date (newest first)
|
||||||
|
- Actions: Edit, Delete
|
||||||
|
|
||||||
|
### VersionFormComponent
|
||||||
|
- Modal or inline form
|
||||||
|
- Fields:
|
||||||
|
- Version Identifier (required, must be unique per application)
|
||||||
|
- External Reference (optional, URL)
|
||||||
|
- Release Date (required, date picker)
|
||||||
|
- End of Life Date (optional, must be after release date)
|
||||||
|
- Validation:
|
||||||
|
- Version identifier unique within application
|
||||||
|
- Dates valid (EOL after release)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Rules
|
||||||
|
|
||||||
|
1. **Unique Version**: Version identifier must be unique within an application
|
||||||
|
2. **Date Logic**: End-of-life date (if set) must be after release date
|
||||||
|
3. **Latest Version**: System tracks the version with the most recent release date as "latest"
|
||||||
|
4. **Deletion**: Can only delete version if not deployed anywhere (checked in Story 6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Version can be created for an application
|
||||||
|
- [ ] Version identifier must be unique within application
|
||||||
|
- [ ] Duplicate version identifiers are rejected
|
||||||
|
- [ ] Version release date is required
|
||||||
|
- [ ] End-of-life date validation (must be after release)
|
||||||
|
- [ ] Versions listed per application
|
||||||
|
- [ ] Versions sorted by release date (newest first)
|
||||||
|
- [ ] Latest version is identifiable
|
||||||
|
- [ ] Version can be edited
|
||||||
|
- [ ] Version can be deleted
|
||||||
|
- [ ] External reference links are clickable
|
||||||
|
- [ ] Tests pass (>80% backend, >70% frontend)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
1. **Create Version**: Add version "1.0.0" to an application
|
||||||
|
2. **Duplicate Version**: Attempt to create "1.0.0" again (should fail)
|
||||||
|
3. **Multiple Versions**: Create versions 1.0.0, 1.1.0, 2.0.0, verify sorting
|
||||||
|
4. **Latest Version**: Verify version with most recent release date marked as latest
|
||||||
|
5. **Edit Version**: Update end-of-life date
|
||||||
|
6. **Delete Version**: Delete version (if not deployed)
|
||||||
|
7. **Date Validation**: Set EOL before release date (should fail)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Application Detail
|
||||||
|
|
||||||
|
The Application Detail page (from Story 2) will be enhanced with a "Versions" tab:
|
||||||
|
|
||||||
|
```
|
||||||
|
Application Detail
|
||||||
|
├── Overview (existing)
|
||||||
|
├── Versions (NEW)
|
||||||
|
│ ├── Version List
|
||||||
|
│ └── Add Version Button
|
||||||
|
├── Deployments (Story 6)
|
||||||
|
└── Contacts (Story 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Story Status**: Ready for Development
|
||||||
|
**Estimated Completion**: 3-4 days
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
# Story 6: Deployment Tracking
|
||||||
|
|
||||||
|
## Story Overview
|
||||||
|
|
||||||
|
**As a** application manager
|
||||||
|
**I want** to record and track deployments
|
||||||
|
**So that** I know which version is deployed in each environment
|
||||||
|
|
||||||
|
**Story Type**: Feature (Core Domain)
|
||||||
|
**Priority**: Highest
|
||||||
|
**Estimated Effort**: 5-7 days
|
||||||
|
**Dependencies**: Story 2 (Applications), Story 4 (Environments), Story 5 (Versions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Value
|
||||||
|
|
||||||
|
Deployments are the core tracking mechanism. This enables:
|
||||||
|
- Record which application version is in which environment
|
||||||
|
- Complete deployment history (audit trail)
|
||||||
|
- Current state visibility (what's in production right now)
|
||||||
|
- Foundation for compliance and change tracking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
✅ Deployment recording (application + version + environment)
|
||||||
|
✅ Deployment history (immutable records)
|
||||||
|
✅ Current deployment state query
|
||||||
|
✅ Deployment metadata (who, when, notes)
|
||||||
|
✅ Basic deployment listing and filtering
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
❌ Automated deployment from CI/CD
|
||||||
|
❌ Deployment approval workflows
|
||||||
|
❌ Rollback functionality
|
||||||
|
❌ Deployment health monitoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE deployment (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
application_id UUID NOT NULL REFERENCES application(id),
|
||||||
|
version_id UUID NOT NULL REFERENCES version(id),
|
||||||
|
environment_id UUID NOT NULL REFERENCES environment(id),
|
||||||
|
deployment_date TIMESTAMP NOT NULL,
|
||||||
|
deployed_by VARCHAR(255), -- Username or system identifier
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT fk_deployment_application FOREIGN KEY (application_id) REFERENCES application(id),
|
||||||
|
CONSTRAINT fk_deployment_version FOREIGN KEY (version_id) REFERENCES version(id),
|
||||||
|
CONSTRAINT fk_deployment_environment FOREIGN KEY (environment_id) REFERENCES environment(id),
|
||||||
|
CONSTRAINT check_version_belongs_to_app CHECK (
|
||||||
|
EXISTS (SELECT 1 FROM version WHERE id = version_id AND application_id = deployment.application_id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for common queries
|
||||||
|
CREATE INDEX idx_deployment_application ON deployment(application_id);
|
||||||
|
CREATE INDEX idx_deployment_environment ON deployment(environment_id);
|
||||||
|
CREATE INDEX idx_deployment_date ON deployment(deployment_date DESC);
|
||||||
|
CREATE INDEX idx_deployment_app_env ON deployment(application_id, environment_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Business Logic
|
||||||
|
|
||||||
|
### Current Deployment State
|
||||||
|
The "current" deployment for an application in an environment is defined as:
|
||||||
|
**The deployment with the most recent `deployment_date` for that (application, environment) combination**
|
||||||
|
|
||||||
|
Query:
|
||||||
|
```sql
|
||||||
|
SELECT DISTINCT ON (application_id, environment_id) *
|
||||||
|
FROM deployment
|
||||||
|
WHERE application_id = ? AND environment_id = ?
|
||||||
|
ORDER BY application_id, environment_id, deployment_date DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Immutability
|
||||||
|
Deployments are **immutable** once created. No updates or deletions allowed (audit requirement). Only soft-delete for admin cleanup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Endpoints
|
||||||
|
|
||||||
|
### Recording Deployments
|
||||||
|
- `POST /api/deployments` - Record new deployment
|
||||||
|
- Request: `{ applicationId, versionId, environmentId, deploymentDate, deployedBy?, notes? }`
|
||||||
|
- Validates version belongs to application
|
||||||
|
- Validates deployment date not in future
|
||||||
|
|
||||||
|
### Querying Deployments
|
||||||
|
- `GET /api/deployments` - List all deployments (paginated, filtered)
|
||||||
|
- Filters: `applicationId`, `environmentId`, `versionId`, `dateFrom`, `dateTo`
|
||||||
|
- `GET /api/deployments/{id}` - Get deployment details
|
||||||
|
- `GET /api/deployments/current` - Get current state across all environments
|
||||||
|
- Query params: `applicationId?`, `environmentId?`
|
||||||
|
- `GET /api/applications/{appId}/deployments` - Deployment history for application
|
||||||
|
- `GET /api/applications/{appId}/deployments/current` - Current state per environment for app
|
||||||
|
- `GET /api/environments/{envId}/deployments/current` - All current deployments in environment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DTOs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Deployment {
|
||||||
|
id: string;
|
||||||
|
application: { id: string; name: string };
|
||||||
|
version: { id: string; versionIdentifier: string };
|
||||||
|
environment: { id: string; name: string };
|
||||||
|
deploymentDate: Date;
|
||||||
|
deployedBy?: string;
|
||||||
|
notes?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordDeploymentRequest {
|
||||||
|
applicationId: string;
|
||||||
|
versionId: string;
|
||||||
|
environmentId: string;
|
||||||
|
deploymentDate: Date;
|
||||||
|
deployedBy?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentDeploymentState {
|
||||||
|
application: { id: string; name: string };
|
||||||
|
environment: { id: string; name: string };
|
||||||
|
version: { id: string; versionIdentifier: string };
|
||||||
|
deploymentDate: Date;
|
||||||
|
deployedBy?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Components
|
||||||
|
|
||||||
|
### RecordDeploymentComponent
|
||||||
|
- Form to record new deployment:
|
||||||
|
- Application (dropdown or pre-selected from context)
|
||||||
|
- Version (dropdown - load versions for selected application)
|
||||||
|
- Environment (dropdown)
|
||||||
|
- Deployment Date (datetime picker, default: now)
|
||||||
|
- Deployed By (text input, default: current user)
|
||||||
|
- Notes (textarea, optional)
|
||||||
|
- Validation:
|
||||||
|
- All required fields
|
||||||
|
- Version belongs to selected application
|
||||||
|
- Date not in future
|
||||||
|
- Submit creates immutable deployment record
|
||||||
|
|
||||||
|
### DeploymentHistoryComponent (tab in ApplicationDetailComponent)
|
||||||
|
- Timeline view of deployments for an application
|
||||||
|
- Group by environment
|
||||||
|
- Show: Version, Environment, Date, Deployed By
|
||||||
|
- Sort: Most recent first
|
||||||
|
- Filter: By environment, by date range
|
||||||
|
|
||||||
|
### CurrentDeploymentStateComponent
|
||||||
|
- Dashboard view showing current state
|
||||||
|
- Matrix: Applications (rows) × Environments (columns)
|
||||||
|
- Each cell shows: Version number, deployment date
|
||||||
|
- Color coding:
|
||||||
|
- Green: Recently deployed (< 30 days)
|
||||||
|
- Yellow: Older deployment (30-90 days)
|
||||||
|
- Red: Very old (> 90 days)
|
||||||
|
- Click cell to see deployment details
|
||||||
|
- "Record Deployment" action per cell
|
||||||
|
|
||||||
|
### EnvironmentDeploymentsComponent (tab in Environment detail)
|
||||||
|
- List all applications currently deployed in this environment
|
||||||
|
- Columns: Application, Version, Deployment Date, Deployed By
|
||||||
|
- Link to application detail
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] Deployment can be recorded with valid data
|
||||||
|
- [ ] Version must belong to the specified application
|
||||||
|
- [ ] Deployment date cannot be in future
|
||||||
|
- [ ] Deployments are immutable (no updates/deletes)
|
||||||
|
- [ ] Current deployment state query returns correct version per environment
|
||||||
|
- [ ] Deployment history is complete and ordered correctly
|
||||||
|
- [ ] All endpoints authenticated
|
||||||
|
- [ ] Tests pass (>80% coverage)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] User can record new deployment
|
||||||
|
- [ ] Form validates all fields
|
||||||
|
- [ ] Version dropdown shows only versions for selected application
|
||||||
|
- [ ] Deployment appears immediately in history
|
||||||
|
- [ ] Current state dashboard shows accurate data
|
||||||
|
- [ ] Timeline view shows deployment history
|
||||||
|
- [ ] User can filter deployment history
|
||||||
|
- [ ] Deployment date defaults to current time
|
||||||
|
- [ ] Deployed by defaults to current user
|
||||||
|
- [ ] Tests pass (>70% coverage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Record Deployment
|
||||||
|
1. Navigate to "Record Deployment"
|
||||||
|
2. Select Application: "Customer Portal"
|
||||||
|
3. Version dropdown loads versions for Customer Portal
|
||||||
|
4. Select Version: "1.2.0"
|
||||||
|
5. Select Environment: "PROD-EU"
|
||||||
|
6. Deployment Date defaults to now
|
||||||
|
7. Deployed By defaults to current user
|
||||||
|
8. Add notes: "Hotfix for login issue"
|
||||||
|
9. Submit
|
||||||
|
10. Verify success notification
|
||||||
|
11. Verify deployment appears in history
|
||||||
|
|
||||||
|
### Scenario 2: Invalid Version
|
||||||
|
1. Attempt to record deployment
|
||||||
|
2. Manually set versionId for different application
|
||||||
|
3. Submit
|
||||||
|
4. Verify error: "Version does not belong to selected application"
|
||||||
|
|
||||||
|
### Scenario 3: Future Date Validation
|
||||||
|
1. Record deployment
|
||||||
|
2. Set deployment date to tomorrow
|
||||||
|
3. Submit
|
||||||
|
4. Verify error: "Deployment date cannot be in future"
|
||||||
|
|
||||||
|
### Scenario 4: Current State Query
|
||||||
|
1. Record multiple deployments for same app in different environments
|
||||||
|
2. Navigate to current state dashboard
|
||||||
|
3. Verify each environment shows the most recent deployment
|
||||||
|
4. Record new deployment for same app/env
|
||||||
|
5. Verify dashboard updates to show new deployment
|
||||||
|
|
||||||
|
### Scenario 5: Deployment History
|
||||||
|
1. Navigate to Application Detail → Deployments tab
|
||||||
|
2. Verify all deployments shown, grouped by environment
|
||||||
|
3. Verify sorted by date (most recent first)
|
||||||
|
4. Filter by environment
|
||||||
|
5. Verify only deployments to that environment shown
|
||||||
|
|
||||||
|
### Scenario 6: Environment View
|
||||||
|
1. Navigate to Environment Detail → Deployments tab
|
||||||
|
2. Verify all applications deployed to this environment shown
|
||||||
|
3. Verify versions and dates correct
|
||||||
|
4. Click on application
|
||||||
|
5. Verify navigates to application detail
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### Application Detail Enhancement
|
||||||
|
Add "Deployments" tab showing:
|
||||||
|
- Current deployment state (per environment)
|
||||||
|
- Deployment history timeline
|
||||||
|
- "Record New Deployment" button
|
||||||
|
|
||||||
|
### Environment Detail Enhancement
|
||||||
|
Add "Deployments" tab showing:
|
||||||
|
- All applications currently deployed
|
||||||
|
- Quick record deployment action
|
||||||
|
|
||||||
|
### Dashboard (new)
|
||||||
|
Create main dashboard showing:
|
||||||
|
- Current deployment state matrix
|
||||||
|
- Recent deployments (last 10)
|
||||||
|
- Quick stats (total apps, total deployments this week)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- [ ] Index on (application_id, environment_id, deployment_date) for current state queries
|
||||||
|
- [ ] Pagination for deployment history (can grow large)
|
||||||
|
- [ ] Consider caching current deployment state (refresh every 5 min)
|
||||||
|
- [ ] Optimize DISTINCT ON query for PostgreSQL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Story Status**: Ready for Development
|
||||||
|
**Estimated Completion**: 5-7 days
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
# Story 7: Current State Dashboard & Advanced History
|
||||||
|
|
||||||
|
## Story Overview
|
||||||
|
|
||||||
|
**As a** application manager
|
||||||
|
**I want** a comprehensive view of deployment state and history
|
||||||
|
**So that** I can quickly understand what's deployed where and track changes over time
|
||||||
|
|
||||||
|
**Story Type**: Feature (UI/UX Enhancement)
|
||||||
|
**Priority**: High
|
||||||
|
**Estimated Effort**: 4-5 days
|
||||||
|
**Dependencies**: Story 6 (Deployments)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Value
|
||||||
|
|
||||||
|
This story enhances deployment visibility by providing:
|
||||||
|
- Executive dashboard with at-a-glance deployment status
|
||||||
|
- Advanced filtering and search capabilities
|
||||||
|
- Visual deployment timeline
|
||||||
|
- Deployment analytics and trends
|
||||||
|
- Export capabilities for reporting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
✅ Main deployment dashboard (matrix view)
|
||||||
|
✅ Advanced filtering (multi-criteria)
|
||||||
|
✅ Deployment timeline visualization
|
||||||
|
✅ Quick stats and KPIs
|
||||||
|
✅ Export to CSV/Excel
|
||||||
|
✅ Deployment calendar view
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
❌ Automated alerts/notifications (Phase 2)
|
||||||
|
❌ Deployment approval workflows (Phase 2)
|
||||||
|
❌ Integration with monitoring tools (Phase 2)
|
||||||
|
❌ Predictive analytics (Phase 2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Components to Build
|
||||||
|
|
||||||
|
### 1. MainDashboardComponent (New Landing Page)
|
||||||
|
|
||||||
|
**Layout**:
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Dashboard Header │
|
||||||
|
│ ┌─────────────┬─────────────┬─────────────────────┐│
|
||||||
|
│ │ Total Apps │ Deployments │ Prod Deployments ││
|
||||||
|
│ │ 42 │ This Week │ This Month ││
|
||||||
|
│ │ │ 18 │ 156 ││
|
||||||
|
│ └─────────────┴─────────────┴─────────────────────┘│
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ Current Deployment State Matrix │
|
||||||
|
│ │
|
||||||
|
│ │ PROD-EU │ PROD-US │ INT │ TEST │ DEV │
|
||||||
|
│ ────────┼─────────┼─────────┼──────┼──────┼────── │
|
||||||
|
│ App1 │ v2.1.0 │ v2.1.0 │v2.2.0│v2.3.0│v3.0.0 │
|
||||||
|
│ App2 │ v1.5.2 │ v1.5.2 │v1.6.0│v1.6.0│v1.6.1 │
|
||||||
|
│ ... │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ Recent Deployments (Last 10) │
|
||||||
|
│ • Customer Portal v2.1.0 → PROD-EU (2 hours ago) │
|
||||||
|
│ • Mobile App v3.2.1 → TEST (5 hours ago) │
|
||||||
|
│ ... │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- [ ] KPI cards at top (total apps, deployments this week/month, production deployments)
|
||||||
|
- [ ] Deployment matrix (applications × environments)
|
||||||
|
- Cell shows version number and deployment date
|
||||||
|
- Color coding by age (green < 30d, yellow 30-90d, red > 90d)
|
||||||
|
- Click cell to see deployment details
|
||||||
|
- Hover shows tooltip with full details
|
||||||
|
- [ ] Recent deployments list (last 10, with "View All" link)
|
||||||
|
- [ ] Filters:
|
||||||
|
- By status (all, production only, non-production)
|
||||||
|
- By business unit
|
||||||
|
- By application
|
||||||
|
- Date range
|
||||||
|
|
||||||
|
### 2. DeploymentHistoryComponent (Enhanced)
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- [ ] Timeline visualization
|
||||||
|
- Vertical timeline showing deployments chronologically
|
||||||
|
- Group by application or environment (toggle)
|
||||||
|
- Show version transitions (v1.0 → v1.1 → v2.0)
|
||||||
|
- [ ] Advanced filters:
|
||||||
|
- Application (multi-select)
|
||||||
|
- Environment (multi-select)
|
||||||
|
- Version
|
||||||
|
- Date range (from/to)
|
||||||
|
- Deployed by (user)
|
||||||
|
- [ ] Sorting options:
|
||||||
|
- By date (asc/desc)
|
||||||
|
- By application
|
||||||
|
- By environment
|
||||||
|
- [ ] Export functionality:
|
||||||
|
- Export to CSV
|
||||||
|
- Export to Excel
|
||||||
|
- Include all filtered results
|
||||||
|
|
||||||
|
### 3. DeploymentCalendarComponent (New)
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- [ ] Calendar view of deployments
|
||||||
|
- [ ] Each day shows number of deployments
|
||||||
|
- [ ] Click day to see details
|
||||||
|
- [ ] Color coding:
|
||||||
|
- High activity days (>10 deployments) - red
|
||||||
|
- Normal activity (3-10) - yellow
|
||||||
|
- Low activity (1-2) - green
|
||||||
|
- No deployments - gray
|
||||||
|
- [ ] Filter by environment type (production/non-production)
|
||||||
|
|
||||||
|
### 4. DeploymentSearchComponent (New)
|
||||||
|
|
||||||
|
**Advanced Search Form**:
|
||||||
|
- [ ] Multi-criteria search:
|
||||||
|
- Application name (autocomplete)
|
||||||
|
- Version identifier (text)
|
||||||
|
- Environment name (multi-select)
|
||||||
|
- Date range
|
||||||
|
- Deployed by (autocomplete)
|
||||||
|
- Notes/comments contain (text search)
|
||||||
|
- [ ] Save search criteria as "favorite"
|
||||||
|
- [ ] Share search URL (encode filters in URL params)
|
||||||
|
- [ ] Results table with all deployment details
|
||||||
|
- [ ] Export results
|
||||||
|
|
||||||
|
### 5. DeploymentStatsComponent (New)
|
||||||
|
|
||||||
|
**Analytics Dashboard**:
|
||||||
|
- [ ] Deployment frequency chart
|
||||||
|
- Bar chart: Deployments per day/week/month
|
||||||
|
- Toggle time range (last 7 days, 30 days, 90 days)
|
||||||
|
- [ ] Top deployed applications (pie chart)
|
||||||
|
- [ ] Deployment by environment (bar chart)
|
||||||
|
- [ ] Deployment trends:
|
||||||
|
- Average deployments per week (with trend line)
|
||||||
|
- Most active deployment days/times
|
||||||
|
- [ ] Version distribution:
|
||||||
|
- How many environments run each version
|
||||||
|
- Identify outdated versions in production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend Enhancements
|
||||||
|
|
||||||
|
### New Endpoints
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Statistics
|
||||||
|
GET /api/deployments/stats/summary
|
||||||
|
- Returns: { totalApps, totalDeployments, deploymentsThisWeek, deploymentsThisMonth, prodDeployments }
|
||||||
|
|
||||||
|
GET /api/deployments/stats/frequency?range={7|30|90}
|
||||||
|
- Returns: Deployment count per day for specified range
|
||||||
|
|
||||||
|
GET /api/deployments/stats/by-environment
|
||||||
|
- Returns: Deployment count per environment
|
||||||
|
|
||||||
|
GET /api/deployments/stats/by-application
|
||||||
|
- Returns: Top 10 most deployed applications
|
||||||
|
|
||||||
|
GET /api/deployments/stats/version-distribution
|
||||||
|
- Returns: Version counts across all environments
|
||||||
|
|
||||||
|
// Advanced Search
|
||||||
|
GET /api/deployments/search
|
||||||
|
- Query params: applicationName, versionId, environmentIds[], dateFrom, dateTo, deployedBy, notes
|
||||||
|
- Returns: Paginated search results
|
||||||
|
|
||||||
|
// Export
|
||||||
|
GET /api/deployments/export?format={csv|excel}&filters=...
|
||||||
|
- Returns: File download with deployment data
|
||||||
|
|
||||||
|
// Calendar View
|
||||||
|
GET /api/deployments/calendar?year={year}&month={month}
|
||||||
|
- Returns: Deployment counts per day for the month
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Layer
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class DeploymentStatisticsService {
|
||||||
|
DeploymentSummaryDto getSummaryStatistics();
|
||||||
|
List<DeploymentFrequencyDto> getDeploymentFrequency(int days);
|
||||||
|
List<EnvironmentDeploymentCountDto> getDeploymentsByEnvironment();
|
||||||
|
List<ApplicationDeploymentCountDto> getTopDeployedApplications(int limit);
|
||||||
|
Map<String, Integer> getVersionDistribution();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DeploymentExportService {
|
||||||
|
byte[] exportToCsv(DeploymentSearchCriteria criteria);
|
||||||
|
byte[] exportToExcel(DeploymentSearchCriteria criteria);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Services
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class DeploymentStatisticsService {
|
||||||
|
getSummary(): Observable<DeploymentSummary>
|
||||||
|
getFrequency(days: number): Observable<DeploymentFrequency[]>
|
||||||
|
getByEnvironment(): Observable<EnvironmentStats[]>
|
||||||
|
getByApplication(): Observable<ApplicationStats[]>
|
||||||
|
getVersionDistribution(): Observable<VersionDistribution>
|
||||||
|
getCalendarData(year, month): Observable<CalendarData>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class DeploymentExportService {
|
||||||
|
exportToCsv(filters: DeploymentFilters): Observable<Blob>
|
||||||
|
exportToExcel(filters: DeploymentFilters): Observable<Blob>
|
||||||
|
downloadFile(blob: Blob, filename: string): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Charting Library
|
||||||
|
|
||||||
|
Use **Chart.js** or **ng2-charts** (Angular wrapper) for visualizations:
|
||||||
|
- Bar charts (deployment frequency, by environment)
|
||||||
|
- Pie charts (top applications)
|
||||||
|
- Line charts (trends over time)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
- [ ] Dashboard shows KPI cards with accurate counts
|
||||||
|
- [ ] Deployment matrix displays current state correctly
|
||||||
|
- [ ] Matrix cells show version and date
|
||||||
|
- [ ] Color coding reflects deployment age
|
||||||
|
- [ ] Recent deployments list shows last 10
|
||||||
|
- [ ] Dashboard loads in < 2 seconds
|
||||||
|
|
||||||
|
### History & Timeline
|
||||||
|
- [ ] Timeline visualization shows deployment flow
|
||||||
|
- [ ] Advanced filters work correctly (multi-select, date range)
|
||||||
|
- [ ] Filters can be combined
|
||||||
|
- [ ] Results update reactively when filters change
|
||||||
|
- [ ] Timeline can be grouped by application or environment
|
||||||
|
- [ ] Export to CSV works (all filtered results)
|
||||||
|
- [ ] Export to Excel works with formatting
|
||||||
|
|
||||||
|
### Calendar View
|
||||||
|
- [ ] Calendar shows deployment counts per day
|
||||||
|
- [ ] Days are color-coded by activity level
|
||||||
|
- [ ] Clicking day shows deployment details
|
||||||
|
- [ ] Filter by production/non-production works
|
||||||
|
|
||||||
|
### Search
|
||||||
|
- [ ] Advanced search supports all criteria
|
||||||
|
- [ ] Autocomplete works for application and user fields
|
||||||
|
- [ ] Search results are accurate and complete
|
||||||
|
- [ ] Search URL can be shared (filters in URL)
|
||||||
|
- [ ] Saved searches can be recalled
|
||||||
|
|
||||||
|
### Statistics
|
||||||
|
- [ ] All statistics are accurate
|
||||||
|
- [ ] Charts render correctly
|
||||||
|
- [ ] Charts are responsive (resize with window)
|
||||||
|
- [ ] Time range toggle works for frequency chart
|
||||||
|
- [ ] Version distribution identifies outdated versions
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- [ ] Dashboard loads < 2 seconds
|
||||||
|
- [ ] Matrix supports 100+ applications without lag
|
||||||
|
- [ ] Export handles 10,000+ deployments
|
||||||
|
- [ ] Charts render smoothly
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Backend tests pass (>80% coverage)
|
||||||
|
- [ ] Frontend tests pass (>70% coverage)
|
||||||
|
- [ ] E2E tests cover all main workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Dashboard Overview
|
||||||
|
1. Login and navigate to dashboard
|
||||||
|
2. Verify KPIs show correct counts
|
||||||
|
3. Verify deployment matrix displays
|
||||||
|
4. Verify recent deployments list
|
||||||
|
5. Click matrix cell
|
||||||
|
6. Verify deployment details popup
|
||||||
|
7. Filter by production only
|
||||||
|
8. Verify matrix updates
|
||||||
|
|
||||||
|
### Scenario 2: Timeline Visualization
|
||||||
|
1. Navigate to deployment history
|
||||||
|
2. Switch to timeline view
|
||||||
|
3. Verify chronological display
|
||||||
|
4. Group by application
|
||||||
|
5. Verify grouping works
|
||||||
|
6. Group by environment
|
||||||
|
7. Verify regrouping works
|
||||||
|
8. Hover over deployment
|
||||||
|
9. Verify tooltip shows details
|
||||||
|
|
||||||
|
### Scenario 3: Advanced Search
|
||||||
|
1. Navigate to deployment search
|
||||||
|
2. Select multiple applications
|
||||||
|
3. Select date range
|
||||||
|
4. Enter deployed by user
|
||||||
|
5. Click search
|
||||||
|
6. Verify results match criteria
|
||||||
|
7. Export results to CSV
|
||||||
|
8. Verify CSV contains filtered data
|
||||||
|
|
||||||
|
### Scenario 4: Calendar View
|
||||||
|
1. Navigate to deployment calendar
|
||||||
|
2. Verify current month displayed
|
||||||
|
3. Verify days with deployments highlighted
|
||||||
|
4. Click on high-activity day
|
||||||
|
5. Verify deployment details shown
|
||||||
|
6. Navigate to previous month
|
||||||
|
7. Verify data updates
|
||||||
|
|
||||||
|
### Scenario 5: Statistics & Analytics
|
||||||
|
1. Navigate to deployment stats
|
||||||
|
2. Verify frequency chart displays
|
||||||
|
3. Change time range to 90 days
|
||||||
|
4. Verify chart updates
|
||||||
|
5. View top applications chart
|
||||||
|
6. Verify data accurate
|
||||||
|
7. Check version distribution
|
||||||
|
8. Verify outdated versions highlighted
|
||||||
|
|
||||||
|
### Scenario 6: Export Functionality
|
||||||
|
1. Apply filters to deployment history
|
||||||
|
2. Click "Export to CSV"
|
||||||
|
3. Verify file downloads
|
||||||
|
4. Open CSV
|
||||||
|
5. Verify data matches filters
|
||||||
|
6. Repeat with Excel format
|
||||||
|
7. Verify Excel formatting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
- [ ] All components implemented and functional
|
||||||
|
- [ ] All acceptance criteria met
|
||||||
|
- [ ] All tests passing (unit, integration, E2E)
|
||||||
|
- [ ] Dashboard performance < 2 seconds
|
||||||
|
- [ ] Export handles large datasets
|
||||||
|
- [ ] Charts render correctly on all screen sizes
|
||||||
|
- [ ] Code reviewed and approved
|
||||||
|
- [ ] User documentation updated
|
||||||
|
- [ ] Demo conducted successfully
|
||||||
|
- [ ] Code merged to main branch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] Add database indexes for statistics queries
|
||||||
|
- [ ] Cache statistics (refresh every 5 minutes)
|
||||||
|
- [ ] Optimize current state query (materialized view?)
|
||||||
|
- [ ] Paginate export for very large datasets
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] Virtual scrolling for large deployment lists
|
||||||
|
- [ ] Lazy load dashboard widgets
|
||||||
|
- [ ] Debounce filter inputs
|
||||||
|
- [ ] Cache chart data (avoid re-render on resize)
|
||||||
|
- [ ] Use TrackBy for ngFor (performance)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements (Phase 2)
|
||||||
|
|
||||||
|
- [ ] Real-time deployment tracking (WebSocket updates)
|
||||||
|
- [ ] Deployment approval workflows
|
||||||
|
- [ ] Automated deployment notifications
|
||||||
|
- [ ] Integration with CI/CD pipelines
|
||||||
|
- [ ] Deployment health status tracking
|
||||||
|
- [ ] Predictive analytics (deployment patterns)
|
||||||
|
- [ ] Custom dashboards (user-configurable)
|
||||||
|
- [ ] Mobile app for deployment monitoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Story Status**: Ready for Development
|
||||||
|
**Estimated Completion**: 4-5 days
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: ldpv2-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ldpv2
|
||||||
|
POSTGRES_USER: ldpv2_user
|
||||||
|
POSTGRES_PASSWORD: ldpv2_password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- ldpv2-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ldpv2_user -d ldpv2"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ldpv2-backend
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_USERNAME: ldpv2_user
|
||||||
|
DB_PASSWORD: ldpv2_password
|
||||||
|
JWT_SECRET: your-secret-key-change-in-production-minimum-512-bits-for-hs512-algorithm
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
networks:
|
||||||
|
- ldpv2-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/api/actuator/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ldpv2-frontend
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
ports:
|
||||||
|
- "4200:80"
|
||||||
|
networks:
|
||||||
|
- ldpv2-network
|
||||||
|
environment:
|
||||||
|
API_URL: http://backend:8080/api
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ldpv2-network:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"ldpv2-frontend": {
|
||||||
|
"projectType": "application",
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/ldpv2-frontend",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": ["zone.js"],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"assets": ["src/assets"],
|
||||||
|
"styles": ["src/styles.scss"],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "ldpv2-frontend:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "ldpv2-frontend:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development",
|
||||||
|
"options": {
|
||||||
|
"proxyConfig": "proxy.conf.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": ["zone.js", "zone.js/testing"],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"assets": ["src/assets"],
|
||||||
|
"styles": ["src/styles.scss"],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+291
@@ -0,0 +1,291 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
BASE="/home/claude/ldpv2-monorepo/frontend/src/app"
|
||||||
|
|
||||||
|
# ========== MODELS ==========
|
||||||
|
|
||||||
|
cat > "$BASE/shared/models/user.model.ts" << 'TS'
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
token: string;
|
||||||
|
type: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
TS
|
||||||
|
|
||||||
|
cat > "$BASE/shared/models/environment.model.ts" << 'TS'
|
||||||
|
export interface Environment {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isProduction: boolean;
|
||||||
|
criticalityLevel?: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEnvironmentRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isProduction?: boolean;
|
||||||
|
criticalityLevel?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateEnvironmentRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
isProduction?: boolean;
|
||||||
|
criticalityLevel?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Page<T> {
|
||||||
|
content: T[];
|
||||||
|
pageable: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
sort: {
|
||||||
|
sorted: boolean;
|
||||||
|
unsorted: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
totalElements: number;
|
||||||
|
totalPages: number;
|
||||||
|
last: boolean;
|
||||||
|
first: boolean;
|
||||||
|
size: number;
|
||||||
|
number: number;
|
||||||
|
numberOfElements: number;
|
||||||
|
empty: boolean;
|
||||||
|
}
|
||||||
|
TS
|
||||||
|
|
||||||
|
echo "Models created"
|
||||||
|
|
||||||
|
# ========== SERVICES ==========
|
||||||
|
|
||||||
|
cat > "$BASE/core/auth/auth.service.ts" << 'TS'
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable, BehaviorSubject, tap } from 'rxjs';
|
||||||
|
import { LoginRequest, RegisterRequest, AuthResponse, User } from '../../shared/models/user.model';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
private readonly TOKEN_KEY = 'auth_token';
|
||||||
|
private readonly USER_KEY = 'current_user';
|
||||||
|
private currentUserSubject = new BehaviorSubject<User | null>(this.getUserFromStorage());
|
||||||
|
|
||||||
|
public currentUser$ = this.currentUserSubject.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
login(credentials: LoginRequest): Observable<AuthResponse> {
|
||||||
|
return this.http.post<AuthResponse>('/api/auth/login', credentials).pipe(
|
||||||
|
tap(response => {
|
||||||
|
this.setSession(response);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
register(data: RegisterRequest): Observable<AuthResponse> {
|
||||||
|
return this.http.post<AuthResponse>('/api/auth/register', data).pipe(
|
||||||
|
tap(response => {
|
||||||
|
this.setSession(response);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
localStorage.removeItem(this.TOKEN_KEY);
|
||||||
|
localStorage.removeItem(this.USER_KEY);
|
||||||
|
this.currentUserSubject.next(null);
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
return !!this.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken(): string | null {
|
||||||
|
return localStorage.getItem(this.TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser(): User | null {
|
||||||
|
return this.currentUserSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setSession(authResponse: AuthResponse): void {
|
||||||
|
localStorage.setItem(this.TOKEN_KEY, authResponse.token);
|
||||||
|
localStorage.setItem(this.USER_KEY, JSON.stringify(authResponse.user));
|
||||||
|
this.currentUserSubject.next(authResponse.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUserFromStorage(): User | null {
|
||||||
|
const userJson = localStorage.getItem(this.USER_KEY);
|
||||||
|
return userJson ? JSON.parse(userJson) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TS
|
||||||
|
|
||||||
|
cat > "$BASE/features/environments/environment.service.ts" << 'TS'
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
CreateEnvironmentRequest,
|
||||||
|
UpdateEnvironmentRequest,
|
||||||
|
Page
|
||||||
|
} from '../../shared/models/environment.model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class EnvironmentService {
|
||||||
|
private readonly API_URL = '/api/environments';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getEnvironments(
|
||||||
|
page: number = 0,
|
||||||
|
size: number = 20,
|
||||||
|
sortBy: string = 'name',
|
||||||
|
sortDirection: string = 'asc'
|
||||||
|
): Observable<Page<Environment>> {
|
||||||
|
const params = new HttpParams()
|
||||||
|
.set('page', page.toString())
|
||||||
|
.set('size', size.toString())
|
||||||
|
.set('sortBy', sortBy)
|
||||||
|
.set('sortDirection', sortDirection);
|
||||||
|
|
||||||
|
return this.http.get<Page<Environment>>(this.API_URL, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
searchEnvironments(query: string, page: number = 0, size: number = 20): Observable<Page<Environment>> {
|
||||||
|
const params = new HttpParams()
|
||||||
|
.set('query', query)
|
||||||
|
.set('page', page.toString())
|
||||||
|
.set('size', size.toString());
|
||||||
|
|
||||||
|
return this.http.get<Page<Environment>>(`${this.API_URL}/search`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnvironment(id: string): Observable<Environment> {
|
||||||
|
return this.http.get<Environment>(`${this.API_URL}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createEnvironment(data: CreateEnvironmentRequest): Observable<Environment> {
|
||||||
|
return this.http.post<Environment>(this.API_URL, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEnvironment(id: string, data: UpdateEnvironmentRequest): Observable<Environment> {
|
||||||
|
return this.http.put<Environment>(`${this.API_URL}/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEnvironment(id: string): Observable<void> {
|
||||||
|
return this.http.delete<void>(`${this.API_URL}/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TS
|
||||||
|
|
||||||
|
echo "Services created"
|
||||||
|
|
||||||
|
# ========== INTERCEPTORS ==========
|
||||||
|
|
||||||
|
cat > "$BASE/core/interceptors/jwt.interceptor.ts" << 'TS'
|
||||||
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
|
||||||
|
export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
const token = authService.getToken();
|
||||||
|
|
||||||
|
if (token && !req.url.includes('/auth/')) {
|
||||||
|
req = req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(req);
|
||||||
|
};
|
||||||
|
TS
|
||||||
|
|
||||||
|
cat > "$BASE/core/interceptors/error.interceptor.ts" << 'TS'
|
||||||
|
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { catchError, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
const router = inject(Router);
|
||||||
|
|
||||||
|
return next(req).pipe(
|
||||||
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
// Unauthorized - redirect to login
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
localStorage.removeItem('current_user');
|
||||||
|
router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
TS
|
||||||
|
|
||||||
|
echo "Interceptors created"
|
||||||
|
|
||||||
|
# ========== GUARDS ==========
|
||||||
|
|
||||||
|
cat > "$BASE/core/guards/auth.guard.ts" << 'TS'
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { Router, CanActivateFn } from '@angular/router';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
|
||||||
|
export const authGuard: CanActivateFn = (route, state) => {
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
const router = inject(Router);
|
||||||
|
|
||||||
|
if (authService.isAuthenticated()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the attempted URL for redirecting
|
||||||
|
router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
TS
|
||||||
|
|
||||||
|
echo "Guards created"
|
||||||
|
|
||||||
|
echo "✓ All Angular TypeScript files created successfully!"
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "ldpv2-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test",
|
||||||
|
"lint": "ng lint"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^18.0.0",
|
||||||
|
"@angular/common": "^18.0.0",
|
||||||
|
"@angular/compiler": "^18.0.0",
|
||||||
|
"@angular/core": "^18.0.0",
|
||||||
|
"@angular/forms": "^18.0.0",
|
||||||
|
"@angular/material": "^18.0.0",
|
||||||
|
"@angular/platform-browser": "^18.0.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||||
|
"@angular/router": "^18.0.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^18.0.0",
|
||||||
|
"@angular/cli": "^18.0.0",
|
||||||
|
"@angular/compiler-cli": "^18.0.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.1.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:8080",
|
||||||
|
"secure": false,
|
||||||
|
"changeOrigin": true,
|
||||||
|
"logLevel": "debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
template: '<router-outlet></router-outlet>',
|
||||||
|
styles: []
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'LDPv2 - Lifecycle Data Platform';
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { jwtInterceptor } from './core/interceptors/jwt.interceptor';
|
||||||
|
import { errorInterceptor } from './core/interceptors/error.interceptor';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(
|
||||||
|
withInterceptors([jwtInterceptor, errorInterceptor])
|
||||||
|
),
|
||||||
|
provideAnimations()
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { authGuard } from './core/guards/auth.guard';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: '/environments',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
loadComponent: () => import('./core/auth/login/login.component').then(m => m.LoginComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'environments',
|
||||||
|
canActivate: [authGuard],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadComponent: () => import('./features/environments/environment-list/environment-list.component')
|
||||||
|
.then(m => m.EnvironmentListComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'new',
|
||||||
|
loadComponent: () => import('./features/environments/environment-form/environment-form.component')
|
||||||
|
.then(m => m.EnvironmentFormComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
loadComponent: () => import('./features/environments/environment-detail/environment-detail.component')
|
||||||
|
.then(m => m.EnvironmentDetailComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit',
|
||||||
|
loadComponent: () => import('./features/environments/environment-form/environment-form.component')
|
||||||
|
.then(m => m.EnvironmentFormComponent)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
token: string;
|
||||||
|
type: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>LDPv2 - Lifecycle Data Platform</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { AppComponent } from './app/app.component';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, appConfig)
|
||||||
|
.catch((err) => console.error(err));
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* Global Styles */
|
||||||
|
@import '@angular/material/prebuilt-themes/indigo-pink.css';
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
color: #4caf50;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": ["src/main.ts"],
|
||||||
|
"include": ["src/**/*.d.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"lib": ["ES2022", "dom"]
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": ["jasmine"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user