@qwickapps/server 1.1.9 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +318 -0
  2. package/dist/core/control-panel.d.ts +7 -2
  3. package/dist/core/control-panel.d.ts.map +1 -1
  4. package/dist/core/control-panel.js +99 -60
  5. package/dist/core/control-panel.js.map +1 -1
  6. package/dist/core/gateway.d.ts +159 -79
  7. package/dist/core/gateway.d.ts.map +1 -1
  8. package/dist/core/gateway.js +683 -315
  9. package/dist/core/gateway.js.map +1 -1
  10. package/dist/core/index.d.ts +3 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/plugin-registry.d.ts +271 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +326 -0
  17. package/dist/core/plugin-registry.js.map +1 -0
  18. package/dist/core/types.d.ts +16 -33
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
  25. package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
  26. package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
  27. package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
  28. package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
  29. package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
  30. package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
  31. package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
  32. package/dist/plugins/auth/adapters/index.d.ts +9 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +9 -0
  35. package/dist/plugins/auth/adapters/index.js.map +1 -0
  36. package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
  37. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
  38. package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
  39. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
  40. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  41. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  42. package/dist/plugins/auth/auth-plugin.js +255 -0
  43. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  45. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  47. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  48. package/dist/plugins/auth/index.d.ts +12 -0
  49. package/dist/plugins/auth/index.d.ts.map +1 -0
  50. package/dist/plugins/auth/index.js +13 -0
  51. package/dist/plugins/auth/index.js.map +1 -0
  52. package/dist/plugins/auth/types.d.ts +148 -0
  53. package/dist/plugins/auth/types.d.ts.map +1 -0
  54. package/dist/plugins/auth/types.js +14 -0
  55. package/dist/plugins/auth/types.js.map +1 -0
  56. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  57. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  58. package/dist/plugins/bans/bans-plugin.js +428 -0
  59. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  60. package/dist/plugins/bans/index.d.ts +9 -0
  61. package/dist/plugins/bans/index.d.ts.map +1 -0
  62. package/dist/plugins/bans/index.js +10 -0
  63. package/dist/plugins/bans/index.js.map +1 -0
  64. package/dist/plugins/bans/stores/index.d.ts +7 -0
  65. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  66. package/dist/plugins/bans/stores/index.js +7 -0
  67. package/dist/plugins/bans/stores/index.js.map +1 -0
  68. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  69. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  70. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  71. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  72. package/dist/plugins/bans/types.d.ts +128 -0
  73. package/dist/plugins/bans/types.d.ts.map +1 -0
  74. package/dist/plugins/bans/types.js +11 -0
  75. package/dist/plugins/bans/types.js.map +1 -0
  76. package/dist/plugins/cache-plugin.d.ts +14 -3
  77. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  78. package/dist/plugins/cache-plugin.js +27 -7
  79. package/dist/plugins/cache-plugin.js.map +1 -1
  80. package/dist/plugins/cache-plugin.test.js +96 -32
  81. package/dist/plugins/cache-plugin.test.js.map +1 -1
  82. package/dist/plugins/config-plugin.d.ts +3 -2
  83. package/dist/plugins/config-plugin.d.ts.map +1 -1
  84. package/dist/plugins/config-plugin.js +17 -10
  85. package/dist/plugins/config-plugin.js.map +1 -1
  86. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  87. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  88. package/dist/plugins/diagnostics-plugin.js +17 -10
  89. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  90. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  91. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  92. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  93. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  94. package/dist/plugins/entitlements/index.d.ts +12 -0
  95. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  96. package/dist/plugins/entitlements/index.js +16 -0
  97. package/dist/plugins/entitlements/index.js.map +1 -0
  98. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  99. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  100. package/dist/plugins/entitlements/sources/index.js +9 -0
  101. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  102. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  103. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  105. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  106. package/dist/plugins/entitlements/types.d.ts +232 -0
  107. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/types.js +11 -0
  109. package/dist/plugins/entitlements/types.js.map +1 -0
  110. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  111. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  112. package/dist/plugins/frontend-app-plugin.js +14 -9
  113. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  114. package/dist/plugins/health-plugin.d.ts +5 -2
  115. package/dist/plugins/health-plugin.d.ts.map +1 -1
  116. package/dist/plugins/health-plugin.js +20 -5
  117. package/dist/plugins/health-plugin.js.map +1 -1
  118. package/dist/plugins/index.d.ts +8 -2
  119. package/dist/plugins/index.d.ts.map +1 -1
  120. package/dist/plugins/index.js +8 -2
  121. package/dist/plugins/index.js.map +1 -1
  122. package/dist/plugins/logs-plugin.d.ts +3 -2
  123. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  124. package/dist/plugins/logs-plugin.js +21 -12
  125. package/dist/plugins/logs-plugin.js.map +1 -1
  126. package/dist/plugins/postgres-plugin.d.ts +3 -3
  127. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  128. package/dist/plugins/postgres-plugin.js +9 -7
  129. package/dist/plugins/postgres-plugin.js.map +1 -1
  130. package/dist/plugins/postgres-plugin.test.js +47 -29
  131. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  132. package/dist/plugins/users/index.d.ts +12 -0
  133. package/dist/plugins/users/index.d.ts.map +1 -0
  134. package/dist/plugins/users/index.js +13 -0
  135. package/dist/plugins/users/index.js.map +1 -0
  136. package/dist/plugins/users/stores/index.d.ts +7 -0
  137. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  138. package/dist/plugins/users/stores/index.js +7 -0
  139. package/dist/plugins/users/stores/index.js.map +1 -0
  140. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  141. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  142. package/dist/plugins/users/stores/postgres-store.js +157 -0
  143. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  144. package/dist/plugins/users/types.d.ts +189 -0
  145. package/dist/plugins/users/types.d.ts.map +1 -0
  146. package/dist/plugins/users/types.js +12 -0
  147. package/dist/plugins/users/types.js.map +1 -0
  148. package/dist/plugins/users/users-plugin.d.ts +39 -0
  149. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  150. package/dist/plugins/users/users-plugin.js +242 -0
  151. package/dist/plugins/users/users-plugin.js.map +1 -0
  152. package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
  153. package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
  154. package/dist-ui/index.html +1 -1
  155. package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
  156. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  157. package/dist-ui-lib/components/index.d.ts +18 -0
  158. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  159. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  160. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  161. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  162. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
  163. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
  164. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  165. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  166. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  167. package/dist-ui-lib/index.js +6441 -0
  168. package/dist-ui-lib/index.js.map +1 -0
  169. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  170. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  171. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  172. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  173. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  174. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  175. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  176. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  177. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  178. package/package.json +18 -6
  179. package/src/core/control-panel.ts +122 -68
  180. package/src/core/gateway.ts +870 -399
  181. package/src/core/index.ts +21 -2
  182. package/src/core/plugin-registry.ts +653 -0
  183. package/src/core/types.ts +31 -37
  184. package/src/index.ts +118 -19
  185. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  186. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  187. package/src/plugins/auth/adapters/index.ts +9 -0
  188. package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
  189. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  190. package/src/plugins/auth/auth-plugin.ts +303 -0
  191. package/src/plugins/auth/index.ts +33 -0
  192. package/src/plugins/auth/types.ts +165 -0
  193. package/src/plugins/bans/bans-plugin.ts +485 -0
  194. package/src/plugins/bans/index.ts +31 -0
  195. package/src/plugins/bans/stores/index.ts +7 -0
  196. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  197. package/src/plugins/bans/types.ts +141 -0
  198. package/src/plugins/cache-plugin.test.ts +105 -32
  199. package/src/plugins/cache-plugin.ts +40 -9
  200. package/src/plugins/config-plugin.ts +23 -12
  201. package/src/plugins/diagnostics-plugin.ts +22 -12
  202. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  203. package/src/plugins/entitlements/index.ts +51 -0
  204. package/src/plugins/entitlements/sources/index.ts +9 -0
  205. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  206. package/src/plugins/entitlements/types.ts +256 -0
  207. package/src/plugins/frontend-app-plugin.ts +24 -12
  208. package/src/plugins/health-plugin.ts +27 -7
  209. package/src/plugins/index.ts +106 -4
  210. package/src/plugins/logs-plugin.ts +28 -14
  211. package/src/plugins/postgres-plugin.test.ts +49 -29
  212. package/src/plugins/postgres-plugin.ts +11 -9
  213. package/src/plugins/users/index.ts +35 -0
  214. package/src/plugins/users/stores/index.ts +7 -0
  215. package/src/plugins/users/stores/postgres-store.ts +225 -0
  216. package/src/plugins/users/types.ts +209 -0
  217. package/src/plugins/users/users-plugin.ts +281 -0
  218. package/ui/src/App.tsx +185 -31
  219. package/ui/src/api/controlPanelApi.ts +354 -1
  220. package/ui/src/components/ControlPanelApp.tsx +209 -0
  221. package/ui/src/components/index.ts +62 -0
  222. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  223. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  224. package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
  225. package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
  226. package/ui/src/dashboard/builtInWidgets.tsx +29 -0
  227. package/ui/src/dashboard/index.ts +35 -0
  228. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  229. package/ui/src/dashboard/widgets/index.ts +7 -0
  230. package/ui/src/pages/DashboardPage.tsx +28 -149
  231. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  232. package/ui/src/pages/LogsPage.tsx +174 -8
  233. package/ui/src/pages/PluginPage.tsx +148 -0
  234. package/ui/src/pages/SystemPage.tsx +445 -0
  235. package/ui/src/pages/UsersPage.tsx +837 -0
  236. package/ui/tsconfig.lib.json +11 -0
  237. package/ui/vite.lib.config.ts +51 -0
  238. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  239. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  240. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Entitlements Plugin
3
+ *
4
+ * User entitlement management for @qwickapps/server.
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ // Main plugin
10
+ export { createEntitlementsPlugin } from './entitlements-plugin.js';
11
+
12
+ // Helper functions
13
+ export {
14
+ getEntitlementSource,
15
+ isSourceReadonly,
16
+ getEntitlements,
17
+ refreshEntitlements,
18
+ hasEntitlement,
19
+ hasAnyEntitlement,
20
+ hasAllEntitlements,
21
+ grantEntitlement,
22
+ revokeEntitlement,
23
+ setEntitlements,
24
+ getAvailableEntitlements,
25
+ getEntitlementStats,
26
+ invalidateEntitlementCache,
27
+ storeExternalIdMapping,
28
+ invalidateByExternalId,
29
+ // Middleware
30
+ requireEntitlement,
31
+ requireAnyEntitlement,
32
+ requireAllEntitlements,
33
+ } from './entitlements-plugin.js';
34
+
35
+ // Sources
36
+ export { postgresEntitlementSource } from './sources/index.js';
37
+
38
+ // Types
39
+ export type {
40
+ EntitlementSource,
41
+ EntitlementResult,
42
+ EntitlementDefinition,
43
+ EntitlementsPluginConfig,
44
+ EntitlementCallbacks,
45
+ EntitlementsCacheConfig,
46
+ EntitlementsApiConfig,
47
+ PostgresEntitlementSourceConfig,
48
+ UserEntitlement,
49
+ CachedEntitlements,
50
+ EntitlementStats,
51
+ } from './types.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Entitlement Sources
3
+ *
4
+ * Export all available entitlement source implementations.
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ export { postgresEntitlementSource } from './postgres-source.js';
@@ -0,0 +1,253 @@
1
+ /**
2
+ * PostgreSQL Entitlement Source
3
+ *
4
+ * Entitlement storage implementation using PostgreSQL.
5
+ * Stores user entitlements and entitlement definitions.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import type {
11
+ EntitlementSource,
12
+ EntitlementDefinition,
13
+ EntitlementStats,
14
+ PostgresEntitlementSourceConfig,
15
+ } from '../types.js';
16
+
17
+ // Pool interface (from pg package)
18
+ interface PgPool {
19
+ query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
20
+ }
21
+
22
+ /**
23
+ * Create a PostgreSQL entitlement source
24
+ *
25
+ * @param config Configuration including a pg Pool instance or a function that returns one
26
+ * @returns EntitlementSource implementation
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { Pool } from 'pg';
31
+ * import { postgresEntitlementSource } from '@qwickapps/server';
32
+ *
33
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
34
+ * const source = postgresEntitlementSource({ pool });
35
+ *
36
+ * // Or with lazy initialization:
37
+ * const source = postgresEntitlementSource({ pool: () => getPostgres().getPool() });
38
+ * ```
39
+ */
40
+ export function postgresEntitlementSource(config: PostgresEntitlementSourceConfig): EntitlementSource {
41
+ const {
42
+ pool: poolOrFn,
43
+ tableName = 'user_entitlements',
44
+ definitionsTable = 'entitlement_definitions',
45
+ schema = 'public',
46
+ autoCreateTables = true,
47
+ } = config;
48
+
49
+ // Helper to get pool (supports lazy initialization via function)
50
+ const getPool = (): PgPool => {
51
+ const pool = typeof poolOrFn === 'function' ? poolOrFn() : poolOrFn;
52
+ return pool as PgPool;
53
+ };
54
+
55
+ const entitlementsTable = `"${schema}"."${tableName}"`;
56
+ const defsTable = `"${schema}"."${definitionsTable}"`;
57
+
58
+ return {
59
+ name: 'postgres',
60
+ description: 'PostgreSQL local entitlements',
61
+ readonly: false,
62
+
63
+ async initialize(): Promise<void> {
64
+ if (!autoCreateTables) return;
65
+
66
+ // Create entitlement definitions table (catalog of available entitlements)
67
+ await getPool().query(`
68
+ CREATE TABLE IF NOT EXISTS ${defsTable} (
69
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
70
+ name VARCHAR(255) NOT NULL UNIQUE,
71
+ category VARCHAR(100),
72
+ description TEXT,
73
+ created_at TIMESTAMPTZ DEFAULT NOW()
74
+ );
75
+ `);
76
+
77
+ // Create user entitlements table (many-to-many style)
78
+ await getPool().query(`
79
+ CREATE TABLE IF NOT EXISTS ${entitlementsTable} (
80
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
81
+ user_id UUID,
82
+ email VARCHAR(255) NOT NULL,
83
+ entitlement VARCHAR(255) NOT NULL,
84
+ granted_at TIMESTAMPTZ DEFAULT NOW(),
85
+ granted_by VARCHAR(255),
86
+ expires_at TIMESTAMPTZ,
87
+ metadata JSONB DEFAULT '{}',
88
+ UNIQUE(email, entitlement)
89
+ );
90
+
91
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_email ON ${entitlementsTable}(LOWER(email));
92
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${entitlementsTable}(user_id) WHERE user_id IS NOT NULL;
93
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_entitlement ON ${entitlementsTable}(entitlement);
94
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_expires_at ON ${entitlementsTable}(expires_at) WHERE expires_at IS NOT NULL;
95
+ `);
96
+ },
97
+
98
+ async getEntitlements(identifier: string): Promise<string[]> {
99
+ const email = identifier.toLowerCase();
100
+
101
+ const result = await getPool().query(
102
+ `SELECT entitlement FROM ${entitlementsTable}
103
+ WHERE LOWER(email) = $1
104
+ AND (expires_at IS NULL OR expires_at > NOW())
105
+ ORDER BY entitlement`,
106
+ [email]
107
+ );
108
+
109
+ return result.rows.map((row) => (row as { entitlement: string }).entitlement);
110
+ },
111
+
112
+ async getAllAvailable(): Promise<EntitlementDefinition[]> {
113
+ const result = await getPool().query(
114
+ `SELECT id, name, category, description FROM ${defsTable}
115
+ ORDER BY category NULLS LAST, name`
116
+ );
117
+
118
+ return result.rows as EntitlementDefinition[];
119
+ },
120
+
121
+ async getUsersWithEntitlement(
122
+ entitlement: string,
123
+ options: { limit?: number; offset?: number } = {}
124
+ ): Promise<{ emails: string[]; total: number }> {
125
+ const { limit = 50, offset = 0 } = options;
126
+
127
+ // Get total count
128
+ const countResult = await getPool().query(
129
+ `SELECT COUNT(DISTINCT email) FROM ${entitlementsTable}
130
+ WHERE entitlement = $1
131
+ AND (expires_at IS NULL OR expires_at > NOW())`,
132
+ [entitlement]
133
+ );
134
+ const total = parseInt((countResult.rows[0] as { count: string }).count, 10);
135
+
136
+ // Get emails
137
+ const result = await getPool().query(
138
+ `SELECT DISTINCT email FROM ${entitlementsTable}
139
+ WHERE entitlement = $1
140
+ AND (expires_at IS NULL OR expires_at > NOW())
141
+ ORDER BY email
142
+ LIMIT $2 OFFSET $3`,
143
+ [entitlement, limit, offset]
144
+ );
145
+
146
+ return {
147
+ emails: result.rows.map((row) => (row as { email: string }).email),
148
+ total,
149
+ };
150
+ },
151
+
152
+ async addEntitlement(identifier: string, entitlement: string, grantedBy?: string): Promise<void> {
153
+ const email = identifier.toLowerCase();
154
+
155
+ // Use ON CONFLICT to handle duplicates (update granted_at if re-granting)
156
+ await getPool().query(
157
+ `INSERT INTO ${entitlementsTable} (email, entitlement, granted_by)
158
+ VALUES ($1, $2, $3)
159
+ ON CONFLICT (email, entitlement) DO UPDATE SET
160
+ granted_at = NOW(),
161
+ granted_by = EXCLUDED.granted_by,
162
+ expires_at = NULL`,
163
+ [email, entitlement, grantedBy || 'system']
164
+ );
165
+
166
+ // Auto-create definition if it doesn't exist
167
+ await getPool().query(
168
+ `INSERT INTO ${defsTable} (name)
169
+ VALUES ($1)
170
+ ON CONFLICT (name) DO NOTHING`,
171
+ [entitlement]
172
+ );
173
+ },
174
+
175
+ async removeEntitlement(identifier: string, entitlement: string): Promise<void> {
176
+ const email = identifier.toLowerCase();
177
+
178
+ await getPool().query(
179
+ `DELETE FROM ${entitlementsTable}
180
+ WHERE LOWER(email) = $1 AND entitlement = $2`,
181
+ [email, entitlement]
182
+ );
183
+ },
184
+
185
+ async setEntitlements(identifier: string, entitlements: string[]): Promise<void> {
186
+ const email = identifier.toLowerCase();
187
+ const pool = getPool();
188
+
189
+ // Start a transaction
190
+ await pool.query('BEGIN');
191
+
192
+ try {
193
+ // Remove all existing entitlements for this email
194
+ await pool.query(
195
+ `DELETE FROM ${entitlementsTable} WHERE LOWER(email) = $1`,
196
+ [email]
197
+ );
198
+
199
+ // Insert new entitlements
200
+ if (entitlements.length > 0) {
201
+ const values = entitlements
202
+ .map((_, i) => `($1, $${i + 2}, 'system')`)
203
+ .join(', ');
204
+
205
+ await pool.query(
206
+ `INSERT INTO ${entitlementsTable} (email, entitlement, granted_by)
207
+ VALUES ${values}`,
208
+ [email, ...entitlements]
209
+ );
210
+
211
+ // Auto-create definitions for any new entitlements
212
+ for (const ent of entitlements) {
213
+ await pool.query(
214
+ `INSERT INTO ${defsTable} (name)
215
+ VALUES ($1)
216
+ ON CONFLICT (name) DO NOTHING`,
217
+ [ent]
218
+ );
219
+ }
220
+ }
221
+
222
+ await pool.query('COMMIT');
223
+ } catch (error) {
224
+ await pool.query('ROLLBACK');
225
+ throw error;
226
+ }
227
+ },
228
+
229
+ async getStats(): Promise<EntitlementStats> {
230
+ const result = await getPool().query(
231
+ `SELECT
232
+ COUNT(DISTINCT email) as users_with_entitlements,
233
+ COUNT(DISTINCT entitlement) as total_entitlements
234
+ FROM ${entitlementsTable}
235
+ WHERE expires_at IS NULL OR expires_at > NOW()`
236
+ );
237
+
238
+ const row = result.rows[0] as {
239
+ users_with_entitlements: string;
240
+ total_entitlements: string;
241
+ };
242
+
243
+ return {
244
+ usersWithEntitlements: parseInt(row.users_with_entitlements, 10) || 0,
245
+ totalEntitlements: parseInt(row.total_entitlements, 10) || 0,
246
+ };
247
+ },
248
+
249
+ async shutdown(): Promise<void> {
250
+ // Pool is managed externally, nothing to do here
251
+ },
252
+ };
253
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Entitlements Plugin Types
3
+ *
4
+ * Type definitions for entitlement management.
5
+ * Entitlements are string-based tags (e.g., 'pro', 'enterprise', 'feature:analytics').
6
+ * Sources can be local (PostgreSQL) or remote (Keap, Stripe, etc.).
7
+ *
8
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
9
+ */
10
+
11
+ /**
12
+ * Entitlement definition - describes an available entitlement
13
+ */
14
+ export interface EntitlementDefinition {
15
+ /** Unique ID (UUID for Postgres, tag ID for external sources) */
16
+ id: string;
17
+ /** Display name */
18
+ name: string;
19
+ /** Category for grouping (optional) */
20
+ category?: string;
21
+ /** Description (optional) */
22
+ description?: string;
23
+ }
24
+
25
+ /**
26
+ * Result from entitlement lookup
27
+ */
28
+ export interface EntitlementResult {
29
+ /** Identifier used for lookup (email) */
30
+ identifier: string;
31
+ /** Array of entitlement strings (tag names) */
32
+ entitlements: string[];
33
+ /** Source that provided the entitlements */
34
+ source: 'cache' | string;
35
+ /** When the data was cached (ISO string) */
36
+ cachedAt?: string;
37
+ /** When the cache expires (ISO string) */
38
+ expiresAt?: string;
39
+ /** Per-source breakdown (when multiple sources) */
40
+ bySource?: Record<string, string[]>;
41
+ /** Additional metadata from source */
42
+ metadata?: Record<string, unknown>;
43
+ }
44
+
45
+ /**
46
+ * User entitlement record (for writable sources)
47
+ */
48
+ export interface UserEntitlement {
49
+ /** Record ID */
50
+ id: string;
51
+ /** User ID (optional, for linking to users table) */
52
+ user_id?: string;
53
+ /** User email (always present) */
54
+ email: string;
55
+ /** Entitlement name/tag */
56
+ entitlement: string;
57
+ /** When granted */
58
+ granted_at: Date;
59
+ /** Who granted it */
60
+ granted_by?: string;
61
+ /** When it expires (null = permanent) */
62
+ expires_at?: Date;
63
+ /** Additional metadata */
64
+ metadata?: Record<string, unknown>;
65
+ }
66
+
67
+ /**
68
+ * EntitlementSource interface - adapter pattern for pluggable sources
69
+ *
70
+ * Identifier is typically email, but sources may support other identifiers
71
+ * (e.g., Keap contact ID). The plugin normalizes to email for caching.
72
+ */
73
+ export interface EntitlementSource {
74
+ /** Unique source name (e.g., 'postgres', 'keap') */
75
+ name: string;
76
+
77
+ /** Human-readable description */
78
+ description?: string;
79
+
80
+ /** Whether this source is read-only (no add/remove operations) */
81
+ readonly?: boolean;
82
+
83
+ /**
84
+ * Initialize the source (create tables, establish connections, etc.)
85
+ */
86
+ initialize(): Promise<void>;
87
+
88
+ /**
89
+ * Get entitlements for an identifier
90
+ * @param identifier Email or other identifier
91
+ * @returns Array of entitlement strings (tag names)
92
+ */
93
+ getEntitlements(identifier: string): Promise<string[]>;
94
+
95
+ /**
96
+ * Get all available entitlements that can be assigned
97
+ * Optional - returns empty array if not implemented
98
+ */
99
+ getAllAvailable?(): Promise<EntitlementDefinition[]>;
100
+
101
+ /**
102
+ * Search for users with a specific entitlement
103
+ * Optional - for sources that support reverse lookup
104
+ */
105
+ getUsersWithEntitlement?(entitlement: string, options?: {
106
+ limit?: number;
107
+ offset?: number;
108
+ }): Promise<{ emails: string[]; total: number }>;
109
+
110
+ /**
111
+ * Add an entitlement to a user
112
+ * @throws Error if source is read-only
113
+ */
114
+ addEntitlement?(identifier: string, entitlement: string, grantedBy?: string): Promise<void>;
115
+
116
+ /**
117
+ * Remove an entitlement from a user
118
+ * @throws Error if source is read-only
119
+ */
120
+ removeEntitlement?(identifier: string, entitlement: string): Promise<void>;
121
+
122
+ /**
123
+ * Bulk set entitlements for a user (replaces all existing)
124
+ * Optional - for sources that support batch operations
125
+ */
126
+ setEntitlements?(identifier: string, entitlements: string[]): Promise<void>;
127
+
128
+ /**
129
+ * Shutdown the source (close connections, cleanup)
130
+ */
131
+ shutdown(): Promise<void>;
132
+
133
+ /**
134
+ * Check if the source is healthy (optional)
135
+ * Used for health checks without making expensive API calls.
136
+ * If not implemented, health check will assume healthy if initialized.
137
+ */
138
+ isHealthy?(): Promise<boolean>;
139
+
140
+ /**
141
+ * Get statistics about entitlements (optional)
142
+ * Used for dashboard widgets showing user counts with entitlements.
143
+ */
144
+ getStats?(): Promise<EntitlementStats>;
145
+ }
146
+
147
+ /**
148
+ * Statistics about entitlements from a source
149
+ */
150
+ export interface EntitlementStats {
151
+ /** Total users with at least one entitlement */
152
+ usersWithEntitlements: number;
153
+ /** Total number of unique entitlements/tags */
154
+ totalEntitlements?: number;
155
+ /** Additional source-specific stats */
156
+ [key: string]: unknown;
157
+ }
158
+
159
+ /**
160
+ * Entitlement callbacks
161
+ */
162
+ export interface EntitlementCallbacks {
163
+ /** Called when entitlements are fetched */
164
+ onFetch?: (identifier: string, entitlements: string[], source: string) => Promise<void>;
165
+ /** Called when entitlements change */
166
+ onChange?: (identifier: string, added: string[], removed: string[]) => Promise<void>;
167
+ /** Called when an entitlement is granted */
168
+ onGrant?: (identifier: string, entitlement: string, grantedBy?: string) => Promise<void>;
169
+ /** Called when an entitlement is revoked */
170
+ onRevoke?: (identifier: string, entitlement: string) => Promise<void>;
171
+ }
172
+
173
+ /**
174
+ * Cache configuration for entitlements
175
+ */
176
+ export interface EntitlementsCacheConfig {
177
+ /** Enable caching (default: true) */
178
+ enabled?: boolean;
179
+ /** Cache instance name from cache plugin (default: 'default') */
180
+ instanceName?: string;
181
+ /** Cache key prefix (default: 'entitlements:') */
182
+ keyPrefix?: string;
183
+ /** TTL in seconds for cached entitlements (default: 300) */
184
+ ttl?: number;
185
+ /** TTL for identifier mappings, e.g., contactId -> email (default: ttl * 2) */
186
+ mappingTtl?: number;
187
+ }
188
+
189
+ /**
190
+ * API configuration for entitlements
191
+ */
192
+ export interface EntitlementsApiConfig {
193
+ /** API route prefix (default: '/entitlements'). Note: routes are mounted under /api by control panel */
194
+ prefix?: string;
195
+ /** Enable API endpoints (default: true) */
196
+ enabled?: boolean;
197
+ /** Enable write endpoints (grant/revoke) - only works with writable source */
198
+ enableWrite?: boolean;
199
+ }
200
+
201
+ /**
202
+ * Entitlements plugin configuration
203
+ */
204
+ export interface EntitlementsPluginConfig {
205
+ /** Primary entitlement source */
206
+ source: EntitlementSource;
207
+
208
+ /** Additional sources to query (results are merged) */
209
+ additionalSources?: EntitlementSource[];
210
+
211
+ /** Cache configuration */
212
+ cache?: EntitlementsCacheConfig;
213
+
214
+ /** Callbacks for entitlement events */
215
+ callbacks?: EntitlementCallbacks;
216
+
217
+ /** API configuration */
218
+ api?: EntitlementsApiConfig;
219
+
220
+ /** Enable debug logging */
221
+ debug?: boolean;
222
+ }
223
+
224
+ /**
225
+ * PostgreSQL entitlement source configuration
226
+ */
227
+ export interface PostgresEntitlementSourceConfig {
228
+ /** PostgreSQL pool instance or a function that returns one (for lazy initialization) */
229
+ pool: unknown | (() => unknown);
230
+ /** User entitlements table name (default: 'user_entitlements') */
231
+ tableName?: string;
232
+ /** Entitlement definitions table (default: 'entitlement_definitions') */
233
+ definitionsTable?: string;
234
+ /** Schema name (default: 'public') */
235
+ schema?: string;
236
+ /** Auto-create tables on init (default: true) */
237
+ autoCreateTables?: boolean;
238
+ }
239
+
240
+ /**
241
+ * Cached entitlements structure (stored in Redis)
242
+ */
243
+ export interface CachedEntitlements {
244
+ /** Email (normalized lowercase) */
245
+ email: string;
246
+ /** Combined entitlements from all sources */
247
+ entitlements: string[];
248
+ /** Per-source breakdown */
249
+ bySource: Record<string, string[]>;
250
+ /** When cached (ISO string) */
251
+ cachedAt: string;
252
+ /** When expires (ISO string) */
253
+ expiresAt: string;
254
+ /** Cache version for invalidation */
255
+ version: number;
256
+ }
@@ -13,7 +13,8 @@
13
13
  import express from 'express';
14
14
  import { existsSync } from 'node:fs';
15
15
  import { resolve } from 'node:path';
16
- import type { ControlPanelPlugin, FrontendAppConfig } from '../core/types.js';
16
+ import type { Plugin, PluginConfig, PluginRegistry } from '../core/plugin-registry.js';
17
+ import type { FrontendAppConfig } from '../core/types.js';
17
18
  import { createRouteGuard } from '../core/guards.js';
18
19
 
19
20
  export interface FrontendAppPluginConfig {
@@ -27,25 +28,32 @@ export interface FrontendAppPluginConfig {
27
28
  heading?: string;
28
29
  description?: string;
29
30
  links?: Array<{ label: string; url: string }>;
31
+ /** URL path to the logo icon (SVG, PNG, etc.) */
32
+ logoIconUrl?: string;
30
33
  branding?: {
31
- logo?: string;
32
34
  primaryColor?: string;
33
35
  };
34
36
  };
35
37
  /** Route guard configuration */
36
38
  guard?: FrontendAppConfig['mount']['guard'];
39
+ /** Product name for default landing page */
40
+ productName?: string;
41
+ /** Mount path for control panel link */
42
+ mountPath?: string;
37
43
  }
38
44
 
39
45
  /**
40
46
  * Create a frontend app plugin that handles the root path
41
47
  */
42
- export function createFrontendAppPlugin(config: FrontendAppPluginConfig): ControlPanelPlugin {
48
+ export function createFrontendAppPlugin(config: FrontendAppPluginConfig): Plugin {
43
49
  return {
44
- name: 'frontend-app',
45
- order: 0, // Run first to capture root path
50
+ id: 'frontend-app',
51
+ name: 'Frontend App Plugin',
52
+ version: '1.0.0',
46
53
 
47
- async onInit(context) {
48
- const { app, logger } = context;
54
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
55
+ const logger = registry.getLogger('frontend-app');
56
+ const app = registry.getApp();
49
57
 
50
58
  // Apply guard if configured
51
59
  if (config.guard && config.guard.type !== 'none') {
@@ -95,16 +103,20 @@ export function createFrontendAppPlugin(config: FrontendAppPluginConfig): Contro
95
103
  logger.info(`Frontend app: Serving default welcome page`);
96
104
  app.get('/', (_req, res) => {
97
105
  const html = generateLandingPageHtml({
98
- title: context.config.productName,
99
- heading: `Welcome to ${context.config.productName}`,
106
+ title: config.productName || 'QwickApps Server',
107
+ heading: `Welcome to ${config.productName || 'QwickApps Server'}`,
100
108
  description: 'Your application is running.',
101
109
  links: [
102
- { label: 'Control Panel', url: context.config.mountPath || '/cpanel' },
110
+ { label: 'Control Panel', url: config.mountPath || '/cpanel' },
103
111
  ],
104
112
  });
105
113
  res.type('html').send(html);
106
114
  });
107
115
  },
116
+
117
+ async onStop(): Promise<void> {
118
+ // Nothing to cleanup
119
+ },
108
120
  };
109
121
  }
110
122
 
@@ -143,7 +155,7 @@ function generateLandingPageHtml(config: NonNullable<FrontendAppPluginConfig['la
143
155
  max-width: 600px;
144
156
  padding: 2rem;
145
157
  }
146
- ${config.branding?.logo ? `
158
+ ${config.logoIconUrl ? `
147
159
  .logo {
148
160
  width: 80px;
149
161
  height: 80px;
@@ -198,7 +210,7 @@ function generateLandingPageHtml(config: NonNullable<FrontendAppPluginConfig['la
198
210
  </head>
199
211
  <body>
200
212
  <div class="container">
201
- ${config.branding?.logo ? `<img src="${config.branding.logo}" alt="Logo" class="logo">` : ''}
213
+ ${config.logoIconUrl ? `<img src="${config.logoIconUrl}" alt="Logo" class="logo">` : ''}
202
214
  <h1>${config.heading || config.title}</h1>
203
215
  ${config.description ? `<p>${config.description}</p>` : ''}
204
216
  ${linksHtml ? `<div class="links">${linksHtml}</div>` : ''}