4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
|
|
||||||
"nrwl.angular-console",
|
"nrwl.angular-console",
|
||||||
"esbenp.prettier-vscode"
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Controller, Get, Req } from '@nestjs/common';
|
||||||
|
import { Roles } from 'nest-keycloak-connect';
|
||||||
|
|
||||||
|
@Controller('shop')
|
||||||
|
export class UserController {
|
||||||
|
@Get('group')
|
||||||
|
@Roles({ roles: ['shop'] })
|
||||||
|
async getGroups(@Req() req) {
|
||||||
|
return {
|
||||||
|
user: req.user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class HeaderConstants {
|
||||||
|
static readonly AUTHORIZATION = 'authorization';
|
||||||
|
static readonly TRACE_ID = 'x-traceid';
|
||||||
|
|
||||||
|
// Puoi aggiungere qui altri header futuri
|
||||||
|
static readonly CORRELATION_ID = 'x-correlation-id';
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// apps/gateway/src/common/interceptors/axios-forward.service.ts
|
||||||
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { traceStorage } from '../storage/trace-storage';
|
||||||
|
import { HeaderConstants } from '../constants/headers.constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AxiosConfigService implements OnModuleInit {
|
||||||
|
constructor(private readonly httpService: HttpService) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
// Accediamo all'istanza Axios interna di NestJS
|
||||||
|
const axios = this.httpService.axiosRef;
|
||||||
|
|
||||||
|
axios.interceptors.request.use((config) => {
|
||||||
|
// Recuperiamo i dati dal magazzino ALS
|
||||||
|
const store = traceStorage.getStore();
|
||||||
|
|
||||||
|
if (store) {
|
||||||
|
if (store.authorization) {
|
||||||
|
config.headers[HeaderConstants.AUTHORIZATION] = store.authorization;
|
||||||
|
}
|
||||||
|
if (store.traceId) {
|
||||||
|
config.headers[HeaderConstants.TRACE_ID] = store.traceId;
|
||||||
|
}
|
||||||
|
if (store.correlationId) {
|
||||||
|
config.headers[HeaderConstants.CORRELATION_ID] = store.correlationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Axios Outgoing] URL: ${config.url}`);
|
||||||
|
console.log(`[Axios Outgoing] Headers:`, config.headers);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// apps/gateway/src/common/middleware/trace.middleware.ts
|
||||||
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { traceStorage } from '../storage/trace-storage';
|
||||||
|
import { HeaderConstants } from '../constants/headers.constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TraceMiddleware implements NestMiddleware {
|
||||||
|
use(req: any, res: any, next: () => void) {
|
||||||
|
|
||||||
|
// 1. Gestione Authorization: se manca, sarà undefined
|
||||||
|
const auth = req.headers[HeaderConstants.AUTHORIZATION];
|
||||||
|
if (!auth) {
|
||||||
|
console.warn(
|
||||||
|
`[Gateway] Richiesta ricevuta senza header di autorizzazione su: ${req.url}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Gestione TraceID: se manca, ne generiamo uno nuovo (fondamentale per il debug!)
|
||||||
|
const traceId = req.headers[HeaderConstants.TRACE_ID] || uuidv4();
|
||||||
|
const correlationId =
|
||||||
|
req.headers[HeaderConstants.CORRELATION_ID] || uuidv4();
|
||||||
|
|
||||||
|
// Opzionale: rimandiamo il traceId nella risposta per aiutare il frontend
|
||||||
|
res.setHeader(HeaderConstants.TRACE_ID, traceId);
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
authorization: auth,
|
||||||
|
traceId: traceId,
|
||||||
|
correlationId: correlationId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// .run() fa sì che tutto ciò che accade dentro 'next()'
|
||||||
|
// abbia accesso a questo 'store'
|
||||||
|
traceStorage.run(store, () => {
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// apps/gateway/src/common/storage/trace-storage.ts
|
||||||
|
import { AsyncLocalStorage } from 'async_hooks';
|
||||||
|
|
||||||
|
export interface TraceContext {
|
||||||
|
authorization?: string;
|
||||||
|
traceId?: string;
|
||||||
|
correlationId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creiamo un'istanza globale del magazzino
|
||||||
|
export const traceStorage = new AsyncLocalStorage<TraceContext>();
|
||||||
|
|||||||
39
nx.json
39
nx.json
@@ -2,15 +2,38 @@
|
|||||||
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||||
"analytics": false,
|
"analytics": false,
|
||||||
"namedInputs": {
|
"namedInputs": {
|
||||||
"default": [
|
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
||||||
"{projectRoot}/**/*",
|
|
||||||
"sharedGlobals"
|
|
||||||
],
|
|
||||||
"production": [
|
"production": [
|
||||||
"default"
|
"default",
|
||||||
|
"!{projectRoot}/.eslintrc.json",
|
||||||
|
"!{projectRoot}/eslint.config.mjs"
|
||||||
],
|
],
|
||||||
"sharedGlobals": [
|
"sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"]
|
||||||
"{workspaceRoot}/.github/workflows/ci.yml"
|
},
|
||||||
]
|
"plugins": [
|
||||||
|
{
|
||||||
|
"plugin": "@nx/webpack/plugin",
|
||||||
|
"options": {
|
||||||
|
"buildTargetName": "build",
|
||||||
|
"serveTargetName": "serve",
|
||||||
|
"previewTargetName": "preview",
|
||||||
|
"buildDepsTargetName": "build-deps",
|
||||||
|
"watchDepsTargetName": "watch-deps",
|
||||||
|
"serveStaticTargetName": "serve-static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plugin": "@nx/eslint/plugin",
|
||||||
|
"options": {
|
||||||
|
"targetName": "lint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"targetDefaults": {
|
||||||
|
"@nx/js:tsc": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": ["production", "^production"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
package.json
41
package.json
@@ -4,10 +4,47 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.8.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.0",
|
||||||
|
"@nx/eslint": "22.6.4",
|
||||||
|
"@nx/eslint-plugin": "22.6.4",
|
||||||
"@nx/js": "22.6.4",
|
"@nx/js": "22.6.4",
|
||||||
|
"@nx/nest": "^22.6.4",
|
||||||
|
"@nx/node": "22.6.4",
|
||||||
|
"@nx/web": "22.6.4",
|
||||||
|
"@nx/webpack": "22.6.4",
|
||||||
"@nx/workspace": "22.6.4",
|
"@nx/workspace": "22.6.4",
|
||||||
"nx": "22.6.4"
|
"@swc-node/register": "~1.11.1",
|
||||||
|
"@swc/core": "~1.15.5",
|
||||||
|
"@swc/helpers": "~0.5.18",
|
||||||
|
"@types/node": "20.19.9",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"eslint": "^9.8.0",
|
||||||
|
"eslint-config-prettier": "^10.0.0",
|
||||||
|
"jsonc-eslint-parser": "^2.1.0",
|
||||||
|
"nx": "22.6.4",
|
||||||
|
"prettier": "~3.6.2",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"typescript": "~5.9.2",
|
||||||
|
"typescript-eslint": "^8.40.0",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@keycloak/keycloak-admin-client": "^26.5.7",
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
|
"@nestjs/common": "^10.4.22",
|
||||||
|
"@nestjs/config": "^4.0.3",
|
||||||
|
"@nestjs/core": "^10.4.22",
|
||||||
|
"@nestjs/platform-express": "^10.4.22",
|
||||||
|
"axios": "^1.14.0",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"jwks-rsa": "^4.0.1",
|
||||||
|
"keycloak-connect": "^26.1.1",
|
||||||
|
"nest-keycloak-connect": "^1.10.1",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rxjs": "^7.8.0",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8596
pnpm-lock.yaml
generated
8596
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,14 @@
|
|||||||
#
|
#
|
||||||
PORT=3002
|
PORT=3002
|
||||||
#
|
#
|
||||||
KEYCLOAK_URL=http://localhost:8080
|
# Base URL del server
|
||||||
|
KEYCLOAK_BASE_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# URL Auth derivata (concatenazione)
|
||||||
|
KEYCLOAK_URL_AUTH=${KEYCLOAK_BASE_URL}/auth
|
||||||
|
|
||||||
|
# altre configurazioni
|
||||||
KEYCLOAK_REALM=appweb
|
KEYCLOAK_REALM=appweb
|
||||||
KEYCLOAK_CLIENT_ID=gw-negozi
|
KEYCLOAK_CLIENT_ID=user-service
|
||||||
KEYCLOAK_CLIENT_SECRET=Nf0WIneXN9bOleTpgrbj54ypI8AnIbd5
|
KEYCLOAK_CLIENT_SECRET=tCDsN56Y3Ii4h0SfqrT4o6SoV8fw2fe8
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import KeycloakAdminClient from '@keycloak/keycloak-admin-client';
|
||||||
|
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { KeycloakConfigKeys } from '@appweb/shared';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KeycloakService implements OnModuleInit {
|
||||||
|
private kcAdminClient: KeycloakAdminClient;
|
||||||
|
private readonly logger = new Logger(KeycloakService.name);
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
// Inizializzazione del client con i valori presi dal file .env
|
||||||
|
this.kcAdminClient = new KeycloakAdminClient({
|
||||||
|
baseUrl: this.configService.get<string>(KeycloakConfigKeys.BASE_URL),
|
||||||
|
realmName: this.configService.get<string>(KeycloakConfigKeys.REALM),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Autenticazione tramite Service Account
|
||||||
|
await this.kcAdminClient.auth({
|
||||||
|
grantType: 'client_credentials',
|
||||||
|
clientId: this.configService.get<string>(KeycloakConfigKeys.CLIENT_ID),
|
||||||
|
clientSecret: this.configService.get<string>(KeycloakConfigKeys.CLIENT_SECRET),
|
||||||
|
});
|
||||||
|
this.logger.log(
|
||||||
|
'Connessione a Keycloak Admin API stabilita con successo.',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Errore durante l'autenticazione su Keycloak:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esempio: Ottieni tutti i gruppi
|
||||||
|
async getGroups() {
|
||||||
|
return await this.kcAdminClient.groups.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esempio: Ottieni tutti gli utenti
|
||||||
|
async getUsers() {
|
||||||
|
return await this.kcAdminClient.users.find();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Controller, Get, Post, Body } from '@nestjs/common';
|
||||||
|
import { AuthenticatedUser, Roles } from 'nest-keycloak-connect';
|
||||||
|
|
||||||
|
@Controller('user')
|
||||||
|
export class UserController {
|
||||||
|
@Post()
|
||||||
|
@Roles({ roles: ['shop'] }) // Solo chi ha il ruolo admin può creare
|
||||||
|
async createData(@Body() data: any, @AuthenticatedUser() user: any) {
|
||||||
|
// 'user' contiene i dati del JWT (sub, preferred_username, email)
|
||||||
|
const userId = user.sub;
|
||||||
|
const userName = user.preferred_username;
|
||||||
|
|
||||||
|
console.log(`L'utente ${userName} sta creando un record`);
|
||||||
|
|
||||||
|
// Qui chiamerai il tuo service passando userId per il database
|
||||||
|
return { message: 'Dati salvati', creatoDa: userId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user