18 Commits

Author SHA1 Message Date
Florian THIERRY
dee473fe46 Add interceptor to inject correlation id in http requests. 2024-09-26 10:38:52 +02:00
Florian THIERRY
5647a5a959 Implementation of jpa saving for traffic traces. 2024-09-26 10:21:20 +02:00
Florian THIERRY
c817371a15 Add skeletton to save traces. 2024-09-25 21:30:25 +02:00
ff52a198dc Upgrade to angular 18. 2024-09-24 22:47:15 +02:00
Florian THIERRY
f3d59a0ef3 conf cleaning. 2024-09-22 13:18:08 +02:00
Florian THIERRY
d84485e52b test 2024-09-22 13:03:39 +02:00
Florian THIERRY
f789d89995 test 2024-09-22 12:53:55 +02:00
Florian THIERRY
7e0174bcc2 test 2024-09-22 12:46:41 +02:00
Florian THIERRY
fe1d59a3bb test 2024-09-22 12:37:01 +02:00
Florian THIERRY
69a99c9312 test 2024-09-22 12:34:02 +02:00
Florian THIERRY
a6414ae64d test 2024-09-22 12:30:18 +02:00
Florian THIERRY
9cf47f0e2a test 2024-09-22 12:27:59 +02:00
Florian THIERRY
6c89562dc3 test 2024-09-22 12:14:04 +02:00
Florian THIERRY
e85eabbed5 test 2024-09-22 12:01:05 +02:00
Florian THIERRY
1ec4ba8212 test conf prod. 2024-09-22 11:10:01 +02:00
Florian THIERRY
a1ff181443 test conf prod. 2024-09-22 11:09:14 +02:00
Florian THIERRY
ee8f48bc43 Fix prod fr configuration. 2024-09-22 10:55:49 +02:00
Florian THIERRY
7ec1aee884 test 2024-09-21 23:39:46 +02:00
25 changed files with 3031 additions and 2570 deletions

View File

@@ -7,6 +7,6 @@ RUN npm run build-prod-fr
FROM nginx:1.27-alpine AS final FROM nginx:1.27-alpine AS final
WORKDIR /app WORKDIR /app
COPY --from=builder /app/dist/codiki-ng/en/browser /usr/share/nginx/html/en/ COPY --from=builder /app/dist/codiki/en/browser /usr/share/nginx/html/en/
COPY --from=builder /app/dist/codiki-ng/fr/browser /usr/share/nginx/html/fr/ COPY --from=builder /app/dist/codiki/fr/browser/fr /usr/share/nginx/html/fr/
COPY frontend/conf/nginx.conf /etc/nginx/nginx.conf COPY frontend/conf/nginx.conf /etc/nginx/nginx.conf

View File

@@ -25,6 +25,10 @@ public class CustomUserDetails implements UserDetails {
.toList(); .toList();
} }
public User getUser() {
return user;
}
@Override @Override
public String getUsername() { public String getUsername() {
return user.id().toString(); return user.id().toString();

View File

@@ -0,0 +1,47 @@
package org.codiki.application.traffic;
import jakarta.annotation.Nullable;
import org.codiki.domain.traffic.exception.TrafficTraceCreationException;
import org.codiki.domain.traffic.model.TrafficEndpoint;
import org.codiki.domain.traffic.model.TrafficTrace;
import org.codiki.domain.traffic.port.TrafficTracePort;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.UUID;
import static java.util.Objects.isNull;
import static org.codiki.domain.traffic.model.TrafficTrace.aTrafficTrace;
@Component
public class TrafficTraceUseCases {
private final TrafficTracePort trafficTracePort;
private final Clock clock;
public TrafficTraceUseCases(TrafficTracePort trafficTracePort, Clock clock) {
this.trafficTracePort = trafficTracePort;
this.clock = clock;
}
@Async
public void saveNewTrace(
TrafficEndpoint trafficEndpoint,
@Nullable UUID userId,
@Nullable String correlationId
) {
if (isNull(trafficEndpoint)) {
throw new TrafficTraceCreationException("Traffic endpoint should not be null.");
}
TrafficTrace newTrace = aTrafficTrace()
.withId(UUID.randomUUID())
.withDateTime(ZonedDateTime.now(clock))
.withEndpoint(trafficEndpoint)
.withUserId(userId)
.withCorrelationId(correlationId)
.build();
trafficTracePort.save(newTrace);
}
}

View File

@@ -87,9 +87,7 @@ public class UserUseCases {
.map(Authentication::getPrincipal) .map(Authentication::getPrincipal)
.filter(CustomUserDetails.class::isInstance) .filter(CustomUserDetails.class::isInstance)
.map(CustomUserDetails.class::cast) .map(CustomUserDetails.class::cast)
.map(CustomUserDetails::getUsername) .map(CustomUserDetails::getUser);
.map(UUID::fromString)
.flatMap(userPort::findById);
} }
private UserAuthenticationData generateAuthenticationData(User user) { private UserAuthenticationData generateAuthenticationData(User user) {

View File

@@ -0,0 +1,9 @@
package org.codiki.domain.traffic.exception;
import org.codiki.domain.exception.FunctionnalException;
public class TrafficTraceCreationException extends FunctionnalException {
public TrafficTraceCreationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,14 @@
package org.codiki.domain.traffic.model;
import java.util.Arrays;
import java.util.Optional;
public enum HttpMethod {
GET, POST, PUT, DELETE;
public static Optional<HttpMethod> fromString(String methodAsString) {
return Arrays.stream(values())
.filter(method -> method.name().equals(methodAsString))
.findFirst();
}
}

View File

@@ -0,0 +1,6 @@
package org.codiki.domain.traffic.model;
public record TrafficEndpoint(
HttpMethod method,
String path
) {}

View File

@@ -0,0 +1,55 @@
package org.codiki.domain.traffic.model;
import java.time.ZonedDateTime;
import java.util.UUID;
public record TrafficTrace(
UUID id,
ZonedDateTime dateTime,
TrafficEndpoint endpoint,
UUID userId,
String correlationId
) {
public static Builder aTrafficTrace() {
return new Builder();
}
public static class Builder {
private UUID id;
private ZonedDateTime dateTime;
private TrafficEndpoint endpoint;
private UUID userId;
private String correlationId;
private Builder() {}
public Builder withId(UUID id) {
this.id = id;
return this;
}
public Builder withDateTime(ZonedDateTime dateTime) {
this.dateTime = dateTime;
return this;
}
public Builder withEndpoint(TrafficEndpoint endpoint) {
this.endpoint = endpoint;
return this;
}
public Builder withUserId(UUID userId) {
this.userId = userId;
return this;
}
public Builder withCorrelationId(String correlationId) {
this.correlationId = correlationId;
return this;
}
public TrafficTrace build() {
return new TrafficTrace(id, dateTime, endpoint, userId, correlationId);
}
}
}

View File

@@ -0,0 +1,14 @@
package org.codiki.domain.traffic.port;
import org.codiki.domain.traffic.model.TrafficTrace;
import java.time.ZonedDateTime;
import java.util.List;
public interface TrafficTracePort {
void save(TrafficTrace trace);
List<TrafficTrace> getAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate);
List<TrafficTrace> getAllByCorrelationId(String correlationId);
Integer countAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate);
Integer countByCorrelationId(String correlationId);
}

View File

@@ -25,6 +25,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
@@ -33,28 +37,5 @@
<groupId>org.apache.tika</groupId> <groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId> <artifactId>tika-core</artifactId>
</dependency> </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-security</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.postgresql</groupId>-->
<!-- <artifactId>postgresql</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- </dependency>-->
<!-- <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>-->
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,12 @@
package org.codiki.exposition.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAspectJAutoProxy
@EnableAsync
public class TrafficTraceConfiguration {
}

View File

@@ -38,10 +38,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
.filter(authorizationHeader -> !isEmpty(authorizationHeader)) .filter(authorizationHeader -> !isEmpty(authorizationHeader))
.filter(authorizationHeader -> authorizationHeader.startsWith(BEARER_PREFIX)) .filter(authorizationHeader -> authorizationHeader.startsWith(BEARER_PREFIX))
.map(authorizationHeader -> authorizationHeader.substring(BEARER_PREFIX.length())) .map(authorizationHeader -> authorizationHeader.substring(BEARER_PREFIX.length()))
.filter(token -> {
String authorizationHeader = request.getHeader(AUTHORIZATION);
return !isEmpty(authorizationHeader) && authorizationHeader.startsWith(BEARER_PREFIX);
})
.filter(jwtService::isValid) .filter(jwtService::isValid)
.flatMap(jwtService::extractUser) .flatMap(jwtService::extractUser)
.map(CustomUserDetails::new) .map(CustomUserDetails::new)

View File

@@ -0,0 +1,75 @@
package org.codiki.exposition.traffic;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.codiki.application.traffic.TrafficTraceUseCases;
import org.codiki.application.user.UserUseCases;
import org.codiki.domain.traffic.model.HttpMethod;
import org.codiki.domain.traffic.model.TrafficEndpoint;
import org.codiki.domain.user.model.User;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Optional;
import java.util.UUID;
@Component
@Aspect
public class ApiCallsLoggerAspect {
private static final String HTTP_HEADER_CORRELATION_ID = "x-correlation-id";
private final TrafficTraceUseCases trafficTraceUseCases;
private final UserUseCases userUseCases;
public ApiCallsLoggerAspect(
TrafficTraceUseCases trafficTraceUseCases,
UserUseCases userUseCases
) {
this.trafficTraceUseCases = trafficTraceUseCases;
this.userUseCases = userUseCases;
}
@Before("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void logGetApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void logPostApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void logPutApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public void logDeleteApiCall(JoinPoint joinPoint) {
logApiCall();
}
private void logApiCall() {
getHttpServletRequest().ifPresent(request ->
Optional.of(request.getMethod())
.flatMap(HttpMethod::fromString)
.ifPresent(queryHttpMethod -> {
String queryUriPath = request.getRequestURI();
TrafficEndpoint endpoint = new TrafficEndpoint(queryHttpMethod, queryUriPath);
UUID userId = userUseCases.getAuthenticatedUser()
.map(User::id)
.orElse(null);
String correlationId = request.getHeader(HTTP_HEADER_CORRELATION_ID);
trafficTraceUseCases.saveNewTrace(endpoint, userId, correlationId);
})
);
}
private static Optional<HttpServletRequest> getHttpServletRequest() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.filter(ServletRequestAttributes.class::isInstance)
.map(ServletRequestAttributes.class::cast)
.map(ServletRequestAttributes::getRequest);
}
}

View File

@@ -0,0 +1,45 @@
package org.codiki.infrastructure.traffic;
import org.codiki.domain.traffic.model.TrafficTrace;
import org.codiki.domain.traffic.port.TrafficTracePort;
import org.codiki.infrastructure.traffic.model.TrafficTraceEntity;
import org.codiki.infrastructure.traffic.repository.TrafficTraceEntityJpaRepository;
import org.springframework.stereotype.Component;
import java.time.ZonedDateTime;
import java.util.List;
@Component
public class TrafficTraceJpaAdapter implements TrafficTracePort {
private final TrafficTraceEntityJpaRepository repository;
public TrafficTraceJpaAdapter(TrafficTraceEntityJpaRepository repository) {
this.repository = repository;
}
@Override
public void save(TrafficTrace trace) {
TrafficTraceEntity entity = new TrafficTraceEntity(trace);
repository.save(entity);
}
@Override
public List<TrafficTrace> getAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate) {
return List.of();
}
@Override
public List<TrafficTrace> getAllByCorrelationId(String correlationId) {
return List.of();
}
@Override
public Integer countAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate) {
return 0;
}
@Override
public Integer countByCorrelationId(String correlationId) {
return 0;
}
}

View File

@@ -0,0 +1,25 @@
package org.codiki.infrastructure.traffic.model;
import org.codiki.domain.traffic.model.HttpMethod;
public enum HttpMethodEntity {
GET, POST, PUT, DELETE;
public HttpMethod toDomain() {
return switch (this) {
case GET -> HttpMethod.GET;
case POST -> HttpMethod.POST;
case PUT -> HttpMethod.PUT;
case DELETE -> HttpMethod.DELETE;
};
}
public static HttpMethodEntity fromDomain(HttpMethod method) {
return switch (method) {
case HttpMethod.GET -> GET;
case HttpMethod.POST -> POST;
case HttpMethod.PUT -> PUT;
case HttpMethod.DELETE -> DELETE;
};
}
}

View File

@@ -0,0 +1,56 @@
package org.codiki.infrastructure.traffic.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.codiki.domain.traffic.model.TrafficEndpoint;
import org.codiki.domain.traffic.model.TrafficTrace;
import java.time.ZonedDateTime;
import java.util.UUID;
import static org.codiki.domain.traffic.model.TrafficTrace.aTrafficTrace;
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "traffic")
public class TrafficTraceEntity {
@Id
private UUID id;
@Column(nullable = false)
private ZonedDateTime dateTime;
@Column(nullable = false)
@Enumerated
private HttpMethodEntity endpointMethod;
@Column(nullable = false)
private String endpointPath;
private UUID userId;
private String correlationId;
public TrafficTraceEntity(TrafficTrace trace) {
id = trace.id();
dateTime = trace.dateTime();
endpointMethod = HttpMethodEntity.fromDomain(trace.endpoint().method());
endpointPath = trace.endpoint().path();
userId = trace.userId();
correlationId = trace.correlationId();
}
public TrafficTrace toDomain() {
return aTrafficTrace()
.withId(id)
.withDateTime(dateTime)
.withEndpoint(new TrafficEndpoint(
endpointMethod.toDomain(),
endpointPath
))
.withUserId(userId)
.withCorrelationId(correlationId)
.build();
}
}

View File

@@ -0,0 +1,12 @@
package org.codiki.infrastructure.traffic.repository;
import org.codiki.infrastructure.traffic.model.TrafficTraceEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface TrafficTraceEntityJpaRepository extends JpaRepository<TrafficTraceEntity, UUID> {
}

View File

@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS traffic (
id UUID NOT NULL,
date_time TIMESTAMP WITH TIME ZONE NOT NULL,
endpoint_method SMALLINT NOT NULL,
endpoint_path VARCHAR NOT NULL,
user_id UUID,
correlation_id VARCHAR,
CONSTRAINT traffic_pk PRIMARY KEY (id),
CONSTRAINT traffic_user_id_fk FOREIGN KEY (user_id) REFERENCES "user" (id)
);
CREATE INDEX traffic_user_id_idx ON traffic (user_id);

View File

@@ -15,11 +15,11 @@
<java.version>21</java.version> <java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
<jakarta.servlet-api.version>6.0.0</jakarta.servlet-api.version> <jakarta.servlet-api.version>6.1.0</jakarta.servlet-api.version>
<java-jwt.version>4.4.0</java-jwt.version> <java-jwt.version>4.4.0</java-jwt.version>
<postgresql.version>42.7.0</postgresql.version>
<tika-core.version>2.9.0</tika-core.version> <tika-core.version>2.9.0</tika-core.version>
<commons-lang3.version>3.14.0</commons-lang3.version> <postgresql.version>42.7.4</postgresql.version>
<commons-lang3.version>3.17.0</commons-lang3.version>
</properties> </properties>
<modules> <modules>
@@ -35,7 +35,7 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId> <artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version> <version>3.3.4</version>
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
@@ -84,8 +84,6 @@
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version> <version>${commons-lang3.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@@ -3,7 +3,7 @@
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"codiki-ng": { "codiki": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
@@ -14,16 +14,19 @@
"sourceRoot": "src", "sourceRoot": "src",
"prefix": "app", "prefix": "app",
"i18n": { "i18n": {
"sourceLocale": "en-UK", "sourceLocale": "en",
"locales": { "locales": {
"fr": "src/locale/messages-fr.json" "fr": {
"translation": "src/locale/messages-fr.json"
}
} }
}, },
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:application", "builder": "@angular-devkit/build-angular:application",
"options": { "options": {
"outputPath": "dist/codiki-ng", "outputPath": "dist/codiki",
"index": "src/index.html", "index": "src/index.html",
"browser": "src/main.ts", "browser": "src/main.ts",
"polyfills": [ "polyfills": [
@@ -56,7 +59,7 @@
} }
], ],
"outputHashing": "all", "outputHashing": "all",
"outputPath": "dist/codiki-ng/en/" "outputPath": "dist/codiki/en/"
}, },
"production-fr": { "production-fr": {
"budgets": [ "budgets": [
@@ -71,8 +74,10 @@
"maximumError": "4kb" "maximumError": "4kb"
} }
], ],
"localize": ["fr"],
"i18nMissingTranslation": "error",
"outputHashing": "all", "outputHashing": "all",
"outputPath": "dist/codiki-ng/fr/" "outputPath": "dist/codiki/fr/"
}, },
"development": { "development": {
"optimization": false, "optimization": false,
@@ -80,18 +85,18 @@
"sourceMap": true "sourceMap": true
}, },
"en": { "en": {
"outputPath": "dist/codiki-ng/en/", "outputPath": "dist/codiki/en/",
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true
}, },
"fr": { "fr": {
"outputPath": "dist/codiki-ng/fr/", "outputPath": "dist/codiki/fr/",
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": true,
"localize": ["fr"], "localize": ["fr"],
"i18nMissingTranslation": "error" "i18nMissingTranslation": "warning"
} }
}, },
"defaultConfiguration": "" "defaultConfiguration": ""
@@ -100,19 +105,19 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production-en": { "production-en": {
"buildTarget": "codiki-ng:build:production-en" "buildTarget": "codiki:build:production-en"
}, },
"production-fr": { "production-fr": {
"buildTarget": "codiki-ng:build:production-fr" "buildTarget": "codiki:build:production-fr"
}, },
"development": { "development": {
"buildTarget": "codiki-ng:build:development" "buildTarget": "codiki:build:development"
}, },
"en": { "en": {
"buildTarget": "codiki-ng:build:en" "buildTarget": "codiki:build:en"
}, },
"fr": { "fr": {
"buildTarget": "codiki-ng:build:fr" "buildTarget": "codiki:build:fr"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
@@ -120,7 +125,7 @@
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"buildTarget": "codiki-ng:build" "buildTarget": "codiki:build"
} }
}, },
"test": { "test": {

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"start-fr": "ng serve --port 4201 --configuration=fr --proxy-config proxy.conf.json", "start-fr": "ng serve --port 4201 --configuration=fr --proxy-config proxy.conf.json",
"build": "ng build", "build": "ng build",
"build-prod-en": "ng build --configuration=production-en --base-href /en/", "build-prod-en": "ng build --configuration=production-en --base-href /en/",
"build-prod-fr": "ng build --configuration=production-fr --base-href /fr/", "build-prod-fr": "ng build --configuration=production-fr --base-href",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"i18n": "npm run i18n-ng-extraction && npm run i18n-fr-file-completion", "i18n": "npm run i18n-ng-extraction && npm run i18n-fr-file-completion",
@@ -17,32 +17,34 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.0.0", "@angular/animations": "^18.2.5",
"@angular/cdk": "^17.3.1", "@angular/cdk": "^18.2.5",
"@angular/common": "^17.0.0", "@angular/common": "^18.2.5",
"@angular/compiler": "^17.0.0", "@angular/compiler": "^18.2.5",
"@angular/core": "^17.0.0", "@angular/core": "^18.2.5",
"@angular/forms": "^17.0.0", "@angular/forms": "^18.2.5",
"@angular/material": "^17.3.1", "@angular/material": "^18.2.5",
"@angular/platform-browser": "^17.0.0", "@angular/platform-browser": "^18.2.5",
"@angular/platform-browser-dynamic": "^17.0.0", "@angular/platform-browser-dynamic": "^18.2.5",
"@angular/router": "^17.0.0", "@angular/router": "^18.2.5",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.14.2" "uuid": "^10.0.0",
"zone.js": "~0.14.10"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.0.5", "@angular-devkit/build-angular": "^18.2.5",
"@angular/cli": "^17.0.5", "@angular/cli": "^18.2.5",
"@angular/compiler-cli": "^17.0.0", "@angular/compiler-cli": "^18.2.5",
"@angular/localize": "^17.3.12", "@angular/localize": "^18.2.5",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/uuid": "^10.0.0",
"jasmine-core": "~5.1.0", "jasmine-core": "~5.1.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.2.2" "typescript": "~5.5.4"
} }
} }

View File

@@ -5,6 +5,7 @@ import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@a
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { JwtInterceptor } from './core/interceptor/jwt.interceptor'; import { JwtInterceptor } from './core/interceptor/jwt.interceptor';
import { CorrelationIdInterceptor } from './core/interceptor/correlation-id.interceptor';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@@ -18,5 +19,6 @@ export const appConfig: ApplicationConfig = {
provideAnimationsAsync(), provideAnimationsAsync(),
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: CorrelationIdInterceptor, multi: true },
] ]
}; };

View File

@@ -0,0 +1,20 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { CorrelationIdService } from "../service/correlation-id.service";
@Injectable()
export class CorrelationIdInterceptor implements HttpInterceptor {
private readonly correlationIdService = inject(CorrelationIdService);
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const correlationId = this.correlationIdService.getCorrelationId();
const requestWithCorrelationId = request.clone({
headers: request.headers.set('x-correlation-id', correlationId)
});
return next.handle(requestWithCorrelationId);
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from "@angular/core";
import * as uuid from 'uuid';
const CORRELATION_ID_KEY = 'correlationId';
@Injectable({
providedIn: 'root'
})
export class CorrelationIdService {
getCorrelationId(): string {
let correlationId = this.getCorrelationFromLocalStorage();
if (correlationId === undefined) {
correlationId = this.createNewCorrelationId();
}
return correlationId;
}
private getCorrelationFromLocalStorage(): string | undefined {
return localStorage.getItem(CORRELATION_ID_KEY) ?? undefined;
}
private createNewCorrelationId(): string {
const newCorrelationId = uuid.v4();
localStorage.setItem(CORRELATION_ID_KEY, newCorrelationId);
return newCorrelationId;
}
}