@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,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,39 @@ 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
+ getRoutes: vi.fn().mockReturnValue([]),
83
+ getMenuItems: vi.fn().mockReturnValue([]),
84
+ getPages: vi.fn().mockReturnValue([]),
85
+ getWidgets: vi.fn().mockReturnValue([]),
86
+ getConfig: vi.fn().mockReturnValue({}),
87
+ setConfig: vi.fn().mockResolvedValue(undefined),
88
+ subscribe: vi.fn().mockReturnValue(() => {}),
89
+ emit: vi.fn(),
90
+ registerHealthCheck: vi.fn(),
91
+ getApp: vi.fn().mockReturnValue({} as any),
92
+ getRouter: vi.fn().mockReturnValue({} as any),
93
+ getLogger: vi.fn().mockReturnValue({
55
94
  debug: vi.fn(),
56
95
  info: vi.fn(),
57
96
  warn: vi.fn(),
58
97
  error: vi.fn(),
59
- },
60
- registerHealthCheck: vi.fn(),
61
- };
98
+ }),
99
+ });
100
+
101
+ let mockRegistry: PluginRegistry;
62
102
 
63
103
  beforeEach(() => {
64
104
  vi.clearAllMocks();
105
+ mockRegistry = createMockRegistry();
65
106
  });
66
107
 
67
108
  afterEach(async () => {
@@ -75,33 +116,34 @@ describe('Cache Plugin', () => {
75
116
  describe('createCachePlugin', () => {
76
117
  it('should create a plugin with correct name', () => {
77
118
  const plugin = createCachePlugin(mockConfig, 'test');
78
- expect(plugin.name).toBe('cache:test');
119
+ expect(plugin.name).toBe('Redis Cache (test)');
79
120
  });
80
121
 
81
122
  it('should use "default" as instance name when not specified', () => {
82
123
  const plugin = createCachePlugin(mockConfig);
83
- expect(plugin.name).toBe('cache:default');
124
+ expect(plugin.name).toBe('Redis Cache (default)');
84
125
  });
85
126
 
86
- it('should have low order number (initialize early)', () => {
87
- const plugin = createCachePlugin(mockConfig);
88
- expect(plugin.order).toBeLessThan(10);
127
+ it('should have correct plugin id', () => {
128
+ const plugin = createCachePlugin(mockConfig, 'test');
129
+ expect(plugin.id).toBe('cache:test');
89
130
  });
90
131
  });
91
132
 
92
- describe('onInit', () => {
133
+ describe('onStart', () => {
93
134
  it('should register the cache instance', async () => {
94
135
  const plugin = createCachePlugin(mockConfig, 'test');
95
- await plugin.onInit?.(mockContext as any);
136
+ await plugin.onStart({}, mockRegistry);
96
137
 
97
138
  expect(hasCache('test')).toBe(true);
98
139
  });
99
140
 
100
141
  it('should log debug message on successful connection', async () => {
101
142
  const plugin = createCachePlugin(mockConfig, 'test');
102
- await plugin.onInit?.(mockContext as any);
143
+ await plugin.onStart({}, mockRegistry);
103
144
 
104
- expect(mockContext.logger.debug).toHaveBeenCalledWith(
145
+ const logger = mockRegistry.getLogger('cache:test');
146
+ expect(logger.debug).toHaveBeenCalledWith(
105
147
  expect.stringContaining('connected')
106
148
  );
107
149
  });
@@ -109,9 +151,9 @@ describe('Cache Plugin', () => {
109
151
  it('should register health check when enabled', async () => {
110
152
  const configWithHealth = { ...mockConfig, healthCheck: true };
111
153
  const plugin = createCachePlugin(configWithHealth, 'test');
112
- await plugin.onInit?.(mockContext as any);
154
+ await plugin.onStart({}, mockRegistry);
113
155
 
114
- expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
156
+ expect(mockRegistry.registerHealthCheck).toHaveBeenCalledWith(
115
157
  expect.objectContaining({
116
158
  name: 'redis',
117
159
  type: 'custom',
@@ -126,9 +168,9 @@ describe('Cache Plugin', () => {
126
168
  healthCheckName: 'custom-cache',
127
169
  };
128
170
  const plugin = createCachePlugin(configWithCustomName, 'test');
129
- await plugin.onInit?.(mockContext as any);
171
+ await plugin.onStart({}, mockRegistry);
130
172
 
131
- expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
173
+ expect(mockRegistry.registerHealthCheck).toHaveBeenCalledWith(
132
174
  expect.objectContaining({
133
175
  name: 'custom-cache',
134
176
  })
@@ -139,7 +181,7 @@ describe('Cache Plugin', () => {
139
181
  describe('getCache', () => {
140
182
  it('should return registered instance', async () => {
141
183
  const plugin = createCachePlugin(mockConfig, 'test');
142
- await plugin.onInit?.(mockContext as any);
184
+ await plugin.onStart({}, mockRegistry);
143
185
 
144
186
  const cache = getCache('test');
145
187
  expect(cache).toBeDefined();
@@ -162,7 +204,7 @@ describe('Cache Plugin', () => {
162
204
 
163
205
  it('should return true for registered instance', async () => {
164
206
  const plugin = createCachePlugin(mockConfig, 'test');
165
- await plugin.onInit?.(mockContext as any);
207
+ await plugin.onStart({}, mockRegistry);
166
208
 
167
209
  expect(hasCache('test')).toBe(true);
168
210
  });
@@ -171,7 +213,7 @@ describe('Cache Plugin', () => {
171
213
  describe('CacheInstance', () => {
172
214
  it('should get value and parse JSON', async () => {
173
215
  const plugin = createCachePlugin(mockConfig, 'test');
174
- await plugin.onInit?.(mockContext as any);
216
+ await plugin.onStart({}, mockRegistry);
175
217
 
176
218
  const cache = getCache('test');
177
219
  // Mock will return null by default
@@ -181,7 +223,7 @@ describe('Cache Plugin', () => {
181
223
 
182
224
  it('should set value with JSON stringification', async () => {
183
225
  const plugin = createCachePlugin(mockConfig, 'test');
184
- await plugin.onInit?.(mockContext as any);
226
+ await plugin.onStart({}, mockRegistry);
185
227
 
186
228
  const cache = getCache('test');
187
229
  await cache.set('key', { foo: 'bar' }, 3600);
@@ -190,7 +232,7 @@ describe('Cache Plugin', () => {
190
232
 
191
233
  it('should return cache stats', async () => {
192
234
  const plugin = createCachePlugin(mockConfig, 'test');
193
- await plugin.onInit?.(mockContext as any);
235
+ await plugin.onStart({}, mockRegistry);
194
236
 
195
237
  const cache = getCache('test');
196
238
  const stats = await cache.getStats();
@@ -200,7 +242,7 @@ describe('Cache Plugin', () => {
200
242
 
201
243
  it('should check if key exists', async () => {
202
244
  const plugin = createCachePlugin(mockConfig, 'test');
203
- await plugin.onInit?.(mockContext as any);
245
+ await plugin.onStart({}, mockRegistry);
204
246
 
205
247
  const cache = getCache('test');
206
248
  const exists = await cache.exists('key');
@@ -209,7 +251,7 @@ describe('Cache Plugin', () => {
209
251
 
210
252
  it('should get TTL for a key', async () => {
211
253
  const plugin = createCachePlugin(mockConfig, 'test');
212
- await plugin.onInit?.(mockContext as any);
254
+ await plugin.onStart({}, mockRegistry);
213
255
 
214
256
  const cache = getCache('test');
215
257
  const ttl = await cache.ttl('key');
@@ -218,22 +260,53 @@ describe('Cache Plugin', () => {
218
260
 
219
261
  it('should increment a value', async () => {
220
262
  const plugin = createCachePlugin(mockConfig, 'test');
221
- await plugin.onInit?.(mockContext as any);
263
+ await plugin.onStart({}, mockRegistry);
222
264
 
223
265
  const cache = getCache('test');
224
266
  const value = await cache.incr('counter');
225
267
  expect(typeof value).toBe('number');
226
268
  });
269
+
270
+ it('should scan keys using cursor-based iteration', async () => {
271
+ const plugin = createCachePlugin(mockConfig, 'test');
272
+ await plugin.onStart({}, mockRegistry);
273
+
274
+ const cache = getCache('test');
275
+ const keys = await cache.scanKeys('*');
276
+
277
+ // Mock returns ['test:key1', 'test:key2', 'test:key3']
278
+ // After prefix removal, should be ['key1', 'key2', 'key3']
279
+ expect(Array.isArray(keys)).toBe(true);
280
+ expect(keys).toHaveLength(3);
281
+ expect(keys).toContain('key1');
282
+ expect(keys).toContain('key2');
283
+ expect(keys).toContain('key3');
284
+ });
285
+
286
+ it('should pass count option to scanStream', async () => {
287
+ const plugin = createCachePlugin(mockConfig, 'test');
288
+ await plugin.onStart({}, mockRegistry);
289
+
290
+ const cache = getCache('test');
291
+ const client = cache.getClient();
292
+
293
+ await cache.scanKeys('*', { count: 500 });
294
+
295
+ expect(client.scanStream).toHaveBeenCalledWith({
296
+ match: 'test:*',
297
+ count: 500,
298
+ });
299
+ });
227
300
  });
228
301
 
229
- describe('onShutdown', () => {
302
+ describe('onStop', () => {
230
303
  it('should close client and unregister instance', async () => {
231
304
  const plugin = createCachePlugin(mockConfig, 'test');
232
- await plugin.onInit?.(mockContext as any);
305
+ await plugin.onStart({}, mockRegistry);
233
306
 
234
307
  expect(hasCache('test')).toBe(true);
235
308
 
236
- await plugin.onShutdown?.();
309
+ await plugin.onStop();
237
310
 
238
311
  expect(hasCache('test')).toBe(false);
239
312
  });