Skip to content

Commit b69b02e

Browse files
authored
feat: add admin user creation capability (#13)
Allows administrators to create users directly from the admin dashboard, eliminating the need to enable public signup for controlled user onboarding. Backend Changes: - Add adminCreateUserSchema validation with role selection and active status - Implement POST /api/admin/users endpoint with comprehensive validation - Check for duplicate email/username before user creation - Hash passwords with bcrypt (12 rounds) - Audit logging for CREATE_USER action with full metadata - Return proper validation error messages Frontend Changes: - Add createUser() API function in adminApi.ts - Create user creation dialog in UserManagement component - Form fields: email, username, password, role (USER/ADMIN), active status - Real-time validation error display - Password requirements helper text - Loading states during user creation - Auto-refresh user list after successful creation Security Features: - Admin-only endpoint (requireAdmin middleware) - Full password complexity validation (12+ chars, mixed case, numbers, special chars) - Weak password dictionary blocking - Duplicate user prevention - Comprehensive audit logging with IP and user agent tracking - CSRF protection via existing middleware User Experience: - Single-click user creation from admin dashboard - Clear validation feedback - Role selection (User/Admin) - Optional active status (user can login immediately or requires activation) - Seamless integration with existing user management UI Documentation: - Updated README to highlight admin user creation capability - Updated audit events tracking list - Enhanced admin dashboard feature description Use Cases: - Controlled user onboarding without public signup - Pre-create accounts for known team members - Bulk user provisioning by admins - Closed beta or private deployment scenarios This feature complements the existing user approval system, giving admins complete control over user account creation and management.
1 parent bd0008e commit b69b02e

File tree

5 files changed

+338
-10
lines changed

5 files changed

+338
-10
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ TriTerm brings the power of your terminal to the browser with enterprise feature
5757
### Admin Dashboard
5858

5959
-**System overview** with real-time statistics
60-
-**User management** (create, update, delete, role assignment)
60+
-**User management** (create users directly, update roles, activate/deactivate, delete)
61+
-**User approval system** - Approve or reject pending registrations
6162
-**Session monitoring** - Track active terminal sessions
6263
-**Audit log viewer** - Security event tracking
6364
-**System metrics** - CPU, memory, uptime monitoring
65+
-**System settings** - Toggle public signup on/off
6466

6567
### Developer Experience
6668

@@ -516,7 +518,8 @@ TriTerm implements enterprise-grade security measures aligned with OWASP Top 10
516518
**Account Protection**
517519

518520
- ✅ Account lockout: 5 failed attempts = 15 min lockout
519-
- ✅ User approval system (admin-controlled)
521+
- ✅ User approval system (admin-controlled registration)
522+
- ✅ Admin user creation (admins can create users directly)
520523
- ✅ Generic error messages (prevents user enumeration)
521524
- ✅ Comprehensive audit logging
522525

@@ -559,7 +562,7 @@ X-Permitted-Cross-Domain-Policies: none
559562
- Account lockouts & unlocks
560563
- Password changes
561564
- Token refresh & revocation
562-
- Admin actions (user activation/deactivation)
565+
- Admin actions (user creation/activation/deactivation)
563566
- Role changes
564567
- Failed authentication attempts
565568

client/src/lib/adminApi.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ export async function getAllUsers(): Promise<User[]> {
118118
return fetchWithAuth(`${API_URL}/admin/users`);
119119
}
120120

121+
export interface CreateUserInput {
122+
email: string;
123+
username: string;
124+
password: string;
125+
role: 'USER' | 'ADMIN';
126+
isActive: boolean;
127+
}
128+
129+
export async function createUser(input: CreateUserInput): Promise<User> {
130+
return fetchWithAuth(`${API_URL}/admin/users`, {
131+
method: 'POST',
132+
body: JSON.stringify(input),
133+
});
134+
}
135+
121136
export async function updateUserRole(userId: string, role: 'USER' | 'ADMIN'): Promise<User> {
122137
return fetchWithAuth(`${API_URL}/admin/users/${userId}`, {
123138
method: 'PATCH',

client/src/pages/Admin/UserManagement.tsx

Lines changed: 216 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ import {
1616
SelectTrigger,
1717
SelectValue,
1818
} from '../../components/ui/select';
19+
import {
20+
Dialog,
21+
DialogContent,
22+
DialogDescription,
23+
DialogFooter,
24+
DialogHeader,
25+
DialogTitle,
26+
} from '../../components/ui/dialog';
1927
import {
2028
AlertDialog,
2129
AlertDialogAction,
@@ -27,9 +35,11 @@ import {
2735
AlertDialogTitle,
2836
} from '../../components/ui/alert-dialog';
2937
import { Badge } from '../../components/ui/badge';
30-
import { Users, Shield, UserX, AlertCircle, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
31-
import { getAllUsers, updateUserRole, deleteUser, activateUser, deactivateUser } from '../../lib/adminApi';
32-
import type { User } from '../../lib/adminApi';
38+
import { Input } from '../../components/ui/input';
39+
import { Label } from '../../components/ui/label';
40+
import { Users, Shield, UserX, AlertCircle, RefreshCw, CheckCircle, XCircle, UserPlus } from 'lucide-react';
41+
import { getAllUsers, updateUserRole, deleteUser, activateUser, deactivateUser, createUser } from '../../lib/adminApi';
42+
import type { User, CreateUserInput } from '../../lib/adminApi';
3343
import { useAuth } from '../../contexts/AuthContext';
3444

3545
export function UserManagement() {
@@ -41,6 +51,16 @@ export function UserManagement() {
4151
const [userToDelete, setUserToDelete] = useState<User | null>(null);
4252
const [updatingRole, setUpdatingRole] = useState<string | null>(null);
4353
const [togglingStatus, setTogglingStatus] = useState<string | null>(null);
54+
const [createDialogOpen, setCreateDialogOpen] = useState(false);
55+
const [creating, setCreating] = useState(false);
56+
const [createFormData, setCreateFormData] = useState<CreateUserInput>({
57+
email: '',
58+
username: '',
59+
password: '',
60+
role: 'USER',
61+
isActive: true,
62+
});
63+
const [createErrors, setCreateErrors] = useState<string[]>([]);
4464

4565
useEffect(() => {
4666
loadUsers();
@@ -125,6 +145,52 @@ export function UserManagement() {
125145
return new Date(date).toLocaleString();
126146
};
127147

148+
const openCreateDialog = () => {
149+
setCreateFormData({
150+
email: '',
151+
username: '',
152+
password: '',
153+
role: 'USER',
154+
isActive: true,
155+
});
156+
setCreateErrors([]);
157+
setCreateDialogOpen(true);
158+
};
159+
160+
const handleCreateUser = async () => {
161+
try {
162+
setCreateErrors([]);
163+
setCreating(true);
164+
165+
const newUser = await createUser(createFormData);
166+
setUsers([newUser, ...users]);
167+
setCreateDialogOpen(false);
168+
setCreateFormData({
169+
email: '',
170+
username: '',
171+
password: '',
172+
role: 'USER',
173+
isActive: true,
174+
});
175+
} catch (err) {
176+
const errorMessage = err instanceof Error ? err.message : 'Failed to create user';
177+
178+
// Parse validation errors if they exist
179+
if (errorMessage.includes('Validation failed')) {
180+
try {
181+
const errorData = JSON.parse(errorMessage.split('Validation failed: ')[1] || '[]');
182+
setCreateErrors(errorData.map((e: any) => e.message || e));
183+
} catch {
184+
setCreateErrors([errorMessage]);
185+
}
186+
} else {
187+
setCreateErrors([errorMessage]);
188+
}
189+
} finally {
190+
setCreating(false);
191+
}
192+
};
193+
128194
if (loading) {
129195
return (
130196
<Card>
@@ -174,10 +240,16 @@ export function UserManagement() {
174240
Manage user accounts and permissions ({users.length} total)
175241
</CardDescription>
176242
</div>
177-
<Button onClick={loadUsers} variant="outline" size="sm">
178-
<RefreshCw className="h-4 w-4 mr-2" />
179-
Refresh
180-
</Button>
243+
<div className="flex items-center gap-2">
244+
<Button onClick={openCreateDialog} variant="default" size="sm">
245+
<UserPlus className="h-4 w-4 mr-2" />
246+
Create User
247+
</Button>
248+
<Button onClick={loadUsers} variant="outline" size="sm">
249+
<RefreshCw className="h-4 w-4 mr-2" />
250+
Refresh
251+
</Button>
252+
</div>
181253
</div>
182254
</CardHeader>
183255
<CardContent>
@@ -306,6 +378,143 @@ export function UserManagement() {
306378
</AlertDialogFooter>
307379
</AlertDialogContent>
308380
</AlertDialog>
381+
382+
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
383+
<DialogContent className="sm:max-w-[500px]">
384+
<DialogHeader>
385+
<DialogTitle className="flex items-center gap-2">
386+
<UserPlus className="h-5 w-5" />
387+
Create New User
388+
</DialogTitle>
389+
<DialogDescription>
390+
Create a new user account. The user will be able to login immediately if active.
391+
</DialogDescription>
392+
</DialogHeader>
393+
394+
<div className="grid gap-4 py-4">
395+
{createErrors.length > 0 && (
396+
<div className="p-3 rounded-md bg-destructive/10 border border-destructive/20">
397+
<div className="flex items-start gap-2">
398+
<AlertCircle className="h-4 w-4 text-destructive mt-0.5" />
399+
<div className="flex-1">
400+
<p className="text-sm font-medium text-destructive">Validation errors:</p>
401+
<ul className="list-disc list-inside text-sm text-destructive mt-1">
402+
{createErrors.map((error, index) => (
403+
<li key={index}>{error}</li>
404+
))}
405+
</ul>
406+
</div>
407+
</div>
408+
</div>
409+
)}
410+
411+
<div className="grid gap-2">
412+
<Label htmlFor="email">Email</Label>
413+
<Input
414+
id="email"
415+
type="email"
416+
placeholder="user@example.com"
417+
value={createFormData.email}
418+
onChange={(e) => setCreateFormData({ ...createFormData, email: e.target.value })}
419+
disabled={creating}
420+
/>
421+
</div>
422+
423+
<div className="grid gap-2">
424+
<Label htmlFor="username">Username</Label>
425+
<Input
426+
id="username"
427+
type="text"
428+
placeholder="johndoe"
429+
value={createFormData.username}
430+
onChange={(e) => setCreateFormData({ ...createFormData, username: e.target.value })}
431+
disabled={creating}
432+
/>
433+
</div>
434+
435+
<div className="grid gap-2">
436+
<Label htmlFor="password">Password</Label>
437+
<Input
438+
id="password"
439+
type="password"
440+
placeholder="Min 12 chars, uppercase, lowercase, number, special"
441+
value={createFormData.password}
442+
onChange={(e) => setCreateFormData({ ...createFormData, password: e.target.value })}
443+
disabled={creating}
444+
/>
445+
<p className="text-xs text-muted-foreground">
446+
Min 12 characters with uppercase, lowercase, number, and special character
447+
</p>
448+
</div>
449+
450+
<div className="grid gap-2">
451+
<Label htmlFor="role">Role</Label>
452+
<Select
453+
value={createFormData.role}
454+
onValueChange={(value: 'USER' | 'ADMIN') =>
455+
setCreateFormData({ ...createFormData, role: value })
456+
}
457+
disabled={creating}
458+
>
459+
<SelectTrigger id="role">
460+
<SelectValue />
461+
</SelectTrigger>
462+
<SelectContent>
463+
<SelectItem value="USER">
464+
<div className="flex items-center gap-2">
465+
<Users className="h-4 w-4" />
466+
User
467+
</div>
468+
</SelectItem>
469+
<SelectItem value="ADMIN">
470+
<div className="flex items-center gap-2">
471+
<Shield className="h-4 w-4" />
472+
Admin
473+
</div>
474+
</SelectItem>
475+
</SelectContent>
476+
</Select>
477+
</div>
478+
479+
<div className="flex items-center gap-2">
480+
<input
481+
id="isActive"
482+
type="checkbox"
483+
checked={createFormData.isActive}
484+
onChange={(e) => setCreateFormData({ ...createFormData, isActive: e.target.checked })}
485+
disabled={creating}
486+
className="h-4 w-4 rounded border-gray-300"
487+
/>
488+
<Label htmlFor="isActive" className="cursor-pointer">
489+
Active (user can login immediately)
490+
</Label>
491+
</div>
492+
</div>
493+
494+
<DialogFooter>
495+
<Button
496+
variant="outline"
497+
onClick={() => setCreateDialogOpen(false)}
498+
disabled={creating}
499+
>
500+
Cancel
501+
</Button>
502+
<Button onClick={handleCreateUser} disabled={creating}>
503+
{creating ? (
504+
<>
505+
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
506+
Creating...
507+
</>
508+
) : (
509+
<>
510+
<UserPlus className="h-4 w-4 mr-2" />
511+
Create User
512+
</>
513+
)}
514+
</Button>
515+
</DialogFooter>
516+
</DialogContent>
517+
</Dialog>
309518
</>
310519
);
311520
}

server/lib/validation.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,21 @@ export const refreshTokenSchema = z.object({
6363
refreshToken: z.string().min(1, 'Refresh token is required'),
6464
});
6565

66+
// Admin user creation schema (includes role selection)
67+
export const adminCreateUserSchema = z.object({
68+
email: z.string().email('Invalid email address'),
69+
username: z
70+
.string()
71+
.min(3, 'Username must be at least 3 characters')
72+
.max(20, 'Username must be at most 20 characters')
73+
.regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and hyphens'),
74+
password: passwordValidator,
75+
role: z.enum(['USER', 'ADMIN']),
76+
isActive: z.boolean().default(true),
77+
});
78+
6679
// Types inferred from schemas
6780
export type RegisterInput = z.infer<typeof registerSchema>;
6881
export type LoginInput = z.infer<typeof loginSchema>;
6982
export type RefreshTokenInput = z.infer<typeof refreshTokenSchema>;
83+
export type AdminCreateUserInput = z.infer<typeof adminCreateUserSchema>;

0 commit comments

Comments
 (0)