@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,225 @@
1
+ /**
2
+ * PostgreSQL User Store
3
+ *
4
+ * User storage implementation using PostgreSQL.
5
+ * Requires the 'pg' package to be installed.
6
+ *
7
+ * Note: Ban management is handled by the separate Bans Plugin.
8
+ *
9
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
10
+ */
11
+
12
+ import type {
13
+ UserStore,
14
+ User,
15
+ CreateUserInput,
16
+ UpdateUserInput,
17
+ UserSearchParams,
18
+ UserListResponse,
19
+ PostgresUserStoreConfig,
20
+ } from '../types.js';
21
+
22
+ // Pool interface (from pg package)
23
+ interface PgPool {
24
+ query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
25
+ }
26
+
27
+ /**
28
+ * Create a PostgreSQL user store
29
+ *
30
+ * @param config Configuration including a pg Pool instance
31
+ * @returns UserStore implementation
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { Pool } from 'pg';
36
+ * import { postgresUserStore } from '@qwickapps/server';
37
+ *
38
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
39
+ * const store = postgresUserStore({ pool });
40
+ * ```
41
+ */
42
+ export function postgresUserStore(config: PostgresUserStoreConfig): UserStore {
43
+ const {
44
+ pool: poolOrFn,
45
+ usersTable = 'users',
46
+ schema = 'public',
47
+ autoCreateTables = true,
48
+ } = config;
49
+
50
+ // Helper to get pool (supports lazy initialization via function)
51
+ const getPool = (): PgPool => {
52
+ const pool = typeof poolOrFn === 'function' ? poolOrFn() : poolOrFn;
53
+ return pool as PgPool;
54
+ };
55
+
56
+ const usersTableFull = `"${schema}"."${usersTable}"`;
57
+
58
+ return {
59
+ name: 'postgres',
60
+
61
+ async initialize(): Promise<void> {
62
+ if (!autoCreateTables) return;
63
+
64
+ // Create users table
65
+ await getPool().query(`
66
+ CREATE TABLE IF NOT EXISTS ${usersTableFull} (
67
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
68
+ email VARCHAR(255) NOT NULL UNIQUE,
69
+ name VARCHAR(255),
70
+ external_id VARCHAR(255),
71
+ provider VARCHAR(50),
72
+ picture TEXT,
73
+ metadata JSONB DEFAULT '{}',
74
+ created_at TIMESTAMPTZ DEFAULT NOW(),
75
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
76
+ last_login_at TIMESTAMPTZ
77
+ );
78
+
79
+ CREATE INDEX IF NOT EXISTS idx_${usersTable}_email ON ${usersTableFull}(email);
80
+ CREATE INDEX IF NOT EXISTS idx_${usersTable}_external_id ON ${usersTableFull}(external_id, provider);
81
+ `);
82
+ },
83
+
84
+ async getById(id: string): Promise<User | null> {
85
+ const result = await getPool().query(`SELECT * FROM ${usersTableFull} WHERE id = $1`, [id]);
86
+ return (result.rows[0] as User) || null;
87
+ },
88
+
89
+ async getByEmail(email: string): Promise<User | null> {
90
+ const result = await getPool().query(`SELECT * FROM ${usersTableFull} WHERE LOWER(email) = LOWER($1)`, [
91
+ email,
92
+ ]);
93
+ return (result.rows[0] as User) || null;
94
+ },
95
+
96
+ async getByExternalId(externalId: string, provider: string): Promise<User | null> {
97
+ const result = await getPool().query(
98
+ `SELECT * FROM ${usersTableFull} WHERE external_id = $1 AND provider = $2`,
99
+ [externalId, provider]
100
+ );
101
+ return (result.rows[0] as User) || null;
102
+ },
103
+
104
+ async create(input: CreateUserInput): Promise<User> {
105
+ const result = await getPool().query(
106
+ `INSERT INTO ${usersTableFull} (email, name, external_id, provider, picture, metadata)
107
+ VALUES ($1, $2, $3, $4, $5, $6)
108
+ RETURNING *`,
109
+ [
110
+ input.email.toLowerCase(),
111
+ input.name,
112
+ input.external_id,
113
+ input.provider,
114
+ input.picture,
115
+ JSON.stringify(input.metadata || {}),
116
+ ]
117
+ );
118
+ return result.rows[0] as User;
119
+ },
120
+
121
+ async update(id: string, input: UpdateUserInput): Promise<User | null> {
122
+ const updates: string[] = [];
123
+ const values: unknown[] = [];
124
+ let paramIndex = 1;
125
+
126
+ if (input.name !== undefined) {
127
+ updates.push(`name = $${paramIndex++}`);
128
+ values.push(input.name);
129
+ }
130
+ if (input.picture !== undefined) {
131
+ updates.push(`picture = $${paramIndex++}`);
132
+ values.push(input.picture);
133
+ }
134
+ if (input.metadata !== undefined) {
135
+ updates.push(`metadata = $${paramIndex++}`);
136
+ values.push(JSON.stringify(input.metadata));
137
+ }
138
+
139
+ if (updates.length === 0) {
140
+ return this.getById(id);
141
+ }
142
+
143
+ updates.push(`updated_at = NOW()`);
144
+ values.push(id);
145
+
146
+ const result = await getPool().query(
147
+ `UPDATE ${usersTableFull} SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
148
+ values
149
+ );
150
+ return (result.rows[0] as User) || null;
151
+ },
152
+
153
+ async delete(id: string): Promise<boolean> {
154
+ const result = await getPool().query(`DELETE FROM ${usersTableFull} WHERE id = $1`, [id]);
155
+ return (result.rowCount ?? 0) > 0;
156
+ },
157
+
158
+ async search(params: UserSearchParams): Promise<UserListResponse> {
159
+ const {
160
+ query,
161
+ provider,
162
+ page = 1,
163
+ limit = 20,
164
+ sortBy = 'created_at',
165
+ sortOrder = 'desc',
166
+ } = params;
167
+
168
+ const conditions: string[] = [];
169
+ const values: unknown[] = [];
170
+ let paramIndex = 1;
171
+
172
+ if (query) {
173
+ conditions.push(`(LOWER(email) LIKE $${paramIndex} OR LOWER(name) LIKE $${paramIndex})`);
174
+ values.push(`%${query.toLowerCase()}%`);
175
+ paramIndex++;
176
+ }
177
+
178
+ if (provider) {
179
+ conditions.push(`provider = $${paramIndex}`);
180
+ values.push(provider);
181
+ paramIndex++;
182
+ }
183
+
184
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
185
+
186
+ // Validate sort column to prevent SQL injection
187
+ const validSortColumns = ['email', 'name', 'created_at', 'last_login_at'];
188
+ const sortColumn = validSortColumns.includes(sortBy) ? sortBy : 'created_at';
189
+ const sortDir = sortOrder === 'asc' ? 'ASC' : 'DESC';
190
+
191
+ const offset = (page - 1) * limit;
192
+
193
+ // Get total count
194
+ const countResult = await getPool().query(
195
+ `SELECT COUNT(*) FROM ${usersTableFull} ${whereClause}`,
196
+ values
197
+ );
198
+ const total = parseInt((countResult.rows[0] as { count: string }).count, 10);
199
+
200
+ // Get users
201
+ const result = await getPool().query(
202
+ `SELECT * FROM ${usersTableFull} ${whereClause}
203
+ ORDER BY ${sortColumn} ${sortDir}
204
+ LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
205
+ [...values, limit, offset]
206
+ );
207
+
208
+ return {
209
+ users: result.rows as User[],
210
+ total,
211
+ page,
212
+ limit,
213
+ totalPages: Math.ceil(total / limit),
214
+ };
215
+ },
216
+
217
+ async updateLastLogin(id: string): Promise<void> {
218
+ await getPool().query(`UPDATE ${usersTableFull} SET last_login_at = NOW() WHERE id = $1`, [id]);
219
+ },
220
+
221
+ async shutdown(): Promise<void> {
222
+ // Pool is managed externally, nothing to do here
223
+ },
224
+ };
225
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Users Plugin Types
3
+ *
4
+ * Type definitions for user identity management.
5
+ * Storage-agnostic - supports any database through the UserStore interface.
6
+ *
7
+ * Note: Ban management is handled by the separate Bans Plugin.
8
+ *
9
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
10
+ */
11
+
12
+ /**
13
+ * User record in the database
14
+ */
15
+ export interface User {
16
+ /** Primary key - UUID */
17
+ id: string;
18
+ /** User's email address (unique) */
19
+ email: string;
20
+ /** User's display name */
21
+ name?: string;
22
+ /** External provider ID (e.g., Auth0 sub) */
23
+ external_id?: string;
24
+ /** Provider name (e.g., 'auth0', 'supabase') */
25
+ provider?: string;
26
+ /** Profile picture URL */
27
+ picture?: string;
28
+ /** Additional metadata (JSON) */
29
+ metadata?: Record<string, unknown>;
30
+ /** When the user was created */
31
+ created_at: Date;
32
+ /** When the user was last updated */
33
+ updated_at: Date;
34
+ /** When the user last logged in */
35
+ last_login_at?: Date;
36
+ }
37
+
38
+ /**
39
+ * User creation payload
40
+ */
41
+ export interface CreateUserInput {
42
+ email: string;
43
+ name?: string;
44
+ external_id?: string;
45
+ provider?: string;
46
+ picture?: string;
47
+ metadata?: Record<string, unknown>;
48
+ }
49
+
50
+ /**
51
+ * User update payload
52
+ */
53
+ export interface UpdateUserInput {
54
+ name?: string;
55
+ picture?: string;
56
+ metadata?: Record<string, unknown>;
57
+ }
58
+
59
+ /**
60
+ * User search parameters
61
+ */
62
+ export interface UserSearchParams {
63
+ /** Search query (searches email and name) */
64
+ query?: string;
65
+ /** Filter by provider */
66
+ provider?: string;
67
+ /** Page number (1-indexed) */
68
+ page?: number;
69
+ /** Items per page */
70
+ limit?: number;
71
+ /** Sort field */
72
+ sortBy?: 'email' | 'name' | 'created_at' | 'last_login_at';
73
+ /** Sort direction */
74
+ sortOrder?: 'asc' | 'desc';
75
+ }
76
+
77
+ /**
78
+ * Paginated user list response
79
+ */
80
+ export interface UserListResponse {
81
+ users: User[];
82
+ total: number;
83
+ page: number;
84
+ limit: number;
85
+ totalPages: number;
86
+ }
87
+
88
+ /**
89
+ * User store interface - all storage backends must implement this
90
+ */
91
+ export interface UserStore {
92
+ /** Store name (e.g., 'postgres', 'memory') */
93
+ name: string;
94
+
95
+ /**
96
+ * Initialize the store (create tables, etc.)
97
+ */
98
+ initialize(): Promise<void>;
99
+
100
+ /**
101
+ * Get a user by ID
102
+ */
103
+ getById(id: string): Promise<User | null>;
104
+
105
+ /**
106
+ * Get a user by email
107
+ */
108
+ getByEmail(email: string): Promise<User | null>;
109
+
110
+ /**
111
+ * Get a user by external ID
112
+ */
113
+ getByExternalId(externalId: string, provider: string): Promise<User | null>;
114
+
115
+ /**
116
+ * Create a new user
117
+ */
118
+ create(input: CreateUserInput): Promise<User>;
119
+
120
+ /**
121
+ * Update an existing user
122
+ */
123
+ update(id: string, input: UpdateUserInput): Promise<User | null>;
124
+
125
+ /**
126
+ * Delete a user
127
+ */
128
+ delete(id: string): Promise<boolean>;
129
+
130
+ /**
131
+ * Search/list users
132
+ */
133
+ search(params: UserSearchParams): Promise<UserListResponse>;
134
+
135
+ /**
136
+ * Update last login timestamp
137
+ */
138
+ updateLastLogin(id: string): Promise<void>;
139
+
140
+ /**
141
+ * Shutdown the store
142
+ */
143
+ shutdown(): Promise<void>;
144
+ }
145
+
146
+ /**
147
+ * PostgreSQL user store configuration
148
+ * Note: Import Pool type from 'pg' when using this store
149
+ */
150
+ export interface PostgresUserStoreConfig {
151
+ /** PostgreSQL pool instance or a function that returns one (for lazy initialization) */
152
+ pool: unknown | (() => unknown);
153
+ /** Users table name (default: 'users') */
154
+ usersTable?: string;
155
+ /** Schema name (default: 'public') */
156
+ schema?: string;
157
+ /** Auto-create tables on init (default: true) */
158
+ autoCreateTables?: boolean;
159
+ }
160
+
161
+ /**
162
+ * User sync configuration
163
+ */
164
+ export interface UserSyncConfig {
165
+ /** Enable sync */
166
+ enabled: boolean;
167
+ /** Create local user on first login */
168
+ onFirstLogin?: boolean;
169
+ /** Fields to sync from external provider */
170
+ syncFields?: Array<'email' | 'name' | 'picture'>;
171
+ }
172
+
173
+ /**
174
+ * API configuration
175
+ */
176
+ export interface UsersApiConfig {
177
+ /** API route prefix (default: '/api/users') */
178
+ prefix?: string;
179
+ /** Enable CRUD endpoints */
180
+ crud?: boolean;
181
+ /** Enable search endpoint */
182
+ search?: boolean;
183
+ }
184
+
185
+ /**
186
+ * UI configuration
187
+ */
188
+ export interface UsersUiConfig {
189
+ /** Enable UI pages */
190
+ enabled: boolean;
191
+ /** UI page path (default: '/users') */
192
+ page?: string;
193
+ }
194
+
195
+ /**
196
+ * Users plugin configuration
197
+ */
198
+ export interface UsersPluginConfig {
199
+ /** User storage backend */
200
+ store: UserStore;
201
+ /** Sync configuration (optional) */
202
+ sync?: UserSyncConfig;
203
+ /** API configuration */
204
+ api?: UsersApiConfig;
205
+ /** UI configuration */
206
+ ui?: UsersUiConfig;
207
+ /** Enable debug logging */
208
+ debug?: boolean;
209
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Users Plugin
3
+ *
4
+ * User identity management plugin for @qwickapps/server.
5
+ * Provides CRUD operations, search, and user lookup functionality.
6
+ *
7
+ * Note: Ban management is handled by the separate Bans Plugin.
8
+ *
9
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
10
+ */
11
+
12
+ import type { Request, Response } from 'express';
13
+ import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
14
+ import type {
15
+ UsersPluginConfig,
16
+ UserStore,
17
+ User,
18
+ CreateUserInput,
19
+ UpdateUserInput,
20
+ UserSearchParams,
21
+ } from './types.js';
22
+
23
+ // Store instance for helper access
24
+ let currentStore: UserStore | null = null;
25
+
26
+ /**
27
+ * Create the Users plugin
28
+ */
29
+ export function createUsersPlugin(config: UsersPluginConfig): Plugin {
30
+ const debug = config.debug || false;
31
+ // Routes are mounted under /api by the control panel, so don't include /api in prefix
32
+ const apiPrefix = config.api?.prefix || '/users';
33
+
34
+ function log(message: string, data?: Record<string, unknown>) {
35
+ if (debug) {
36
+ console.log(`[UsersPlugin] ${message}`, data || '');
37
+ }
38
+ }
39
+
40
+ return {
41
+ id: 'users',
42
+ name: 'Users',
43
+ version: '1.0.0',
44
+
45
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
46
+ log('Starting users plugin');
47
+
48
+ // Initialize the store (creates tables if needed)
49
+ await config.store.initialize();
50
+ log('Users plugin migrations complete');
51
+
52
+ // Store reference for helper access
53
+ currentStore = config.store;
54
+
55
+ // Register health check
56
+ registry.registerHealthCheck({
57
+ name: 'users-store',
58
+ type: 'custom',
59
+ check: async () => {
60
+ try {
61
+ // Simple health check - try to search with limit 1
62
+ await config.store.search({ limit: 1 });
63
+ return { healthy: true };
64
+ } catch {
65
+ return { healthy: false };
66
+ }
67
+ },
68
+ });
69
+
70
+ // Add API routes if enabled
71
+ if (config.api?.crud !== false) {
72
+ // List/Search users
73
+ registry.addRoute({
74
+ method: 'get',
75
+ path: apiPrefix,
76
+ pluginId: 'users',
77
+ handler: async (req: Request, res: Response) => {
78
+ try {
79
+ const params: UserSearchParams = {
80
+ query: req.query.q as string,
81
+ provider: req.query.provider as string,
82
+ page: parseInt(req.query.page as string) || 1,
83
+ limit: Math.min(parseInt(req.query.limit as string) || 20, 100),
84
+ sortBy: (req.query.sortBy as UserSearchParams['sortBy']) || 'created_at',
85
+ sortOrder: (req.query.sortOrder as UserSearchParams['sortOrder']) || 'desc',
86
+ };
87
+
88
+ const result = await config.store.search(params);
89
+ res.json(result);
90
+ } catch (error) {
91
+ console.error('[UsersPlugin] Search error:', error);
92
+ res.status(500).json({ error: 'Failed to search users' });
93
+ }
94
+ },
95
+ });
96
+
97
+ // Get user by ID
98
+ registry.addRoute({
99
+ method: 'get',
100
+ path: `${apiPrefix}/:id`,
101
+ pluginId: 'users',
102
+ handler: async (req: Request, res: Response) => {
103
+ try {
104
+ const user = await config.store.getById(req.params.id);
105
+ if (!user) {
106
+ return res.status(404).json({ error: 'User not found' });
107
+ }
108
+ res.json(user);
109
+ } catch (error) {
110
+ console.error('[UsersPlugin] Get user error:', error);
111
+ res.status(500).json({ error: 'Failed to get user' });
112
+ }
113
+ },
114
+ });
115
+
116
+ // Create user
117
+ registry.addRoute({
118
+ method: 'post',
119
+ path: apiPrefix,
120
+ pluginId: 'users',
121
+ handler: async (req: Request, res: Response) => {
122
+ try {
123
+ const input: CreateUserInput = {
124
+ email: req.body.email,
125
+ name: req.body.name,
126
+ external_id: req.body.external_id,
127
+ provider: req.body.provider,
128
+ picture: req.body.picture,
129
+ metadata: req.body.metadata,
130
+ };
131
+
132
+ if (!input.email) {
133
+ return res.status(400).json({ error: 'Email is required' });
134
+ }
135
+
136
+ // Check if user already exists
137
+ const existing = await config.store.getByEmail(input.email);
138
+ if (existing) {
139
+ return res.status(409).json({ error: 'User with this email already exists' });
140
+ }
141
+
142
+ const user = await config.store.create(input);
143
+ res.status(201).json(user);
144
+ } catch (error) {
145
+ console.error('[UsersPlugin] Create user error:', error);
146
+ res.status(500).json({ error: 'Failed to create user' });
147
+ }
148
+ },
149
+ });
150
+
151
+ // Update user
152
+ registry.addRoute({
153
+ method: 'put',
154
+ path: `${apiPrefix}/:id`,
155
+ pluginId: 'users',
156
+ handler: async (req: Request, res: Response) => {
157
+ try {
158
+ const input: UpdateUserInput = {
159
+ name: req.body.name,
160
+ picture: req.body.picture,
161
+ metadata: req.body.metadata,
162
+ };
163
+
164
+ const user = await config.store.update(req.params.id, input);
165
+ if (!user) {
166
+ return res.status(404).json({ error: 'User not found' });
167
+ }
168
+ res.json(user);
169
+ } catch (error) {
170
+ console.error('[UsersPlugin] Update user error:', error);
171
+ res.status(500).json({ error: 'Failed to update user' });
172
+ }
173
+ },
174
+ });
175
+
176
+ // Delete user
177
+ registry.addRoute({
178
+ method: 'delete',
179
+ path: `${apiPrefix}/:id`,
180
+ pluginId: 'users',
181
+ handler: async (req: Request, res: Response) => {
182
+ try {
183
+ const deleted = await config.store.delete(req.params.id);
184
+ if (!deleted) {
185
+ return res.status(404).json({ error: 'User not found' });
186
+ }
187
+ res.status(204).send();
188
+ } catch (error) {
189
+ console.error('[UsersPlugin] Delete user error:', error);
190
+ res.status(500).json({ error: 'Failed to delete user' });
191
+ }
192
+ },
193
+ });
194
+ }
195
+
196
+ log('Users plugin started');
197
+ },
198
+
199
+ async onStop(): Promise<void> {
200
+ log('Stopping users plugin');
201
+ await config.store.shutdown();
202
+ currentStore = null;
203
+ log('Users plugin stopped');
204
+ },
205
+ };
206
+ }
207
+
208
+ // ========================================
209
+ // Helper Functions
210
+ // ========================================
211
+
212
+ /**
213
+ * Get the current user store instance
214
+ */
215
+ export function getUserStore(): UserStore | null {
216
+ return currentStore;
217
+ }
218
+
219
+ /**
220
+ * Get a user by ID
221
+ */
222
+ export async function getUserById(id: string): Promise<User | null> {
223
+ if (!currentStore) {
224
+ throw new Error('Users plugin not initialized');
225
+ }
226
+ return currentStore.getById(id);
227
+ }
228
+
229
+ /**
230
+ * Get a user by email
231
+ */
232
+ export async function getUserByEmail(email: string): Promise<User | null> {
233
+ if (!currentStore) {
234
+ throw new Error('Users plugin not initialized');
235
+ }
236
+ return currentStore.getByEmail(email);
237
+ }
238
+
239
+ /**
240
+ * Find or create a user from auth provider data
241
+ */
242
+ export async function findOrCreateUser(data: {
243
+ email: string;
244
+ name?: string;
245
+ external_id: string;
246
+ provider: string;
247
+ picture?: string;
248
+ }): Promise<User> {
249
+ if (!currentStore) {
250
+ throw new Error('Users plugin not initialized');
251
+ }
252
+
253
+ // Try to find by external ID first
254
+ let user = await currentStore.getByExternalId(data.external_id, data.provider);
255
+ if (user) {
256
+ await currentStore.updateLastLogin(user.id);
257
+ return user;
258
+ }
259
+
260
+ // Try to find by email
261
+ user = await currentStore.getByEmail(data.email);
262
+ if (user) {
263
+ // Update with external ID if not set
264
+ if (!user.external_id) {
265
+ await currentStore.update(user.id, {});
266
+ }
267
+ await currentStore.updateLastLogin(user.id);
268
+ return user;
269
+ }
270
+
271
+ // Create new user
272
+ user = await currentStore.create({
273
+ email: data.email,
274
+ name: data.name,
275
+ external_id: data.external_id,
276
+ provider: data.provider,
277
+ picture: data.picture,
278
+ });
279
+
280
+ return user;
281
+ }