Skip to content

Commit c4f00dc

Browse files
committed
updateed for user management
1 parent 4698f5a commit c4f00dc

File tree

7 files changed

+1363
-0
lines changed

7 files changed

+1363
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { apiClient } from '../client';
2+
import type {
3+
UserInfo,
4+
CreateUserRequest,
5+
UpdateUserRequest,
6+
ChangeRoleRequest,
7+
ResetPasswordRequest,
8+
UsersListParams,
9+
PagedResult,
10+
} from '../types';
11+
12+
export class UserManagementService {
13+
/**
14+
* Get paginated list of users with optional filtering
15+
*/
16+
static async getUsers(params: UsersListParams = {}): Promise<PagedResult<UserInfo>> {
17+
const searchParams = new URLSearchParams();
18+
19+
if (params.page) {
20+
searchParams.append('page', params.page.toString());
21+
}
22+
23+
if (params.pageSize) {
24+
searchParams.append('pageSize', params.pageSize.toString());
25+
}
26+
27+
if (params.search) {
28+
searchParams.append('search', params.search);
29+
}
30+
31+
if (params.role) {
32+
searchParams.append('role', params.role);
33+
}
34+
35+
if (params.isActive !== undefined) {
36+
searchParams.append('isActive', params.isActive.toString());
37+
}
38+
39+
const queryString = searchParams.toString();
40+
const url = `/api/UserManagement${queryString ? `?${queryString}` : ''}`;
41+
42+
return apiClient.get<PagedResult<UserInfo>>(url);
43+
}
44+
45+
/**
46+
* Get a single user by ID
47+
*/
48+
static async getUserById(id: string): Promise<UserInfo> {
49+
return apiClient.get<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`);
50+
}
51+
52+
/**
53+
* Create a new user (admin only)
54+
*/
55+
static async createUser(request: CreateUserRequest): Promise<UserInfo> {
56+
return apiClient.post<UserInfo>('/api/UserManagement', request);
57+
}
58+
59+
/**
60+
* Update user details
61+
*/
62+
static async updateUser(id: string, request: UpdateUserRequest): Promise<UserInfo> {
63+
return apiClient.put<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`, request);
64+
}
65+
66+
/**
67+
* Delete a user
68+
*/
69+
static async deleteUser(id: string): Promise<void> {
70+
return apiClient.delete<void>(`/api/UserManagement/${encodeURIComponent(id)}`);
71+
}
72+
73+
/**
74+
* Change user role
75+
*/
76+
static async changeUserRole(id: string, request: ChangeRoleRequest): Promise<UserInfo> {
77+
return apiClient.patch<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}/role`, request);
78+
}
79+
80+
/**
81+
* Reset user password (admin only)
82+
*/
83+
static async resetUserPassword(id: string, request: ResetPasswordRequest): Promise<void> {
84+
return apiClient.post<void>(`/api/UserManagement/${encodeURIComponent(id)}/reset-password`, request);
85+
}
86+
87+
/**
88+
* Toggle user active status
89+
*/
90+
static async toggleUserStatus(id: string, isActive: boolean): Promise<UserInfo> {
91+
return apiClient.patch<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`, {
92+
isActive,
93+
});
94+
}
95+
}

thingconnect.pulse.client/src/api/types.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,51 @@ export interface ValidationErrorsDto {
182182
message: string;
183183
errors: ValidationError[];
184184
}
185+
186+
// User Management Types
187+
export interface UserInfo {
188+
id: string;
189+
username: string;
190+
email: string;
191+
role: 'User' | 'Administrator';
192+
createdAt: string;
193+
lastLoginAt?: string | null;
194+
isActive: boolean;
195+
}
196+
197+
export interface CreateUserRequest {
198+
username: string;
199+
email: string;
200+
password: string;
201+
role: 'User' | 'Administrator';
202+
}
203+
204+
export interface UpdateUserRequest {
205+
username?: string;
206+
email?: string;
207+
isActive?: boolean;
208+
}
209+
210+
export interface ChangeRoleRequest {
211+
role: 'User' | 'Administrator';
212+
}
213+
214+
export interface ResetPasswordRequest {
215+
newPassword: string;
216+
}
217+
218+
export interface UsersListParams {
219+
page?: number;
220+
pageSize?: number;
221+
search?: string;
222+
role?: string;
223+
isActive?: boolean;
224+
}
225+
226+
export interface PagedResult<T> {
227+
items: T[];
228+
page: number;
229+
pageSize: number;
230+
totalCount: number;
231+
totalPages: number;
232+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import {
2+
Dialog,
3+
DialogContent,
4+
DialogHeader,
5+
DialogTitle,
6+
DialogFooter,
7+
Button,
8+
Input,
9+
FormControl,
10+
FormLabel,
11+
FormErrorMessage,
12+
VStack,
13+
Select,
14+
Alert,
15+
PasswordInput,
16+
} from '@chakra-ui/react';
17+
import { useState, useCallback } from 'react';
18+
import { useForm } from 'react-hook-form';
19+
import { LoadingButton } from '@/components/ui/LoadingButton';
20+
import type { CreateUserRequest } from '@/api/types';
21+
22+
interface CreateUserModalProps {
23+
isOpen: boolean;
24+
onClose: () => void;
25+
onCreateUser: (request: CreateUserRequest) => Promise<void>;
26+
loading?: boolean;
27+
}
28+
29+
interface FormData extends CreateUserRequest {
30+
confirmPassword: string;
31+
}
32+
33+
export function CreateUserModal({
34+
isOpen,
35+
onClose,
36+
onCreateUser,
37+
loading = false,
38+
}: CreateUserModalProps) {
39+
const [submitError, setSubmitError] = useState<string | null>(null);
40+
41+
const {
42+
register,
43+
handleSubmit,
44+
formState: { errors, isSubmitting },
45+
reset,
46+
watch,
47+
} = useForm<FormData>({
48+
defaultValues: {
49+
username: '',
50+
email: '',
51+
password: '',
52+
confirmPassword: '',
53+
role: 'User',
54+
},
55+
});
56+
57+
const password = watch('password');
58+
59+
const handleClose = useCallback(() => {
60+
reset();
61+
setSubmitError(null);
62+
onClose();
63+
}, [reset, onClose]);
64+
65+
const onSubmit = useCallback(
66+
async (data: FormData) => {
67+
try {
68+
setSubmitError(null);
69+
70+
// Create user request without confirmPassword
71+
const { confirmPassword, ...request } = data;
72+
await onCreateUser(request);
73+
74+
handleClose();
75+
} catch (err) {
76+
console.error('Create user error:', err);
77+
if (err instanceof Error) {
78+
try {
79+
const apiError = JSON.parse(err.message);
80+
setSubmitError(apiError.message || 'Failed to create user');
81+
} catch {
82+
setSubmitError(err.message || 'Failed to create user');
83+
}
84+
} else {
85+
setSubmitError('Failed to create user');
86+
}
87+
}
88+
},
89+
[onCreateUser, handleClose]
90+
);
91+
92+
return (
93+
<Dialog.Root open={isOpen} onOpenChange={(e) => !e.open && handleClose()}>
94+
<Dialog.Backdrop />
95+
<Dialog.Positioner>
96+
<DialogContent maxW="md">
97+
<DialogHeader>
98+
<DialogTitle>Create New User</DialogTitle>
99+
</DialogHeader>
100+
101+
<form onSubmit={handleSubmit(onSubmit)}>
102+
<VStack gap={4} py={4}>
103+
{submitError && (
104+
<Alert.Root status="error" variant="subtle">
105+
<Alert.Indicator />
106+
<Alert.Title>{submitError}</Alert.Title>
107+
</Alert.Root>
108+
)}
109+
110+
{/* Username */}
111+
<FormControl isInvalid={!!errors.username}>
112+
<FormLabel>Username</FormLabel>
113+
<Input
114+
{...register('username', {
115+
required: 'Username is required',
116+
maxLength: {
117+
value: 256,
118+
message: 'Username must be 256 characters or less',
119+
},
120+
})}
121+
placeholder="Enter username"
122+
disabled={isSubmitting || loading}
123+
/>
124+
{errors.username && (
125+
<FormErrorMessage>{errors.username.message}</FormErrorMessage>
126+
)}
127+
</FormControl>
128+
129+
{/* Email */}
130+
<FormControl isInvalid={!!errors.email}>
131+
<FormLabel>Email</FormLabel>
132+
<Input
133+
type="email"
134+
{...register('email', {
135+
required: 'Email is required',
136+
pattern: {
137+
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
138+
message: 'Please enter a valid email address',
139+
},
140+
maxLength: {
141+
value: 256,
142+
message: 'Email must be 256 characters or less',
143+
},
144+
})}
145+
placeholder="Enter email address"
146+
disabled={isSubmitting || loading}
147+
/>
148+
{errors.email && (
149+
<FormErrorMessage>{errors.email.message}</FormErrorMessage>
150+
)}
151+
</FormControl>
152+
153+
{/* Password */}
154+
<FormControl isInvalid={!!errors.password}>
155+
<FormLabel>Password</FormLabel>
156+
<PasswordInput
157+
{...register('password', {
158+
required: 'Password is required',
159+
minLength: {
160+
value: 8,
161+
message: 'Password must be at least 8 characters',
162+
},
163+
maxLength: {
164+
value: 100,
165+
message: 'Password must be 100 characters or less',
166+
},
167+
})}
168+
placeholder="Enter password"
169+
disabled={isSubmitting || loading}
170+
/>
171+
{errors.password && (
172+
<FormErrorMessage>{errors.password.message}</FormErrorMessage>
173+
)}
174+
</FormControl>
175+
176+
{/* Confirm Password */}
177+
<FormControl isInvalid={!!errors.confirmPassword}>
178+
<FormLabel>Confirm Password</FormLabel>
179+
<PasswordInput
180+
{...register('confirmPassword', {
181+
required: 'Please confirm your password',
182+
validate: (value) =>
183+
value === password || 'Passwords do not match',
184+
})}
185+
placeholder="Confirm password"
186+
disabled={isSubmitting || loading}
187+
/>
188+
{errors.confirmPassword && (
189+
<FormErrorMessage>{errors.confirmPassword.message}</FormErrorMessage>
190+
)}
191+
</FormControl>
192+
193+
{/* Role */}
194+
<FormControl isInvalid={!!errors.role}>
195+
<FormLabel>Role</FormLabel>
196+
<Select.Root
197+
{...register('role', { required: 'Role is required' })}
198+
disabled={isSubmitting || loading}
199+
>
200+
<Select.Trigger>
201+
<Select.ValueText placeholder="Select role" />
202+
</Select.Trigger>
203+
<Select.Content>
204+
<Select.Item value="User">User</Select.Item>
205+
<Select.Item value="Administrator">Administrator</Select.Item>
206+
</Select.Content>
207+
</Select.Root>
208+
{errors.role && (
209+
<FormErrorMessage>{errors.role.message}</FormErrorMessage>
210+
)}
211+
</FormControl>
212+
</VStack>
213+
214+
<DialogFooter>
215+
<Button
216+
variant="outline"
217+
onClick={handleClose}
218+
disabled={isSubmitting || loading}
219+
>
220+
Cancel
221+
</Button>
222+
<LoadingButton
223+
type="submit"
224+
colorPalette="blue"
225+
loading={isSubmitting || loading}
226+
loadingText="Creating..."
227+
>
228+
Create User
229+
</LoadingButton>
230+
</DialogFooter>
231+
</form>
232+
</DialogContent>
233+
</Dialog.Positioner>
234+
</Dialog.Root>
235+
);
236+
}

0 commit comments

Comments
 (0)