فهرست منبع

Merge pull request #314 from ollama-webui/delete-user

feat: delete user
Timothy Jaeryang Baek 1 سال پیش
والد
کامیت
3fce09eb3d

+ 16 - 0
backend/apps/web/models/auths.py

@@ -109,5 +109,21 @@ class AuthsTable:
         except:
             return None
 
+    def delete_auth_by_id(self, id: str) -> Optional[UserModel]:
+        try:
+            # Delete User
+            result = Users.delete_user_by_id(id)
+
+            if result:
+                # Delete Auth
+                query = Auth.delete().where(Auth.id == id)
+                query.execute()  # Remove the rows, return number of rows removed.
+
+                return True
+            else:
+                return False
+        except:
+            return False
+
 
 Auths = AuthsTable(DB)

+ 9 - 0
backend/apps/web/models/chats.py

@@ -153,5 +153,14 @@ class ChatTable:
         except:
             return False
 
+    def delete_chats_by_user_id(self, user_id: str) -> bool:
+        try:
+            query = Chat.delete().where(Chat.user_id == user_id)
+            query.execute()  # Remove the rows, return number of rows removed.
+
+            return True
+        except:
+            return False
+
 
 Chats = ChatTable(DB)

+ 18 - 0
backend/apps/web/models/users.py

@@ -8,6 +8,8 @@ from utils.utils import decode_token
 from utils.misc import get_gravatar_url
 
 from apps.web.internal.db import DB
+from apps.web.models.chats import Chats
+
 
 ####################
 # User DB Schema
@@ -110,5 +112,21 @@ class UsersTable:
         except:
             return None
 
+    def delete_user_by_id(self, id: str) -> bool:
+        try:
+            # Delete User Chats
+            result = Chats.delete_chats_by_user_id(id)
+
+            if result:
+                # Delete User
+                query = User.delete().where(User.id == id)
+                query.execute()  # Remove the rows, return number of rows removed.
+
+                return True
+            else:
+                return False
+        except:
+            return False
+
 
 Users = UsersTable(DB)

+ 41 - 0
backend/apps/web/routers/users.py

@@ -9,6 +9,8 @@ import time
 import uuid
 
 from apps.web.models.users import UserModel, UserRoleUpdateForm, Users
+from apps.web.models.auths import Auths
+
 
 from utils.utils import (
     get_password_hash,
@@ -73,3 +75,42 @@ async def update_user_role(form_data: UserRoleUpdateForm, cred=Depends(bearer_sc
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.INVALID_TOKEN,
         )
+
+
+############################
+# DeleteUserById
+############################
+
+
+@router.delete("/{user_id}", response_model=bool)
+async def delete_user_by_id(user_id: str, cred=Depends(bearer_scheme)):
+    token = cred.credentials
+    user = Users.get_user_by_token(token)
+
+    if user:
+        if user.role == "admin":
+            if user.id != user_id:
+                result = Auths.delete_auth_by_id(user_id)
+
+                if result:
+                    return True
+                else:
+                    raise HTTPException(
+                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+                        detail=ERROR_MESSAGES.DELETE_USER_ERROR,
+                    )
+            else:
+                raise HTTPException(
+                    status_code=status.HTTP_403_FORBIDDEN,
+                    detail=ERROR_MESSAGES.ACTION_PROHIBITED,
+                )
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_403_FORBIDDEN,
+                detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+            )
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.INVALID_TOKEN,
+        )

+ 2 - 0
backend/constants.py

@@ -12,6 +12,7 @@ class ERROR_MESSAGES(str, Enum):
     DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
     ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
     CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
+    DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
     EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
     USERNAME_TAKEN = (
         "Uh-oh! This username is already registered. Please choose another username."
@@ -27,4 +28,5 @@ class ERROR_MESSAGES(str, Enum):
     )
     NOT_FOUND = "We could not find what you're looking for :/"
     USER_NOT_FOUND = "We could not find what you're looking for :/"
+
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."

+ 30 - 2
src/lib/apis/users/index.ts

@@ -45,8 +45,9 @@ export const getUsers = async (token: string) => {
 			if (!res.ok) throw await res.json();
 			return res.json();
 		})
-		.catch((error) => {
-			console.log(error);
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
 			return null;
 		});
 
@@ -56,3 +57,30 @@ export const getUsers = async (token: string) => {
 
 	return res ? res : [];
 };
+
+export const deleteUserById = async (token: string, userId: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}`, {
+		method: 'DELETE',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		}
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 38 - 7
src/routes/(app)/admin/+page.svelte

@@ -6,7 +6,7 @@
 
 	import toast from 'svelte-french-toast';
 
-	import { updateUserRole, getUsers } from '$lib/apis/users';
+	import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users';
 
 	let loaded = false;
 	let users = [];
@@ -22,6 +22,16 @@
 		}
 	};
 
+	const deleteUserHandler = async (id) => {
+		const res = await deleteUserById(localStorage.token, id).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+		if (res) {
+			users = await getUsers(localStorage.token);
+		}
+	};
+
 	onMount(async () => {
 		if ($user?.role !== 'admin') {
 			await goto('/');
@@ -55,7 +65,7 @@
 									<th scope="col" class="px-6 py-3"> Name </th>
 									<th scope="col" class="px-6 py-3"> Email </th>
 									<th scope="col" class="px-6 py-3"> Role </th>
-									<!-- <th scope="col" class="px-6 py-3"> Action </th> -->
+									<th scope="col" class="px-6 py-3"> Action </th>
 								</tr>
 							</thead>
 							<tbody>
@@ -63,15 +73,16 @@
 									<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
 										<th
 											scope="row"
-											class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
+											class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white w-fit"
 										>
 											<div class="flex flex-row">
 												<img
 													class=" rounded-full max-w-[30px] max-h-[30px] object-cover mr-4"
 													src={user.profile_image_url}
+													alt="user"
 												/>
 
-												<div class=" font-semibold md:self-center">{user.name}</div>
+												<div class=" font-semibold self-center">{user.name}</div>
 											</div>
 										</th>
 										<td class="px-6 py-4"> {user.email} </td>
@@ -89,9 +100,29 @@
 												}}>{user.role}</button
 											>
 										</td>
-										<!-- <td class="px-6 py-4 text-center">
-											<button class="  text-white underline"> Edit </button>
-										</td> -->
+										<td class="px-6 py-4 text-center flex justify-center">
+											<button
+												class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
+												on:click={async () => {
+													deleteUserHandler(user.id);
+												}}
+											>
+												<svg
+													xmlns="http://www.w3.org/2000/svg"
+													fill="none"
+													viewBox="0 0 24 24"
+													stroke-width="1.5"
+													stroke="currentColor"
+													class="w-4 h-4"
+												>
+													<path
+														stroke-linecap="round"
+														stroke-linejoin="round"
+														d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
+													/>
+												</svg>
+											</button>
+										</td>
 									</tr>
 								{/each}
 							</tbody>