docs: fix remaining warnings in SaaS design spec (round 2)

- Fix metadata.json shown as unencrypted in tree (now .enc)
- Fix admin bypass order in checkPlanLimits (moved before status check)
- Add PM2 cross-worker cache invalidation via process messaging
- Fix fiel_credentials "no changes" contradiction with per-component IV
- Backup all tenant DBs regardless of active status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-03-15 22:53:34 +00:00
parent 3c9268ea30
commit 536a5abd33

View File

@@ -186,16 +186,16 @@ When a client uploads their FIEL (.cer + .key + password):
├── CAS2408138W2/ ├── CAS2408138W2/
│ ├── certificate.cer.enc ← AES-256-GCM encrypted │ ├── certificate.cer.enc ← AES-256-GCM encrypted
│ ├── private_key.key.enc ← AES-256-GCM encrypted │ ├── private_key.key.enc ← AES-256-GCM encrypted
│ └── metadata.json ← serial, validity dates, upload date │ └── metadata.json.enc ← serial, validity dates, upload date (also encrypted)
└── ROEM691011EZ4/ └── ROEM691011EZ4/
├── certificate.cer.enc ├── certificate.cer.enc
├── private_key.key.enc ├── private_key.key.enc
└── metadata.json └── metadata.json.enc
``` ```
**B. Central DB (`fiel_credentials` table):** **B. Central DB (`fiel_credentials` table):**
- Existing structure: `cer_data`, `key_data`, `key_password_encrypted`, `encryption_iv`, `encryption_tag` - Existing structure: `cer_data`, `key_data`, `key_password_encrypted`, `encryption_iv`, `encryption_tag`
- No changes needed to the table structure - **Schema change required:** Add per-component IV/tag columns (`cer_iv`, `cer_tag`, `key_iv`, `key_tag`, `password_iv`, `password_tag`) to support independent encryption per component. Alternatively, use a single JSON column for all encryption metadata. The existing `encryption_iv` and `encryption_tag` columns can be dropped after migration.
### Encryption ### Encryption
@@ -554,7 +554,7 @@ mkdir -p $DAILY_DIR $WEEKLY_DIR
pg_dump -h localhost -U postgres horux360 | gzip > $DAILY_DIR/horux360_$DATE.sql.gz pg_dump -h localhost -U postgres horux360 | gzip > $DAILY_DIR/horux360_$DATE.sql.gz
# Backup each tenant DB # Backup each tenant DB
for db in $(psql -h localhost -U postgres -t -c "SELECT database_name FROM tenants WHERE active = true" horux360); do for db in $(psql -h localhost -U postgres -t -c "SELECT database_name FROM tenants WHERE database_name IS NOT NULL" horux360); do
db_trimmed=$(echo $db | xargs) # trim whitespace db_trimmed=$(echo $db | xargs) # trim whitespace
pg_dump -h localhost -U postgres "$db_trimmed" | gzip > $DAILY_DIR/${db_trimmed}_${DATE}.sql.gz pg_dump -h localhost -U postgres "$db_trimmed" | gzip > $DAILY_DIR/${db_trimmed}_${DATE}.sql.gz
done done
@@ -618,6 +618,12 @@ async function checkPlanLimits(req, res, next) {
const tenant = await getTenantWithCache(req.user.tenantId); // cached 5 min const tenant = await getTenantWithCache(req.user.tenantId); // cached 5 min
const subscription = await getActiveSubscription(tenant.id); const subscription = await getActiveSubscription(tenant.id);
// Admin-impersonated requests bypass subscription check
// (admin needs to complete client setup regardless of payment status)
if (req.headers['x-view-tenant'] && req.user.role === 'admin') {
return next();
}
// Allowed statuses: 'authorized' (paid) or 'pending' (grace period for new clients) // Allowed statuses: 'authorized' (paid) or 'pending' (grace period for new clients)
const allowedStatuses = ['authorized', 'pending']; const allowedStatuses = ['authorized', 'pending'];
@@ -631,12 +637,6 @@ async function checkPlanLimits(req, res, next) {
} }
} }
// Admin-impersonated requests bypass subscription check
// (admin needs to complete client setup regardless of payment status)
if (req.headers['x-view-tenant'] && req.user.role === 'admin') {
return next();
}
next(); next();
} }
``` ```
@@ -733,6 +733,8 @@ Pages also show an "upgrade" message if accessed directly via URL without the re
Plan checks and CFDI counts are cached in-memory with 5-minute TTL to avoid database queries on every request. Plan checks and CFDI counts are cached in-memory with 5-minute TTL to avoid database queries on every request.
**Cache invalidation across PM2 workers:** Since each PM2 cluster worker has its own in-memory cache, subscription status changes (via webhook) must invalidate the cache in all workers. The webhook handler writes the status to the DB, then sends a `process.send()` message to the PM2 master which broadcasts to all workers to invalidate the specific tenant's cache entry. This ensures all workers reflect subscription changes within seconds, not minutes.
--- ---
## Architecture Diagram ## Architecture Diagram