feat(security): harden the API and lock synchronizations across instances
- Validate the environment at startup (src/config): required variables must be present, HIBOUTIK_BASE_URL must be HTTPS. - Global ValidationPipe (whitelist, forbidNonWhitelisted, transform). - Helmet, CORS disabled unless CORS_ORIGINS is set, TRUST_PROXY support, HTTP header/request timeouts, shutdown hooks. - Rate limiting: 60 req/min/IP globally, 5 req/min on POST /sync. - AllExceptionsFilter: unexpected errors are logged (message + stack, never the raw error object) and answered with a generic 500. - Postgres advisory lock (dedicated connection) so only one synchronization runs at a time across all instances; released by Postgres itself if the process dies. - Fix the Jest moduleNameMapper (unescaped dot also matched .cjs files). - Document the input validation and security rules in CLAUDE.md. - Add class-validator, class-transformer, helmet and @nestjs/throttler; npm audit fix refreshed package-lock.json. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,13 @@ import { CategoryHiboutikService } from './category.hiboutik.service';
|
||||
import { CustomerHiboutikService } from './customer.hiboutik.service';
|
||||
import { SupplierHiboutikService } from './supplier.hiboutik.service';
|
||||
import { ProductHiboutikService } from './product.hiboutik.service';
|
||||
import { ReleaseLock, SyncLockDatabaseRepository } from '../../database/syncLock.database.repository';
|
||||
|
||||
/** Entry point of the Hiboutik → database synchronization: runs each entity sync in dependency order. */
|
||||
@Injectable()
|
||||
export class HiboutikService {
|
||||
private readonly logger = new Logger('HiboutikService');
|
||||
/** In-memory lock: only valid while the app runs as a single instance. */
|
||||
/** Cheap in-process guard, checked before the database lock (which is the one that holds across instances). */
|
||||
private syncInProgress = false;
|
||||
|
||||
constructor(
|
||||
@@ -18,6 +19,7 @@ export class HiboutikService {
|
||||
private readonly customerHiboutikService: CustomerHiboutikService,
|
||||
private readonly productHiboutikService: ProductHiboutikService,
|
||||
private readonly supplierHiboutikService: SupplierHiboutikService,
|
||||
private readonly syncLock: SyncLockDatabaseRepository,
|
||||
) {}
|
||||
|
||||
get isSyncInProgress(): boolean {
|
||||
@@ -25,19 +27,44 @@ export class HiboutikService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a full synchronization in the background.
|
||||
* Starts a full synchronization in the background, if no other one is running on this instance or any other
|
||||
* (Postgres advisory lock, held until the synchronization ends).
|
||||
* @returns false if a synchronization is already running (nothing is started).
|
||||
* @throws if the lock cannot be requested (e.g. database unreachable).
|
||||
*/
|
||||
startSync(): boolean {
|
||||
async startSync(): Promise<boolean> {
|
||||
if (this.syncInProgress) return false;
|
||||
|
||||
// Set before the first await so that concurrent calls on this instance are refused while the lock is requested
|
||||
this.syncInProgress = true;
|
||||
void this.syncAll()
|
||||
.catch((error) => this.logger.error('Global Hiboutik synchronization crashed', (error as Error)?.stack))
|
||||
.finally(() => (this.syncInProgress = false));
|
||||
|
||||
let release: ReleaseLock | null;
|
||||
try {
|
||||
release = await this.syncLock.tryAcquire();
|
||||
} catch (error) {
|
||||
this.syncInProgress = false;
|
||||
throw error;
|
||||
}
|
||||
if (!release) {
|
||||
this.syncInProgress = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
void this.runLocked(release);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Runs the synchronization, then always releases the lock. */
|
||||
private async runLocked(release: ReleaseLock): Promise<void> {
|
||||
try {
|
||||
await this.syncAll();
|
||||
} catch (error) {
|
||||
this.logger.error('Global Hiboutik synchronization crashed', (error as Error)?.stack);
|
||||
} finally {
|
||||
await release();
|
||||
this.syncInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs every sync step in order. A failing step is logged and does not stop the next ones. */
|
||||
async syncAll() {
|
||||
this.logger.log('Starting global Hiboutik synchronization');
|
||||
|
||||
Reference in New Issue
Block a user