@jhits/plugin-users 0.0.4 → 0.0.6
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 +36 -24
- package/src/api/auth.ts +38 -70
- package/src/api/router.ts +0 -12
- package/src/views/UserManagement.tsx +45 -39
package/package.json
CHANGED
|
@@ -1,45 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhits/plugin-users",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "User management and authentication plugin for the JHITS ecosystem",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"browser": {
|
|
8
|
-
"mongodb": false,
|
|
9
|
-
"bcrypt": false,
|
|
10
|
-
"bcryptjs": false,
|
|
11
|
-
"server-only": false,
|
|
12
|
-
"./src/api/users.ts": false,
|
|
13
|
-
"./src/index.server.ts": false
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
14
7
|
},
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
15
10
|
"exports": {
|
|
16
11
|
".": {
|
|
17
|
-
"types": "./
|
|
18
|
-
"
|
|
19
|
-
"default": "./src/index.tsx"
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
20
14
|
},
|
|
21
15
|
"./server": {
|
|
22
|
-
"types": "./
|
|
23
|
-
"
|
|
24
|
-
"default": "./src/index.server.ts"
|
|
16
|
+
"types": "./dist/index.server.d.ts",
|
|
17
|
+
"default": "./dist/index.server.js"
|
|
25
18
|
}
|
|
26
19
|
},
|
|
27
20
|
"dependencies": {
|
|
28
|
-
"@jhits/plugin-core": "^0.0.
|
|
21
|
+
"@jhits/plugin-core": "^0.0.2",
|
|
29
22
|
"bcrypt": "^6.0.0",
|
|
30
|
-
"bcryptjs": "^
|
|
31
|
-
"framer-motion": "^12.
|
|
32
|
-
"lucide-react": "^0.
|
|
33
|
-
"mongodb": "^7.
|
|
34
|
-
"next-auth": "^4.24.13"
|
|
23
|
+
"bcryptjs": "^3.0.3",
|
|
24
|
+
"framer-motion": "^12.34.0",
|
|
25
|
+
"lucide-react": "^0.564.0",
|
|
26
|
+
"mongodb": "^7.1.0",
|
|
27
|
+
"next-auth": "^4.24.13",
|
|
28
|
+
"@types/bcrypt": "^6.0.0",
|
|
29
|
+
"@types/bcryptjs": "^3.0.0",
|
|
30
|
+
"@types/node": "^25.2.3",
|
|
31
|
+
"@types/react": "^19.2.14",
|
|
32
|
+
"@types/react-dom": "^19.2.3"
|
|
35
33
|
},
|
|
36
34
|
"peerDependencies": {
|
|
37
35
|
"next": ">=15.0.0",
|
|
38
36
|
"react": ">=18.0.0",
|
|
39
37
|
"react-dom": ">=18.0.0"
|
|
40
38
|
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
41
|
+
"eslint": "^10.0.0",
|
|
42
|
+
"eslint-config-next": "16.1.6",
|
|
43
|
+
"next": "16.1.6",
|
|
44
|
+
"react": "19.2.4",
|
|
45
|
+
"react-dom": "19.2.4",
|
|
46
|
+
"tailwindcss": "^4.1.18",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
},
|
|
41
49
|
"files": [
|
|
50
|
+
"dist",
|
|
42
51
|
"src",
|
|
43
52
|
"package.json"
|
|
44
|
-
]
|
|
45
|
-
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/api/auth.ts
CHANGED
|
@@ -36,80 +36,48 @@ function getHandler() {
|
|
|
36
36
|
// Export handlers - NextAuth handler works for both GET and POST
|
|
37
37
|
// NextAuth expects the route to be /api/auth/[...nextauth] with path segments in context.params.nextauth
|
|
38
38
|
export async function GET(req: any, context?: { params?: Promise<{ nextauth?: string[] }> }) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
nextauthPath = pathSegments.length > 2 ? pathSegments.slice(2) : [];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Create context with nextauth parameter (NextAuth expects this)
|
|
56
|
-
const nextauthContext = {
|
|
57
|
-
params: Promise.resolve({ nextauth: nextauthPath })
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
return await handler(req, nextauthContext);
|
|
61
|
-
} catch (error: any) {
|
|
62
|
-
console.error('[NextAuth GET] Error:', error);
|
|
63
|
-
// Return a proper error response instead of throwing
|
|
64
|
-
// This prevents CLIENT_FETCH_ERROR in NextAuth
|
|
65
|
-
return new Response(
|
|
66
|
-
JSON.stringify({
|
|
67
|
-
error: error.message || 'Authentication error',
|
|
68
|
-
code: 'AUTH_ERROR'
|
|
69
|
-
}),
|
|
70
|
-
{
|
|
71
|
-
status: 500,
|
|
72
|
-
headers: { 'Content-Type': 'application/json' }
|
|
73
|
-
}
|
|
74
|
-
);
|
|
39
|
+
const handler = getHandler();
|
|
40
|
+
|
|
41
|
+
// Use context if provided, otherwise extract from URL
|
|
42
|
+
let nextauthPath: string[] = [];
|
|
43
|
+
if (context?.params) {
|
|
44
|
+
const resolvedParams = await context.params;
|
|
45
|
+
nextauthPath = resolvedParams.nextauth || [];
|
|
46
|
+
} else {
|
|
47
|
+
// Fallback: extract from URL
|
|
48
|
+
const url = new URL(req.url);
|
|
49
|
+
const pathSegments = url.pathname.split('/').filter(Boolean);
|
|
50
|
+
// Path: /api/auth/session -> segments: ['api', 'auth', 'session'] -> nextauth: ['session']
|
|
51
|
+
nextauthPath = pathSegments.length > 2 ? pathSegments.slice(2) : [];
|
|
75
52
|
}
|
|
53
|
+
|
|
54
|
+
// Create context with nextauth parameter (NextAuth expects this)
|
|
55
|
+
const nextauthContext = {
|
|
56
|
+
params: Promise.resolve({ nextauth: nextauthPath })
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return handler(req, nextauthContext);
|
|
76
60
|
}
|
|
77
61
|
|
|
78
62
|
export async function POST(req: any, context?: { params?: Promise<{ nextauth?: string[] }> }) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
nextauthPath = pathSegments.length > 2 ? pathSegments.slice(2) : [];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const nextauthContext = {
|
|
95
|
-
params: Promise.resolve({ nextauth: nextauthPath })
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
return await handler(req, nextauthContext);
|
|
99
|
-
} catch (error: any) {
|
|
100
|
-
console.error('[NextAuth POST] Error:', error);
|
|
101
|
-
// Return a proper error response instead of throwing
|
|
102
|
-
// This prevents CLIENT_FETCH_ERROR in NextAuth
|
|
103
|
-
return new Response(
|
|
104
|
-
JSON.stringify({
|
|
105
|
-
error: error.message || 'Authentication error',
|
|
106
|
-
code: 'AUTH_ERROR'
|
|
107
|
-
}),
|
|
108
|
-
{
|
|
109
|
-
status: 500,
|
|
110
|
-
headers: { 'Content-Type': 'application/json' }
|
|
111
|
-
}
|
|
112
|
-
);
|
|
63
|
+
const handler = getHandler();
|
|
64
|
+
|
|
65
|
+
// Use context if provided, otherwise extract from URL
|
|
66
|
+
let nextauthPath: string[] = [];
|
|
67
|
+
if (context?.params) {
|
|
68
|
+
const resolvedParams = await context.params;
|
|
69
|
+
nextauthPath = resolvedParams.nextauth || [];
|
|
70
|
+
} else {
|
|
71
|
+
// Fallback: extract from URL
|
|
72
|
+
const url = new URL(req.url);
|
|
73
|
+
const pathSegments = url.pathname.split('/').filter(Boolean);
|
|
74
|
+
nextauthPath = pathSegments.length > 2 ? pathSegments.slice(2) : [];
|
|
113
75
|
}
|
|
76
|
+
|
|
77
|
+
const nextauthContext = {
|
|
78
|
+
params: Promise.resolve({ nextauth: nextauthPath })
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return handler(req, nextauthContext);
|
|
114
82
|
}
|
|
115
83
|
|
package/src/api/router.ts
CHANGED
|
@@ -44,18 +44,6 @@ export async function handleUsersApi(
|
|
|
44
44
|
['session', 'signin', 'signout', 'callback', 'csrf', 'providers', 'error'].includes(route);
|
|
45
45
|
|
|
46
46
|
if (isNextAuthRoute) {
|
|
47
|
-
// Ensure authOptions are provided before handling NextAuth routes
|
|
48
|
-
// Check both config.authOptions and ensure it's not undefined/null
|
|
49
|
-
if (!config.authOptions) {
|
|
50
|
-
console.error('[UsersApiRouter] authOptions not provided for NextAuth route');
|
|
51
|
-
console.error('[UsersApiRouter] Config keys:', Object.keys(config));
|
|
52
|
-
console.error('[UsersApiRouter] Config:', JSON.stringify(config, null, 2));
|
|
53
|
-
return NextResponse.json(
|
|
54
|
-
{ error: 'Authentication not configured', debug: 'authOptions missing from config' },
|
|
55
|
-
{ status: 500 }
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
47
|
// This is a NextAuth route - use the path as nextauth segments
|
|
60
48
|
// For /api/auth, path is [] -> nextauthPath is []
|
|
61
49
|
// For /api/auth/session, path is ['session'] -> nextauthPath is ['session']
|
|
@@ -105,32 +105,38 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
105
105
|
);
|
|
106
106
|
|
|
107
107
|
return (
|
|
108
|
-
<div className="
|
|
109
|
-
{/* Header */}
|
|
110
|
-
<div className="flex flex-col
|
|
108
|
+
<div className="h-full w-full rounded-[2.5rem] bg-dashboard-card dark:bg-neutral-900 p-8 overflow-y-auto">
|
|
109
|
+
{/* Header Section */}
|
|
110
|
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
|
111
111
|
<div>
|
|
112
|
-
<h1 className="text-
|
|
113
|
-
|
|
112
|
+
<h1 className="text-3xl font-black text-dashboard-text uppercase tracking-tighter mb-2">
|
|
113
|
+
User Management
|
|
114
|
+
</h1>
|
|
115
|
+
<p className="text-sm text-dashboard-text-secondary">
|
|
116
|
+
Manage who has access to the dashboard.
|
|
117
|
+
</p>
|
|
114
118
|
</div>
|
|
119
|
+
|
|
115
120
|
<button
|
|
116
121
|
onClick={() => { setIsModalOpen(true); generateTempPassword(); }}
|
|
117
|
-
className="flex items-center
|
|
122
|
+
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"
|
|
118
123
|
>
|
|
119
|
-
<UserPlus size={
|
|
124
|
+
<UserPlus size={16} />
|
|
125
|
+
New User
|
|
120
126
|
</button>
|
|
121
127
|
</div>
|
|
122
128
|
|
|
123
129
|
{/* Toolbar */}
|
|
124
|
-
<div className="bg-dashboard-
|
|
125
|
-
<div className="p-4 sm:p-6 border-b border-dashboard-border flex items-center justify-between
|
|
130
|
+
<div className="bg-dashboard-bg dark:bg-neutral-800/50 rounded-2xl sm:rounded-[2rem] border border-dashboard-border overflow-hidden">
|
|
131
|
+
<div className="p-4 sm:p-6 border-b border-dashboard-border flex items-center justify-between">
|
|
126
132
|
<div className="relative w-full sm:w-72">
|
|
127
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-
|
|
133
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-dashboard-text-secondary" size={16} />
|
|
128
134
|
<input
|
|
129
135
|
type="text"
|
|
130
136
|
placeholder="Search users..."
|
|
131
137
|
value={searchTerm}
|
|
132
138
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
133
|
-
className="w-full pl-10 pr-4 py-2 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"
|
|
139
|
+
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"
|
|
134
140
|
/>
|
|
135
141
|
</div>
|
|
136
142
|
{loading && <Loader2 className="animate-spin text-primary ml-4" size={20} />}
|
|
@@ -140,16 +146,16 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
140
146
|
<div className="hidden md:block overflow-x-auto">
|
|
141
147
|
<table className="w-full text-left border-collapse">
|
|
142
148
|
<thead>
|
|
143
|
-
<tr className="text-[11px] font-bold text-
|
|
149
|
+
<tr className="text-[11px] font-bold text-dashboard-text-secondary uppercase tracking-widest border-b border-dashboard-border">
|
|
144
150
|
<th className="px-8 py-4">User</th>
|
|
145
151
|
<th className="px-8 py-4">Role</th>
|
|
146
152
|
<th className="px-8 py-4">Since</th>
|
|
147
153
|
<th className="px-8 py-4 text-right">Actions</th>
|
|
148
154
|
</tr>
|
|
149
155
|
</thead>
|
|
150
|
-
<tbody className="divide-y divide-
|
|
156
|
+
<tbody className="divide-y divide-dashboard-border">
|
|
151
157
|
{filteredUsers.map((user) => (
|
|
152
|
-
<tr key={user._id} className="group hover:bg-dashboard-
|
|
158
|
+
<tr key={user._id} className="group hover:bg-dashboard-card transition-colors">
|
|
153
159
|
<td className="px-8 py-5">
|
|
154
160
|
<div className="flex items-center gap-3">
|
|
155
161
|
<div className="w-10 h-10 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold border border-primary/20">
|
|
@@ -157,7 +163,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
157
163
|
</div>
|
|
158
164
|
<div>
|
|
159
165
|
<p className="font-bold text-dashboard-text text-sm">{user.name}</p>
|
|
160
|
-
<p className="text-xs text-
|
|
166
|
+
<p className="text-xs text-dashboard-text-secondary">{user.email}</p>
|
|
161
167
|
</div>
|
|
162
168
|
</div>
|
|
163
169
|
</td>
|
|
@@ -167,13 +173,13 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
167
173
|
<Shield size={10} /> {user.role}
|
|
168
174
|
</span>
|
|
169
175
|
</td>
|
|
170
|
-
<td className="px-8 py-5 text-sm text-
|
|
176
|
+
<td className="px-8 py-5 text-sm text-dashboard-text-secondary">
|
|
171
177
|
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
|
|
172
178
|
</td>
|
|
173
179
|
<td className="px-8 py-5 text-right">
|
|
174
180
|
<button
|
|
175
181
|
onClick={() => handleDelete(user._id, user.role)}
|
|
176
|
-
className="p-2 text-
|
|
182
|
+
className="p-2 text-dashboard-text-secondary hover:text-red-500 transition-colors disabled:opacity-0"
|
|
177
183
|
disabled={user.role === 'dev'}
|
|
178
184
|
>
|
|
179
185
|
<Trash2 size={16} />
|
|
@@ -186,7 +192,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
186
192
|
</div>
|
|
187
193
|
|
|
188
194
|
{/* Mobile Card List */}
|
|
189
|
-
<div className="md:hidden divide-y divide-
|
|
195
|
+
<div className="md:hidden divide-y divide-dashboard-border">
|
|
190
196
|
{filteredUsers.map((user) => (
|
|
191
197
|
<div key={user._id} className="p-5 space-y-4">
|
|
192
198
|
<div className="flex justify-between items-start">
|
|
@@ -195,8 +201,8 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
195
201
|
{user.name?.[0] || 'U'}
|
|
196
202
|
</div>
|
|
197
203
|
<div className="min-w-0">
|
|
198
|
-
<p className="font-bold text-
|
|
199
|
-
<p className="text-[11px] text-
|
|
204
|
+
<p className="font-bold text-dashboard-text text-sm truncate">{user.name}</p>
|
|
205
|
+
<p className="text-[11px] text-dashboard-text-secondary truncate">{user.email}</p>
|
|
200
206
|
</div>
|
|
201
207
|
</div>
|
|
202
208
|
<button
|
|
@@ -207,12 +213,12 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
207
213
|
</button>
|
|
208
214
|
</div>
|
|
209
215
|
|
|
210
|
-
<div className="flex items-center justify-between pt-2 border-t border-
|
|
211
|
-
<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-
|
|
216
|
+
<div className="flex items-center justify-between pt-2 border-t border-dashboard-border">
|
|
217
|
+
<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'
|
|
212
218
|
}`}>
|
|
213
219
|
<Shield size={10} /> {user.role}
|
|
214
220
|
</span>
|
|
215
|
-
<div className="flex items-center gap-1.5 text-[10px] text-
|
|
221
|
+
<div className="flex items-center gap-1.5 text-[10px] text-dashboard-text-secondary font-medium">
|
|
216
222
|
<Calendar size={12} />
|
|
217
223
|
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
|
|
218
224
|
</div>
|
|
@@ -222,7 +228,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
222
228
|
</div>
|
|
223
229
|
|
|
224
230
|
{filteredUsers.length === 0 && !loading && (
|
|
225
|
-
<div className="p-12 text-center text-
|
|
231
|
+
<div className="p-12 text-center text-dashboard-text-secondary text-sm">
|
|
226
232
|
No users found matching your search.
|
|
227
233
|
</div>
|
|
228
234
|
)}
|
|
@@ -230,10 +236,10 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
230
236
|
|
|
231
237
|
{/* Create User Modal */}
|
|
232
238
|
{isModalOpen && (
|
|
233
|
-
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/
|
|
234
|
-
<div className="bg-
|
|
239
|
+
<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">
|
|
240
|
+
<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">
|
|
235
241
|
<div className="p-6 sm:p-8 bg-primary text-white flex justify-between items-center sticky top-0 z-10">
|
|
236
|
-
<h2 className="text-xl sm:text-2xl font-
|
|
242
|
+
<h2 className="text-xl sm:text-2xl font-black uppercase tracking-tighter">New User</h2>
|
|
237
243
|
<button onClick={() => setIsModalOpen(false)} className="hover:rotate-90 transition-transform p-2">
|
|
238
244
|
<X size={24} />
|
|
239
245
|
</button>
|
|
@@ -241,10 +247,10 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
241
247
|
|
|
242
248
|
<form onSubmit={handleCreateUser} className="p-6 sm:p-8 space-y-5">
|
|
243
249
|
<div className="space-y-2">
|
|
244
|
-
<label className="text-xs font-bold text-
|
|
250
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Name</label>
|
|
245
251
|
<input
|
|
246
252
|
required
|
|
247
|
-
className="w-full px-5 py-3 bg-dashboard-bg border-
|
|
253
|
+
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"
|
|
248
254
|
value={formData.name}
|
|
249
255
|
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
|
250
256
|
placeholder="e.g. John Doe"
|
|
@@ -252,11 +258,11 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
252
258
|
</div>
|
|
253
259
|
|
|
254
260
|
<div className="space-y-2">
|
|
255
|
-
<label className="text-xs font-bold text-
|
|
261
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Email Address</label>
|
|
256
262
|
<input
|
|
257
263
|
required
|
|
258
264
|
type="email"
|
|
259
|
-
className="w-full px-5 py-3 bg-dashboard-bg border-
|
|
265
|
+
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"
|
|
260
266
|
value={formData.email}
|
|
261
267
|
onChange={e => setFormData({ ...formData, email: e.target.value })}
|
|
262
268
|
placeholder="email@example.com"
|
|
@@ -264,20 +270,20 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
264
270
|
</div>
|
|
265
271
|
|
|
266
272
|
<div className="space-y-2">
|
|
267
|
-
<label className="text-xs font-bold text-
|
|
273
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Temporary Password</label>
|
|
268
274
|
<div className="relative">
|
|
269
275
|
<input
|
|
270
276
|
required
|
|
271
277
|
type={showPassword ? "text" : "password"}
|
|
272
|
-
className="w-full px-5 py-3 bg-
|
|
278
|
+
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"
|
|
273
279
|
value={formData.password}
|
|
274
280
|
onChange={e => setFormData({ ...formData, password: e.target.value })}
|
|
275
281
|
/>
|
|
276
282
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
|
277
|
-
<button type="button" onClick={() => setShowPassword(!showPassword)} className="p-1.5 text-
|
|
283
|
+
<button type="button" onClick={() => setShowPassword(!showPassword)} className="p-1.5 text-dashboard-text-secondary hover:text-primary">
|
|
278
284
|
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
279
285
|
</button>
|
|
280
|
-
<button type="button" onClick={copyToClipboard} className="p-1.5 text-
|
|
286
|
+
<button type="button" onClick={copyToClipboard} className="p-1.5 text-dashboard-text-secondary hover:text-primary">
|
|
281
287
|
{copied ? <Check size={16} className="text-emerald-500" /> : <Copy size={16} />}
|
|
282
288
|
</button>
|
|
283
289
|
</div>
|
|
@@ -292,10 +298,10 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
292
298
|
</div>
|
|
293
299
|
|
|
294
300
|
<div className="space-y-2">
|
|
295
|
-
<label className="text-xs font-bold text-
|
|
301
|
+
<label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Role</label>
|
|
296
302
|
<div className="relative">
|
|
297
303
|
<select
|
|
298
|
-
className="w-full px-5 py-3 bg-
|
|
304
|
+
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"
|
|
299
305
|
value={formData.role}
|
|
300
306
|
onChange={e => setFormData({ ...formData, role: e.target.value as any })}
|
|
301
307
|
>
|
|
@@ -303,7 +309,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
303
309
|
<option value="dev">Developer</option>
|
|
304
310
|
<option value="admin">Admin (All settings)</option>
|
|
305
311
|
</select>
|
|
306
|
-
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-
|
|
312
|
+
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-dashboard-text-secondary">
|
|
307
313
|
<Shield size={16} />
|
|
308
314
|
</div>
|
|
309
315
|
</div>
|
|
@@ -312,7 +318,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
|
|
|
312
318
|
<button
|
|
313
319
|
type="submit"
|
|
314
320
|
disabled={isCreating}
|
|
315
|
-
className="w-full py-4 bg-primary text-white rounded-
|
|
321
|
+
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"
|
|
316
322
|
>
|
|
317
323
|
{isCreating ? <Loader2 className="animate-spin" size={20} /> : "Add User"}
|
|
318
324
|
</button>
|