Skip to content

Commit 366d394

Browse files
authored
Merge pull request #83 from njxue/admin-user-controls
Add deactivate user and user management page for admins
2 parents 2ff44fd + 1129167 commit 366d394

21 files changed

+578
-55
lines changed

backend/user-service/controller/auth-controller.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ export async function handleLogin(req, res, next) {
1111
try {
1212
const user = await _findUserByEmail(email);
1313
if (!user) {
14-
throw new UnauthorisedError("Wrong email");
14+
throw new UnauthorisedError("Wrong email/password");
1515
}
1616

1717
const match = await bcrypt.compare(password, user.password);
1818
if (!match) {
19-
throw new UnauthorisedError("Wrong password");
19+
throw new UnauthorisedError("Wrong email/password");
2020
}
2121

2222
// Generate access and refresh token

frontend/src/data/users/UserImpl.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ export class UserImpl implements IUser {
3434
return this.dataSource.getUser(userId);
3535
}
3636

37+
async getAllUsers(): Promise<any> {
38+
return this.dataSource.getAllUsers();
39+
}
40+
41+
async deleteUser(userId: string): Promise<any> {
42+
return this.dataSource.deleteUser(userId);
43+
}
44+
45+
async updateUserPrivilege(userId: string, isAdmin: boolean): Promise<any> {
46+
return this.dataSource.updateUserPrivilege(userId, isAdmin);
47+
}
3748
async forgetPassword(email: string): Promise<any> {
3849
return this.dataSource.forgetPassword(email);
3950
}

frontend/src/data/users/UserRemoteDataSource.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ export class UserRemoteDataSource extends BaseApi {
5151
return await this.protectedGet<any>(`/users/${userId}`);
5252
}
5353

54+
async getAllUsers() {
55+
return await this.protectedGet<any>("/users/");
56+
}
57+
58+
async deleteUser(userId: string) {
59+
return await this.protectedDelete<any>(`/users/${userId}`);
60+
}
61+
62+
async updateUserPrivilege(userId: string, isAdmin: boolean) {
63+
return await this.protectedPatch(`/users/${userId}/privilege`, { isAdmin });
64+
}
5465
async forgetPassword(email: string) {
5566
return await this.post<any>("/users/forgetPassword", { email });
5667
}

frontend/src/data/users/mockUser.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import AuthClientStore from "data/auth/AuthClientStore";
22
import { User } from "domain/entities/User";
33
import { IUserRegisterInput, IUserLoginInput, IUserUpdateInput } from "domain/users/IUser";
44

5-
const users: User[] = [
5+
let users: User[] = [
66
{
77
_id: "1",
88
username: "SampleUserName",
@@ -156,6 +156,31 @@ export class MockUser {
156156
});
157157
}
158158

159+
async getAllUsers(): Promise<any> {
160+
return new Promise((resolve, reject) => ({ message: "Fetched all users", data: resolve(users) }));
161+
}
162+
163+
async deleteUser(userId: string): Promise<any> {
164+
return new Promise((resolve, reject) => {
165+
users = users.filter((user) => user._id !== userId);
166+
});
167+
}
168+
169+
async updateUserPrivilege(userId: string, isAdmin: boolean): Promise<any> {
170+
return new Promise((resolve, reject) => {
171+
try {
172+
const foundUser = this.users.find((u) => u._id === userId);
173+
if (!foundUser) {
174+
resolve({ message: "User not found" });
175+
} else {
176+
foundUser.isAdmin = isAdmin;
177+
resolve({ message: "User privileges updated", data: foundUser });
178+
}
179+
} catch (error) {
180+
reject(error);
181+
}
182+
});
183+
}
159184
async forgetPassword(email: string): Promise<any> {}
160185

161186
async resetPassword(password: string, token: string): Promise<any> {}

frontend/src/domain/context/AuthContext.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ interface AuthContextType {
1111
login: (email: string, password: string) => Promise<void>;
1212
logout: () => Promise<void>;
1313
register: (email: string, password: string, username: string) => Promise<void>;
14-
updateUser: (userUpdateInput: IUserUpdateInput) => Promise<void>;
14+
updateUser: (userId: string, userUpdateInput: IUserUpdateInput) => Promise<User>;
15+
deactivateUser: (userId: string) => Promise<void>;
1516
}
1617

1718
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -70,11 +71,19 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
7071
handleSuccessfulAuth(accessToken, user);
7172
};
7273

73-
const updateUser = async (userUpdateInput: IUserUpdateInput) => {
74-
if (user) {
75-
const updatedUser = await userUseCases.updateUser(user?._id, userUpdateInput);
74+
const updateUser = async (userId: string, userUpdateInput: IUserUpdateInput) => {
75+
const updatedUser = await userUseCases.updateUser(userId, userUpdateInput);
76+
if (userId === user?._id) {
7677
setUser(updatedUser);
7778
}
79+
return updatedUser;
80+
};
81+
82+
const deactivateUser = async (userId: string) => {
83+
await userUseCases.deleteUser(userId);
84+
if (userId === user?._id) {
85+
logout();
86+
}
7887
};
7988

8089
const handleSuccessfulAuth = (accessToken: string, user: User) => {
@@ -87,7 +96,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
8796
const isUserAdmin = true;
8897

8998
return (
90-
<AuthContext.Provider value={{ user, isLoggedIn, isUserAdmin, login, logout, register, updateUser }}>
99+
<AuthContext.Provider
100+
value={{ user, isLoggedIn, isUserAdmin, login, logout, register, updateUser, deactivateUser }}
101+
>
91102
{children}
92103
</AuthContext.Provider>
93104
);

frontend/src/domain/usecases/UserUseCases.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,32 @@ export class UserUseCases {
108108
*/
109109
async getUser(userId: string): Promise<User> {
110110
const data = await this.user.getUser(userId);
111-
if (!data) {
112-
throw new AuthenticationError("User not found or access denied");
111+
if (!data.data) {
112+
throw new AuthenticationError(data.message);
113+
}
114+
return data.data;
115+
}
116+
117+
async getAllUsers(): Promise<User[]> {
118+
const data = await this.user.getAllUsers();
119+
if (!data.data) {
120+
throw new AuthenticationError(data.message);
113121
}
114122
return data.data;
115123
}
116124

125+
async deleteUser(userId: string): Promise<void> {
126+
await this.user.deleteUser(userId);
127+
}
128+
129+
async updateUserPrivilege(userId: string, isAdmin: boolean): Promise<User> {
130+
const data = await this.user.updateUserPrivilege(userId, isAdmin);
131+
if (!data.data) {
132+
throw new AuthenticationError(data.message);
133+
}
134+
return data.data;
135+
}
136+
117137
async forgetPassword(email: string): Promise<void> {
118138
await this.user.forgetPassword(email);
119139
}

frontend/src/domain/users/IUser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export interface IUser {
2525
logoutUser(userId: string): Promise<any>;
2626
updateUser(userId: string, userUpdateInput: IUserUpdateInput): Promise<any>;
2727
getUser(userId: string): Promise<any>;
28+
getAllUsers(): Promise<any>;
29+
deleteUser(userId: string): Promise<any>;
30+
updateUserPrivilege(userId: string, isAdmin: boolean): Promise<any>;
2831
forgetPassword(email: string): Promise<any>;
2932
resetPassword(password: string, token: string): Promise<any>;
3033
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.btnGroup {
2+
display: flex;
3+
gap: 8px;
4+
margin-top: 8px;
5+
}
6+
7+
.btnGroup button {
8+
width: 100%;
9+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Button, Input } from "antd";
2+
import { User } from "domain/entities/User";
3+
import styles from "./DeleteUserForm.module.css";
4+
import { useAuth } from "domain/context/AuthContext";
5+
import { toast } from "react-toastify";
6+
import { handleError } from "presentation/utils/errorHandler";
7+
import { useState } from "react";
8+
9+
interface DeleteUserFormProps {
10+
user: User;
11+
onSubmit?: () => void;
12+
onCancel?: () => void;
13+
}
14+
export const DeleteUserForm: React.FC<DeleteUserFormProps> = ({ user: userToDelete, onSubmit, onCancel }) => {
15+
const [confirmationText, setConfirmationText] = useState("");
16+
const { user, deactivateUser } = useAuth();
17+
const isDeletingSelf = user?._id === userToDelete._id;
18+
19+
const handleDeleteUser = async () => {
20+
try {
21+
await deactivateUser(userToDelete?._id);
22+
toast.success(`Successfully deleted ${isDeletingSelf ? "your account" : userToDelete.username}`);
23+
onSubmit?.();
24+
} catch (err) {
25+
console.error(err);
26+
toast.error(handleError(err));
27+
}
28+
};
29+
30+
const handleCancel = () => {
31+
setConfirmationText("");
32+
onCancel?.();
33+
};
34+
35+
const textToMatch = `delete/${user?.username}`;
36+
const isConfirmBtnDisabled = isDeletingSelf && confirmationText !== textToMatch;
37+
38+
return (
39+
<>
40+
<p>Are you sure you want to delete {isDeletingSelf ? "your account" : <b>{userToDelete.username}</b>}?</p>
41+
{isDeletingSelf && (
42+
<Input
43+
type="text"
44+
value={confirmationText}
45+
onChange={(e) => setConfirmationText(e.target.value)}
46+
placeholder={`Type "${textToMatch}" to confirm`}
47+
/>
48+
)}
49+
<div className={styles.btnGroup}>
50+
<Button onClick={handleCancel}>Cancel</Button>
51+
<Button type="primary" onClick={handleDeleteUser} disabled={isConfirmBtnDisabled}>
52+
Confirm
53+
</Button>
54+
</div>
55+
</>
56+
);
57+
};
Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
.container {
22
display: flex;
33
flex-direction: row;
4-
justify-content: center;
4+
justify-content: space-between;
5+
align-items: end;
6+
gap: 2em;
7+
padding: 8px 0;
8+
}
9+
10+
.profileContainer {
11+
display: flex;
12+
flex-direction: row;
13+
justify-content: start;
514
align-items: center;
6-
column-gap: calc(1.5rem + 1.5vw);
7-
margin-bottom: 2em;
15+
gap: 2em;
16+
height: 100%;
817
}
918

1019
.profilePicture {
@@ -16,34 +25,86 @@
1625

1726
.profileDetailsContainer {
1827
display: flex;
19-
flex-direction: row;
20-
justify-content: center;
21-
align-items: center;
22-
column-gap: calc(1.5rem + 1.5vw);
23-
padding: 0 2em;
28+
flex-direction: column;
29+
justify-content: start;
30+
align-items: start;
31+
gap: 8px;
2432
}
2533

2634
.nameRow {
2735
display: flex;
2836
flex-direction: row;
2937
align-items: center;
30-
column-gap: 1em;
31-
margin-bottom: 0;
38+
gap: 8px;
3239
}
3340

3441
.name {
3542
font-size: 3em;
3643
font-weight: 300;
3744
margin: 0;
45+
max-width: 500px;
46+
text-overflow: ellipsis;
47+
white-space: nowrap;
48+
overflow: hidden;
3849
}
3950

4051
.editIcon {
41-
font-size: 1.5em;
52+
font-size: 1.2em;
4253
cursor: pointer;
54+
transition: opacity 0.2s ease;
55+
opacity: 50%;
56+
}
57+
.editIcon:hover {
58+
opacity: 100%;
4359
}
4460

45-
.linksContainer {
61+
.deactivateAndadminLinksContainer {
4662
display: flex;
47-
flex-direction: row;
48-
column-gap: 2em;
63+
gap: 12px;
64+
}
65+
66+
.adminLinkItem {
67+
display: flex;
68+
align-items: center;
69+
gap: 4px;
70+
font-weight: bold;
71+
}
72+
.adminLinkItem span[role="img"],
73+
.adminLinkItem a {
74+
color: black;
75+
transition: all 0.5s ease;
76+
}
77+
78+
.adminLinkItem:hover a,
79+
.adminLinkItem:hover span[role="img"] {
80+
color: #ffa500;
81+
transform: translateY(-5px);
82+
}
83+
84+
.emailAndDeactivate {
85+
display: flex;
86+
flex-direction: column;
87+
align-items: start;
88+
gap: 4px;
89+
}
90+
91+
.deactivate {
92+
color: #007bff;
93+
cursor: pointer;
94+
transition: color 0.2s ease;
95+
margin-right: 12px;
96+
}
97+
98+
.deactivate:hover {
99+
color: red;
100+
}
101+
102+
.email {
103+
max-width: 250px;
104+
text-overflow: ellipsis;
105+
white-space: nowrap;
106+
overflow: hidden;
107+
display: flex;
108+
align-items: flex-end;
109+
gap: 8px;
49110
}

0 commit comments

Comments
 (0)