@qwickapps/server 1.3.0 → 1.4.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 (241) hide show
  1. package/README.md +311 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +144 -2
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/plugin-registry.d.ts +36 -0
  6. package/dist/core/plugin-registry.d.ts.map +1 -1
  7. package/dist/core/plugin-registry.js +26 -0
  8. package/dist/core/plugin-registry.js.map +1 -1
  9. package/dist/core/types.d.ts +19 -0
  10. package/dist/core/types.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
  16. package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
  17. package/dist/plugins/auth/adapter-wrapper.js +166 -0
  18. package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
  19. package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
  20. package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
  21. package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
  22. package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
  23. package/dist/plugins/auth/adapters/index.d.ts +1 -0
  24. package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
  25. package/dist/plugins/auth/adapters/index.js +1 -0
  26. package/dist/plugins/auth/adapters/index.js.map +1 -1
  27. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
  28. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
  29. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  30. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  31. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  32. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  33. package/dist/plugins/auth/config-store.d.ts +11 -0
  34. package/dist/plugins/auth/config-store.d.ts.map +1 -0
  35. package/dist/plugins/auth/config-store.js +232 -0
  36. package/dist/plugins/auth/config-store.js.map +1 -0
  37. package/dist/plugins/auth/config-store.test.d.ts +7 -0
  38. package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
  39. package/dist/plugins/auth/config-store.test.js +299 -0
  40. package/dist/plugins/auth/config-store.test.js.map +1 -0
  41. package/dist/plugins/auth/env-config.d.ts +138 -0
  42. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  43. package/dist/plugins/auth/env-config.js +1122 -0
  44. package/dist/plugins/auth/env-config.js.map +1 -0
  45. package/dist/plugins/auth/index.d.ts +7 -1
  46. package/dist/plugins/auth/index.d.ts.map +1 -1
  47. package/dist/plugins/auth/index.js +7 -0
  48. package/dist/plugins/auth/index.js.map +1 -1
  49. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  50. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  51. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  52. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  53. package/dist/plugins/auth/types.d.ts +176 -0
  54. package/dist/plugins/auth/types.d.ts.map +1 -1
  55. package/dist/plugins/auth/types.js.map +1 -1
  56. package/dist/plugins/cache-plugin.test.js +3 -0
  57. package/dist/plugins/cache-plugin.test.js.map +1 -1
  58. package/dist/plugins/index.d.ts +6 -2
  59. package/dist/plugins/index.d.ts.map +1 -1
  60. package/dist/plugins/index.js +5 -1
  61. package/dist/plugins/index.js.map +1 -1
  62. package/dist/plugins/postgres-plugin.test.js +3 -0
  63. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  64. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  65. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  66. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  67. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  68. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  69. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  70. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  71. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  72. package/dist/plugins/preferences/index.d.ts +12 -0
  73. package/dist/plugins/preferences/index.d.ts.map +1 -0
  74. package/dist/plugins/preferences/index.js +13 -0
  75. package/dist/plugins/preferences/index.js.map +1 -0
  76. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  77. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  78. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  79. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  80. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  81. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  82. package/dist/plugins/preferences/stores/index.js +9 -0
  83. package/dist/plugins/preferences/stores/index.js.map +1 -0
  84. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  85. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  86. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  87. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  88. package/dist/plugins/preferences/types.d.ts +91 -0
  89. package/dist/plugins/preferences/types.d.ts.map +1 -0
  90. package/dist/plugins/preferences/types.js +10 -0
  91. package/dist/plugins/preferences/types.js.map +1 -0
  92. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
  93. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
  94. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
  95. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
  96. package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
  97. package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
  98. package/dist/plugins/rate-limit/cleanup.js +72 -0
  99. package/dist/plugins/rate-limit/cleanup.js.map +1 -0
  100. package/dist/plugins/rate-limit/env-config.d.ts +91 -0
  101. package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
  102. package/dist/plugins/rate-limit/env-config.js +318 -0
  103. package/dist/plugins/rate-limit/env-config.js.map +1 -0
  104. package/dist/plugins/rate-limit/index.d.ts +76 -0
  105. package/dist/plugins/rate-limit/index.d.ts.map +1 -0
  106. package/dist/plugins/rate-limit/index.js +79 -0
  107. package/dist/plugins/rate-limit/index.js.map +1 -0
  108. package/dist/plugins/rate-limit/middleware.d.ts +40 -0
  109. package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
  110. package/dist/plugins/rate-limit/middleware.js +169 -0
  111. package/dist/plugins/rate-limit/middleware.js.map +1 -0
  112. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
  113. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
  114. package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
  115. package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
  116. package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
  117. package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
  118. package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
  119. package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
  120. package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
  121. package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
  122. package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
  123. package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
  124. package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
  125. package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
  126. package/dist/plugins/rate-limit/stores/index.js +8 -0
  127. package/dist/plugins/rate-limit/stores/index.js.map +1 -0
  128. package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
  129. package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
  130. package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
  131. package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
  132. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
  133. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
  134. package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
  135. package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
  136. package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
  137. package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
  138. package/dist/plugins/rate-limit/strategies/index.js +27 -0
  139. package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
  140. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
  141. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
  142. package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
  143. package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
  144. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
  145. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
  146. package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
  147. package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
  148. package/dist/plugins/rate-limit/types.d.ts +265 -0
  149. package/dist/plugins/rate-limit/types.d.ts.map +1 -0
  150. package/dist/plugins/rate-limit/types.js +9 -0
  151. package/dist/plugins/rate-limit/types.js.map +1 -0
  152. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  153. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  154. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  155. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  156. package/dist/plugins/users/index.d.ts +2 -2
  157. package/dist/plugins/users/index.d.ts.map +1 -1
  158. package/dist/plugins/users/index.js +1 -1
  159. package/dist/plugins/users/index.js.map +1 -1
  160. package/dist/plugins/users/types.d.ts +36 -0
  161. package/dist/plugins/users/types.d.ts.map +1 -1
  162. package/dist/plugins/users/users-plugin.d.ts +8 -2
  163. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  164. package/dist/plugins/users/users-plugin.js +122 -5
  165. package/dist/plugins/users/users-plugin.js.map +1 -1
  166. package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
  167. package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
  168. package/dist-ui/index.html +1 -1
  169. package/dist-ui-lib/api/controlPanelApi.d.ts +194 -7
  170. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
  171. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
  172. package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
  173. package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
  174. package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
  175. package/dist-ui-lib/index.js +3665 -3945
  176. package/dist-ui-lib/index.js.map +1 -1
  177. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  178. package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
  179. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  180. package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
  181. package/package.json +7 -2
  182. package/src/core/control-panel.ts +161 -2
  183. package/src/core/plugin-registry.ts +63 -0
  184. package/src/core/types.ts +17 -0
  185. package/src/index.ts +45 -0
  186. package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
  187. package/src/plugins/auth/adapter-wrapper.ts +205 -0
  188. package/src/plugins/auth/adapters/index.ts +1 -0
  189. package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
  190. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  191. package/src/plugins/auth/config-store.test.ts +417 -0
  192. package/src/plugins/auth/config-store.ts +305 -0
  193. package/src/plugins/auth/env-config.ts +1279 -0
  194. package/src/plugins/auth/index.ts +30 -0
  195. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  196. package/src/plugins/auth/types.ts +218 -0
  197. package/src/plugins/cache-plugin.test.ts +3 -0
  198. package/src/plugins/index.ts +75 -0
  199. package/src/plugins/postgres-plugin.test.ts +3 -0
  200. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  201. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  202. package/src/plugins/preferences/index.ts +30 -0
  203. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  204. package/src/plugins/preferences/stores/index.ts +9 -0
  205. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  206. package/src/plugins/preferences/types.ts +100 -0
  207. package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
  208. package/src/plugins/rate-limit/cleanup.ts +117 -0
  209. package/src/plugins/rate-limit/env-config.ts +400 -0
  210. package/src/plugins/rate-limit/index.ts +128 -0
  211. package/src/plugins/rate-limit/middleware.ts +212 -0
  212. package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
  213. package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
  214. package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
  215. package/src/plugins/rate-limit/stores/index.ts +8 -0
  216. package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
  217. package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
  218. package/src/plugins/rate-limit/strategies/index.ts +30 -0
  219. package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
  220. package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
  221. package/src/plugins/rate-limit/types.ts +338 -0
  222. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  223. package/src/plugins/users/index.ts +3 -0
  224. package/src/plugins/users/types.ts +38 -0
  225. package/src/plugins/users/users-plugin.ts +142 -5
  226. package/ui/src/App.tsx +35 -14
  227. package/ui/src/api/controlPanelApi.ts +326 -1
  228. package/ui/src/components/ControlPanelApp.tsx +3 -0
  229. package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
  230. package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
  231. package/ui/src/dashboard/builtInWidgets.tsx +13 -3
  232. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
  233. package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
  234. package/ui/src/dashboard/widgets/index.ts +2 -0
  235. package/ui/src/pages/AuthPage.tsx +1103 -0
  236. package/ui/src/pages/IntegrationsPage.tsx +288 -0
  237. package/ui/src/pages/PluginsPage.tsx +394 -0
  238. package/ui/src/pages/RateLimitPage.tsx +292 -0
  239. package/ui/vite.lib.config.ts +5 -0
  240. package/dist-ui/assets/index-Bsp2ntcw.js +0 -465
  241. package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
@@ -207,3 +207,41 @@ export interface UsersPluginConfig {
207
207
  /** Enable debug logging */
208
208
  debug?: boolean;
209
209
  }
210
+
211
+ /**
212
+ * Comprehensive user information aggregated from multiple plugins.
213
+ * Used by /users/:id/info and /users/sync endpoints.
214
+ */
215
+ export interface UserInfo {
216
+ /** Core user data from users plugin */
217
+ user: User;
218
+ /** User's entitlements (if entitlements plugin loaded) */
219
+ entitlements?: string[];
220
+ /** User's preferences (if preferences plugin loaded) */
221
+ preferences?: Record<string, unknown>;
222
+ /** Active ban info (if bans plugin loaded, null if not banned) */
223
+ ban?: {
224
+ id: string;
225
+ reason: string;
226
+ banned_at: Date;
227
+ expires_at?: Date;
228
+ } | null;
229
+ /** User's roles (if roles plugin loaded - future) */
230
+ roles?: string[];
231
+ }
232
+
233
+ /**
234
+ * Input for POST /users/sync endpoint
235
+ */
236
+ export interface UserSyncInput {
237
+ /** User's email address */
238
+ email: string;
239
+ /** External provider ID (e.g., Auth0 user_id) */
240
+ external_id: string;
241
+ /** Provider name (e.g., 'auth0', 'google') */
242
+ provider: string;
243
+ /** User's display name (optional) */
244
+ name?: string;
245
+ /** Profile picture URL (optional) */
246
+ picture?: string;
247
+ }
@@ -18,10 +18,18 @@ import type {
18
18
  CreateUserInput,
19
19
  UpdateUserInput,
20
20
  UserSearchParams,
21
+ UserInfo,
22
+ UserSyncInput,
21
23
  } from './types.js';
24
+ // Import helpers from other plugins for buildUserInfo
25
+ // Note: These imports are used dynamically based on registry.hasPlugin() checks
26
+ import { getEntitlements } from '../entitlements/entitlements-plugin.js';
27
+ import { getPreferences } from '../preferences/preferences-plugin.js';
28
+ import { getActiveBan } from '../bans/bans-plugin.js';
22
29
 
23
30
  // Store instance for helper access
24
31
  let currentStore: UserStore | null = null;
32
+ let currentRegistry: PluginRegistry | null = null;
25
33
 
26
34
  /**
27
35
  * Create the Users plugin
@@ -49,8 +57,9 @@ export function createUsersPlugin(config: UsersPluginConfig): Plugin {
49
57
  await config.store.initialize();
50
58
  log('Users plugin migrations complete');
51
59
 
52
- // Store reference for helper access
60
+ // Store references for helper access
53
61
  currentStore = config.store;
62
+ currentRegistry = registry;
54
63
 
55
64
  // Register health check
56
65
  registry.registerHealthCheck({
@@ -191,6 +200,69 @@ export function createUsersPlugin(config: UsersPluginConfig): Plugin {
191
200
  }
192
201
  },
193
202
  });
203
+
204
+ // GET /users/:id/info - Get comprehensive user info
205
+ registry.addRoute({
206
+ method: 'get',
207
+ path: `${apiPrefix}/:id/info`,
208
+ pluginId: 'users',
209
+ handler: async (req: Request, res: Response) => {
210
+ try {
211
+ const user = await config.store.getById(req.params.id);
212
+ if (!user) {
213
+ return res.status(404).json({ error: 'User not found' });
214
+ }
215
+
216
+ const info = await buildUserInfo(user, registry);
217
+ res.json(info);
218
+ } catch (error) {
219
+ console.error('[UsersPlugin] Get user info error:', error);
220
+ res.status(500).json({ error: 'Failed to get user info' });
221
+ }
222
+ },
223
+ });
224
+
225
+ // POST /users/sync - Find or create user, return comprehensive info
226
+ registry.addRoute({
227
+ method: 'post',
228
+ path: `${apiPrefix}/sync`,
229
+ pluginId: 'users',
230
+ handler: async (req: Request, res: Response) => {
231
+ try {
232
+ const input = req.body as UserSyncInput;
233
+
234
+ // Normalize and validate email
235
+ const email = input.email?.trim().toLowerCase();
236
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
237
+ if (!email || !emailRegex.test(email)) {
238
+ return res.status(400).json({ error: 'Valid email is required' });
239
+ }
240
+
241
+ // Validate required fields
242
+ if (!input.external_id) {
243
+ return res.status(400).json({ error: 'external_id is required' });
244
+ }
245
+ if (!input.provider) {
246
+ return res.status(400).json({ error: 'provider is required' });
247
+ }
248
+
249
+ // Find or create user
250
+ const user = await findOrCreateUser({
251
+ email: email,
252
+ external_id: input.external_id,
253
+ provider: input.provider,
254
+ name: input.name?.trim(),
255
+ picture: input.picture?.trim(),
256
+ });
257
+
258
+ const info = await buildUserInfo(user, registry);
259
+ res.json(info);
260
+ } catch (error) {
261
+ console.error('[UsersPlugin] User sync error:', error);
262
+ res.status(500).json({ error: 'Failed to sync user' });
263
+ }
264
+ },
265
+ });
194
266
  }
195
267
 
196
268
  log('Users plugin started');
@@ -200,6 +272,7 @@ export function createUsersPlugin(config: UsersPluginConfig): Plugin {
200
272
  log('Stopping users plugin');
201
273
  await config.store.shutdown();
202
274
  currentStore = null;
275
+ currentRegistry = null;
203
276
  log('Users plugin stopped');
204
277
  },
205
278
  };
@@ -260,10 +333,7 @@ export async function findOrCreateUser(data: {
260
333
  // Try to find by email
261
334
  user = await currentStore.getByEmail(data.email);
262
335
  if (user) {
263
- // Update with external ID if not set
264
- if (!user.external_id) {
265
- await currentStore.update(user.id, {});
266
- }
336
+ // Note: external_id cannot be updated after user creation for security reasons
267
337
  await currentStore.updateLastLogin(user.id);
268
338
  return user;
269
339
  }
@@ -279,3 +349,70 @@ export async function findOrCreateUser(data: {
279
349
 
280
350
  return user;
281
351
  }
352
+
353
+ /**
354
+ * Build comprehensive user info by aggregating data from all loaded plugins.
355
+ * This helper fetches data from entitlements, preferences, and bans plugins
356
+ * in parallel (if they are loaded) and returns a unified UserInfo object.
357
+ */
358
+ export async function buildUserInfo(user: User, registry: PluginRegistry): Promise<UserInfo> {
359
+ const info: UserInfo = { user };
360
+
361
+ // Fetch data from other plugins in parallel
362
+ const promises: Promise<void>[] = [];
363
+
364
+ if (registry.hasPlugin('entitlements')) {
365
+ promises.push(
366
+ getEntitlements(user.email)
367
+ .then((result) => {
368
+ info.entitlements = result.entitlements;
369
+ })
370
+ .catch((error) => {
371
+ console.error('[UsersPlugin] Failed to fetch entitlements:', error);
372
+ // Continue without entitlements - don't fail the whole request
373
+ })
374
+ );
375
+ }
376
+
377
+ if (registry.hasPlugin('preferences')) {
378
+ promises.push(
379
+ getPreferences(user.id)
380
+ .then((prefs) => {
381
+ info.preferences = prefs;
382
+ })
383
+ .catch((error) => {
384
+ console.error('[UsersPlugin] Failed to fetch preferences:', error);
385
+ // Continue without preferences - don't fail the whole request
386
+ })
387
+ );
388
+ }
389
+
390
+ if (registry.hasPlugin('bans')) {
391
+ promises.push(
392
+ getActiveBan(user.id)
393
+ .then((ban) => {
394
+ // Transform Ban to UserInfo.ban shape (only include relevant fields)
395
+ info.ban = ban
396
+ ? {
397
+ id: ban.id,
398
+ reason: ban.reason,
399
+ banned_at: ban.banned_at,
400
+ expires_at: ban.expires_at,
401
+ }
402
+ : null;
403
+ })
404
+ .catch((error) => {
405
+ console.error('[UsersPlugin] Failed to fetch ban status:', error);
406
+ // Continue without ban info - don't fail the whole request
407
+ })
408
+ );
409
+ }
410
+
411
+ // Future: roles plugin
412
+ // if (registry.hasPlugin('roles')) {
413
+ // promises.push(getUserRoles(user.id).then(roles => info.roles = roles));
414
+ // }
415
+
416
+ await Promise.all(promises);
417
+ return info;
418
+ }
package/ui/src/App.tsx CHANGED
@@ -3,12 +3,20 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
3
3
  import { QwickApp, ProductLogo, Text } from '@qwickapps/react-framework';
4
4
  import { Link, Box } from '@mui/material';
5
5
  import { defaultConfig } from './config/AppConfig';
6
- import { DashboardWidgetProvider } from './dashboard';
6
+ import {
7
+ DashboardWidgetProvider,
8
+ WidgetComponentRegistryProvider,
9
+ getBuiltInWidgetComponents,
10
+ } from './dashboard';
7
11
  import { DashboardPage } from './pages/DashboardPage';
8
12
  import { LogsPage } from './pages/LogsPage';
9
13
  import { SystemPage } from './pages/SystemPage';
14
+ import { PluginsPage } from './pages/PluginsPage';
10
15
  import { UsersPage } from './pages/UsersPage';
11
16
  import { EntitlementsPage } from './pages/EntitlementsPage';
17
+ import { AuthPage } from './pages/AuthPage';
18
+ import { RateLimitPage } from './pages/RateLimitPage';
19
+ import { IntegrationsPage } from './pages/IntegrationsPage';
12
20
  import { PluginPage } from './pages/PluginPage';
13
21
  import { NotFoundPage } from './pages/NotFoundPage';
14
22
  import { api, type MenuContribution } from './api/controlPanelApi';
@@ -24,6 +32,7 @@ interface NavigationItem {
24
32
  // Core navigation items always shown
25
33
  const coreNavigationItems: NavigationItem[] = [
26
34
  { id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
35
+ { id: 'plugins', label: 'Plugins', route: '/plugins', icon: 'extension' },
27
36
  { id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
28
37
  { id: 'system', label: 'System', route: '/system', icon: 'settings' },
29
38
  ];
@@ -34,7 +43,7 @@ const builtInPluginNavItems: Record<string, NavigationItem> = {
34
43
  };
35
44
 
36
45
  // Routes that have dedicated page components
37
- const dedicatedRoutes = new Set(['/', '/logs', '/system', '/users', '/entitlements']);
46
+ const dedicatedRoutes = new Set(['/', '/plugins', '/logs', '/system', '/users', '/entitlements', '/auth', '/rate-limits', '/integrations']);
38
47
 
39
48
  // Package version - injected at build time or fallback
40
49
  const SERVER_VERSION = '1.0.0';
@@ -175,19 +184,21 @@ export function App() {
175
184
 
176
185
  return (
177
186
  <BrowserRouter basename={basePath || undefined}>
178
- <DashboardWidgetProvider>
179
- <QwickApp
180
- config={defaultConfig}
181
- logo={logo}
182
- footerContent={footerContent}
183
- enableScaffolding={true}
184
- navigationItems={navigationItems}
185
- showThemeSwitcher={true}
186
- showPaletteSwitcher={true}
187
- >
187
+ <WidgetComponentRegistryProvider initialComponents={getBuiltInWidgetComponents()}>
188
+ <DashboardWidgetProvider>
189
+ <QwickApp
190
+ config={defaultConfig}
191
+ logo={logo}
192
+ footerContent={footerContent}
193
+ enableScaffolding={true}
194
+ navigationItems={navigationItems}
195
+ showThemeSwitcher={true}
196
+ showPaletteSwitcher={true}
197
+ >
188
198
  <Routes>
189
199
  {/* Core routes */}
190
200
  <Route path="/" element={<DashboardPage />} />
201
+ <Route path="/plugins" element={<PluginsPage />} />
191
202
  <Route path="/logs" element={<LogsPage />} />
192
203
  <Route path="/system" element={<SystemPage />} />
193
204
 
@@ -198,6 +209,15 @@ export function App() {
198
209
  {registeredPlugins.has('entitlements') && (
199
210
  <Route path="/entitlements" element={<EntitlementsPage />} />
200
211
  )}
212
+ {registeredPlugins.has('auth') && (
213
+ <Route path="/auth" element={<AuthPage />} />
214
+ )}
215
+ {registeredPlugins.has('rate-limit') && (
216
+ <Route path="/rate-limits" element={<RateLimitPage />} />
217
+ )}
218
+ {registeredPlugins.has('ai-proxy') && (
219
+ <Route path="/integrations" element={<IntegrationsPage />} />
220
+ )}
201
221
 
202
222
  {/* Dynamic plugin routes - render generic PluginPage for non-dedicated routes */}
203
223
  {pluginMenuItems
@@ -212,8 +232,9 @@ export function App() {
212
232
 
213
233
  <Route path="*" element={<NotFoundPage />} />
214
234
  </Routes>
215
- </QwickApp>
216
- </DashboardWidgetProvider>
235
+ </QwickApp>
236
+ </DashboardWidgetProvider>
237
+ </WidgetComponentRegistryProvider>
217
238
  </BrowserRouter>
218
239
  );
219
240
  }
@@ -203,6 +203,180 @@ export interface UiContributionsResponse {
203
203
  plugins: Array<{ id: string; name: string; version?: string; status: string }>;
204
204
  }
205
205
 
206
+ // ==================
207
+ // Plugin Detail Types
208
+ // ==================
209
+
210
+ export interface ConfigContribution {
211
+ id: string;
212
+ component: string;
213
+ title?: string;
214
+ pluginId: string;
215
+ }
216
+
217
+ export interface PluginContributions {
218
+ routes: Array<{ method: string; path: string }>;
219
+ menuItems: MenuContribution[];
220
+ pages: PageContribution[];
221
+ widgets: WidgetContribution[];
222
+ config?: ConfigContribution;
223
+ }
224
+
225
+ export interface PluginInfo {
226
+ id: string;
227
+ name: string;
228
+ version?: string;
229
+ status: 'starting' | 'active' | 'stopped' | 'error';
230
+ error?: string;
231
+ contributionCounts: {
232
+ routes: number;
233
+ menuItems: number;
234
+ pages: number;
235
+ widgets: number;
236
+ hasConfig: boolean;
237
+ };
238
+ }
239
+
240
+ export interface PluginsResponse {
241
+ plugins: PluginInfo[];
242
+ }
243
+
244
+ export interface PluginDetailResponse {
245
+ id: string;
246
+ name: string;
247
+ version?: string;
248
+ status: 'starting' | 'active' | 'stopped' | 'error';
249
+ error?: string;
250
+ contributions: PluginContributions;
251
+ }
252
+
253
+ // ==================
254
+ // Auth Config Types
255
+ // ==================
256
+
257
+ export type AuthPluginState = 'disabled' | 'enabled' | 'error';
258
+ export type AuthAdapterType = 'auth0' | 'supabase' | 'supertokens' | 'basic';
259
+
260
+ export interface AuthConfigStatus {
261
+ state: AuthPluginState;
262
+ adapter: string | null;
263
+ error?: string;
264
+ missingVars?: string[];
265
+ config?: Record<string, string>;
266
+ /** Runtime config from database (if available) */
267
+ runtimeConfig?: RuntimeAuthConfig;
268
+ }
269
+
270
+ export interface RuntimeAuthConfig {
271
+ adapter: AuthAdapterType | null;
272
+ config: {
273
+ auth0?: Auth0AdapterConfig;
274
+ supabase?: SupabaseAdapterConfig;
275
+ supertokens?: SupertokensAdapterConfig;
276
+ basic?: BasicAdapterConfig;
277
+ };
278
+ settings: {
279
+ authRequired?: boolean;
280
+ excludePaths?: string[];
281
+ debug?: boolean;
282
+ };
283
+ updatedAt: string;
284
+ updatedBy?: string;
285
+ }
286
+
287
+ export interface Auth0AdapterConfig {
288
+ domain: string;
289
+ clientId: string;
290
+ clientSecret: string;
291
+ baseUrl: string;
292
+ secret: string;
293
+ audience?: string;
294
+ scopes?: string[];
295
+ allowedRoles?: string[];
296
+ allowedDomains?: string[];
297
+ }
298
+
299
+ export interface SupabaseAdapterConfig {
300
+ url: string;
301
+ anonKey: string;
302
+ }
303
+
304
+ export interface BasicAdapterConfig {
305
+ username: string;
306
+ password: string;
307
+ realm?: string;
308
+ }
309
+
310
+ export interface SupertokensAdapterConfig {
311
+ connectionUri: string;
312
+ apiKey?: string;
313
+ appName: string;
314
+ apiDomain: string;
315
+ websiteDomain: string;
316
+ apiBasePath?: string;
317
+ websiteBasePath?: string;
318
+ enableEmailPassword?: boolean;
319
+ socialProviders?: {
320
+ google?: { clientId: string; clientSecret: string };
321
+ apple?: { clientId: string; clientSecret: string; keyId: string; teamId: string };
322
+ github?: { clientId: string; clientSecret: string };
323
+ };
324
+ }
325
+
326
+ export interface UpdateAuthConfigRequest {
327
+ adapter: AuthAdapterType;
328
+ config: Record<string, unknown>;
329
+ settings?: {
330
+ authRequired?: boolean;
331
+ excludePaths?: string[];
332
+ };
333
+ }
334
+
335
+ export interface TestProviderRequest {
336
+ adapter: AuthAdapterType;
337
+ config: Record<string, unknown>;
338
+ provider?: 'google' | 'github' | 'apple';
339
+ }
340
+
341
+ export interface TestProviderResponse {
342
+ success: boolean;
343
+ message: string;
344
+ details?: {
345
+ latency?: number;
346
+ version?: string;
347
+ };
348
+ }
349
+
350
+ // ==================
351
+ // Rate Limit Config Types
352
+ // ==================
353
+
354
+ export type RateLimitStrategy = 'sliding-window' | 'fixed-window' | 'token-bucket';
355
+
356
+ export interface RateLimitConfig {
357
+ windowMs: number;
358
+ maxRequests: number;
359
+ strategy: RateLimitStrategy;
360
+ cleanupEnabled: boolean;
361
+ cleanupIntervalMs: number;
362
+ store: string;
363
+ cache: string;
364
+ cacheAvailable: boolean;
365
+ }
366
+
367
+ export interface RateLimitConfigUpdateRequest {
368
+ windowMs?: number;
369
+ maxRequests?: number;
370
+ strategy?: RateLimitStrategy;
371
+ cleanupEnabled?: boolean;
372
+ cleanupIntervalMs?: number;
373
+ }
374
+
375
+ export interface RateLimitConfigUpdateResponse {
376
+ success: boolean;
377
+ config: RateLimitConfig;
378
+ }
379
+
206
380
  class ControlPanelApi {
207
381
  private baseUrl: string;
208
382
 
@@ -218,6 +392,33 @@ class ControlPanelApi {
218
392
  this.baseUrl = baseUrl;
219
393
  }
220
394
 
395
+ /**
396
+ * Get the base URL for API requests.
397
+ */
398
+ getBaseUrl(): string {
399
+ return this.baseUrl;
400
+ }
401
+
402
+ /**
403
+ * Generic fetch method for API requests.
404
+ * Automatically prepends the base URL and /api prefix.
405
+ */
406
+ async fetch<T = unknown>(path: string, options?: RequestInit): Promise<T> {
407
+ const url = `${this.baseUrl}/api${path.startsWith('/') ? path : `/${path}`}`;
408
+ const response = await fetch(url, {
409
+ ...options,
410
+ headers: {
411
+ 'Content-Type': 'application/json',
412
+ ...options?.headers,
413
+ },
414
+ });
415
+ if (!response.ok) {
416
+ const error = await response.json().catch(() => ({}));
417
+ throw new Error(error.error || error.message || `Request failed: ${response.statusText}`);
418
+ }
419
+ return response.json();
420
+ }
421
+
221
422
  // ==================
222
423
  // Plugin Feature Detection
223
424
  // ==================
@@ -477,7 +678,7 @@ class ControlPanelApi {
477
678
  // Plugins API
478
679
  // ==================
479
680
 
480
- async getPlugins(): Promise<{ plugins: Array<{ id: string; name: string; version?: string }> }> {
681
+ async getPlugins(): Promise<PluginsResponse> {
481
682
  const response = await fetch(`${this.baseUrl}/api/plugins`);
482
683
  if (!response.ok) {
483
684
  throw new Error(`Plugins request failed: ${response.statusText}`);
@@ -485,6 +686,17 @@ class ControlPanelApi {
485
686
  return response.json();
486
687
  }
487
688
 
689
+ async getPluginDetail(id: string): Promise<PluginDetailResponse> {
690
+ const response = await fetch(`${this.baseUrl}/api/plugins/${encodeURIComponent(id)}`);
691
+ if (!response.ok) {
692
+ if (response.status === 404) {
693
+ throw new Error(`Plugin not found: ${id}`);
694
+ }
695
+ throw new Error(`Plugin detail request failed: ${response.statusText}`);
696
+ }
697
+ return response.json();
698
+ }
699
+
488
700
  // ==================
489
701
  // UI Contributions API
490
702
  // ==================
@@ -496,6 +708,119 @@ class ControlPanelApi {
496
708
  }
497
709
  return response.json();
498
710
  }
711
+
712
+ // ==================
713
+ // Auth Config API
714
+ // ==================
715
+
716
+ async getAuthConfigStatus(): Promise<AuthConfigStatus> {
717
+ const response = await fetch(`${this.baseUrl}/api/auth/config/status`);
718
+ if (!response.ok) {
719
+ // Return disabled state if endpoint not available
720
+ if (response.status === 404) {
721
+ return { state: 'disabled', adapter: null };
722
+ }
723
+ throw new Error(`Auth config status request failed: ${response.statusText}`);
724
+ }
725
+ return response.json();
726
+ }
727
+
728
+ async getAuthConfig(): Promise<AuthConfigStatus> {
729
+ const response = await fetch(`${this.baseUrl}/api/auth/config`);
730
+ if (!response.ok) {
731
+ if (response.status === 404) {
732
+ return { state: 'disabled', adapter: null };
733
+ }
734
+ throw new Error(`Auth config request failed: ${response.statusText}`);
735
+ }
736
+ return response.json();
737
+ }
738
+
739
+ /**
740
+ * Update auth configuration (save to database for hot-reload)
741
+ */
742
+ async updateAuthConfig(request: UpdateAuthConfigRequest): Promise<{ success: boolean; message: string }> {
743
+ const response = await fetch(`${this.baseUrl}/api/auth/config`, {
744
+ method: 'PUT',
745
+ headers: { 'Content-Type': 'application/json' },
746
+ body: JSON.stringify(request),
747
+ });
748
+ if (!response.ok) {
749
+ const error = await response.json().catch(() => ({}));
750
+ throw new Error(error.error || `Auth config update failed: ${response.statusText}`);
751
+ }
752
+ return response.json();
753
+ }
754
+
755
+ /**
756
+ * Delete auth configuration (revert to environment variables)
757
+ */
758
+ async deleteAuthConfig(): Promise<{ success: boolean; message: string }> {
759
+ const response = await fetch(`${this.baseUrl}/api/auth/config`, {
760
+ method: 'DELETE',
761
+ });
762
+ if (!response.ok) {
763
+ const error = await response.json().catch(() => ({}));
764
+ throw new Error(error.error || `Auth config delete failed: ${response.statusText}`);
765
+ }
766
+ return response.json();
767
+ }
768
+
769
+ /**
770
+ * Test auth provider connection without saving
771
+ */
772
+ async testAuthProvider(request: TestProviderRequest): Promise<TestProviderResponse> {
773
+ const response = await fetch(`${this.baseUrl}/api/auth/test-provider`, {
774
+ method: 'POST',
775
+ headers: { 'Content-Type': 'application/json' },
776
+ body: JSON.stringify(request),
777
+ });
778
+ if (!response.ok) {
779
+ const error = await response.json().catch(() => ({}));
780
+ throw new Error(error.error || `Provider test failed: ${response.statusText}`);
781
+ }
782
+ return response.json();
783
+ }
784
+
785
+ /**
786
+ * Test current auth provider connection (uses existing env/runtime config)
787
+ */
788
+ async testCurrentAuthProvider(): Promise<TestProviderResponse> {
789
+ const response = await fetch(`${this.baseUrl}/api/auth/test-current`, {
790
+ method: 'POST',
791
+ headers: { 'Content-Type': 'application/json' },
792
+ });
793
+ if (!response.ok) {
794
+ const error = await response.json().catch(() => ({}));
795
+ throw new Error(error.error || `Provider test failed: ${response.statusText}`);
796
+ }
797
+ return response.json();
798
+ }
799
+
800
+ // ==================
801
+ // Rate Limit Config API
802
+ // ==================
803
+
804
+ async getRateLimitConfig(): Promise<RateLimitConfig> {
805
+ const response = await fetch(`${this.baseUrl}/api/rate-limit/config`);
806
+ if (!response.ok) {
807
+ throw new Error(`Rate limit config request failed: ${response.statusText}`);
808
+ }
809
+ return response.json();
810
+ }
811
+
812
+ async updateRateLimitConfig(updates: RateLimitConfigUpdateRequest): Promise<RateLimitConfigUpdateResponse> {
813
+ const response = await fetch(`${this.baseUrl}/api/rate-limit/config`, {
814
+ method: 'PUT',
815
+ headers: { 'Content-Type': 'application/json' },
816
+ body: JSON.stringify(updates),
817
+ });
818
+ if (!response.ok) {
819
+ const error = await response.json().catch(() => ({}));
820
+ throw new Error(error.error || `Rate limit config update failed: ${response.statusText}`);
821
+ }
822
+ return response.json();
823
+ }
499
824
  }
500
825
 
501
826
  export const api = new ControlPanelApi();