Merge main checkpoint c01a388 into feat/erp-inventar-export
Brings the ERP inventory export up to date with the platform backbone and field capture work. Conflicts in ARCHITECTURE.md, the API key service and the API key settings tab resolved by keeping both sides: - read and write keys stay separate; erp-inventory:read is read-only and cannot be combined with a write scope - the failed-attempt lockout counts in the shared ThrottlerHit table so it holds across API pods - the UI offers only scopes the user holds and creates one scope per key Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
commit
53fab102f2
347 files changed
+26087
-2141
No files matched your search
@@ -1,10 +1,11 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { PrismaService } from '../src/prisma/prisma.service';
|
||||
import { encodeCursor } from '../src/erp-export/erp-cursor';
|
||||
import { API_KEY_FAILURE_LIMIT } from '../src/api-keys/api-keys.service';
|
||||
import { API_KEY_FAILURE_LIMIT } from '../src/api-keys/failed-attempt-limiter';
|
||||
import { createTestApp } from './utils/create-test-app';
|
||||
import { login } from './utils/api';
|
||||
|
||||
@@ -46,6 +47,9 @@ describe('ERP-Inventar-Export (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
let prisma: PrismaService;
|
||||
const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`;
|
||||
// Eigene Zähler-Menge dieses Laufs; eine zweite Instanz mit derselben
|
||||
// Menge spielt einen zweiten api-Pod.
|
||||
const throttlerNamespace = randomUUID();
|
||||
const password = 'SuperSecret123!';
|
||||
let adminToken: string;
|
||||
let readonlyToken: string;
|
||||
@@ -104,7 +108,7 @@ describe('ERP-Inventar-Export (e2e)', () => {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
app = await createTestApp({ throttlerNamespace });
|
||||
prisma = app.get(PrismaService);
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
const owner = await prisma.user.create({
|
||||
@@ -128,7 +132,8 @@ describe('ERP-Inventar-Export (e2e)', () => {
|
||||
readonlyToken = (await login(app, `erp-ro-${suffix}@example.com`, password))
|
||||
.accessToken;
|
||||
erpKey = (await createKey(['erp-inventory:read'])).token;
|
||||
shopKey = (await createKey(['shop-sync:read', 'shop-sync:write'])).token;
|
||||
// Lesen und Buchen sind getrennte Schlüssel; zum Gegenprüfen reicht Lesen.
|
||||
shopKey = (await createKey(['shop-sync:read'])).token;
|
||||
|
||||
const apiary = await prisma.apiary.create({
|
||||
data: { name: `Waldrand ${suffix}`, notes: 'Schlüssel unter dem Stein' },
|
||||
@@ -279,6 +284,9 @@ describe('ERP-Inventar-Export (e2e)', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.throttlerHit.deleteMany({
|
||||
where: { key: { startsWith: `${throttlerNamespace}:` } },
|
||||
});
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -692,6 +700,34 @@ describe('ERP-Inventar-Export (e2e)', () => {
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('zählt Fehlversuche über alle API-Pods gemeinsam', async () => {
|
||||
// Die API läuft mit mehreren Pods: ein Angreifer darf sein Budget
|
||||
// nicht vervielfachen, indem der Load-Balancer ihn verteilt.
|
||||
const secondPod = await createTestApp({ throttlerNamespace });
|
||||
try {
|
||||
const ip = '203.0.113.80';
|
||||
const wrong = `ack_abcd1234_${'V'.repeat(43)}`;
|
||||
const via = (pod: INestApplication<App>) =>
|
||||
request(pod.getHttpServer())
|
||||
.get(`${BASE}/produkte`)
|
||||
.set('Authorization', `Bearer ${wrong}`)
|
||||
.set('X-Forwarded-For', ip);
|
||||
for (let i = 0; i < API_KEY_FAILURE_LIMIT.maxFailures; i++) {
|
||||
await via(i % 2 ? secondPod : app).expect(401);
|
||||
}
|
||||
await via(app).expect(429);
|
||||
await via(secondPod).expect(429);
|
||||
// Ein gültiger Schlüssel kommt auf beiden Pods weiter durch.
|
||||
await request(secondPod.getHttpServer())
|
||||
.get(`${BASE}/produkte`)
|
||||
.set('Authorization', `Bearer ${erpKey}`)
|
||||
.set('X-Forwarded-For', ip)
|
||||
.expect(200);
|
||||
} finally {
|
||||
await secondPod.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('zählt fehlerhaft aufgebaute Schlüssel nicht', async () => {
|
||||
const ip = '203.0.113.70';
|
||||
for (let i = 0; i < API_KEY_FAILURE_LIMIT.maxFailures + 5; i++) {
|
||||
|
||||
Reference in new issue
Block a user