@qwickapps/server 1.2.0 → 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 +238 -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 +92 -54
- 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 +679 -319
- 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 +114 -61
- package/src/core/gateway.ts +863 -403
- 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,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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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('
|
|
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('
|
|
124
|
+
expect(plugin.name).toBe('Redis Cache (default)');
|
|
84
125
|
});
|
|
85
126
|
|
|
86
|
-
it('should have
|
|
87
|
-
const plugin = createCachePlugin(mockConfig);
|
|
88
|
-
expect(plugin.
|
|
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('
|
|
133
|
+
describe('onStart', () => {
|
|
93
134
|
it('should register the cache instance', async () => {
|
|
94
135
|
const plugin = createCachePlugin(mockConfig, 'test');
|
|
95
|
-
await plugin.
|
|
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.
|
|
143
|
+
await plugin.onStart({}, mockRegistry);
|
|
103
144
|
|
|
104
|
-
|
|
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.
|
|
154
|
+
await plugin.onStart({}, mockRegistry);
|
|
113
155
|
|
|
114
|
-
expect(
|
|
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.
|
|
171
|
+
await plugin.onStart({}, mockRegistry);
|
|
130
172
|
|
|
131
|
-
expect(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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('
|
|
302
|
+
describe('onStop', () => {
|
|
230
303
|
it('should close client and unregister instance', async () => {
|
|
231
304
|
const plugin = createCachePlugin(mockConfig, 'test');
|
|
232
|
-
await plugin.
|
|
305
|
+
await plugin.onStart({}, mockRegistry);
|
|
233
306
|
|
|
234
307
|
expect(hasCache('test')).toBe(true);
|
|
235
308
|
|
|
236
|
-
await plugin.
|
|
309
|
+
await plugin.onStop();
|
|
237
310
|
|
|
238
311
|
expect(hasCache('test')).toBe(false);
|
|
239
312
|
});
|