feat(crm): add standalone dev environment and test token generator

- docker-compose.crm-dev.yml: isolierte Testumgebung (PostgreSQL + Redis + CRM)
- scripts/generate-token.ts: generiert Test-JWTs fuer curl-basiertes API-Testing
- scripts/init-schema.sql: erstellt app_crm Schema beim DB-Start
- keys/: Test-RSA-Schluessel (nur fuer lokale Entwicklung!)
- Fix: multiSchema previewFeature entfernt (deprecated)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-10 16:03:59 +01:00
parent 8783d01fc0
commit 094db465cb
6 changed files with 203 additions and 3 deletions

View file

@ -0,0 +1,70 @@
# ============================================================
# CRM-Service - Standalone Entwicklungsumgebung
# ============================================================
# Startet PostgreSQL + Redis + CRM isoliert (ohne Core/Frontend)
#
# Starten: docker compose -f docker-compose.crm-dev.yml up -d
# Stoppen: docker compose -f docker-compose.crm-dev.yml down
# Logs: docker compose -f docker-compose.crm-dev.yml logs -f crm
# Reset DB: docker compose -f docker-compose.crm-dev.yml down -v
services:
crm-postgres:
image: postgres:16-alpine
container_name: crm-dev-postgres
environment:
POSTGRES_USER: insight
POSTGRES_PASSWORD: devpassword
POSTGRES_DB: platform_core
ports:
- "15432:5432"
volumes:
- crm-pg-data:/var/lib/postgresql/data
- ./scripts/init-schema.sql:/docker-entrypoint-initdb.d/01-init-schema.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U insight -d platform_core"]
interval: 5s
timeout: 3s
retries: 5
crm-redis:
image: redis:7-alpine
container_name: crm-dev-redis
ports:
- "16379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
crm:
build:
context: .
dockerfile: Dockerfile
target: development
container_name: crm-dev-service
environment:
- NODE_ENV=development
- APP_PORT=3100
- DATABASE_URL=postgresql://insight:devpassword@crm-postgres:5432/platform_core?schema=app_crm
- DATABASE_URL_DIRECT=postgresql://insight:devpassword@crm-postgres:5432/platform_core?schema=app_crm
- REDIS_HOST=crm-redis
- REDIS_PORT=6379
- JWT_PUBLIC_KEY_PATH=/app/keys/jwt-public.pem
- JWT_ISSUER=insight-platform
- CORS_ORIGINS=http://localhost:3100,http://127.0.0.1:3100
volumes:
- .:/app
- /app/node_modules
- ./keys/jwt-public.pem:/app/keys/jwt-public.pem:ro
ports:
- "3100:3100"
depends_on:
crm-postgres:
condition: service_healthy
crm-redis:
condition: service_healthy
volumes:
crm-pg-data:

View file

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwyZKQNL8oGoKlRyZzNtHXiLgDE8Zw+7Ll8XUsgI93CZBPEbL
UCh6C2AZOaar5YdfgD6acX5PQxHnn28x8cAhLDhXcutX+riyJ0Mq+yNnuJrAt645
Yp1pbDRseePm+Z6OnCbKnULvfqautWBRfBDFmAPfF91/tnQPb6CtC7iUVjNpBuK0
eX5UnbSxI5IgDk8tLyG/j8IufVQnNRN7t9oSpvS0wKQpKjMX4hGcESd/zyO0gnam
47mT8v2x7noS6NGZncB2RSmajMvvYx/mZxLk7kf1MRiLUlM+BOUw/hh7Sod0gOLa
S9iLhe+sgu54Tc9cgU2+QY/9EHMqUiT7jxeuewIDAQABAoIBAA+iB5y9yiMHm6Vq
Tx/MCSGPZadWxtedT43u+STxbQcvwVYUzcC0HWK/5gVqRqbye0IIwdKJrcvPqWBq
RfP50i56rPa1x6x8Ezl19gF8SpjNPNb6C/rMZV+Xq4DwMtaYTxBPQzfp3smkjKZW
+vAXX9zXoo2E3vA8x+fHVVV1Cd0FyUvcVdeLT16eEqsOfS347KfUDCQ5Nv63xpj9
dGXVOOmUwWY8pCmW+mz15q7Y1stegCV/XMv7vuxyhFpD1W29NyLvJ8pzKhQxN+xS
/ivE0L+pq+PeQ9/4MERt6q5xp5TQy0lj4kxjnvv+5FkZj2s4KVUr5F0b6vkOhvip
4P9rV6ECgYEA9zxVtI7SBNsRcIo7gRwdy+L0kz6qYWRUpZ+hc8OnZjvtw8BcfzwO
XnlGFUTSfMaPwzJc7Ny/uKHpCovnszUzzgJ0FMv+mLKLU8Noaay2lFRc9zrGef2S
+2bdjTZYjRLt1S7nczowuGesvMJqic30ic0t7xgUZ4AhWCVSgue9pcMCgYEAyhFG
BE+mzkCRcLh9U4mkbXOKsy3SHBhgFpAUPjY9dlnIFpjOITDLS07Wd3NQ7u72qQw/
dDtq17LsoUzpkzcMDVSUUWawfdYwza0IaGfnprECtk38pj0MtRSDJfT8AQQvzdSC
ZAhMFdfa9ZqVQ/1jbU+2aJ2esFXUZ20Mg7VH8OkCgYEAr/pKJtLhuoMTzr8Vy7hv
nQhWfdhE/j2j4p/VE8lYBfTyMDtjm0zsDWLU956dFCNhgNcAPbiC3rCgZ9ldermL
vj8Q0RzCg33SnjSgxVciPkIuSeuUCpDrZfa6DCF6ti+bCfrw05u/wgJJebIIkz39
qXhaEa1aGLGjClLGgFbjLnECgYBAdvpLbc3dhyPfPjxdZlAayv245Clf5Tbie9y5
bDx3gXUgIGfClvqEcAZj3Vo4n+v5SnsD7eDMJ7zuSMdLvAgQSKd4wLYVrzuqokVI
ab2xpE8lMgQkVN2093JPrbSn7loB5IYku7DqVw73w/VS14fc281p2r9BqmA1Dskr
S65D4QKBgHOpjCDmRqzrl+EDOjRZinXHWZqm/lX5pBFBxtTGYstbg7gY4D6nLhsR
mCbYQr7WOf+ouQYkg5ck82K/0Gu46rjkpBcIdbnW1wC9iE1iJxd5u96iEcJNu2oM
H5gx73pkmO4rVnInv67VYeDrGHGd/aix6b5nSDaYg5Ct1yENlzCD
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwyZKQNL8oGoKlRyZzNtH
XiLgDE8Zw+7Ll8XUsgI93CZBPEbLUCh6C2AZOaar5YdfgD6acX5PQxHnn28x8cAh
LDhXcutX+riyJ0Mq+yNnuJrAt645Yp1pbDRseePm+Z6OnCbKnULvfqautWBRfBDF
mAPfF91/tnQPb6CtC7iUVjNpBuK0eX5UnbSxI5IgDk8tLyG/j8IufVQnNRN7t9oS
pvS0wKQpKjMX4hGcESd/zyO0gnam47mT8v2x7noS6NGZncB2RSmajMvvYx/mZxLk
7kf1MRiLUlM+BOUw/hh7Sod0gOLaS9iLhe+sgu54Tc9cgU2+QY/9EHMqUiT7jxeu
ewIDAQAB
-----END PUBLIC KEY-----

View file

@ -6,9 +6,8 @@
// ============================================================
generator client {
provider = "prisma-client-js"
output = "../node_modules/.prisma/crm-client"
previewFeatures = ["multiSchema"]
provider = "prisma-client-js"
output = "../node_modules/.prisma/crm-client"
}
datasource db {

View file

@ -0,0 +1,93 @@
/**
* Test-Token-Generator fuer CRM-Service Entwicklung.
*
* Generiert ein gueltiges JWT mit konfigurierbaren Claims.
*
* Verwendung:
* npx ts-node scripts/generate-token.ts
* npx ts-node scripts/generate-token.ts --role TENANT_ADMIN
* npx ts-node scripts/generate-token.ts --expiry 60 (60 Minuten)
*/
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
function base64url(data: Buffer | string): string {
const buf = typeof data === 'string' ? Buffer.from(data) : data;
return buf.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
function generateToken(options: {
role?: string;
tenantId?: string;
tenantSlug?: string;
userId?: string;
email?: string;
expiryMinutes?: number;
}): string {
const privateKeyPath = path.join(__dirname, '..', 'keys', 'jwt-private.pem');
const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
const now = Math.floor(Date.now() / 1000);
const expiryMinutes = options.expiryMinutes ?? 60;
const header = {
alg: 'RS256',
typ: 'JWT',
};
const payload = {
sub: options.userId ?? 'test-user-00000000-0000-0000-0000-000000000001',
email: options.email ?? 'testuser@insight.local',
role: options.role ?? 'TENANT_ADMIN',
tenantId: options.tenantId ?? 'test-tenant-00000000-0000-0000-0000-000000000001',
tenantSlug: options.tenantSlug ?? 'test_tenant',
jti: crypto.randomUUID(),
iss: 'insight-platform',
iat: now,
exp: now + expiryMinutes * 60,
};
const headerB64 = base64url(JSON.stringify(header));
const payloadB64 = base64url(JSON.stringify(payload));
const signingInput = `${headerB64}.${payloadB64}`;
const sign = crypto.createSign('RSA-SHA256');
sign.update(signingInput);
const signature = sign.sign(privateKey);
const signatureB64 = base64url(signature);
return `${signingInput}.${signatureB64}`;
}
// CLI args parsen
const args = process.argv.slice(2);
const getArg = (name: string): string | undefined => {
const idx = args.indexOf(`--${name}`);
return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined;
};
const token = generateToken({
role: getArg('role'),
tenantId: getArg('tenantId'),
tenantSlug: getArg('tenantSlug'),
userId: getArg('userId'),
email: getArg('email'),
expiryMinutes: getArg('expiry') ? parseInt(getArg('expiry')!, 10) : undefined,
});
console.log('\n=== INSIGHT CRM Test-Token ===\n');
console.log('Token:');
console.log(token);
console.log('\n--- Verwendung mit curl ---\n');
console.log(`export CRM_TOKEN="${token}"\n`);
console.log('# Kontakte auflisten:');
console.log('curl -s http://localhost:3100/api/v1/crm/contacts -H "Authorization: Bearer $CRM_TOKEN" | jq\n');
console.log('# Kontakt erstellen:');
console.log(`curl -s -X POST http://localhost:3100/api/v1/crm/contacts \\
-H "Authorization: Bearer $CRM_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"type":"PERSON","firstName":"Max","lastName":"Mustermann","email":"max@firma.de","companyName":"Firma GmbH"}' | jq\n`);
console.log('# Health-Check (kein Token noetig):');
console.log('curl -s http://localhost:3100/health | jq\n');

View file

@ -0,0 +1,2 @@
-- Erstellt das app_crm Schema fuer die CRM-Tabellen
CREATE SCHEMA IF NOT EXISTS app_crm;