KC@FlessingLabs 1.0.0: Testmodus ohne Lizenz, Installationsanleitung, Nutzungsbedingungen
Testmodus (ohne gültige Lizenz): 1 Personal-Konto, 2 Kunden; Discord-Bot, E-Mail-Posteingang (IMAP), DATEV-Export und Backups auf externe Ziele nur mit Lizenz. Mit Lizenz gelten deren Grenzen und Module (Lizenz ohne Modulliste = voller Umfang). Bestehende Daten bleiben bei Ablauf vollständig nutzbar, nur Neuanlagen über die Grenzen und die Zusatzfunktionen pausieren. - Edition zentral in @kc/platform/license (getEdition, hasFeature, assertCustomerCapacity, assertStaffCapacity), auch in der CLI durchgesetzt - Einstellungen → Lizenz zeigt die Edition, Hinweisbanner im Testmodus - README mit Installation, Update und Betrieb; systemd-Units unter ops/systemd - .env.example bereinigt (Kommentare nicht mehr hinter Werten, veraltete SMTP/Discord-Variablen entfernt; beides wird in der Oberfläche eingestellt) - LICENSE.md: Nutzungsbedingungen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fdd51b18e2
commit
17267b7c05
26 changed files with 368 additions and 73 deletions
|
|
@ -127,36 +127,75 @@ export async function getEntitlement(): Promise<Entitlement> {
|
|||
trial: !!row.is_trial, expiresAt, configured: true, source: withinOfflineGrace ? 'live' : 'offline-grace', checkedAt, message: row.message,
|
||||
};
|
||||
}
|
||||
/** Wie assertCustomerCapacity: solange Lizenzierung nicht konfiguriert/geprüft ist, nichts einschränken. */
|
||||
// ---- Edition: Testmodus ohne Lizenz, voller Umfang mit Lizenz ---------------------------------------------------
|
||||
/** Funktionen, die eine Lizenz voraussetzen. Modul-Schlüssel im Lizenzsystem (Produkt/Lizenz "modules"). */
|
||||
export const LICENSED_FEATURES = ['discord', 'imap', 'datev', 'backup_remote'] as const;
|
||||
export type LicensedFeature = (typeof LICENSED_FEATURES)[number];
|
||||
export const FEATURE_LABEL: Record<LicensedFeature, string> = { discord: 'Discord-Bot', imap: 'E-Mail-Posteingang (IMAP)', datev: 'DATEV-Export', backup_remote: 'Backups auf externe Ziele' };
|
||||
/** Grenzen des Testmodus (ohne gültige Lizenz). Bestehende Daten bleiben unangetastet, nur Neuanlagen werden begrenzt. */
|
||||
export const TRIAL_LIMITS = { staff: 1, customers: 2 } as const;
|
||||
export interface Edition {
|
||||
mode: 'licensed' | 'trial'; features: LicensedFeature[]; customerLimit: number | null; staffLimit: number | null;
|
||||
/** Warum Testmodus (z. B. kein Schlüssel, abgelaufen); bei Lizenz der Plan. */ reason: string | null;
|
||||
}
|
||||
/** Maßgebliche Edition dieser Installation. Gültige Lizenz: deren Grenzen; eine Lizenz ohne Modulliste schaltet alle
|
||||
* Funktionen frei (unbeschränkter Plan). Ohne gültige Lizenz (kein Schlüssel, ungeprüft, ungültig, abgelaufen oder
|
||||
* länger als die Offline-Frist nicht geprüft): Testmodus mit TRIAL_LIMITS und ohne LICENSED_FEATURES. */
|
||||
export async function getEdition(): Promise<Edition> {
|
||||
// Integrationstests: voller Umfang (wie die abgeschalteten Rate-Limits im Testmodus)
|
||||
if (config.env === 'test') return { mode: 'licensed', features: [...LICENSED_FEATURES], customerLimit: null, staffLimit: null, reason: 'test' };
|
||||
const e = await getEntitlement();
|
||||
if (e.valid) {
|
||||
const mods = e.modules.filter((m): m is LicensedFeature => (LICENSED_FEATURES as readonly string[]).includes(m));
|
||||
return { mode: 'licensed', features: e.modules.length ? mods : [...LICENSED_FEATURES], customerLimit: e.customerLimit, staffLimit: e.userLimit, reason: e.plan };
|
||||
}
|
||||
const reason = e.source === 'unconfigured' ? 'Kein Lizenzschlüssel hinterlegt' : e.source === 'unchecked' ? 'Lizenz noch nicht geprüft' : (e.message ?? 'Lizenz ungültig oder abgelaufen');
|
||||
return { mode: 'trial', features: [], customerLimit: TRIAL_LIMITS.customers, staffLimit: TRIAL_LIMITS.staff, reason };
|
||||
}
|
||||
export async function hasFeature(f: LicensedFeature): Promise<boolean> { return (await getEdition()).features.includes(f); }
|
||||
/** Fehlertext für gesperrte Funktionen (einheitlich in API und Worker). */
|
||||
export const featureRequiresLicense = (f: LicensedFeature) => `${FEATURE_LABEL[f]} ist nur mit Lizenz verfügbar (Einstellungen → Lizenz).`;
|
||||
/** Modul aus der Lizenz (allgemein, z. B. für künftige Zusatzmodule). */
|
||||
export async function hasModule(key: string): Promise<boolean> {
|
||||
const e = await getEntitlement();
|
||||
if (e.source === 'unconfigured' || e.source === 'unchecked') return true;
|
||||
return e.valid && e.modules.includes(key);
|
||||
return e.valid && (e.modules.length === 0 || e.modules.includes(key));
|
||||
}
|
||||
|
||||
export interface CapacityCheck { allowed: boolean; reason?: string; count: number; limit: number | null }
|
||||
/** Transaktionale Kundengrenze: in DERSELBEN DB-Verbindung/Transaktion wie die Kundenanlage/-reaktivierung
|
||||
* aufrufen. GET_LOCK serialisiert gegen gleichzeitige Anfragen auf den letzten freien Platz. Zählt aktive
|
||||
* und gesperrte (suspended) Organisationen; beendete (closed) zählen nicht mehr.
|
||||
*
|
||||
* Solange die Lizenzierung gar nicht konfiguriert ist (kein LICENSE_PROGRAM_KEY/-schlüssel) oder noch nie
|
||||
* erfolgreich geprüft wurde, wird NICHT durchgesetzt - sonst wäre jede frische Installation (und jede Test-
|
||||
* umgebung) ab dem ersten Kunden blockiert. Erst eine tatsächlich geprüfte, ungültige/überschrittene Lizenz
|
||||
* sperrt neue Kunden. */
|
||||
* und gesperrte (suspended) Organisationen; beendete (closed) zählen nicht mehr. Grenze aus der Edition
|
||||
* (Lizenz oder Testmodus). */
|
||||
export async function assertCustomerCapacity(c: PoolConnection): Promise<CapacityCheck> {
|
||||
const ent = await getEntitlement();
|
||||
if (ent.source === 'unconfigured' || ent.source === 'unchecked') return { allowed: true, count: 0, limit: null };
|
||||
if (!ent.valid) return { allowed: false, reason: ent.message ?? 'Keine gültige Lizenz', count: 0, limit: null };
|
||||
const ed = await getEdition();
|
||||
await c.query('SELECT GET_LOCK(?, 10) AS got', ['kc_customer_limit']);
|
||||
try {
|
||||
const row = await one('SELECT COUNT(*) AS n FROM organizations WHERE status IN (\'active\',\'suspended\')', [], c);
|
||||
const count = Number(row?.n ?? 0);
|
||||
if (ent.customerLimit !== null && count >= ent.customerLimit) return { allowed: false, reason: `Kundengrenze erreicht (${count}/${ent.customerLimit}). Bitte Lizenz upgraden.`, count, limit: ent.customerLimit };
|
||||
return { allowed: true, count, limit: ent.customerLimit };
|
||||
if (ed.customerLimit !== null && count >= ed.customerLimit) {
|
||||
const reason = ed.mode === 'trial'
|
||||
? `Testmodus: höchstens ${ed.customerLimit} Kunden. Für weitere Kunden bitte eine Lizenz hinterlegen (Einstellungen → Lizenz).`
|
||||
: `Kundengrenze der Lizenz erreicht (${count}/${ed.customerLimit}). Bitte Lizenz upgraden.`;
|
||||
return { allowed: false, reason, count, limit: ed.customerLimit };
|
||||
}
|
||||
return { allowed: true, count, limit: ed.customerLimit };
|
||||
} finally {
|
||||
await c.query('SELECT RELEASE_LOCK(?)', ['kc_customer_limit']);
|
||||
}
|
||||
}
|
||||
/** Grenze für Personal-Konten (aktive und eingeladene Mitarbeiter). */
|
||||
export async function assertStaffCapacity(): Promise<CapacityCheck> {
|
||||
const ed = await getEdition();
|
||||
const row = await one("SELECT COUNT(*) AS n FROM users WHERE kind = 'staff' AND status IN ('active','invited')");
|
||||
const count = Number(row?.n ?? 0);
|
||||
if (ed.staffLimit !== null && count >= ed.staffLimit) {
|
||||
const reason = ed.mode === 'trial'
|
||||
? `Testmodus: höchstens ${ed.staffLimit} Personal-Konto. Für weitere Mitarbeiter bitte eine Lizenz hinterlegen (Einstellungen → Lizenz).`
|
||||
: `Benutzergrenze der Lizenz erreicht (${count}/${ed.staffLimit}).`;
|
||||
return { allowed: false, reason, count, limit: ed.staffLimit };
|
||||
}
|
||||
return { allowed: true, count, limit: ed.staffLimit };
|
||||
}
|
||||
|
||||
export async function setLicenseKey(licenseKey: string): Promise<void> {
|
||||
await run('UPDATE license_state SET license_key_enc = ?, checked_at = NULL, last_error = NULL WHERE id = 1', [encrypt(licenseKey)]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue