feat(plugins): sell khanegi plugins as priced subscriptions

Catalog prices, purchase requests, and setting-key activation live on SuperAdmin so module admins can buy add-ons. Also rewrite legacy nerkh logo hosts to the SuperAdmin upload origin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 16:56:51 +03:30
co-authored by Cursor
parent ccf7e90922
commit 1f776cf6e2
16 changed files with 1386 additions and 14 deletions
@@ -22,4 +22,17 @@ export class RuntimeSettingsController {
this.settings.setAdminSmsEnabled(dto.enabled);
return this.settings.getPublic();
}
@Get('plugin-store')
@RequirePermissions('settings:write')
getPluginStore() {
return this.settings.getPublic();
}
@Patch('plugin-store')
@RequirePermissions('settings:write')
setPluginStore(@Body() dto: UpdateAdminSmsDto) {
this.settings.setPluginStoreEnabled(dto.enabled);
return this.settings.getPublic();
}
}
@@ -5,10 +5,13 @@ import { join } from 'path';
type RuntimeSettings = {
/** When false, SuperAdmin stops SMS alerts to managers */
adminSmsEnabled: boolean;
/** When false, khanegi admin plugin store menu is hidden */
pluginStoreEnabled: boolean;
};
const DEFAULTS: RuntimeSettings = {
adminSmsEnabled: true,
pluginStoreEnabled: false,
};
@Injectable()
@@ -28,9 +31,21 @@ export class RuntimeSettingsService {
return next;
}
getPluginStoreEnabled(): boolean {
return this.read().pluginStoreEnabled === true;
}
setPluginStoreEnabled(enabled: boolean) {
const next = { ...this.read(), pluginStoreEnabled: Boolean(enabled) };
this.write(next);
this.logger.log(`Plugin store ${next.pluginStoreEnabled ? 'enabled' : 'disabled'}`);
return next;
}
getPublic() {
return {
adminSmsEnabled: this.getAdminSmsEnabled(),
pluginStoreEnabled: this.getPluginStoreEnabled(),
};
}
@@ -44,6 +59,7 @@ export class RuntimeSettingsService {
const raw = JSON.parse(readFileSync(this.filePath, 'utf8')) as Partial<RuntimeSettings>;
this.cache = {
adminSmsEnabled: raw.adminSmsEnabled !== false,
pluginStoreEnabled: raw.pluginStoreEnabled === true,
};
return this.cache;
} catch (error: any) {