@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/dist/api/bridge.d.ts +28 -0
- package/dist/api/bridge.d.ts.map +1 -0
- package/dist/api/bridge.js +85 -0
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +32 -24
- package/dist/api/users.d.ts +12 -0
- package/dist/api/users.d.ts.map +1 -1
- package/dist/api/users.js +94 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.server.d.ts +1 -0
- package/dist/index.server.d.ts.map +1 -1
- package/dist/index.server.js +1 -0
- package/dist/views/UserManagement.d.ts.map +1 -1
- package/dist/views/UserManagement.js +94 -22
- package/package.json +9 -9
- package/src/api/bridge.ts +98 -0
- package/src/api/router.ts +35 -31
- package/src/api/users.ts +106 -0
- package/src/index.server.ts +1 -0
- package/src/index.tsx +1 -7
- package/src/views/UserManagement.tsx +404 -211
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Users - Session Bridge API
|
|
3
|
+
* Handles cross-domain session transfer via secure one-time tokens
|
|
4
|
+
*/
|
|
5
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/plugin-users/bridge/generate - Generate a one-time transfer token
|
|
8
|
+
* Call this from the "Primary" domain (.com)
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateTransferToken(req: NextRequest, config: any): Promise<NextResponse<{
|
|
11
|
+
error: string;
|
|
12
|
+
}> | NextResponse<{
|
|
13
|
+
token: string;
|
|
14
|
+
}>>;
|
|
15
|
+
/**
|
|
16
|
+
* GET /api/plugin-users/bridge/verify - Verify a transfer token and return user data
|
|
17
|
+
* Call this from the "Target" domain (.nl, .se) via proxy
|
|
18
|
+
*/
|
|
19
|
+
export declare function verifyTransferToken(req: NextRequest, config: any): Promise<NextResponse<{
|
|
20
|
+
error: string;
|
|
21
|
+
}> | NextResponse<{
|
|
22
|
+
id: any;
|
|
23
|
+
email: any;
|
|
24
|
+
name: any;
|
|
25
|
+
role: any;
|
|
26
|
+
image: any;
|
|
27
|
+
}>>;
|
|
28
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/api/bridge.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgBxD;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG;;;;IAwBxE;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG;;;;;;;;IA0CtE"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Users - Session Bridge API
|
|
3
|
+
* Handles cross-domain session transfer via secure one-time tokens
|
|
4
|
+
*/
|
|
5
|
+
import { NextResponse } from 'next/server';
|
|
6
|
+
import { getToken } from 'next-auth/jwt';
|
|
7
|
+
import { randomBytes } from 'crypto';
|
|
8
|
+
// Token store (in-memory for simplicity, as tokens are extremely short-lived)
|
|
9
|
+
// In a large multi-server setup, this should be in Redis
|
|
10
|
+
const tokenStore = new Map();
|
|
11
|
+
// Cleanup expired tokens periodically
|
|
12
|
+
setInterval(() => {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
for (const [token, data] of tokenStore.entries()) {
|
|
15
|
+
if (data.expires < now)
|
|
16
|
+
tokenStore.delete(token);
|
|
17
|
+
}
|
|
18
|
+
}, 60000);
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/plugin-users/bridge/generate - Generate a one-time transfer token
|
|
21
|
+
* Call this from the "Primary" domain (.com)
|
|
22
|
+
*/
|
|
23
|
+
export async function generateTransferToken(req, config) {
|
|
24
|
+
try {
|
|
25
|
+
const token = await getToken({
|
|
26
|
+
req,
|
|
27
|
+
secret: process.env.NEXTAUTH_SECRET || config.jwtSecret
|
|
28
|
+
});
|
|
29
|
+
if (!token || !token.sub) {
|
|
30
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
31
|
+
}
|
|
32
|
+
const transferToken = randomBytes(32).toString('hex');
|
|
33
|
+
const expires = Date.now() + 30000; // 30 seconds expiry
|
|
34
|
+
tokenStore.set(transferToken, {
|
|
35
|
+
userId: token.sub,
|
|
36
|
+
expires
|
|
37
|
+
});
|
|
38
|
+
return NextResponse.json({ token: transferToken });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error('[BridgeAPI] Generate error:', err);
|
|
42
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* GET /api/plugin-users/bridge/verify - Verify a transfer token and return user data
|
|
47
|
+
* Call this from the "Target" domain (.nl, .se) via proxy
|
|
48
|
+
*/
|
|
49
|
+
export async function verifyTransferToken(req, config) {
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(req.url);
|
|
52
|
+
const transferToken = url.searchParams.get('token');
|
|
53
|
+
if (!transferToken) {
|
|
54
|
+
return NextResponse.json({ error: 'Token required' }, { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
const data = tokenStore.get(transferToken);
|
|
57
|
+
if (!data || data.expires < Date.now()) {
|
|
58
|
+
tokenStore.delete(transferToken);
|
|
59
|
+
return NextResponse.json({ error: 'Invalid or expired token' }, { status: 401 });
|
|
60
|
+
}
|
|
61
|
+
// Token used, remove it
|
|
62
|
+
tokenStore.delete(transferToken);
|
|
63
|
+
// Fetch user data
|
|
64
|
+
const dbConnection = await config.getDb();
|
|
65
|
+
const db = dbConnection.db();
|
|
66
|
+
const { ObjectId } = require('mongodb');
|
|
67
|
+
const user = await db.collection('users').findOne({
|
|
68
|
+
_id: new ObjectId(data.userId)
|
|
69
|
+
});
|
|
70
|
+
if (!user) {
|
|
71
|
+
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
72
|
+
}
|
|
73
|
+
return NextResponse.json({
|
|
74
|
+
id: user._id.toString(),
|
|
75
|
+
email: user.email,
|
|
76
|
+
name: user.name,
|
|
77
|
+
role: user.role || 'user',
|
|
78
|
+
image: user.image || null
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error('[BridgeAPI] Verify error:', err);
|
|
83
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/api/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAIzC;;;;GAIG;AACH,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAIzC;;;;GAIG;AACH,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,CA6EvB"}
|
package/dist/api/router.js
CHANGED
|
@@ -25,52 +25,60 @@ export async function handleUsersApi(req, path, config) {
|
|
|
25
25
|
initAuthHandler(config.authOptions);
|
|
26
26
|
}
|
|
27
27
|
// Route: /api/auth/[...nextauth] - NextAuth routes
|
|
28
|
-
// When routed from /api/auth, the path contains the nextauth segments
|
|
29
|
-
// Path structure: /api/auth/session -> path: ['session']
|
|
30
|
-
// Path structure: /api/auth/signin -> path: ['signin']
|
|
31
|
-
// Path structure: /api/auth -> path: [] (empty)
|
|
32
|
-
// We handle this by checking if route is empty (root /api/auth) or if it's a known NextAuth route
|
|
33
28
|
const isNextAuthRoute = route === '' ||
|
|
34
|
-
['session', 'signin', 'signout', 'callback', 'csrf', 'providers', 'error'].includes(route);
|
|
29
|
+
['session', 'signin', 'signout', 'callback', 'csrf', 'providers', 'error', '_log'].includes(route);
|
|
35
30
|
if (isNextAuthRoute) {
|
|
36
|
-
// This is a NextAuth route - use the path as nextauth segments
|
|
37
|
-
// For /api/auth, path is [] -> nextauthPath is []
|
|
38
|
-
// For /api/auth/session, path is ['session'] -> nextauthPath is ['session']
|
|
39
31
|
const nextauthPath = path;
|
|
40
|
-
// Create NextAuth context
|
|
41
32
|
const nextauthContext = {
|
|
42
33
|
params: Promise.resolve({ nextauth: nextauthPath })
|
|
43
34
|
};
|
|
44
|
-
if (method === 'GET')
|
|
35
|
+
if (method === 'GET')
|
|
45
36
|
return await AuthGET(req, nextauthContext);
|
|
46
|
-
|
|
47
|
-
if (method === 'POST') {
|
|
37
|
+
if (method === 'POST')
|
|
48
38
|
return await AuthPOST(req, nextauthContext);
|
|
39
|
+
}
|
|
40
|
+
// Route: /api/plugin-users/bridge (Session Bridge)
|
|
41
|
+
if (route === 'bridge') {
|
|
42
|
+
const bridgeModule = await import('./bridge');
|
|
43
|
+
const subRoute = path.length > 1 ? path[1] : '';
|
|
44
|
+
if (subRoute === 'generate' && method === 'GET') {
|
|
45
|
+
return await bridgeModule.generateTransferToken(req, config);
|
|
46
|
+
}
|
|
47
|
+
if (subRoute === 'verify' && method === 'GET') {
|
|
48
|
+
return await bridgeModule.verifyTransferToken(req, config);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
// Route: /api/users or /api/plugin-users/users (user management)
|
|
52
52
|
if (route === 'users') {
|
|
53
53
|
if (path.length === 1) {
|
|
54
|
-
|
|
55
|
-
if (method === 'GET') {
|
|
54
|
+
if (method === 'GET')
|
|
56
55
|
return await GET_USERS(req, config);
|
|
57
|
-
|
|
58
|
-
if (method === 'POST') {
|
|
56
|
+
if (method === 'POST')
|
|
59
57
|
return await POST_USERS(req, config);
|
|
60
|
-
}
|
|
61
58
|
}
|
|
62
59
|
else if (path.length === 2) {
|
|
63
|
-
// /api/users/[id] or /api/plugin-users/users/[id]
|
|
64
60
|
const userId = path[1];
|
|
65
|
-
if (method === 'PATCH')
|
|
61
|
+
if (method === 'PATCH')
|
|
66
62
|
return await PATCH_USER(req, userId, config);
|
|
67
|
-
|
|
68
|
-
if (method === 'DELETE') {
|
|
63
|
+
if (method === 'DELETE')
|
|
69
64
|
return await DELETE_USER(req, userId, config);
|
|
70
|
-
}
|
|
71
65
|
}
|
|
72
66
|
}
|
|
73
|
-
//
|
|
67
|
+
// Route: /api/plugin-users/bootstrap (Initial Setup)
|
|
68
|
+
if (route === 'bootstrap' && method === 'POST') {
|
|
69
|
+
const { BOOTSTRAP_USER } = await import('./users');
|
|
70
|
+
return await BOOTSTRAP_USER(req, config);
|
|
71
|
+
}
|
|
72
|
+
// Route: /api/plugin-users/setup-status (Check if setup is needed)
|
|
73
|
+
if (route === 'setup-status') {
|
|
74
|
+
const { CHECK_SETUP_STATUS } = await import('./users');
|
|
75
|
+
return await CHECK_SETUP_STATUS(req, config);
|
|
76
|
+
}
|
|
77
|
+
// Route: /api/plugin-users/setup-request (Generate temporary key)
|
|
78
|
+
if (route === 'setup-request') {
|
|
79
|
+
const { REQUEST_SETUP_KEY } = await import('./users');
|
|
80
|
+
return await REQUEST_SETUP_KEY(req, config);
|
|
81
|
+
}
|
|
74
82
|
return NextResponse.json({ error: `Method ${method} not allowed for route: ${route || '/'}` }, { status: 405 });
|
|
75
83
|
}
|
|
76
84
|
catch (error) {
|
package/dist/api/users.d.ts
CHANGED
|
@@ -29,4 +29,16 @@ export declare function PATCH_USER(req: NextRequest, userId: string, config: Use
|
|
|
29
29
|
* DELETE /api/plugin-users/users/[id] - Delete user
|
|
30
30
|
*/
|
|
31
31
|
export declare function DELETE_USER(req: NextRequest, userId: string, config: UsersApiConfig): Promise<NextResponse>;
|
|
32
|
+
/**
|
|
33
|
+
* POST /api/plugin-users/setup-request - Generate a temporary setup key
|
|
34
|
+
*/
|
|
35
|
+
export declare function REQUEST_SETUP_KEY(req: NextRequest, config: UsersApiConfig): Promise<NextResponse>;
|
|
36
|
+
/**
|
|
37
|
+
* GET /api/plugin-users/setup-status - Check if any users exist
|
|
38
|
+
*/
|
|
39
|
+
export declare function CHECK_SETUP_STATUS(req: NextRequest, config: UsersApiConfig): Promise<NextResponse>;
|
|
40
|
+
/**
|
|
41
|
+
* POST /api/plugin-users/bootstrap - Create first admin via secret key
|
|
42
|
+
*/
|
|
43
|
+
export declare function BOOTSTRAP_USER(req: NextRequest, config: UsersApiConfig): Promise<NextResponse>;
|
|
32
44
|
//# sourceMappingURL=users.d.ts.map
|
package/dist/api/users.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/api/users.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,MAAM,WAAW,cAAc;IAC3B,6BAA6B;IAC7B,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;IACxC,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,WAAW,CAAC,EAAE,GAAG,CAAC;CACrB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAmB/F;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAiDhG;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC5B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,CAiEvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,CA6BvB"}
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/api/users.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,MAAM,WAAW,cAAc;IAC3B,6BAA6B;IAC7B,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;IACxC,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,WAAW,CAAC,EAAE,GAAG,CAAC;CACrB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAmB/F;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAiDhG;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC5B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,CAiEvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,CA6BvB;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAuBvG;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAcxG;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAsDpG"}
|
package/dist/api/users.js
CHANGED
|
@@ -136,3 +136,97 @@ export async function DELETE_USER(req, userId, config) {
|
|
|
136
136
|
return NextResponse.json({ error: 'Delete failed', detail: err.message }, { status: 500 });
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* POST /api/plugin-users/setup-request - Generate a temporary setup key
|
|
141
|
+
*/
|
|
142
|
+
export async function REQUEST_SETUP_KEY(req, config) {
|
|
143
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
144
|
+
return NextResponse.json({ error: 'Not available' }, { status: 403 });
|
|
145
|
+
}
|
|
146
|
+
const key = Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
147
|
+
const expiry = Date.now() + 60000; // 1 minute
|
|
148
|
+
// Store in global shared store to persist across federated module re-evaluations
|
|
149
|
+
const g = globalThis;
|
|
150
|
+
if (g.__JHITS_SETUP_STORE__) {
|
|
151
|
+
g.__JHITS_SETUP_STORE__.key = key;
|
|
152
|
+
g.__JHITS_SETUP_STORE__.expiry = expiry;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Fallback if provider didn't run yet
|
|
156
|
+
g.__JHITS_SETUP_STORE__ = { key, expiry };
|
|
157
|
+
}
|
|
158
|
+
console.log('\n\x1b[41m\x1b[37m [SECURITY] BOOTSTRAP ACCESS REQUESTED \x1b[0m');
|
|
159
|
+
console.log(`\x1b[31m Temporary Setup Key: \x1b[1m${key}\x1b[0m`);
|
|
160
|
+
console.log('\x1b[31m This key will expire in 60 seconds.\x1b[0m\n');
|
|
161
|
+
return NextResponse.json({ message: 'Key generated and logged to console' });
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* GET /api/plugin-users/setup-status - Check if any users exist
|
|
165
|
+
*/
|
|
166
|
+
export async function CHECK_SETUP_STATUS(req, config) {
|
|
167
|
+
try {
|
|
168
|
+
const dbConnection = await config.getDb();
|
|
169
|
+
const db = dbConnection.db();
|
|
170
|
+
const users = db.collection(config.collectionName || 'users');
|
|
171
|
+
const count = await users.countDocuments();
|
|
172
|
+
return NextResponse.json({
|
|
173
|
+
needsSetup: count === 0,
|
|
174
|
+
isDev: process.env.NODE_ENV === 'development'
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
return NextResponse.json({ needsSetup: false, error: 'DB connection failed' });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* POST /api/plugin-users/bootstrap - Create first admin via secret key
|
|
183
|
+
*/
|
|
184
|
+
export async function BOOTSTRAP_USER(req, config) {
|
|
185
|
+
try {
|
|
186
|
+
const { email, name, password, setupKey } = await req.json();
|
|
187
|
+
// Security check: validate dynamic key from shared store
|
|
188
|
+
const g = globalThis;
|
|
189
|
+
const store = g.__JHITS_SETUP_STORE__ || {};
|
|
190
|
+
const systemSetupKey = store.key;
|
|
191
|
+
const systemExpiry = store.expiry;
|
|
192
|
+
if (!systemSetupKey || !setupKey || setupKey !== systemSetupKey) {
|
|
193
|
+
return NextResponse.json({ error: 'Invalid or missing setup key' }, { status: 403 });
|
|
194
|
+
}
|
|
195
|
+
if (Date.now() > systemExpiry) {
|
|
196
|
+
if (g.__JHITS_SETUP_STORE__)
|
|
197
|
+
g.__JHITS_SETUP_STORE__.key = null;
|
|
198
|
+
return NextResponse.json({ error: 'Setup key has expired' }, { status: 403 });
|
|
199
|
+
}
|
|
200
|
+
// Invalidate key immediately after use
|
|
201
|
+
if (g.__JHITS_SETUP_STORE__)
|
|
202
|
+
g.__JHITS_SETUP_STORE__.key = null;
|
|
203
|
+
const dbConnection = await config.getDb();
|
|
204
|
+
const db = dbConnection.db();
|
|
205
|
+
const users = db.collection(config.collectionName || 'users');
|
|
206
|
+
// Check if any admin already exists (double check)
|
|
207
|
+
const adminExists = await users.findOne({ role: 'admin' });
|
|
208
|
+
if (adminExists && process.env.NODE_ENV !== 'development') {
|
|
209
|
+
return NextResponse.json({ error: 'System already has an administrator' }, { status: 400 });
|
|
210
|
+
}
|
|
211
|
+
// Hash the password
|
|
212
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
213
|
+
// Save to DB
|
|
214
|
+
const result = await users.insertOne({
|
|
215
|
+
email,
|
|
216
|
+
name,
|
|
217
|
+
role: 'admin',
|
|
218
|
+
password: hashedPassword,
|
|
219
|
+
createdAt: new Date(),
|
|
220
|
+
});
|
|
221
|
+
return NextResponse.json({
|
|
222
|
+
message: 'Bootstrap successful',
|
|
223
|
+
_id: result.insertedId,
|
|
224
|
+
email,
|
|
225
|
+
role: 'admin'
|
|
226
|
+
}, { status: 201 });
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
console.error('[UsersAPI] Bootstrap error:', err);
|
|
230
|
+
return NextResponse.json({ error: 'Bootstrap failed', detail: err.message }, { status: 500 });
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAE/B,CAAC;AAEF,eAAe,KAAK,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import UserManagement from './views/UserManagement';
|
|
4
4
|
// User Management Plugin - Always enabled by default
|
|
5
5
|
export const Index = ({ subPath = [], siteId, locale: dashboardLocale = 'en' }) => {
|
|
6
|
-
return
|
|
6
|
+
return _jsx(UserManagement, { locale: dashboardLocale });
|
|
7
7
|
};
|
|
8
8
|
export default Index;
|
package/dist/index.server.d.ts
CHANGED
|
@@ -8,5 +8,6 @@ import 'server-only';
|
|
|
8
8
|
*/
|
|
9
9
|
export { handleUsersApi as handleApi } from './api/router';
|
|
10
10
|
export { handleUsersApi } from './api/router';
|
|
11
|
+
export { generateTransferToken, verifyTransferToken } from './api/bridge';
|
|
11
12
|
export type { UsersApiConfig } from './api/users';
|
|
12
13
|
//# sourceMappingURL=index.server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.server.d.ts","sourceRoot":"","sources":["../src/index.server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,IAAI,SAAS,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.server.d.ts","sourceRoot":"","sources":["../src/index.server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,IAAI,SAAS,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAC1E,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserManagement.d.ts","sourceRoot":"","sources":["../../src/views/UserManagement.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"UserManagement.d.ts","sourceRoot":"","sources":["../../src/views/UserManagement.tsx"],"names":[],"mappings":"AAqBA,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EAAE,MAAa,EAAE,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,2CAuf5E"}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
|
-
import { UserPlus, Shield, Trash2, Search, Loader2, X, Eye, EyeOff, Copy, Check, Calendar } from "lucide-react";
|
|
4
|
+
import { Users, UserPlus, Shield, Trash2, Key, Search, Loader2, X, Eye, EyeOff, Copy, Check, Calendar, UserCircle, Mail, Edit2, ShieldCheck, Activity, ChevronRight } from "lucide-react";
|
|
5
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
6
|
+
import { useSession } from "next-auth/react";
|
|
5
7
|
export default function UserManagement({ locale = 'en' }) {
|
|
8
|
+
const { data: session } = useSession();
|
|
6
9
|
const [users, setUsers] = useState([]);
|
|
7
10
|
const [loading, setLoading] = useState(true);
|
|
8
11
|
const [searchTerm, setSearchTerm] = useState("");
|
|
12
|
+
// Current User Info
|
|
13
|
+
const currentUserEmail = session?.user?.email;
|
|
14
|
+
const currentUserRole = session?.user?.role || 'editor';
|
|
9
15
|
// Modal State
|
|
10
16
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
11
|
-
const [
|
|
17
|
+
const [modalMode, setModalMode] = useState('create');
|
|
18
|
+
const [selectedUser, setSelectedUser] = useState(null);
|
|
19
|
+
const [activeUserIdx, setActiveUserIdx] = useState(0);
|
|
20
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
12
21
|
const [showPassword, setShowPassword] = useState(false);
|
|
13
22
|
const [copied, setCopied] = useState(false);
|
|
14
23
|
// Form State
|
|
@@ -34,39 +43,83 @@ export default function UserManagement({ locale = 'en' }) {
|
|
|
34
43
|
}
|
|
35
44
|
};
|
|
36
45
|
useEffect(() => { fetchUsers(); }, []);
|
|
46
|
+
// Permission Logic
|
|
47
|
+
const canManageEcosystem = currentUserRole === 'dev' || currentUserRole === 'admin';
|
|
48
|
+
const canActionUser = (targetUser) => {
|
|
49
|
+
if (!canManageEcosystem)
|
|
50
|
+
return false;
|
|
51
|
+
if (targetUser.email === currentUserEmail)
|
|
52
|
+
return false; // Can't delete/edit role of self in this view
|
|
53
|
+
// Dev can manage everyone
|
|
54
|
+
if (currentUserRole === 'dev')
|
|
55
|
+
return true;
|
|
56
|
+
// Admin can only manage editors (cannot touch devs or other admins)
|
|
57
|
+
if (currentUserRole === 'admin') {
|
|
58
|
+
return targetUser.role === 'editor';
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
};
|
|
37
62
|
const generateTempPassword = () => {
|
|
38
63
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%";
|
|
39
64
|
let retVal = "";
|
|
40
65
|
for (let i = 0, n = charset.length; i < 10; ++i) {
|
|
41
66
|
retVal += charset.charAt(Math.floor(Math.random() * n));
|
|
42
67
|
}
|
|
43
|
-
setFormData({ ...
|
|
68
|
+
setFormData(prev => ({ ...prev, password: retVal }));
|
|
69
|
+
};
|
|
70
|
+
const handleOpenCreate = () => {
|
|
71
|
+
setModalMode('create');
|
|
72
|
+
setFormData({ name: "", email: "", password: "", role: "editor" });
|
|
73
|
+
generateTempPassword();
|
|
74
|
+
setIsModalOpen(true);
|
|
44
75
|
};
|
|
45
|
-
const
|
|
76
|
+
const handleOpenEdit = (user) => {
|
|
77
|
+
setModalMode('edit');
|
|
78
|
+
setSelectedUser(user);
|
|
79
|
+
setFormData({
|
|
80
|
+
name: user.name,
|
|
81
|
+
email: user.email,
|
|
82
|
+
password: "", // Don't show existing password
|
|
83
|
+
role: user.role
|
|
84
|
+
});
|
|
85
|
+
setIsModalOpen(true);
|
|
86
|
+
};
|
|
87
|
+
const handleSubmit = async (e) => {
|
|
46
88
|
e.preventDefault();
|
|
47
|
-
|
|
89
|
+
setIsProcessing(true);
|
|
48
90
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
91
|
+
const url = modalMode === 'create' ? '/api/users' : `/api/users/${selectedUser?._id}`;
|
|
92
|
+
const method = modalMode === 'create' ? 'POST' : 'PATCH';
|
|
93
|
+
// For editing, only send password if provided
|
|
94
|
+
const payload = { ...formData };
|
|
95
|
+
if (modalMode === 'edit' && !payload.password) {
|
|
96
|
+
delete payload.password;
|
|
97
|
+
}
|
|
98
|
+
const res = await fetch(url, {
|
|
99
|
+
method,
|
|
51
100
|
headers: { 'Content-Type': 'application/json' },
|
|
52
|
-
body: JSON.stringify(
|
|
101
|
+
body: JSON.stringify(payload)
|
|
53
102
|
});
|
|
54
103
|
if (res.ok) {
|
|
55
|
-
const
|
|
56
|
-
|
|
104
|
+
const updatedUser = await res.json();
|
|
105
|
+
if (modalMode === 'create') {
|
|
106
|
+
setUsers([...users, updatedUser]);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
setUsers(users.map(u => u._id === updatedUser._id ? updatedUser : u));
|
|
110
|
+
}
|
|
57
111
|
setIsModalOpen(false);
|
|
58
|
-
setFormData({ name: "", email: "", password: "", role: "editor" });
|
|
59
112
|
}
|
|
60
113
|
else {
|
|
61
114
|
const err = await res.json();
|
|
62
|
-
alert(err.error ||
|
|
115
|
+
alert(err.error || `Failed to ${modalMode} user`);
|
|
63
116
|
}
|
|
64
117
|
}
|
|
65
118
|
catch (err) {
|
|
66
|
-
alert("Network error
|
|
119
|
+
alert("Network error occurred");
|
|
67
120
|
}
|
|
68
121
|
finally {
|
|
69
|
-
|
|
122
|
+
setIsProcessing(false);
|
|
70
123
|
}
|
|
71
124
|
};
|
|
72
125
|
const copyToClipboard = () => {
|
|
@@ -74,15 +127,15 @@ export default function UserManagement({ locale = 'en' }) {
|
|
|
74
127
|
setCopied(true);
|
|
75
128
|
setTimeout(() => setCopied(false), 2000);
|
|
76
129
|
};
|
|
77
|
-
const handleDelete = async (
|
|
78
|
-
if (
|
|
79
|
-
return
|
|
80
|
-
if (!confirm(
|
|
130
|
+
const handleDelete = async (user) => {
|
|
131
|
+
if (!canActionUser(user))
|
|
132
|
+
return;
|
|
133
|
+
if (!confirm(`Are you certain you want to remove ${user.name}? This action is final.`))
|
|
81
134
|
return;
|
|
82
135
|
try {
|
|
83
|
-
const res = await fetch(`/api/users/${
|
|
136
|
+
const res = await fetch(`/api/users/${user._id}`, { method: 'DELETE' });
|
|
84
137
|
if (res.ok)
|
|
85
|
-
setUsers(users.filter(u => u._id !==
|
|
138
|
+
setUsers(users.filter(u => u._id !== user._id));
|
|
86
139
|
}
|
|
87
140
|
catch (err) {
|
|
88
141
|
alert("Error deleting user.");
|
|
@@ -90,5 +143,24 @@ export default function UserManagement({ locale = 'en' }) {
|
|
|
90
143
|
};
|
|
91
144
|
const filteredUsers = users.filter(user => user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
92
145
|
user.email?.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
93
|
-
return (_jsxs("div", { className: "h-full w-full rounded-[2.5rem] bg-dashboard-card dark:bg-neutral-900 p-8 overflow-y-auto", children: [_jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-black text-dashboard-text uppercase tracking-tighter mb-2", children: "User Management" }), _jsx("p", { className: "text-sm text-dashboard-text-secondary", children: "Manage who has access to the dashboard." })] }), _jsxs("button", { onClick: () => { setIsModalOpen(true); generateTempPassword(); }, className: "inline-flex items-center gap-2 px-6 py-3 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20", children: [_jsx(UserPlus, { size: 16 }), "New User"] })] }), _jsxs("div", { className: "bg-dashboard-bg dark:bg-neutral-800/50 rounded-2xl sm:rounded-[2rem] border border-dashboard-border overflow-hidden", children: [_jsxs("div", { className: "p-4 sm:p-6 border-b border-dashboard-border flex items-center justify-between", children: [_jsxs("div", { className: "relative w-full sm:w-72", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-dashboard-text-secondary", size: 16 }), _jsx("input", { type: "text", placeholder: "Search users...", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: "w-full pl-10 pr-4 py-2.5 bg-dashboard-card border border-dashboard-border rounded-lg text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary" })] }), loading && _jsx(Loader2, { className: "animate-spin text-primary ml-4", size: 20 })] }), _jsx("div", { className: "hidden md:block overflow-x-auto", children: _jsxs("table", { className: "w-full text-left border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-[11px] font-bold text-dashboard-text-secondary uppercase tracking-widest border-b border-dashboard-border", children: [_jsx("th", { className: "px-8 py-4", children: "User" }), _jsx("th", { className: "px-8 py-4", children: "Role" }), _jsx("th", { className: "px-8 py-4", children: "Since" }), _jsx("th", { className: "px-8 py-4 text-right", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-dashboard-border", children: filteredUsers.map((user) => (_jsxs("tr", { className: "group hover:bg-dashboard-card transition-colors", children: [_jsx("td", { className: "px-8 py-5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold border border-primary/20", children: user.name?.[0] || 'U' }), _jsxs("div", { children: [_jsx("p", { className: "font-bold text-dashboard-text text-sm", children: user.name }), _jsx("p", { className: "text-xs text-dashboard-text-secondary", children: user.email })] })] }) }), _jsx("td", { className: "px-8 py-5", children: _jsxs("span", { className: `inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider ${user.role === 'dev' ? 'bg-primary text-white' : 'bg-dashboard-bg text-dashboard-text'}`, children: [_jsx(Shield, { size: 10 }), " ", user.role] }) }), _jsx("td", { className: "px-8 py-5 text-sm text-dashboard-text-secondary", children: user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-' }), _jsx("td", { className: "px-8 py-5 text-right", children: _jsx("button", { onClick: () => handleDelete(user._id, user.role), className: "p-2 text-dashboard-text-secondary hover:text-red-500 transition-colors disabled:opacity-0", disabled: user.role === 'dev', children: _jsx(Trash2, { size: 16 }) }) })] }, user._id))) })] }) }), _jsx("div", { className: "md:hidden divide-y divide-dashboard-border", children: filteredUsers.map((user) => (_jsxs("div", { className: "p-5 space-y-4", children: [_jsxs("div", { className: "flex justify-between items-start", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold border border-primary/20", children: user.name?.[0] || 'U' }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-bold text-dashboard-text text-sm truncate", children: user.name }), _jsx("p", { className: "text-[11px] text-dashboard-text-secondary truncate", children: user.email })] })] }), _jsx("button", { onClick: () => handleDelete(user._id, user.role), className: `p-2 rounded-lg bg-red-50 dark:bg-red-950/20 text-red-400 ${user.role === 'dev' ? 'hidden' : 'block'}`, children: _jsx(Trash2, { size: 16 }) })] }), _jsxs("div", { className: "flex items-center justify-between pt-2 border-t border-dashboard-border", children: [_jsxs("span", { className: `inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${user.role === 'dev' ? 'bg-primary text-white' : 'bg-dashboard-bg text-dashboard-text'}`, children: [_jsx(Shield, { size: 10 }), " ", user.role] }), _jsxs("div", { className: "flex items-center gap-1.5 text-[10px] text-dashboard-text-secondary font-medium", children: [_jsx(Calendar, { size: 12 }), user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'] })] })] }, user._id))) }), filteredUsers.length === 0 && !loading && (_jsx("div", { className: "p-12 text-center text-dashboard-text-secondary text-sm", children: "No users found matching your search." }))] }), isModalOpen && (_jsx("div", { className: "fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/40 backdrop-blur-sm animate-in fade-in duration-200", children: _jsxs("div", { className: "bg-dashboard-card w-full max-w-md rounded-t-[2.5rem] sm:rounded-[2.5rem] shadow-2xl overflow-hidden animate-in slide-in-from-bottom sm:zoom-in-95 duration-300 max-h-[90vh] overflow-y-auto border border-dashboard-border", children: [_jsxs("div", { className: "p-6 sm:p-8 bg-primary text-white flex justify-between items-center sticky top-0 z-10", children: [_jsx("h2", { className: "text-xl sm:text-2xl font-black uppercase tracking-tighter", children: "New User" }), _jsx("button", { onClick: () => setIsModalOpen(false), className: "hover:rotate-90 transition-transform p-2", children: _jsx(X, { size: 24 }) })] }), _jsxs("form", { onSubmit: handleCreateUser, className: "p-6 sm:p-8 space-y-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1", children: "Name" }), _jsx("input", { required: true, className: "w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text", value: formData.name, onChange: e => setFormData({ ...formData, name: e.target.value }), placeholder: "e.g. John Doe" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1", children: "Email Address" }), _jsx("input", { required: true, type: "email", className: "w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text", value: formData.email, onChange: e => setFormData({ ...formData, email: e.target.value }), placeholder: "email@example.com" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1", children: "Temporary Password" }), _jsxs("div", { className: "relative", children: [_jsx("input", { required: true, type: showPassword ? "text" : "password", className: "w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all font-mono text-dashboard-text", value: formData.password, onChange: e => setFormData({ ...formData, password: e.target.value }) }), _jsxs("div", { className: "absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: "p-1.5 text-dashboard-text-secondary hover:text-primary", children: showPassword ? _jsx(EyeOff, { size: 16 }) : _jsx(Eye, { size: 16 }) }), _jsx("button", { type: "button", onClick: copyToClipboard, className: "p-1.5 text-dashboard-text-secondary hover:text-primary", children: copied ? _jsx(Check, { size: 16, className: "text-emerald-500" }) : _jsx(Copy, { size: 16 }) })] })] }), _jsx("button", { type: "button", onClick: generateTempPassword, className: "text-[10px] text-primary font-bold uppercase tracking-wider hover:underline ml-1", children: "Generate new password" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1", children: "Role" }), _jsxs("div", { className: "relative", children: [_jsxs("select", { className: "w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm appearance-none cursor-pointer text-dashboard-text", value: formData.role, onChange: e => setFormData({ ...formData, role: e.target.value }), children: [_jsx("option", { value: "editor", children: "Editor (Content only)" }), _jsx("option", { value: "dev", children: "Developer" }), _jsx("option", { value: "admin", children: "Admin (All settings)" })] }), _jsx("div", { className: "absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-dashboard-text-secondary", children: _jsx(Shield, { size: 16 }) })] })] }), _jsx("button", { type: "submit", disabled: isCreating, className: "w-full py-4 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest shadow-lg shadow-primary/20 hover:bg-primary/90 transition-all mt-4 flex items-center justify-center gap-2", children: isCreating ? _jsx(Loader2, { className: "animate-spin", size: 20 }) : "Add User" })] })] }) }))] }));
|
|
146
|
+
const getRoleStyles = (role) => {
|
|
147
|
+
switch (role) {
|
|
148
|
+
case 'dev': return 'bg-primary/20 text-primary border-primary/30 shadow-[0_0_20px_rgba(var(--color-primary),0.2)]';
|
|
149
|
+
case 'admin': return 'bg-emerald-500/20 text-emerald-500 border-emerald-500/30 shadow-[0_0_20px_rgba(16,185,129,0.1)]';
|
|
150
|
+
default: return 'bg-amber-500/20 text-amber-500 border-amber-500/30';
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const permissions = {
|
|
154
|
+
dev: ['Full System Access', 'Kernel Management', 'Database Write', 'Ecosystem Logic', 'Security Protocol'],
|
|
155
|
+
admin: ['User Management', 'Content Orchestration', 'Settings Access', 'Analytical Review', 'Standard Security'],
|
|
156
|
+
editor: ['Content Creation', 'Media Management', 'Basic Settings', 'Public Preview']
|
|
157
|
+
};
|
|
158
|
+
const activeUser = filteredUsers[activeUserIdx] || filteredUsers[0];
|
|
159
|
+
return (_jsxs("div", { className: "h-full flex flex-col overflow-hidden bg-transparent", children: [_jsxs("div", { className: "shrink-0 p-8 lg:p-10 pb-6 border-b border-dashboard-border/30 bg-dashboard-card/20 backdrop-blur-md relative overflow-hidden", children: [_jsx("div", { className: "absolute top-0 right-0 w-1/3 h-full bg-gradient-to-l from-primary/10 to-transparent pointer-events-none" }), _jsxs("div", { className: "flex flex-col lg:flex-row lg:items-center justify-between gap-8 relative z-10", children: [_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary text-[10px] font-bold uppercase tracking-[0.2em] shadow-lg shadow-primary/5", children: [_jsx(Activity, { size: 14, className: "animate-pulse" }), _jsx("span", { children: "System Access Control" })] }), _jsxs("h1", { className: "text-5xl font-bold text-dashboard-text tracking-tight leading-none", children: ["User ", _jsx("span", { className: "text-primary italic", children: "Management" })] })] }), _jsxs("div", { className: "flex items-center gap-6", children: [_jsxs("div", { className: "flex flex-col items-end px-6 border-r border-dashboard-border/40", children: [_jsx("span", { className: "text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-[0.3em] opacity-50 mb-1", children: "Team Members" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex -space-x-3", children: [users.slice(0, 3).map((u, i) => (_jsx("div", { className: "size-8 rounded-full border-2 border-dashboard-card bg-primary flex items-center justify-center text-[10px] font-bold text-white shadow-xl overflow-hidden", children: u.image ? _jsx("img", { src: u.image, className: "size-full object-cover" }) : u.name[0] }, i))), users.length > 3 && (_jsxs("div", { className: "size-8 rounded-full border-2 border-dashboard-card bg-dashboard-card flex items-center justify-center text-[10px] font-bold text-dashboard-text-secondary", children: ["+", users.length - 3] }))] }), _jsx("span", { className: "text-2xl font-bold text-dashboard-text", children: users.length })] })] }), canManageEcosystem && (_jsxs("button", { onClick: handleOpenCreate, className: "group flex items-center gap-4 px-8 py-4 bg-primary text-white rounded-2xl text-xs font-bold uppercase tracking-widest shadow-2xl shadow-primary/30 hover:shadow-primary/50 hover:scale-105 active:scale-95 transition-all", children: [_jsx(UserPlus, { size: 20 }), _jsx("span", { children: "Add New User" })] }))] })] })] }), _jsxs("div", { className: "flex-1 flex overflow-hidden p-6 lg:p-8 gap-8", children: [_jsxs("aside", { className: "w-96 flex flex-col gap-6 shrink-0 h-full", children: [_jsxs("div", { className: "relative group", children: [_jsx(Search, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-primary/40 group-focus-within:text-primary transition-colors", size: 18 }), _jsx("input", { type: "text", placeholder: "Search users...", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: "w-full pl-12 pr-6 py-3.5 bg-dashboard-card/40 border border-dashboard-border/40 rounded-2xl text-sm font-semibold outline-none focus:ring-2 focus:ring-primary/20 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/30" })] }), _jsx("div", { className: "flex-1 overflow-y-auto custom-scrollbar pr-2 space-y-3", children: _jsx(AnimatePresence, { mode: "popLayout", children: filteredUsers.map((user, index) => {
|
|
160
|
+
const isActive = activeUser?._id === user._id;
|
|
161
|
+
const isSelf = user.email === currentUserEmail;
|
|
162
|
+
return (_jsxs(motion.button, { onClick: () => setActiveUserIdx(index), initial: { opacity: 0, x: -20 }, animate: { opacity: 1, x: 0 }, className: `w-full text-left p-4 rounded-2xl border transition-all flex items-center gap-4 group ${isActive
|
|
163
|
+
? 'bg-primary text-white border-primary shadow-xl shadow-primary/20'
|
|
164
|
+
: 'bg-dashboard-card/30 border-dashboard-border/40 hover:border-primary/40 text-dashboard-text'}`, children: [_jsx("div", { className: `size-12 rounded-xl flex items-center justify-center font-bold text-white shrink-0 shadow-lg ${isActive ? 'bg-white/20' : 'bg-primary'}`, children: user.image ? _jsx("img", { src: user.image, className: "size-full object-cover rounded-xl" }) : user.name[0].toUpperCase() }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `block text-sm font-bold truncate ${isActive ? 'text-white' : 'text-dashboard-text'}`, children: user.name }), isSelf && _jsx("span", { className: "size-1.5 rounded-full bg-emerald-400 animate-pulse" })] }), _jsx("span", { className: `block text-[10px] font-medium uppercase tracking-wider opacity-60 truncate ${isActive ? 'text-white' : 'text-dashboard-text-secondary'}`, children: user.role })] }), _jsx(ChevronRight, { size: 16, className: `transition-transform ${isActive ? 'translate-x-1 opacity-100' : 'opacity-0 group-hover:opacity-40'}` })] }, user._id));
|
|
165
|
+
}) }) })] }), _jsx("main", { className: "flex-1 min-w-0 h-full", children: _jsx(AnimatePresence, { mode: "wait", children: activeUser ? (_jsxs(motion.div, { initial: { opacity: 0, scale: 0.98 }, animate: { opacity: 1, scale: 1 }, className: "h-full bg-dashboard-card/40 backdrop-blur-2xl rounded-[2.5rem] border border-dashboard-border/40 overflow-hidden flex flex-col relative", children: [_jsx("div", { className: "absolute top-0 right-0 w-full h-64 bg-gradient-to-b from-primary/10 to-transparent pointer-events-none" }), _jsxs("div", { className: "flex-1 overflow-y-auto custom-scrollbar p-10 space-y-12 relative z-10", children: [_jsxs("div", { className: "flex items-center gap-10", children: [_jsxs("div", { className: "relative", children: [_jsx("div", { className: "size-32 rounded-[2.5rem] bg-primary flex items-center justify-center text-4xl font-bold text-white shadow-2xl shadow-primary/30 overflow-hidden", children: activeUser.image ? _jsx("img", { src: activeUser.image, className: "size-full object-cover" }) : activeUser.name[0].toUpperCase() }), _jsx("div", { className: "absolute -bottom-2 -right-2 p-3 bg-dashboard-card border border-dashboard-border rounded-2xl shadow-xl", children: activeUser.role === 'dev' ? _jsx(ShieldCheck, { className: "text-primary", size: 24 }) : _jsx(Shield, { className: "text-emerald-500", size: 24 }) })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("h2", { className: "text-4xl font-bold text-dashboard-text tracking-tight uppercase italic", children: activeUser.name }), activeUser.email === currentUserEmail && (_jsx("span", { className: "px-3 py-1 bg-emerald-500/10 text-emerald-500 text-[10px] font-bold uppercase tracking-widest rounded-lg border border-emerald-500/20", children: "Active Session" }))] }), _jsxs("div", { className: "flex items-center gap-6 text-sm text-dashboard-text-secondary/70", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Mail, { size: 16, className: "text-primary/60" }), activeUser.email] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { size: 16, className: "text-primary/60" }), "Joined ", new Date(activeUser.createdAt).toLocaleDateString()] })] }), _jsxs("div", { className: `inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-bold uppercase tracking-[0.2em] ${getRoleStyles(activeUser.role)}`, children: [activeUser.role, " Account"] })] })] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-8", children: [_jsxs("div", { className: "space-y-6", children: [_jsx("h4", { className: "text-[10px] font-bold text-primary uppercase tracking-[0.3em] ml-1", children: "Account Permissions" }), _jsx("div", { className: "space-y-3", children: permissions[activeUser.role].map((perm, i) => (_jsxs("div", { className: "flex items-center gap-4 p-4 bg-dashboard-bg/40 border border-dashboard-border/30 rounded-2xl group hover:border-primary/40 transition-all", children: [_jsx("div", { className: "size-2 rounded-full bg-primary/40 group-hover:bg-primary shadow-[0_0_8px_rgba(var(--color-primary),0.5)] transition-all" }), _jsx("span", { className: "text-sm font-semibold text-dashboard-text/90", children: perm }), _jsx(Check, { size: 14, className: "ml-auto text-emerald-500" })] }, i))) })] }), _jsxs("div", { className: "space-y-6", children: [_jsx("h4", { className: "text-[10px] font-bold text-primary uppercase tracking-[0.3em] ml-1", children: "Account Actions" }), _jsxs("div", { className: "bg-dashboard-bg/40 border border-dashboard-border/30 rounded-3xl p-8 space-y-8 h-fit", children: [_jsx("p", { className: "text-xs text-dashboard-text-secondary leading-relaxed font-medium", children: "Manage this user's details, password, and access level within your dashboard." }), _jsx("div", { className: "flex flex-col gap-3", children: canActionUser(activeUser) ? (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: () => handleOpenEdit(activeUser), className: "w-full flex items-center justify-center gap-3 py-4 bg-dashboard-card border border-dashboard-border/60 rounded-2xl text-xs font-bold uppercase tracking-widest text-dashboard-text hover:bg-primary hover:text-white hover:border-primary transition-all active:scale-[0.98]", children: [_jsx(Edit2, { size: 16 }), "Edit User Details"] }), _jsxs("button", { onClick: () => handleDelete(activeUser), className: "w-full flex items-center justify-center gap-3 py-4 bg-red-500/10 border border-red-500/20 rounded-2xl text-xs font-bold uppercase tracking-widest text-red-500 hover:bg-red-500 hover:text-white transition-all active:scale-[0.98]", children: [_jsx(Trash2, { size: 16 }), "Remove User"] })] })) : (_jsxs("div", { className: "p-6 rounded-2xl bg-neutral-500/5 border border-dashed border-neutral-500/20 text-center", children: [_jsx(Shield, { size: 24, className: "mx-auto mb-3 text-neutral-500/40" }), _jsx("span", { className: "text-[10px] font-bold text-neutral-500 uppercase tracking-widest", children: "Protected Account" })] })) })] })] })] })] })] }, activeUser._id)) : (_jsx("div", { className: "h-full flex items-center justify-center bg-dashboard-card/20 rounded-[2.5rem] border border-dashed border-dashboard-border/40", children: _jsxs("div", { className: "text-center space-y-4", children: [_jsx("div", { className: "size-20 bg-primary/10 rounded-3xl flex items-center justify-center mx-auto text-primary/40", children: _jsx(Users, { size: 40 }) }), _jsx("p", { className: "text-sm font-bold text-dashboard-text-secondary uppercase tracking-widest", children: "Select a node to inspect" })] }) })) }) })] }), _jsx(AnimatePresence, { children: isModalOpen && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/40 backdrop-blur-md", children: _jsxs(motion.div, { initial: { opacity: 0, scale: 0.95, y: 10 }, animate: { opacity: 1, scale: 1, y: 0 }, exit: { opacity: 0, scale: 0.95, y: 10 }, className: "bg-dashboard-card/90 backdrop-blur-2xl w-full max-w-md rounded-[2rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh] border border-dashboard-border/40", children: [_jsxs("div", { className: "p-8 pb-4 flex justify-between items-center shrink-0", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-bold text-dashboard-text tracking-tight leading-none mb-2", children: modalMode === 'create' ? 'New Account' : 'Update Account' }), _jsx("p", { className: "text-[10px] font-bold text-primary uppercase tracking-widest opacity-80", children: modalMode === 'create' ? 'Define identity and permissions' : `Modifying ${selectedUser?.name}` })] }), _jsx("button", { onClick: () => setIsModalOpen(false), className: "hover:text-red-500 transition-all p-2 bg-dashboard-bg/50 rounded-xl border border-dashboard-border/40 active:scale-90", children: _jsx(X, { size: 20 }) })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex-1 overflow-y-auto px-8 pb-8 space-y-6 custom-scrollbar", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1 opacity-60", children: "Account Identity" }), _jsxs("div", { className: "relative group", children: [_jsx(UserCircle, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors", size: 18 }), _jsx("input", { required: true, className: "w-full pl-12 pr-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary/30 outline-none text-sm font-semibold transition-all text-dashboard-text", value: formData.name, onChange: e => setFormData({ ...formData, name: e.target.value }), placeholder: "e.g. Sarah Jenkins" })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1 opacity-60", children: "Email Endpoint" }), _jsxs("div", { className: "relative group", children: [_jsx(Mail, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors", size: 18 }), _jsx("input", { required: true, type: "email", className: "w-full pl-12 pr-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary/30 outline-none text-sm font-semibold transition-all text-dashboard-text", value: formData.email, onChange: e => setFormData({ ...formData, email: e.target.value }), placeholder: "sarah@yourteam.com" })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1 opacity-60", children: modalMode === 'create' ? 'Security Credentials' : 'New Password (Optional)' }), _jsxs("div", { className: "relative group", children: [_jsx(Key, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors", size: 18 }), _jsx("input", { required: modalMode === 'create', type: showPassword ? "text" : "password", className: "w-full pl-12 pr-28 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary/30 outline-none text-sm font-mono font-bold transition-all text-dashboard-text", value: formData.password, onChange: e => setFormData({ ...formData, password: e.target.value }), placeholder: modalMode === 'edit' ? "Keep current" : "" }), _jsxs("div", { className: "absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1.5", children: [_jsx("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: "p-1.5 text-dashboard-text-secondary/60 hover:text-primary transition-colors bg-dashboard-card/50 rounded-lg border border-dashboard-border/40", children: showPassword ? _jsx(EyeOff, { size: 14 }) : _jsx(Eye, { size: 14 }) }), _jsx("button", { type: "button", onClick: copyToClipboard, className: "p-1.5 text-dashboard-text-secondary/60 hover:text-primary transition-colors bg-dashboard-card/50 rounded-lg border border-dashboard-border/40", children: copied ? _jsx(Check, { size: 14, className: "text-emerald-500" }) : _jsx(Copy, { size: 14 }) })] })] }), _jsxs("button", { type: "button", onClick: generateTempPassword, className: "text-[9px] text-primary/80 font-bold uppercase tracking-widest hover:text-primary transition-colors ml-1 flex items-center gap-2", children: [_jsx(Key, { size: 10 }), " Auto-generate secure protocol"] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1 opacity-60", children: "Access Configuration" }), _jsxs("div", { className: "relative", children: [_jsxs("select", { className: "w-full px-5 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary/30 outline-none text-xs font-bold uppercase tracking-wider appearance-none cursor-pointer text-dashboard-text", value: formData.role, onChange: e => setFormData({ ...formData, role: e.target.value }), children: [_jsx("option", { value: "editor", children: "Content Editor" }), _jsx("option", { value: "admin", children: "System Administrator" }), currentUserRole === 'dev' && _jsx("option", { value: "dev", children: "Technical Developer" })] }), _jsx("div", { className: "absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-primary/60", children: _jsx(Shield, { size: 16 }) })] })] }), _jsx("button", { type: "submit", disabled: isProcessing, className: "group relative w-full py-4 bg-primary text-white rounded-xl text-xs font-bold uppercase tracking-widest overflow-hidden transition-all shadow-lg shadow-primary/20 hover:scale-[1.01] active:scale-95 flex items-center justify-center gap-3 mt-4", children: isProcessing ? _jsx(Loader2, { className: "animate-spin relative z-10", size: 18 }) : (_jsxs(_Fragment, { children: [modalMode === 'create' ? _jsx(UserPlus, { size: 18, className: "relative z-10" }) : _jsx(Edit2, { size: 18, className: "relative z-10" }), _jsx("span", { className: "relative z-10", children: modalMode === 'create' ? 'Initialize Account' : 'Commit Updates' })] })) })] })] }) })) })] }));
|
|
94
166
|
}
|