@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,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
|
+
}
|