@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.
- package/README.md +318 -0
- package/dist/core/control-panel.d.ts +7 -2
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +99 -60
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +159 -79
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +683 -315
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +271 -0
- package/dist/core/plugin-registry.d.ts.map +1 -0
- package/dist/core/plugin-registry.js +326 -0
- package/dist/core/plugin-registry.js.map +1 -0
- package/dist/core/types.d.ts +16 -33
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
- package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/index.d.ts +9 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/index.js +9 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.d.ts +40 -0
- package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.js +255 -0
- package/dist/plugins/auth/auth-plugin.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.js +147 -0
- package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +12 -0
- package/dist/plugins/auth/index.d.ts.map +1 -0
- package/dist/plugins/auth/index.js +13 -0
- package/dist/plugins/auth/index.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +148 -0
- package/dist/plugins/auth/types.d.ts.map +1 -0
- package/dist/plugins/auth/types.js +14 -0
- package/dist/plugins/auth/types.js.map +1 -0
- package/dist/plugins/bans/bans-plugin.d.ts +59 -0
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
- package/dist/plugins/bans/bans-plugin.js +428 -0
- package/dist/plugins/bans/bans-plugin.js.map +1 -0
- package/dist/plugins/bans/index.d.ts +9 -0
- package/dist/plugins/bans/index.d.ts.map +1 -0
- package/dist/plugins/bans/index.js +10 -0
- package/dist/plugins/bans/index.js.map +1 -0
- package/dist/plugins/bans/stores/index.d.ts +7 -0
- package/dist/plugins/bans/stores/index.d.ts.map +1 -0
- package/dist/plugins/bans/stores/index.js +7 -0
- package/dist/plugins/bans/stores/index.js.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.js +132 -0
- package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
- package/dist/plugins/bans/types.d.ts +128 -0
- package/dist/plugins/bans/types.d.ts.map +1 -0
- package/dist/plugins/bans/types.js +11 -0
- package/dist/plugins/bans/types.js.map +1 -0
- package/dist/plugins/cache-plugin.d.ts +14 -3
- package/dist/plugins/cache-plugin.d.ts.map +1 -1
- package/dist/plugins/cache-plugin.js +27 -7
- package/dist/plugins/cache-plugin.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +96 -32
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +3 -2
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +17 -10
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.d.ts +2 -2
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +17 -10
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
- package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
- package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
- package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
- package/dist/plugins/entitlements/index.d.ts +12 -0
- package/dist/plugins/entitlements/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/index.js +16 -0
- package/dist/plugins/entitlements/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/index.d.ts +9 -0
- package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/index.js +9 -0
- package/dist/plugins/entitlements/sources/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
- package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
- package/dist/plugins/entitlements/types.d.ts +232 -0
- package/dist/plugins/entitlements/types.d.ts.map +1 -0
- package/dist/plugins/entitlements/types.js +11 -0
- package/dist/plugins/entitlements/types.js.map +1 -0
- package/dist/plugins/frontend-app-plugin.d.ts +9 -3
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +14 -9
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.d.ts +5 -2
- package/dist/plugins/health-plugin.d.ts.map +1 -1
- package/dist/plugins/health-plugin.js +20 -5
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +8 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +8 -2
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts +3 -2
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +21 -12
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +3 -3
- package/dist/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/plugins/postgres-plugin.js +9 -7
- package/dist/plugins/postgres-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +47 -29
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/users/index.d.ts +12 -0
- package/dist/plugins/users/index.d.ts.map +1 -0
- package/dist/plugins/users/index.js +13 -0
- package/dist/plugins/users/index.js.map +1 -0
- package/dist/plugins/users/stores/index.d.ts +7 -0
- package/dist/plugins/users/stores/index.d.ts.map +1 -0
- package/dist/plugins/users/stores/index.js +7 -0
- package/dist/plugins/users/stores/index.js.map +1 -0
- package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/users/stores/postgres-store.js +157 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -0
- package/dist/plugins/users/types.d.ts +189 -0
- package/dist/plugins/users/types.d.ts.map +1 -0
- package/dist/plugins/users/types.js +12 -0
- package/dist/plugins/users/types.js.map +1 -0
- package/dist/plugins/users/users-plugin.d.ts +39 -0
- package/dist/plugins/users/users-plugin.d.ts.map +1 -0
- package/dist/plugins/users/users-plugin.js +242 -0
- package/dist/plugins/users/users-plugin.js.map +1 -0
- package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
- package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
- package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
- package/dist-ui-lib/components/index.d.ts +18 -0
- package/dist-ui-lib/config/AppConfig.d.ts +7 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
- package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
- package/dist-ui-lib/dashboard/index.d.ts +13 -0
- package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
- package/dist-ui-lib/index.js +6441 -0
- package/dist-ui-lib/index.js.map +1 -0
- package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
- package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
- package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
- package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
- package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
- package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
- package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
- package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
- package/package.json +18 -6
- package/src/core/control-panel.ts +122 -68
- package/src/core/gateway.ts +870 -399
- package/src/core/index.ts +21 -2
- package/src/core/plugin-registry.ts +653 -0
- package/src/core/types.ts +31 -37
- package/src/index.ts +118 -19
- package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
- package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
- package/src/plugins/auth/adapters/index.ts +9 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
- package/src/plugins/auth/auth-plugin.test.ts +176 -0
- package/src/plugins/auth/auth-plugin.ts +303 -0
- package/src/plugins/auth/index.ts +33 -0
- package/src/plugins/auth/types.ts +165 -0
- package/src/plugins/bans/bans-plugin.ts +485 -0
- package/src/plugins/bans/index.ts +31 -0
- package/src/plugins/bans/stores/index.ts +7 -0
- package/src/plugins/bans/stores/postgres-store.ts +195 -0
- package/src/plugins/bans/types.ts +141 -0
- package/src/plugins/cache-plugin.test.ts +105 -32
- package/src/plugins/cache-plugin.ts +40 -9
- package/src/plugins/config-plugin.ts +23 -12
- package/src/plugins/diagnostics-plugin.ts +22 -12
- package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
- package/src/plugins/entitlements/index.ts +51 -0
- package/src/plugins/entitlements/sources/index.ts +9 -0
- package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
- package/src/plugins/entitlements/types.ts +256 -0
- package/src/plugins/frontend-app-plugin.ts +24 -12
- package/src/plugins/health-plugin.ts +27 -7
- package/src/plugins/index.ts +106 -4
- package/src/plugins/logs-plugin.ts +28 -14
- package/src/plugins/postgres-plugin.test.ts +49 -29
- package/src/plugins/postgres-plugin.ts +11 -9
- package/src/plugins/users/index.ts +35 -0
- package/src/plugins/users/stores/index.ts +7 -0
- package/src/plugins/users/stores/postgres-store.ts +225 -0
- package/src/plugins/users/types.ts +209 -0
- package/src/plugins/users/users-plugin.ts +281 -0
- package/ui/src/App.tsx +185 -31
- package/ui/src/api/controlPanelApi.ts +354 -1
- package/ui/src/components/ControlPanelApp.tsx +209 -0
- package/ui/src/components/index.ts +62 -0
- package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
- package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
- package/ui/src/dashboard/builtInWidgets.tsx +29 -0
- package/ui/src/dashboard/index.ts +35 -0
- package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
- package/ui/src/dashboard/widgets/index.ts +7 -0
- package/ui/src/pages/DashboardPage.tsx +28 -149
- package/ui/src/pages/EntitlementsPage.tsx +557 -0
- package/ui/src/pages/LogsPage.tsx +174 -8
- package/ui/src/pages/PluginPage.tsx +148 -0
- package/ui/src/pages/SystemPage.tsx +445 -0
- package/ui/src/pages/UsersPage.tsx +837 -0
- package/ui/tsconfig.lib.json +11 -0
- package/ui/vite.lib.config.ts +51 -0
- package/dist-ui/assets/index-CW1BviRn.js +0 -465
- package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
- 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';
|