@qwickapps/server 1.2.0 → 1.3.1

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 (299) hide show
  1. package/README.md +392 -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 +120 -54
  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 +679 -319
  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 +307 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +352 -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 +10 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +10 -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/adapters/supertokens-adapter.d.ts +18 -0
  41. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  42. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  43. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  45. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.js +255 -0
  47. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  48. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  49. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  51. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  52. package/dist/plugins/auth/env-config.d.ts +88 -0
  53. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  54. package/dist/plugins/auth/env-config.js +489 -0
  55. package/dist/plugins/auth/env-config.js.map +1 -0
  56. package/dist/plugins/auth/index.d.ts +14 -0
  57. package/dist/plugins/auth/index.d.ts.map +1 -0
  58. package/dist/plugins/auth/index.js +16 -0
  59. package/dist/plugins/auth/index.js.map +1 -0
  60. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  61. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  62. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  63. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  64. package/dist/plugins/auth/types.d.ts +218 -0
  65. package/dist/plugins/auth/types.d.ts.map +1 -0
  66. package/dist/plugins/auth/types.js +14 -0
  67. package/dist/plugins/auth/types.js.map +1 -0
  68. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  69. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  70. package/dist/plugins/bans/bans-plugin.js +428 -0
  71. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  72. package/dist/plugins/bans/index.d.ts +9 -0
  73. package/dist/plugins/bans/index.d.ts.map +1 -0
  74. package/dist/plugins/bans/index.js +10 -0
  75. package/dist/plugins/bans/index.js.map +1 -0
  76. package/dist/plugins/bans/stores/index.d.ts +7 -0
  77. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  78. package/dist/plugins/bans/stores/index.js +7 -0
  79. package/dist/plugins/bans/stores/index.js.map +1 -0
  80. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  81. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  82. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  83. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  84. package/dist/plugins/bans/types.d.ts +128 -0
  85. package/dist/plugins/bans/types.d.ts.map +1 -0
  86. package/dist/plugins/bans/types.js +11 -0
  87. package/dist/plugins/bans/types.js.map +1 -0
  88. package/dist/plugins/cache-plugin.d.ts +14 -3
  89. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  90. package/dist/plugins/cache-plugin.js +27 -7
  91. package/dist/plugins/cache-plugin.js.map +1 -1
  92. package/dist/plugins/cache-plugin.test.js +99 -32
  93. package/dist/plugins/cache-plugin.test.js.map +1 -1
  94. package/dist/plugins/config-plugin.d.ts +3 -2
  95. package/dist/plugins/config-plugin.d.ts.map +1 -1
  96. package/dist/plugins/config-plugin.js +17 -10
  97. package/dist/plugins/config-plugin.js.map +1 -1
  98. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  99. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  100. package/dist/plugins/diagnostics-plugin.js +17 -10
  101. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  102. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  103. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  105. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  106. package/dist/plugins/entitlements/index.d.ts +12 -0
  107. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/index.js +16 -0
  109. package/dist/plugins/entitlements/index.js.map +1 -0
  110. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  111. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  112. package/dist/plugins/entitlements/sources/index.js +9 -0
  113. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  114. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  115. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  116. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  117. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  118. package/dist/plugins/entitlements/types.d.ts +232 -0
  119. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  120. package/dist/plugins/entitlements/types.js +11 -0
  121. package/dist/plugins/entitlements/types.js.map +1 -0
  122. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  123. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  124. package/dist/plugins/frontend-app-plugin.js +14 -9
  125. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  126. package/dist/plugins/health-plugin.d.ts +5 -2
  127. package/dist/plugins/health-plugin.d.ts.map +1 -1
  128. package/dist/plugins/health-plugin.js +20 -5
  129. package/dist/plugins/health-plugin.js.map +1 -1
  130. package/dist/plugins/index.d.ts +10 -2
  131. package/dist/plugins/index.d.ts.map +1 -1
  132. package/dist/plugins/index.js +10 -2
  133. package/dist/plugins/index.js.map +1 -1
  134. package/dist/plugins/logs-plugin.d.ts +3 -2
  135. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  136. package/dist/plugins/logs-plugin.js +21 -12
  137. package/dist/plugins/logs-plugin.js.map +1 -1
  138. package/dist/plugins/postgres-plugin.d.ts +3 -3
  139. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  140. package/dist/plugins/postgres-plugin.js +9 -7
  141. package/dist/plugins/postgres-plugin.js.map +1 -1
  142. package/dist/plugins/postgres-plugin.test.js +50 -29
  143. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  144. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  145. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  146. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  147. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  148. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  149. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  150. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  151. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  152. package/dist/plugins/preferences/index.d.ts +12 -0
  153. package/dist/plugins/preferences/index.d.ts.map +1 -0
  154. package/dist/plugins/preferences/index.js +13 -0
  155. package/dist/plugins/preferences/index.js.map +1 -0
  156. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  157. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  158. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  159. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  160. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  161. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  162. package/dist/plugins/preferences/stores/index.js +9 -0
  163. package/dist/plugins/preferences/stores/index.js.map +1 -0
  164. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  165. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  166. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  167. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  168. package/dist/plugins/preferences/types.d.ts +91 -0
  169. package/dist/plugins/preferences/types.d.ts.map +1 -0
  170. package/dist/plugins/preferences/types.js +10 -0
  171. package/dist/plugins/preferences/types.js.map +1 -0
  172. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  173. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  174. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  175. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  176. package/dist/plugins/users/index.d.ts +12 -0
  177. package/dist/plugins/users/index.d.ts.map +1 -0
  178. package/dist/plugins/users/index.js +13 -0
  179. package/dist/plugins/users/index.js.map +1 -0
  180. package/dist/plugins/users/stores/index.d.ts +7 -0
  181. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  182. package/dist/plugins/users/stores/index.js +7 -0
  183. package/dist/plugins/users/stores/index.js.map +1 -0
  184. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  185. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  186. package/dist/plugins/users/stores/postgres-store.js +157 -0
  187. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  188. package/dist/plugins/users/types.d.ts +225 -0
  189. package/dist/plugins/users/types.d.ts.map +1 -0
  190. package/dist/plugins/users/types.js +12 -0
  191. package/dist/plugins/users/types.js.map +1 -0
  192. package/dist/plugins/users/users-plugin.d.ts +45 -0
  193. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  194. package/dist/plugins/users/users-plugin.js +359 -0
  195. package/dist/plugins/users/users-plugin.js.map +1 -0
  196. package/dist-ui/assets/index-BY8OxNgO.js +465 -0
  197. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  198. package/dist-ui/index.html +1 -1
  199. package/dist-ui-lib/api/controlPanelApi.d.ts +278 -0
  200. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  201. package/dist-ui-lib/components/index.d.ts +18 -0
  202. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  203. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  204. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  205. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  206. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +48 -0
  207. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +25 -0
  208. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  209. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  210. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  211. package/dist-ui-lib/index.js +5172 -0
  212. package/dist-ui-lib/index.js.map +1 -0
  213. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  214. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  215. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  216. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  217. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  218. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  219. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  220. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  221. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  222. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  223. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  224. package/package.json +24 -7
  225. package/src/core/control-panel.ts +145 -61
  226. package/src/core/gateway.ts +863 -403
  227. package/src/core/index.ts +21 -2
  228. package/src/core/plugin-registry.ts +716 -0
  229. package/src/core/types.ts +31 -37
  230. package/src/index.ts +125 -19
  231. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  232. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  233. package/src/plugins/auth/adapters/index.ts +10 -0
  234. package/src/plugins/auth/adapters/supabase-adapter.ts +149 -0
  235. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  236. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  237. package/src/plugins/auth/auth-plugin.ts +303 -0
  238. package/src/plugins/auth/env-config.ts +572 -0
  239. package/src/plugins/auth/index.ts +42 -0
  240. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  241. package/src/plugins/auth/types.ts +245 -0
  242. package/src/plugins/bans/bans-plugin.ts +485 -0
  243. package/src/plugins/bans/index.ts +31 -0
  244. package/src/plugins/bans/stores/index.ts +7 -0
  245. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  246. package/src/plugins/bans/types.ts +141 -0
  247. package/src/plugins/cache-plugin.test.ts +108 -32
  248. package/src/plugins/cache-plugin.ts +40 -9
  249. package/src/plugins/config-plugin.ts +23 -12
  250. package/src/plugins/diagnostics-plugin.ts +22 -12
  251. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  252. package/src/plugins/entitlements/index.ts +51 -0
  253. package/src/plugins/entitlements/sources/index.ts +9 -0
  254. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  255. package/src/plugins/entitlements/types.ts +256 -0
  256. package/src/plugins/frontend-app-plugin.ts +24 -12
  257. package/src/plugins/health-plugin.ts +27 -7
  258. package/src/plugins/index.ts +132 -4
  259. package/src/plugins/logs-plugin.ts +28 -14
  260. package/src/plugins/postgres-plugin.test.ts +52 -29
  261. package/src/plugins/postgres-plugin.ts +11 -9
  262. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  263. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  264. package/src/plugins/preferences/index.ts +30 -0
  265. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  266. package/src/plugins/preferences/stores/index.ts +9 -0
  267. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  268. package/src/plugins/preferences/types.ts +100 -0
  269. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  270. package/src/plugins/users/index.ts +38 -0
  271. package/src/plugins/users/stores/index.ts +7 -0
  272. package/src/plugins/users/stores/postgres-store.ts +225 -0
  273. package/src/plugins/users/types.ts +247 -0
  274. package/src/plugins/users/users-plugin.ts +418 -0
  275. package/ui/src/App.tsx +188 -31
  276. package/ui/src/api/controlPanelApi.ts +453 -1
  277. package/ui/src/components/ControlPanelApp.tsx +212 -0
  278. package/ui/src/components/index.ts +62 -0
  279. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  280. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  281. package/ui/src/dashboard/PluginWidgetRenderer.tsx +118 -0
  282. package/ui/src/dashboard/WidgetComponentRegistry.tsx +120 -0
  283. package/ui/src/dashboard/builtInWidgets.tsx +35 -0
  284. package/ui/src/dashboard/index.ts +35 -0
  285. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  286. package/ui/src/dashboard/widgets/index.ts +7 -0
  287. package/ui/src/pages/AuthPage.tsx +259 -0
  288. package/ui/src/pages/DashboardPage.tsx +28 -149
  289. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  290. package/ui/src/pages/LogsPage.tsx +174 -8
  291. package/ui/src/pages/PluginPage.tsx +148 -0
  292. package/ui/src/pages/PluginsPage.tsx +394 -0
  293. package/ui/src/pages/SystemPage.tsx +445 -0
  294. package/ui/src/pages/UsersPage.tsx +837 -0
  295. package/ui/tsconfig.lib.json +11 -0
  296. package/ui/vite.lib.config.ts +56 -0
  297. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  298. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  299. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -0,0 +1,195 @@
1
+ /**
2
+ * PostgreSQL Ban Store
3
+ *
4
+ * Ban storage implementation using PostgreSQL.
5
+ * Requires the 'pg' package and the Users plugin to be installed.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import type {
11
+ BanStore,
12
+ Ban,
13
+ CreateBanInput,
14
+ RemoveBanInput,
15
+ PostgresBanStoreConfig,
16
+ } from '../types.js';
17
+
18
+ // Pool interface (from pg package)
19
+ interface PgPool {
20
+ query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
21
+ }
22
+
23
+ /**
24
+ * Create a PostgreSQL ban store
25
+ *
26
+ * @param config Configuration including a pg Pool instance or a function that returns one
27
+ * @returns BanStore implementation
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { Pool } from 'pg';
32
+ * import { postgresBanStore } from '@qwickapps/server';
33
+ *
34
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
35
+ * const store = postgresBanStore({ pool });
36
+ *
37
+ * // Or with lazy initialization:
38
+ * const store = postgresBanStore({ pool: () => getPostgres().getPool() });
39
+ * ```
40
+ */
41
+ export function postgresBanStore(config: PostgresBanStoreConfig): BanStore {
42
+ const {
43
+ pool: poolOrFn,
44
+ tableName = 'user_bans',
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 tableFullName = `"${schema}"."${tableName}"`;
56
+
57
+ return {
58
+ name: 'postgres',
59
+
60
+ async initialize(): Promise<void> {
61
+ if (!autoCreateTables) return;
62
+
63
+ // Create bans table
64
+ // Note: This does NOT have a foreign key to users table
65
+ // The relationship is enforced at the application level via Users Plugin
66
+ await getPool().query(`
67
+ CREATE TABLE IF NOT EXISTS ${tableFullName} (
68
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
69
+ user_id UUID NOT NULL,
70
+ reason TEXT NOT NULL,
71
+ banned_by VARCHAR(255) NOT NULL,
72
+ banned_at TIMESTAMPTZ DEFAULT NOW(),
73
+ expires_at TIMESTAMPTZ,
74
+ is_active BOOLEAN DEFAULT TRUE,
75
+ removed_at TIMESTAMPTZ,
76
+ removed_by VARCHAR(255),
77
+ metadata JSONB DEFAULT '{}'
78
+ );
79
+
80
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${tableFullName}(user_id);
81
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_is_active ON ${tableFullName}(is_active);
82
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_expires_at ON ${tableFullName}(expires_at) WHERE expires_at IS NOT NULL;
83
+ `);
84
+ },
85
+
86
+ async isBanned(userId: string): Promise<boolean> {
87
+ const ban = await this.getActiveBan(userId);
88
+ return ban !== null;
89
+ },
90
+
91
+ async getActiveBan(userId: string): Promise<Ban | null> {
92
+ const result = await getPool().query(
93
+ `SELECT * FROM ${tableFullName}
94
+ WHERE user_id = $1 AND is_active = TRUE
95
+ AND (expires_at IS NULL OR expires_at > NOW())`,
96
+ [userId]
97
+ );
98
+ return (result.rows[0] as Ban) || null;
99
+ },
100
+
101
+ async createBan(input: CreateBanInput): Promise<Ban> {
102
+ const expiresAt = input.duration
103
+ ? new Date(Date.now() + input.duration * 1000)
104
+ : null;
105
+
106
+ // Deactivate any existing active bans for this user
107
+ await getPool().query(
108
+ `UPDATE ${tableFullName}
109
+ SET is_active = FALSE, removed_at = NOW(), removed_by = $2
110
+ WHERE user_id = $1 AND is_active = TRUE`,
111
+ [input.user_id, input.banned_by]
112
+ );
113
+
114
+ // Create new ban
115
+ const result = await getPool().query(
116
+ `INSERT INTO ${tableFullName} (user_id, reason, banned_by, expires_at, metadata)
117
+ VALUES ($1, $2, $3, $4, $5)
118
+ RETURNING *`,
119
+ [
120
+ input.user_id,
121
+ input.reason,
122
+ input.banned_by,
123
+ expiresAt,
124
+ JSON.stringify(input.metadata || {}),
125
+ ]
126
+ );
127
+
128
+ return result.rows[0] as Ban;
129
+ },
130
+
131
+ async removeBan(input: RemoveBanInput): Promise<boolean> {
132
+ const result = await getPool().query(
133
+ `UPDATE ${tableFullName}
134
+ SET is_active = FALSE, removed_at = NOW(), removed_by = $2
135
+ WHERE user_id = $1 AND is_active = TRUE
136
+ RETURNING *`,
137
+ [input.user_id, input.removed_by]
138
+ );
139
+
140
+ return (result.rowCount ?? 0) > 0;
141
+ },
142
+
143
+ async listBans(userId: string): Promise<Ban[]> {
144
+ const result = await getPool().query(
145
+ `SELECT * FROM ${tableFullName}
146
+ WHERE user_id = $1
147
+ ORDER BY banned_at DESC`,
148
+ [userId]
149
+ );
150
+ return result.rows as Ban[];
151
+ },
152
+
153
+ async listActiveBans(options: { limit?: number; offset?: number } = {}): Promise<{
154
+ bans: Ban[];
155
+ total: number;
156
+ }> {
157
+ const { limit = 50, offset = 0 } = options;
158
+
159
+ // Get total count
160
+ const countResult = await getPool().query(
161
+ `SELECT COUNT(*) FROM ${tableFullName}
162
+ WHERE is_active = TRUE AND (expires_at IS NULL OR expires_at > NOW())`
163
+ );
164
+ const total = parseInt((countResult.rows[0] as { count: string }).count, 10);
165
+
166
+ // Get bans
167
+ const result = await getPool().query(
168
+ `SELECT * FROM ${tableFullName}
169
+ WHERE is_active = TRUE AND (expires_at IS NULL OR expires_at > NOW())
170
+ ORDER BY banned_at DESC
171
+ LIMIT $1 OFFSET $2`,
172
+ [limit, offset]
173
+ );
174
+
175
+ return {
176
+ bans: result.rows as Ban[],
177
+ total,
178
+ };
179
+ },
180
+
181
+ async cleanupExpiredBans(): Promise<number> {
182
+ const result = await getPool().query(
183
+ `UPDATE ${tableFullName}
184
+ SET is_active = FALSE
185
+ WHERE is_active = TRUE AND expires_at IS NOT NULL AND expires_at <= NOW()`
186
+ );
187
+
188
+ return result.rowCount ?? 0;
189
+ },
190
+
191
+ async shutdown(): Promise<void> {
192
+ // Pool is managed externally, nothing to do here
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Bans Plugin Types
3
+ *
4
+ * Type definitions for ban management.
5
+ * Bans are always on USER entities (by user_id), not emails.
6
+ * Email is just an identifier to resolve to a user.
7
+ *
8
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
9
+ */
10
+
11
+ import type { User } from '../users/types.js';
12
+
13
+ /**
14
+ * Ban record
15
+ */
16
+ export interface Ban {
17
+ /** Primary key - UUID */
18
+ id: string;
19
+ /** User ID being banned */
20
+ user_id: string;
21
+ /** Reason for the ban */
22
+ reason: string;
23
+ /** Who created the ban (user ID or system) */
24
+ banned_by: string;
25
+ /** When the ban was created */
26
+ banned_at: Date;
27
+ /** When the ban expires (null = permanent) */
28
+ expires_at?: Date;
29
+ /** Whether the ban is currently active */
30
+ is_active: boolean;
31
+ /** When the ban was removed */
32
+ removed_at?: Date;
33
+ /** Who removed the ban */
34
+ removed_by?: string;
35
+ /** Additional metadata */
36
+ metadata?: Record<string, unknown>;
37
+ }
38
+
39
+ /**
40
+ * Ban creation payload
41
+ */
42
+ export interface CreateBanInput {
43
+ user_id: string;
44
+ reason: string;
45
+ banned_by: string;
46
+ /** Duration in seconds (null = permanent) */
47
+ duration?: number;
48
+ metadata?: Record<string, unknown>;
49
+ }
50
+
51
+ /**
52
+ * Ban removal payload
53
+ */
54
+ export interface RemoveBanInput {
55
+ user_id: string;
56
+ removed_by: string;
57
+ note?: string;
58
+ }
59
+
60
+ /**
61
+ * Ban store interface - storage backend for bans
62
+ */
63
+ export interface BanStore {
64
+ /** Store name (e.g., 'postgres', 'memory') */
65
+ name: string;
66
+
67
+ /** Initialize the store (create tables, etc.) */
68
+ initialize(): Promise<void>;
69
+
70
+ /** Check if user is banned */
71
+ isBanned(userId: string): Promise<boolean>;
72
+
73
+ /** Get active ban for user */
74
+ getActiveBan(userId: string): Promise<Ban | null>;
75
+
76
+ /** Create a ban */
77
+ createBan(input: CreateBanInput): Promise<Ban>;
78
+
79
+ /** Remove a ban */
80
+ removeBan(input: RemoveBanInput): Promise<boolean>;
81
+
82
+ /** List bans for a user (including history) */
83
+ listBans(userId: string): Promise<Ban[]>;
84
+
85
+ /** List all active bans */
86
+ listActiveBans(options?: { limit?: number; offset?: number }): Promise<{
87
+ bans: Ban[];
88
+ total: number;
89
+ }>;
90
+
91
+ /** Cleanup expired bans */
92
+ cleanupExpiredBans(): Promise<number>;
93
+
94
+ /** Shutdown the store */
95
+ shutdown(): Promise<void>;
96
+ }
97
+
98
+ /**
99
+ * Ban callbacks
100
+ */
101
+ export interface BanCallbacks {
102
+ /** Called when a user is banned */
103
+ onBan?: (user: User, ban: Ban) => Promise<void>;
104
+ /** Called when a ban is removed */
105
+ onUnban?: (user: User) => Promise<void>;
106
+ }
107
+
108
+ /**
109
+ * Bans plugin configuration
110
+ */
111
+ export interface BansPluginConfig {
112
+ /** Ban storage backend */
113
+ store: BanStore;
114
+ /** Support temporary bans (with expiration) */
115
+ supportTemporary?: boolean;
116
+ /** Callbacks */
117
+ callbacks?: BanCallbacks;
118
+ /** API configuration */
119
+ api?: {
120
+ /** API route prefix (default: '/api/bans') */
121
+ prefix?: string;
122
+ /** Enable API endpoints */
123
+ enabled?: boolean;
124
+ };
125
+ /** Enable debug logging */
126
+ debug?: boolean;
127
+ }
128
+
129
+ /**
130
+ * PostgreSQL ban store configuration
131
+ */
132
+ export interface PostgresBanStoreConfig {
133
+ /** PostgreSQL pool instance or a function that returns one (for lazy initialization) */
134
+ pool: unknown | (() => unknown);
135
+ /** Bans table name (default: 'user_bans') */
136
+ tableName?: string;
137
+ /** Schema name (default: 'public') */
138
+ schema?: string;
139
+ /** Auto-create tables on init (default: true) */
140
+ autoCreateTables?: boolean;
141
+ }
@@ -9,6 +9,27 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
9
 
10
10
  // Mock ioredis before importing the plugin
11
11
  vi.mock('ioredis', () => {
12
+ // Create a mock stream that emits keys in batches
13
+ const createMockScanStream = (keys: string[]) => {
14
+ const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
15
+ return {
16
+ on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
17
+ if (!handlers[event]) handlers[event] = [];
18
+ handlers[event].push(handler);
19
+ // Simulate async emission after all handlers are registered
20
+ if (event === 'error') {
21
+ setTimeout(() => {
22
+ // Emit data in batches
23
+ handlers['data']?.forEach(h => h(keys));
24
+ // Then emit end
25
+ handlers['end']?.forEach(h => h());
26
+ }, 0);
27
+ }
28
+ return { on: vi.fn() };
29
+ }),
30
+ };
31
+ };
32
+
12
33
  const mockClient = {
13
34
  get: vi.fn().mockResolvedValue(null),
14
35
  setex: vi.fn().mockResolvedValue('OK'),
@@ -19,6 +40,7 @@ vi.mock('ioredis', () => {
19
40
  incr: vi.fn().mockResolvedValue(1),
20
41
  incrby: vi.fn().mockResolvedValue(5),
21
42
  keys: vi.fn().mockResolvedValue([]),
43
+ scanStream: vi.fn(() => createMockScanStream(['test:key1', 'test:key2', 'test:key3'])),
22
44
  info: vi.fn().mockResolvedValue('used_memory_human:1.5M\n'),
23
45
  dbsize: vi.fn().mockResolvedValue(100),
24
46
  ping: vi.fn().mockResolvedValue('PONG'),
@@ -38,6 +60,7 @@ import {
38
60
  hasCache,
39
61
  type CachePluginConfig,
40
62
  } from './cache-plugin.js';
63
+ import type { PluginRegistry } from '../core/plugin-registry.js';
41
64
 
42
65
  describe('Cache Plugin', () => {
43
66
  const mockConfig: CachePluginConfig = {
@@ -47,21 +70,42 @@ describe('Cache Plugin', () => {
47
70
  healthCheck: false, // Disable for unit tests
48
71
  };
49
72
 
50
- const mockContext = {
51
- config: { productName: 'Test', port: 3000 },
52
- app: {} as any,
53
- router: {} as any,
54
- logger: {
73
+ // Create a mock registry that matches the new Plugin interface
74
+ const createMockRegistry = (): PluginRegistry => ({
75
+ hasPlugin: vi.fn().mockReturnValue(false),
76
+ getPlugin: vi.fn().mockReturnValue(null),
77
+ listPlugins: vi.fn().mockReturnValue([]),
78
+ addRoute: vi.fn(),
79
+ addMenuItem: vi.fn(),
80
+ addPage: vi.fn(),
81
+ addWidget: vi.fn(),
82
+ addConfigComponent: vi.fn(),
83
+ getRoutes: vi.fn().mockReturnValue([]),
84
+ getMenuItems: vi.fn().mockReturnValue([]),
85
+ getPages: vi.fn().mockReturnValue([]),
86
+ getWidgets: vi.fn().mockReturnValue([]),
87
+ getConfigComponents: vi.fn().mockReturnValue([]),
88
+ getPluginContributions: vi.fn().mockReturnValue({ routes: [], menuItems: [], pages: [], widgets: [], config: undefined }),
89
+ getConfig: vi.fn().mockReturnValue({}),
90
+ setConfig: vi.fn().mockResolvedValue(undefined),
91
+ subscribe: vi.fn().mockReturnValue(() => {}),
92
+ emit: vi.fn(),
93
+ registerHealthCheck: vi.fn(),
94
+ getApp: vi.fn().mockReturnValue({} as any),
95
+ getRouter: vi.fn().mockReturnValue({} as any),
96
+ getLogger: vi.fn().mockReturnValue({
55
97
  debug: vi.fn(),
56
98
  info: vi.fn(),
57
99
  warn: vi.fn(),
58
100
  error: vi.fn(),
59
- },
60
- registerHealthCheck: vi.fn(),
61
- };
101
+ }),
102
+ });
103
+
104
+ let mockRegistry: PluginRegistry;
62
105
 
63
106
  beforeEach(() => {
64
107
  vi.clearAllMocks();
108
+ mockRegistry = createMockRegistry();
65
109
  });
66
110
 
67
111
  afterEach(async () => {
@@ -75,33 +119,34 @@ describe('Cache Plugin', () => {
75
119
  describe('createCachePlugin', () => {
76
120
  it('should create a plugin with correct name', () => {
77
121
  const plugin = createCachePlugin(mockConfig, 'test');
78
- expect(plugin.name).toBe('cache:test');
122
+ expect(plugin.name).toBe('Redis Cache (test)');
79
123
  });
80
124
 
81
125
  it('should use "default" as instance name when not specified', () => {
82
126
  const plugin = createCachePlugin(mockConfig);
83
- expect(plugin.name).toBe('cache:default');
127
+ expect(plugin.name).toBe('Redis Cache (default)');
84
128
  });
85
129
 
86
- it('should have low order number (initialize early)', () => {
87
- const plugin = createCachePlugin(mockConfig);
88
- expect(plugin.order).toBeLessThan(10);
130
+ it('should have correct plugin id', () => {
131
+ const plugin = createCachePlugin(mockConfig, 'test');
132
+ expect(plugin.id).toBe('cache:test');
89
133
  });
90
134
  });
91
135
 
92
- describe('onInit', () => {
136
+ describe('onStart', () => {
93
137
  it('should register the cache instance', async () => {
94
138
  const plugin = createCachePlugin(mockConfig, 'test');
95
- await plugin.onInit?.(mockContext as any);
139
+ await plugin.onStart({}, mockRegistry);
96
140
 
97
141
  expect(hasCache('test')).toBe(true);
98
142
  });
99
143
 
100
144
  it('should log debug message on successful connection', async () => {
101
145
  const plugin = createCachePlugin(mockConfig, 'test');
102
- await plugin.onInit?.(mockContext as any);
146
+ await plugin.onStart({}, mockRegistry);
103
147
 
104
- expect(mockContext.logger.debug).toHaveBeenCalledWith(
148
+ const logger = mockRegistry.getLogger('cache:test');
149
+ expect(logger.debug).toHaveBeenCalledWith(
105
150
  expect.stringContaining('connected')
106
151
  );
107
152
  });
@@ -109,9 +154,9 @@ describe('Cache Plugin', () => {
109
154
  it('should register health check when enabled', async () => {
110
155
  const configWithHealth = { ...mockConfig, healthCheck: true };
111
156
  const plugin = createCachePlugin(configWithHealth, 'test');
112
- await plugin.onInit?.(mockContext as any);
157
+ await plugin.onStart({}, mockRegistry);
113
158
 
114
- expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
159
+ expect(mockRegistry.registerHealthCheck).toHaveBeenCalledWith(
115
160
  expect.objectContaining({
116
161
  name: 'redis',
117
162
  type: 'custom',
@@ -126,9 +171,9 @@ describe('Cache Plugin', () => {
126
171
  healthCheckName: 'custom-cache',
127
172
  };
128
173
  const plugin = createCachePlugin(configWithCustomName, 'test');
129
- await plugin.onInit?.(mockContext as any);
174
+ await plugin.onStart({}, mockRegistry);
130
175
 
131
- expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
176
+ expect(mockRegistry.registerHealthCheck).toHaveBeenCalledWith(
132
177
  expect.objectContaining({
133
178
  name: 'custom-cache',
134
179
  })
@@ -139,7 +184,7 @@ describe('Cache Plugin', () => {
139
184
  describe('getCache', () => {
140
185
  it('should return registered instance', async () => {
141
186
  const plugin = createCachePlugin(mockConfig, 'test');
142
- await plugin.onInit?.(mockContext as any);
187
+ await plugin.onStart({}, mockRegistry);
143
188
 
144
189
  const cache = getCache('test');
145
190
  expect(cache).toBeDefined();
@@ -162,7 +207,7 @@ describe('Cache Plugin', () => {
162
207
 
163
208
  it('should return true for registered instance', async () => {
164
209
  const plugin = createCachePlugin(mockConfig, 'test');
165
- await plugin.onInit?.(mockContext as any);
210
+ await plugin.onStart({}, mockRegistry);
166
211
 
167
212
  expect(hasCache('test')).toBe(true);
168
213
  });
@@ -171,7 +216,7 @@ describe('Cache Plugin', () => {
171
216
  describe('CacheInstance', () => {
172
217
  it('should get value and parse JSON', async () => {
173
218
  const plugin = createCachePlugin(mockConfig, 'test');
174
- await plugin.onInit?.(mockContext as any);
219
+ await plugin.onStart({}, mockRegistry);
175
220
 
176
221
  const cache = getCache('test');
177
222
  // Mock will return null by default
@@ -181,7 +226,7 @@ describe('Cache Plugin', () => {
181
226
 
182
227
  it('should set value with JSON stringification', async () => {
183
228
  const plugin = createCachePlugin(mockConfig, 'test');
184
- await plugin.onInit?.(mockContext as any);
229
+ await plugin.onStart({}, mockRegistry);
185
230
 
186
231
  const cache = getCache('test');
187
232
  await cache.set('key', { foo: 'bar' }, 3600);
@@ -190,7 +235,7 @@ describe('Cache Plugin', () => {
190
235
 
191
236
  it('should return cache stats', async () => {
192
237
  const plugin = createCachePlugin(mockConfig, 'test');
193
- await plugin.onInit?.(mockContext as any);
238
+ await plugin.onStart({}, mockRegistry);
194
239
 
195
240
  const cache = getCache('test');
196
241
  const stats = await cache.getStats();
@@ -200,7 +245,7 @@ describe('Cache Plugin', () => {
200
245
 
201
246
  it('should check if key exists', async () => {
202
247
  const plugin = createCachePlugin(mockConfig, 'test');
203
- await plugin.onInit?.(mockContext as any);
248
+ await plugin.onStart({}, mockRegistry);
204
249
 
205
250
  const cache = getCache('test');
206
251
  const exists = await cache.exists('key');
@@ -209,7 +254,7 @@ describe('Cache Plugin', () => {
209
254
 
210
255
  it('should get TTL for a key', async () => {
211
256
  const plugin = createCachePlugin(mockConfig, 'test');
212
- await plugin.onInit?.(mockContext as any);
257
+ await plugin.onStart({}, mockRegistry);
213
258
 
214
259
  const cache = getCache('test');
215
260
  const ttl = await cache.ttl('key');
@@ -218,22 +263,53 @@ describe('Cache Plugin', () => {
218
263
 
219
264
  it('should increment a value', async () => {
220
265
  const plugin = createCachePlugin(mockConfig, 'test');
221
- await plugin.onInit?.(mockContext as any);
266
+ await plugin.onStart({}, mockRegistry);
222
267
 
223
268
  const cache = getCache('test');
224
269
  const value = await cache.incr('counter');
225
270
  expect(typeof value).toBe('number');
226
271
  });
272
+
273
+ it('should scan keys using cursor-based iteration', async () => {
274
+ const plugin = createCachePlugin(mockConfig, 'test');
275
+ await plugin.onStart({}, mockRegistry);
276
+
277
+ const cache = getCache('test');
278
+ const keys = await cache.scanKeys('*');
279
+
280
+ // Mock returns ['test:key1', 'test:key2', 'test:key3']
281
+ // After prefix removal, should be ['key1', 'key2', 'key3']
282
+ expect(Array.isArray(keys)).toBe(true);
283
+ expect(keys).toHaveLength(3);
284
+ expect(keys).toContain('key1');
285
+ expect(keys).toContain('key2');
286
+ expect(keys).toContain('key3');
287
+ });
288
+
289
+ it('should pass count option to scanStream', async () => {
290
+ const plugin = createCachePlugin(mockConfig, 'test');
291
+ await plugin.onStart({}, mockRegistry);
292
+
293
+ const cache = getCache('test');
294
+ const client = cache.getClient();
295
+
296
+ await cache.scanKeys('*', { count: 500 });
297
+
298
+ expect(client.scanStream).toHaveBeenCalledWith({
299
+ match: 'test:*',
300
+ count: 500,
301
+ });
302
+ });
227
303
  });
228
304
 
229
- describe('onShutdown', () => {
305
+ describe('onStop', () => {
230
306
  it('should close client and unregister instance', async () => {
231
307
  const plugin = createCachePlugin(mockConfig, 'test');
232
- await plugin.onInit?.(mockContext as any);
308
+ await plugin.onStart({}, mockRegistry);
233
309
 
234
310
  expect(hasCache('test')).toBe(true);
235
311
 
236
- await plugin.onShutdown?.();
312
+ await plugin.onStop();
237
313
 
238
314
  expect(hasCache('test')).toBe(false);
239
315
  });