@jhits/plugin-users 0.0.13 → 0.0.15

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/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@jhits/plugin-users",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "User management and authentication plugin for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "main": "./src/index.ts",
9
- "types": "./src/index.ts",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./src/index.tsx",
13
- "import": "./src/index.tsx",
14
- "default": "./src/index.tsx"
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
15
  },
16
16
  "./src": {
17
17
  "types": "./src/index.tsx",
@@ -19,9 +19,9 @@
19
19
  "default": "./src/index.tsx"
20
20
  },
21
21
  "./server": {
22
- "types": "./src/index.server.ts",
23
- "import": "./src/index.server.ts",
24
- "default": "./src/index.server.ts"
22
+ "types": "./dist/index.server.d.ts",
23
+ "import": "./dist/index.server.js",
24
+ "default": "./dist/index.server.js"
25
25
  }
26
26
  },
27
27
  "dependencies": {
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Plugin Users - Session Bridge API
3
+ * Handles cross-domain session transfer via secure one-time tokens
4
+ */
5
+
6
+ import { NextRequest, NextResponse } from 'next/server';
7
+ import { getToken } from 'next-auth/jwt';
8
+ import { randomBytes, createHmac } from 'crypto';
9
+
10
+ // Token store (in-memory for simplicity, as tokens are extremely short-lived)
11
+ // In a large multi-server setup, this should be in Redis
12
+ const tokenStore = new Map<string, { userId: string; expires: number }>();
13
+
14
+ // Cleanup expired tokens periodically
15
+ setInterval(() => {
16
+ const now = Date.now();
17
+ for (const [token, data] of tokenStore.entries()) {
18
+ if (data.expires < now) tokenStore.delete(token);
19
+ }
20
+ }, 60000);
21
+
22
+ /**
23
+ * GET /api/plugin-users/bridge/generate - Generate a one-time transfer token
24
+ * Call this from the "Primary" domain (.com)
25
+ */
26
+ export async function generateTransferToken(req: NextRequest, config: any) {
27
+ try {
28
+ const token = await getToken({
29
+ req,
30
+ secret: process.env.NEXTAUTH_SECRET || config.jwtSecret
31
+ });
32
+
33
+ if (!token || !token.sub) {
34
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
35
+ }
36
+
37
+ const transferToken = randomBytes(32).toString('hex');
38
+ const expires = Date.now() + 30000; // 30 seconds expiry
39
+
40
+ tokenStore.set(transferToken, {
41
+ userId: token.sub,
42
+ expires
43
+ });
44
+
45
+ return NextResponse.json({ token: transferToken });
46
+ } catch (err) {
47
+ console.error('[BridgeAPI] Generate error:', err);
48
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
49
+ }
50
+ }
51
+
52
+ /**
53
+ * GET /api/plugin-users/bridge/verify - Verify a transfer token and return user data
54
+ * Call this from the "Target" domain (.nl, .se) via proxy
55
+ */
56
+ export async function verifyTransferToken(req: NextRequest, config: any) {
57
+ try {
58
+ const url = new URL(req.url);
59
+ const transferToken = url.searchParams.get('token');
60
+
61
+ if (!transferToken) {
62
+ return NextResponse.json({ error: 'Token required' }, { status: 400 });
63
+ }
64
+
65
+ const data = tokenStore.get(transferToken);
66
+ if (!data || data.expires < Date.now()) {
67
+ tokenStore.delete(transferToken);
68
+ return NextResponse.json({ error: 'Invalid or expired token' }, { status: 401 });
69
+ }
70
+
71
+ // Token used, remove it
72
+ tokenStore.delete(transferToken);
73
+
74
+ // Fetch user data
75
+ const dbConnection = await config.getDb();
76
+ const db = dbConnection.db();
77
+ const { ObjectId } = require('mongodb');
78
+
79
+ const user = await db.collection('users').findOne({
80
+ _id: new ObjectId(data.userId)
81
+ });
82
+
83
+ if (!user) {
84
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
85
+ }
86
+
87
+ return NextResponse.json({
88
+ id: user._id.toString(),
89
+ email: user.email,
90
+ name: user.name,
91
+ role: user.role || 'user',
92
+ image: user.image || null
93
+ });
94
+ } catch (err) {
95
+ console.error('[BridgeAPI] Verify error:', err);
96
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
97
+ }
98
+ }
package/src/api/router.ts CHANGED
@@ -35,56 +35,61 @@ export async function handleUsersApi(
35
35
  }
36
36
 
37
37
  // Route: /api/auth/[...nextauth] - NextAuth routes
38
- // When routed from /api/auth, the path contains the nextauth segments
39
- // Path structure: /api/auth/session -> path: ['session']
40
- // Path structure: /api/auth/signin -> path: ['signin']
41
- // Path structure: /api/auth -> path: [] (empty)
42
- // We handle this by checking if route is empty (root /api/auth) or if it's a known NextAuth route
43
38
  const isNextAuthRoute = route === '' ||
44
- ['session', 'signin', 'signout', 'callback', 'csrf', 'providers', 'error'].includes(route);
39
+ ['session', 'signin', 'signout', 'callback', 'csrf', 'providers', 'error', '_log'].includes(route);
45
40
 
46
41
  if (isNextAuthRoute) {
47
- // This is a NextAuth route - use the path as nextauth segments
48
- // For /api/auth, path is [] -> nextauthPath is []
49
- // For /api/auth/session, path is ['session'] -> nextauthPath is ['session']
50
42
  const nextauthPath = path;
51
-
52
- // Create NextAuth context
53
43
  const nextauthContext = {
54
44
  params: Promise.resolve({ nextauth: nextauthPath })
55
45
  };
56
46
 
57
- if (method === 'GET') {
58
- return await AuthGET(req, nextauthContext);
47
+ if (method === 'GET') return await AuthGET(req, nextauthContext);
48
+ if (method === 'POST') return await AuthPOST(req, nextauthContext);
49
+ }
50
+
51
+ // Route: /api/plugin-users/bridge (Session Bridge)
52
+ if (route === 'bridge') {
53
+ const bridgeModule = await import('./bridge');
54
+ const subRoute = path.length > 1 ? path[1] : '';
55
+ if (subRoute === 'generate' && method === 'GET') {
56
+ return await bridgeModule.generateTransferToken(req, config);
59
57
  }
60
- if (method === 'POST') {
61
- return await AuthPOST(req, nextauthContext);
58
+ if (subRoute === 'verify' && method === 'GET') {
59
+ return await bridgeModule.verifyTransferToken(req, config);
62
60
  }
63
61
  }
64
62
 
65
63
  // Route: /api/users or /api/plugin-users/users (user management)
66
64
  if (route === 'users') {
67
65
  if (path.length === 1) {
68
- // /api/users or /api/plugin-users/users
69
- if (method === 'GET') {
70
- return await GET_USERS(req, config);
71
- }
72
- if (method === 'POST') {
73
- return await POST_USERS(req, config);
74
- }
66
+ if (method === 'GET') return await GET_USERS(req, config);
67
+ if (method === 'POST') return await POST_USERS(req, config);
75
68
  } else if (path.length === 2) {
76
- // /api/users/[id] or /api/plugin-users/users/[id]
77
69
  const userId = path[1];
78
- if (method === 'PATCH') {
79
- return await PATCH_USER(req, userId, config);
80
- }
81
- if (method === 'DELETE') {
82
- return await DELETE_USER(req, userId, config);
83
- }
70
+ if (method === 'PATCH') return await PATCH_USER(req, userId, config);
71
+ if (method === 'DELETE') return await DELETE_USER(req, userId, config);
84
72
  }
85
73
  }
86
74
 
87
- // Method not allowed
75
+ // Route: /api/plugin-users/bootstrap (Initial Setup)
76
+ if (route === 'bootstrap' && method === 'POST') {
77
+ const { BOOTSTRAP_USER } = await import('./users');
78
+ return await BOOTSTRAP_USER(req, config);
79
+ }
80
+
81
+ // Route: /api/plugin-users/setup-status (Check if setup is needed)
82
+ if (route === 'setup-status') {
83
+ const { CHECK_SETUP_STATUS } = await import('./users');
84
+ return await CHECK_SETUP_STATUS(req, config);
85
+ }
86
+
87
+ // Route: /api/plugin-users/setup-request (Generate temporary key)
88
+ if (route === 'setup-request') {
89
+ const { REQUEST_SETUP_KEY } = await import('./users');
90
+ return await REQUEST_SETUP_KEY(req, config);
91
+ }
92
+
88
93
  return NextResponse.json(
89
94
  { error: `Method ${method} not allowed for route: ${route || '/'}` },
90
95
  { status: 405 }
@@ -97,4 +102,3 @@ export async function handleUsersApi(
97
102
  );
98
103
  }
99
104
  }
100
-
package/src/api/users.ts CHANGED
@@ -206,3 +206,109 @@ export async function DELETE_USER(
206
206
  }
207
207
  }
208
208
 
209
+ /**
210
+ * POST /api/plugin-users/setup-request - Generate a temporary setup key
211
+ */
212
+ export async function REQUEST_SETUP_KEY(req: NextRequest, config: UsersApiConfig): Promise<NextResponse> {
213
+ if (process.env.NODE_ENV !== 'development') {
214
+ return NextResponse.json({ error: 'Not available' }, { status: 403 });
215
+ }
216
+
217
+ const key = Math.random().toString(36).substring(2, 8).toUpperCase();
218
+ const expiry = Date.now() + 60000; // 1 minute
219
+
220
+ // Store in global shared store to persist across federated module re-evaluations
221
+ const g = globalThis as any;
222
+ if (g.__JHITS_SETUP_STORE__) {
223
+ g.__JHITS_SETUP_STORE__.key = key;
224
+ g.__JHITS_SETUP_STORE__.expiry = expiry;
225
+ } else {
226
+ // Fallback if provider didn't run yet
227
+ g.__JHITS_SETUP_STORE__ = { key, expiry };
228
+ }
229
+
230
+ console.log('\n\x1b[41m\x1b[37m [SECURITY] BOOTSTRAP ACCESS REQUESTED \x1b[0m');
231
+ console.log(`\x1b[31m Temporary Setup Key: \x1b[1m${key}\x1b[0m`);
232
+ console.log('\x1b[31m This key will expire in 60 seconds.\x1b[0m\n');
233
+
234
+ return NextResponse.json({ message: 'Key generated and logged to console' });
235
+ }
236
+
237
+ /**
238
+ * GET /api/plugin-users/setup-status - Check if any users exist
239
+ */
240
+ export async function CHECK_SETUP_STATUS(req: NextRequest, config: UsersApiConfig): Promise<NextResponse> {
241
+ try {
242
+ const dbConnection = await config.getDb();
243
+ const db = dbConnection.db();
244
+ const users = db.collection(config.collectionName || 'users');
245
+ const count = await users.countDocuments();
246
+
247
+ return NextResponse.json({
248
+ needsSetup: count === 0,
249
+ isDev: process.env.NODE_ENV === 'development'
250
+ });
251
+ } catch (err) {
252
+ return NextResponse.json({ needsSetup: false, error: 'DB connection failed' });
253
+ }
254
+ }
255
+
256
+ /**
257
+ * POST /api/plugin-users/bootstrap - Create first admin via secret key
258
+ */
259
+ export async function BOOTSTRAP_USER(req: NextRequest, config: UsersApiConfig): Promise<NextResponse> {
260
+ try {
261
+ const { email, name, password, setupKey } = await req.json();
262
+
263
+ // Security check: validate dynamic key from shared store
264
+ const g = globalThis as any;
265
+ const store = g.__JHITS_SETUP_STORE__ || {};
266
+ const systemSetupKey = store.key;
267
+ const systemExpiry = store.expiry;
268
+
269
+ if (!systemSetupKey || !setupKey || setupKey !== systemSetupKey) {
270
+ return NextResponse.json({ error: 'Invalid or missing setup key' }, { status: 403 });
271
+ }
272
+
273
+ if (Date.now() > systemExpiry) {
274
+ if (g.__JHITS_SETUP_STORE__) g.__JHITS_SETUP_STORE__.key = null;
275
+ return NextResponse.json({ error: 'Setup key has expired' }, { status: 403 });
276
+ }
277
+
278
+ // Invalidate key immediately after use
279
+ if (g.__JHITS_SETUP_STORE__) g.__JHITS_SETUP_STORE__.key = null;
280
+
281
+ const dbConnection = await config.getDb();
282
+ const db = dbConnection.db();
283
+ const users = db.collection(config.collectionName || 'users');
284
+
285
+ // Check if any admin already exists (double check)
286
+ const adminExists = await users.findOne({ role: 'admin' });
287
+ if (adminExists && process.env.NODE_ENV !== 'development') {
288
+ return NextResponse.json({ error: 'System already has an administrator' }, { status: 400 });
289
+ }
290
+
291
+ // Hash the password
292
+ const hashedPassword = await bcrypt.hash(password, 12);
293
+
294
+ // Save to DB
295
+ const result = await users.insertOne({
296
+ email,
297
+ name,
298
+ role: 'admin',
299
+ password: hashedPassword,
300
+ createdAt: new Date(),
301
+ });
302
+
303
+ return NextResponse.json({
304
+ message: 'Bootstrap successful',
305
+ _id: result.insertedId,
306
+ email,
307
+ role: 'admin'
308
+ }, { status: 201 });
309
+ } catch (err: any) {
310
+ console.error('[UsersAPI] Bootstrap error:', err);
311
+ return NextResponse.json({ error: 'Bootstrap failed', detail: err.message }, { status: 500 });
312
+ }
313
+ }
314
+
@@ -10,5 +10,6 @@ import 'server-only';
10
10
 
11
11
  export { handleUsersApi as handleApi } from './api/router';
12
12
  export { handleUsersApi } from './api/router'; // Keep original export for backward compatibility
13
+ export { generateTransferToken, verifyTransferToken } from './api/bridge';
13
14
  export type { UsersApiConfig } from './api/users';
14
15
 
package/src/index.tsx CHANGED
@@ -5,13 +5,7 @@ import UserManagement from './views/UserManagement';
5
5
 
6
6
  // User Management Plugin - Always enabled by default
7
7
  export const Index: React.FC<any> = ({ subPath = [], siteId, locale: dashboardLocale = 'en' }) => {
8
- return (
9
- <div className="w-full h-full flex flex-col overflow-hidden">
10
- <div className="flex-1 overflow-y-auto">
11
- <UserManagement locale={dashboardLocale} />
12
- </div>
13
- </div>
14
- );
8
+ return <UserManagement locale={dashboardLocale} />;
15
9
  };
16
10
 
17
11
  export default Index;