@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,165 @@
1
+ /**
2
+ * Auth Plugin Types
3
+ *
4
+ * Type definitions for the pluggable authentication system.
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ import type { Request, Response, NextFunction, RequestHandler } from 'express';
10
+
11
+ /**
12
+ * Authenticated user information
13
+ */
14
+ export interface AuthenticatedUser {
15
+ /** Unique user ID from the provider */
16
+ id: string;
17
+ /** User's email address */
18
+ email: string;
19
+ /** User's display name */
20
+ name?: string;
21
+ /** User's profile picture URL */
22
+ picture?: string;
23
+ /** Whether the email is verified */
24
+ emailVerified?: boolean;
25
+ /** User's roles from the provider */
26
+ roles?: string[];
27
+ /** Raw user object from the provider */
28
+ raw?: Record<string, unknown>;
29
+ }
30
+
31
+ /**
32
+ * Auth adapter interface - all adapters must implement this
33
+ */
34
+ export interface AuthAdapter {
35
+ /** Adapter name (e.g., 'auth0', 'supabase', 'basic') */
36
+ name: string;
37
+
38
+ /**
39
+ * Initialize the adapter - called once during plugin setup
40
+ * Returns middleware to apply to the Express app
41
+ */
42
+ initialize(): RequestHandler | RequestHandler[];
43
+
44
+ /**
45
+ * Check if the request is authenticated
46
+ */
47
+ isAuthenticated(req: Request): boolean;
48
+
49
+ /**
50
+ * Get the authenticated user from the request
51
+ * Can be async for adapters that need to validate tokens
52
+ */
53
+ getUser(req: Request): AuthenticatedUser | null | Promise<AuthenticatedUser | null>;
54
+
55
+ /**
56
+ * Check if user has required roles (optional)
57
+ */
58
+ hasRoles?(req: Request, roles: string[]): boolean;
59
+
60
+ /**
61
+ * Get the access token for downstream API calls (optional)
62
+ */
63
+ getAccessToken?(req: Request): string | null;
64
+
65
+ /**
66
+ * Handler for unauthorized requests (optional custom behavior)
67
+ */
68
+ onUnauthorized?(req: Request, res: Response): void;
69
+
70
+ /**
71
+ * Cleanup resources on shutdown (optional)
72
+ */
73
+ shutdown?(): Promise<void>;
74
+ }
75
+
76
+ /**
77
+ * Auth0 adapter configuration
78
+ */
79
+ export interface Auth0AdapterConfig {
80
+ /** Auth0 domain (e.g., 'myapp.auth0.com') */
81
+ domain: string;
82
+ /** Auth0 client ID */
83
+ clientId: string;
84
+ /** Auth0 client secret */
85
+ clientSecret: string;
86
+ /** Base URL of the application */
87
+ baseUrl: string;
88
+ /** Session secret for cookie encryption */
89
+ secret: string;
90
+ /** API audience for access tokens (optional) */
91
+ audience?: string;
92
+ /** Scopes to request (default: ['openid', 'profile', 'email']) */
93
+ scopes?: string[];
94
+ /** Allowed roles - only these roles can access (optional) */
95
+ allowedRoles?: string[];
96
+ /** Allowed email domains - only these domains can access (optional) */
97
+ allowedDomains?: string[];
98
+ /** Whether to expose the access token to handlers (default: false) */
99
+ exposeAccessToken?: boolean;
100
+ /** Auth routes configuration */
101
+ routes?: {
102
+ login?: string;
103
+ logout?: string;
104
+ callback?: string;
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Supabase adapter configuration
110
+ */
111
+ export interface SupabaseAdapterConfig {
112
+ /** Supabase project URL */
113
+ url: string;
114
+ /** Supabase anon key */
115
+ anonKey: string;
116
+ }
117
+
118
+ /**
119
+ * Basic auth adapter configuration
120
+ */
121
+ export interface BasicAdapterConfig {
122
+ /** Username for basic auth */
123
+ username: string;
124
+ /** Password for basic auth */
125
+ password: string;
126
+ /** Realm name for the WWW-Authenticate header */
127
+ realm?: string;
128
+ }
129
+
130
+ /**
131
+ * Auth plugin configuration
132
+ */
133
+ export interface AuthPluginConfig {
134
+ /** Primary adapter for authentication */
135
+ adapter: AuthAdapter;
136
+ /** Fallback adapters checked in order if primary fails (optional) */
137
+ fallback?: AuthAdapter[];
138
+ /** Paths to exclude from authentication */
139
+ excludePaths?: string[];
140
+ /** Whether auth is required for all routes (default: true) */
141
+ authRequired?: boolean;
142
+ /** Custom unauthorized handler */
143
+ onUnauthorized?: (req: Request, res: Response) => void;
144
+ /** Enable debug logging */
145
+ debug?: boolean;
146
+ }
147
+
148
+ /**
149
+ * Extended Express Request with auth info
150
+ */
151
+ export interface AuthenticatedRequest extends Request {
152
+ auth: {
153
+ isAuthenticated: boolean;
154
+ user: AuthenticatedUser | null;
155
+ adapter: string;
156
+ accessToken?: string;
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Helper type guard for authenticated requests
162
+ */
163
+ export function isAuthenticatedRequest(req: Request): req is AuthenticatedRequest {
164
+ return 'auth' in req && (req as AuthenticatedRequest).auth?.isAuthenticated === true;
165
+ }
@@ -0,0 +1,485 @@
1
+ /**
2
+ * Bans Plugin
3
+ *
4
+ * User ban management plugin for @qwickapps/server.
5
+ * Bans are always on USER entities (by user_id), not emails.
6
+ *
7
+ * This plugin depends on the Users Plugin for user resolution.
8
+ * Use `isEmailBanned()` convenience function to check bans by email,
9
+ * which internally resolves email → user_id → ban status.
10
+ *
11
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
12
+ */
13
+
14
+ import type { Request, Response } from 'express';
15
+ import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
16
+ import type {
17
+ BansPluginConfig,
18
+ BanStore,
19
+ Ban,
20
+ CreateBanInput,
21
+ RemoveBanInput,
22
+ } from './types.js';
23
+ import type { AuthenticatedRequest } from '../auth/types.js';
24
+ import { getUserByEmail, getUserById } from '../users/users-plugin.js';
25
+
26
+ // Store instance for helper access
27
+ let currentStore: BanStore | null = null;
28
+ let banCleanupInterval: NodeJS.Timeout | null = null;
29
+ let pluginConfig: BansPluginConfig | null = null;
30
+
31
+ /**
32
+ * Create the Bans plugin
33
+ */
34
+ export function createBansPlugin(config: BansPluginConfig): Plugin {
35
+ const debug = config.debug || false;
36
+ // Routes are mounted under /api by the control panel, so don't include /api in prefix
37
+ const apiPrefix = config.api?.prefix || '/bans';
38
+ const apiEnabled = config.api?.enabled !== false;
39
+
40
+ function log(message: string, data?: Record<string, unknown>) {
41
+ if (debug) {
42
+ console.log(`[BansPlugin] ${message}`, data || '');
43
+ }
44
+ }
45
+
46
+ return {
47
+ id: 'bans',
48
+ name: 'Bans',
49
+ version: '1.0.0',
50
+
51
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
52
+ log('Starting bans plugin');
53
+
54
+ // Check for users plugin dependency
55
+ if (!registry.hasPlugin('users')) {
56
+ throw new Error('Bans plugin requires Users plugin to be loaded first');
57
+ }
58
+
59
+ // Initialize the store (creates tables if needed)
60
+ await config.store.initialize();
61
+ log('Bans plugin migrations complete');
62
+
63
+ // Store references for helper access
64
+ currentStore = config.store;
65
+ pluginConfig = config;
66
+
67
+ // Start ban cleanup interval if temporary bans are supported
68
+ if (config.supportTemporary) {
69
+ banCleanupInterval = setInterval(async () => {
70
+ try {
71
+ const cleaned = await config.store.cleanupExpiredBans();
72
+ if (cleaned > 0) {
73
+ log('Cleaned up expired bans', { count: cleaned });
74
+ }
75
+ } catch (error) {
76
+ console.error('[BansPlugin] Ban cleanup error:', error);
77
+ }
78
+ }, 60 * 1000); // Check every minute
79
+ }
80
+
81
+ // Register health check
82
+ registry.registerHealthCheck({
83
+ name: 'bans-store',
84
+ type: 'custom',
85
+ check: async () => {
86
+ try {
87
+ // Simple health check - list with limit 0
88
+ await config.store.listActiveBans({ limit: 0 });
89
+ return { healthy: true };
90
+ } catch {
91
+ return { healthy: false };
92
+ }
93
+ },
94
+ });
95
+
96
+ // Add API routes if enabled
97
+ if (apiEnabled) {
98
+ // List active bans
99
+ registry.addRoute({
100
+ method: 'get',
101
+ path: apiPrefix,
102
+ pluginId: 'bans',
103
+ handler: async (req: Request, res: Response) => {
104
+ try {
105
+ const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
106
+ const offset = parseInt(req.query.offset as string) || 0;
107
+
108
+ const result = await config.store.listActiveBans({ limit, offset });
109
+ res.json(result);
110
+ } catch (error) {
111
+ console.error('[BansPlugin] List bans error:', error);
112
+ res.status(500).json({ error: 'Failed to list bans' });
113
+ }
114
+ },
115
+ });
116
+
117
+ // Get user ban status by user ID
118
+ registry.addRoute({
119
+ method: 'get',
120
+ path: `${apiPrefix}/user/:userId`,
121
+ pluginId: 'bans',
122
+ handler: async (req: Request, res: Response) => {
123
+ try {
124
+ const ban = await config.store.getActiveBan(req.params.userId);
125
+ res.json({
126
+ isBanned: ban !== null,
127
+ ban,
128
+ });
129
+ } catch (error) {
130
+ console.error('[BansPlugin] Get ban status error:', error);
131
+ res.status(500).json({ error: 'Failed to get ban status' });
132
+ }
133
+ },
134
+ });
135
+
136
+ // Get user ban history
137
+ registry.addRoute({
138
+ method: 'get',
139
+ path: `${apiPrefix}/user/:userId/history`,
140
+ pluginId: 'bans',
141
+ handler: async (req: Request, res: Response) => {
142
+ try {
143
+ const bans = await config.store.listBans(req.params.userId);
144
+ res.json({ bans });
145
+ } catch (error) {
146
+ console.error('[BansPlugin] Get ban history error:', error);
147
+ res.status(500).json({ error: 'Failed to get ban history' });
148
+ }
149
+ },
150
+ });
151
+
152
+ // Ban user by user ID
153
+ registry.addRoute({
154
+ method: 'post',
155
+ path: `${apiPrefix}/user/:userId`,
156
+ pluginId: 'bans',
157
+ handler: async (req: Request, res: Response) => {
158
+ try {
159
+ const authReq = req as AuthenticatedRequest;
160
+ const bannedBy = authReq.auth?.user?.id || authReq.auth?.user?.email || 'system';
161
+
162
+ const input: CreateBanInput = {
163
+ user_id: req.params.userId,
164
+ reason: req.body.reason || 'No reason provided',
165
+ banned_by: bannedBy,
166
+ duration: config.supportTemporary ? req.body.duration : undefined,
167
+ metadata: req.body.metadata,
168
+ };
169
+
170
+ // Verify user exists via Users Plugin
171
+ const user = await getUserById(req.params.userId);
172
+ if (!user) {
173
+ return res.status(404).json({ error: 'User not found' });
174
+ }
175
+
176
+ const ban = await config.store.createBan(input);
177
+
178
+ // Call onBan callback if provided
179
+ if (config.callbacks?.onBan) {
180
+ try {
181
+ await config.callbacks.onBan(user, ban);
182
+ } catch (callbackError) {
183
+ console.error('[BansPlugin] onBan callback error:', callbackError);
184
+ }
185
+ }
186
+
187
+ log('User banned', { userId: req.params.userId, reason: input.reason });
188
+ res.status(201).json(ban);
189
+ } catch (error) {
190
+ console.error('[BansPlugin] Ban user error:', error);
191
+ res.status(500).json({ error: 'Failed to ban user' });
192
+ }
193
+ },
194
+ });
195
+
196
+ // Unban user by user ID
197
+ registry.addRoute({
198
+ method: 'delete',
199
+ path: `${apiPrefix}/user/:userId`,
200
+ pluginId: 'bans',
201
+ handler: async (req: Request, res: Response) => {
202
+ try {
203
+ const authReq = req as AuthenticatedRequest;
204
+ const removedBy = authReq.auth?.user?.id || authReq.auth?.user?.email || 'system';
205
+
206
+ // Verify user exists via Users Plugin
207
+ const user = await getUserById(req.params.userId);
208
+ if (!user) {
209
+ return res.status(404).json({ error: 'User not found' });
210
+ }
211
+
212
+ const input: RemoveBanInput = {
213
+ user_id: req.params.userId,
214
+ removed_by: removedBy,
215
+ note: req.body?.note,
216
+ };
217
+
218
+ const removed = await config.store.removeBan(input);
219
+ if (!removed) {
220
+ return res.status(404).json({ error: 'No active ban found' });
221
+ }
222
+
223
+ // Call onUnban callback if provided
224
+ if (config.callbacks?.onUnban) {
225
+ try {
226
+ await config.callbacks.onUnban(user);
227
+ } catch (callbackError) {
228
+ console.error('[BansPlugin] onUnban callback error:', callbackError);
229
+ }
230
+ }
231
+
232
+ log('User unbanned', { userId: req.params.userId });
233
+ res.status(204).send();
234
+ } catch (error) {
235
+ console.error('[BansPlugin] Unban user error:', error);
236
+ res.status(500).json({ error: 'Failed to unban user' });
237
+ }
238
+ },
239
+ });
240
+
241
+ // Check ban status by email (convenience endpoint)
242
+ registry.addRoute({
243
+ method: 'get',
244
+ path: `${apiPrefix}/email/:email`,
245
+ pluginId: 'bans',
246
+ handler: async (req: Request, res: Response) => {
247
+ try {
248
+ const email = decodeURIComponent(req.params.email);
249
+ const isBanned = await isEmailBanned(email);
250
+ res.json({ email, isBanned });
251
+ } catch (error) {
252
+ console.error('[BansPlugin] Check email ban error:', error);
253
+ res.status(500).json({ error: 'Failed to check ban status' });
254
+ }
255
+ },
256
+ });
257
+
258
+ // Ban user by email (convenience endpoint)
259
+ registry.addRoute({
260
+ method: 'post',
261
+ path: `${apiPrefix}/email/:email`,
262
+ pluginId: 'bans',
263
+ handler: async (req: Request, res: Response) => {
264
+ try {
265
+ const email = decodeURIComponent(req.params.email);
266
+ const authReq = req as AuthenticatedRequest;
267
+ const bannedBy = authReq.auth?.user?.id || authReq.auth?.user?.email || 'system';
268
+
269
+ // Resolve email to user via Users Plugin
270
+ const user = await getUserByEmail(email);
271
+ if (!user) {
272
+ return res.status(404).json({ error: 'User not found' });
273
+ }
274
+
275
+ const input: CreateBanInput = {
276
+ user_id: user.id,
277
+ reason: req.body.reason || 'No reason provided',
278
+ banned_by: bannedBy,
279
+ duration: config.supportTemporary ? req.body.duration : undefined,
280
+ metadata: req.body.metadata,
281
+ };
282
+
283
+ const ban = await config.store.createBan(input);
284
+
285
+ // Call onBan callback if provided
286
+ if (config.callbacks?.onBan) {
287
+ try {
288
+ await config.callbacks.onBan(user, ban);
289
+ } catch (callbackError) {
290
+ console.error('[BansPlugin] onBan callback error:', callbackError);
291
+ }
292
+ }
293
+
294
+ log('User banned by email', { email, userId: user.id, reason: input.reason });
295
+ res.status(201).json(ban);
296
+ } catch (error) {
297
+ console.error('[BansPlugin] Ban user by email error:', error);
298
+ res.status(500).json({ error: 'Failed to ban user' });
299
+ }
300
+ },
301
+ });
302
+
303
+ // Unban user by email (convenience endpoint)
304
+ registry.addRoute({
305
+ method: 'delete',
306
+ path: `${apiPrefix}/email/:email`,
307
+ pluginId: 'bans',
308
+ handler: async (req: Request, res: Response) => {
309
+ try {
310
+ const email = decodeURIComponent(req.params.email);
311
+ const authReq = req as AuthenticatedRequest;
312
+ const removedBy = authReq.auth?.user?.id || authReq.auth?.user?.email || 'system';
313
+
314
+ // Resolve email to user via Users Plugin
315
+ const user = await getUserByEmail(email);
316
+ if (!user) {
317
+ return res.status(404).json({ error: 'User not found' });
318
+ }
319
+
320
+ const input: RemoveBanInput = {
321
+ user_id: user.id,
322
+ removed_by: removedBy,
323
+ note: req.body?.note,
324
+ };
325
+
326
+ const removed = await config.store.removeBan(input);
327
+ if (!removed) {
328
+ return res.status(404).json({ error: 'No active ban found' });
329
+ }
330
+
331
+ // Call onUnban callback if provided
332
+ if (config.callbacks?.onUnban) {
333
+ try {
334
+ await config.callbacks.onUnban(user);
335
+ } catch (callbackError) {
336
+ console.error('[BansPlugin] onUnban callback error:', callbackError);
337
+ }
338
+ }
339
+
340
+ log('User unbanned by email', { email, userId: user.id });
341
+ res.status(204).send();
342
+ } catch (error) {
343
+ console.error('[BansPlugin] Unban user by email error:', error);
344
+ res.status(500).json({ error: 'Failed to unban user' });
345
+ }
346
+ },
347
+ });
348
+ }
349
+
350
+ log('Bans plugin started');
351
+ },
352
+
353
+ async onStop(): Promise<void> {
354
+ log('Stopping bans plugin');
355
+
356
+ if (banCleanupInterval) {
357
+ clearInterval(banCleanupInterval);
358
+ banCleanupInterval = null;
359
+ }
360
+
361
+ await config.store.shutdown();
362
+ currentStore = null;
363
+ pluginConfig = null;
364
+
365
+ log('Bans plugin stopped');
366
+ },
367
+ };
368
+ }
369
+
370
+ // ========================================
371
+ // Helper Functions
372
+ // ========================================
373
+
374
+ /**
375
+ * Get the current ban store instance
376
+ */
377
+ export function getBanStore(): BanStore | null {
378
+ return currentStore;
379
+ }
380
+
381
+ /**
382
+ * Check if a user is banned by user ID
383
+ */
384
+ export async function isUserBanned(userId: string): Promise<boolean> {
385
+ if (!currentStore) {
386
+ throw new Error('Bans plugin not initialized');
387
+ }
388
+ return currentStore.isBanned(userId);
389
+ }
390
+
391
+ /**
392
+ * Check if a user is banned by email
393
+ *
394
+ * This is a convenience function that:
395
+ * 1. Resolves email → user via Users Plugin
396
+ * 2. Checks ban status by user_id
397
+ *
398
+ * Returns false if user doesn't exist (unknown user = not banned)
399
+ */
400
+ export async function isEmailBanned(email: string): Promise<boolean> {
401
+ if (!currentStore) {
402
+ throw new Error('Bans plugin not initialized');
403
+ }
404
+
405
+ // Resolve email to user via Users Plugin
406
+ const user = await getUserByEmail(email);
407
+ if (!user) {
408
+ return false; // Unknown user = not banned
409
+ }
410
+
411
+ return currentStore.isBanned(user.id);
412
+ }
413
+
414
+ /**
415
+ * Get active ban for a user
416
+ */
417
+ export async function getActiveBan(userId: string): Promise<Ban | null> {
418
+ if (!currentStore) {
419
+ throw new Error('Bans plugin not initialized');
420
+ }
421
+ return currentStore.getActiveBan(userId);
422
+ }
423
+
424
+ /**
425
+ * Ban a user
426
+ */
427
+ export async function banUser(input: CreateBanInput): Promise<Ban> {
428
+ if (!currentStore) {
429
+ throw new Error('Bans plugin not initialized');
430
+ }
431
+
432
+ const ban = await currentStore.createBan(input);
433
+
434
+ // Call onBan callback if configured
435
+ if (pluginConfig?.callbacks?.onBan) {
436
+ const user = await getUserById(input.user_id);
437
+ if (user) {
438
+ try {
439
+ await pluginConfig.callbacks.onBan(user, ban);
440
+ } catch (error) {
441
+ console.error('[BansPlugin] onBan callback error:', error);
442
+ }
443
+ }
444
+ }
445
+
446
+ return ban;
447
+ }
448
+
449
+ /**
450
+ * Unban a user
451
+ */
452
+ export async function unbanUser(input: RemoveBanInput): Promise<boolean> {
453
+ if (!currentStore) {
454
+ throw new Error('Bans plugin not initialized');
455
+ }
456
+
457
+ const removed = await currentStore.removeBan(input);
458
+
459
+ // Call onUnban callback if configured
460
+ if (removed && pluginConfig?.callbacks?.onUnban) {
461
+ const user = await getUserById(input.user_id);
462
+ if (user) {
463
+ try {
464
+ await pluginConfig.callbacks.onUnban(user);
465
+ } catch (error) {
466
+ console.error('[BansPlugin] onUnban callback error:', error);
467
+ }
468
+ }
469
+ }
470
+
471
+ return removed;
472
+ }
473
+
474
+ /**
475
+ * List all active bans
476
+ */
477
+ export async function listActiveBans(options?: { limit?: number; offset?: number }): Promise<{
478
+ bans: Ban[];
479
+ total: number;
480
+ }> {
481
+ if (!currentStore) {
482
+ throw new Error('Bans plugin not initialized');
483
+ }
484
+ return currentStore.listActiveBans(options);
485
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Bans Plugin Index
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+
7
+ // Main plugin
8
+ export {
9
+ createBansPlugin,
10
+ getBanStore,
11
+ isUserBanned,
12
+ isEmailBanned,
13
+ getActiveBan,
14
+ banUser,
15
+ unbanUser,
16
+ listActiveBans,
17
+ } from './bans-plugin.js';
18
+
19
+ // Types
20
+ export type {
21
+ BansPluginConfig,
22
+ BanStore,
23
+ Ban,
24
+ CreateBanInput,
25
+ RemoveBanInput,
26
+ BanCallbacks,
27
+ PostgresBanStoreConfig,
28
+ } from './types.js';
29
+
30
+ // Stores
31
+ export { postgresBanStore } from './stores/index.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Ban Stores Index
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+
7
+ export { postgresBanStore } from './postgres-store.js';