Unverified Commit 39b8e443 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge branch 'main' into refac/auth-middleware

parents a01b112f 3ae1a5b1
...@@ -61,7 +61,7 @@ Also check our sibling project, [OllamaHub](https://ollamahub.com/), where you c ...@@ -61,7 +61,7 @@ Also check our sibling project, [OllamaHub](https://ollamahub.com/), where you c
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators. - 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
- 🔒 **Backend Reverse Proxy Support**: Strengthen security by enabling direct communication between Ollama Web UI backend and Ollama, eliminating the need to expose Ollama over LAN. - 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Ollama Web UI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
- 🌟 **Continuous Updates**: We are committed to improving Ollama Web UI with regular updates and new features. - 🌟 **Continuous Updates**: We are committed to improving Ollama Web UI with regular updates and new features.
...@@ -71,11 +71,14 @@ Don't forget to explore our sibling project, [OllamaHub](https://ollamahub.com/) ...@@ -71,11 +71,14 @@ Don't forget to explore our sibling project, [OllamaHub](https://ollamahub.com/)
## How to Install 🚀 ## How to Install 🚀
🌟 **Important Note on User Roles:** 🌟 **Important Note on User Roles and Privacy:**
- **Admin Creation:** The very first account to sign up on the Ollama Web UI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings. - **Admin Creation:** The very first account to sign up on the Ollama Web UI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings.
- **User Registrations:** All subsequent users signing up will initially have their accounts set to **Pending** status by default. These accounts will require approval from the Administrator to gain access to the platform functionalities. - **User Registrations:** All subsequent users signing up will initially have their accounts set to **Pending** status by default. These accounts will require approval from the Administrator to gain access to the platform functionalities.
- **Privacy and Data Security:** We prioritize your privacy and data security above all. Please be reassured that all data entered into the Ollama Web UI is stored locally on your device. Our system is designed to be privacy-first, ensuring that no external requests are made, and your data does not leave your local environment. We are committed to maintaining the highest standards of data privacy and security, ensuring that your information remains confidential and under your control.
### Installing Both Ollama and Ollama Web UI Using Docker Compose ### Installing Both Ollama and Ollama Web UI Using Docker Compose
If you don't have Ollama installed yet, you can use the provided Docker Compose file for a hassle-free installation. Simply run the following command: If you don't have Ollama installed yet, you can use the provided Docker Compose file for a hassle-free installation. Simply run the following command:
......
# Ollama Web UI Troubleshooting Guide # Ollama Web UI Troubleshooting Guide
## Understanding the Ollama WebUI Architecture
The Ollama WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
- **How it Works**: When you make a request (like `/ollama/api/tags`) from the Ollama WebUI, it doesn’t go directly to the Ollama API. Instead, it first reaches the Ollama WebUI backend. The backend then forwards this request to the Ollama API via the route you define in the `OLLAMA_API_BASE_URL` environment variable. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_API_BASE_URL/tags` in the backend.
- **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
## Ollama WebUI: Server Connection Error ## Ollama WebUI: Server Connection Error
If you're running ollama-webui and have chosen to install webui and ollama separately, you might encounter connection issues. This is often due to the docker container being unable to reach the Ollama server at 127.0.0.1:11434(host.docker.internal:11434). To resolve this, you can use the `--network=host` flag in the docker command. When done so port would be changed from 3000 to 8080, and the link would be: http://localhost:8080. If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
Here's an example of the command you should run: **Example Docker Command**:
```bash ```bash
docker run -d --network=host -v ollama-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main docker run -d --network=host -v ollama-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
``` ```
## Connection Errors ### General Connection Errors
Make sure you have the **latest version of Ollama** installed before proceeding with the installation. You can find the latest version of Ollama at [https://ollama.ai/](https://ollama.ai/).
If you encounter difficulties connecting to the Ollama server, please follow these steps to diagnose and resolve the issue:
**1. Check Ollama URL Format**
Ensure that the Ollama URL is correctly formatted in the application settings. Follow these steps: **Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.ai/) for the latest updates.
- If your Ollama runs in a different host than Web UI make sure Ollama host address is provided when running Web UI container via `OLLAMA_API_BASE_URL` environment variable. [(e.g. OLLAMA_API_BASE_URL=http://192.168.1.1:11434/api)](https://github.com/ollama-webui/ollama-webui#accessing-external-ollama-on-a-different-server) **Troubleshooting Steps**:
- Go to "Settings" within the Ollama WebUI.
- Navigate to the "General" section.
- Verify that the Ollama Server URL is set to: `/ollama/api`.
It is crucial to include the `/api` at the end of the URL to ensure that the Ollama Web UI can communicate with the server. 1. **Verify Ollama URL Format**:
- When running the Web UI container, ensure the `OLLAMA_API_BASE_URL` is correctly set, including the `/api` suffix. (e.g., `http://192.168.1.1:11434/api` for different host setups).
- In the Ollama WebUI, navigate to "Settings" > "General".
- Confirm that the Ollama Server URL is correctly set to `/ollama/api`, including the `/api` suffix.
By following these troubleshooting steps, you should be able to identify and resolve connection issues with your Ollama server configuration. If you require further assistance or have additional questions, please don't hesitate to reach out or refer to our documentation for comprehensive guidance. By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
...@@ -64,6 +64,11 @@ class SigninForm(BaseModel): ...@@ -64,6 +64,11 @@ class SigninForm(BaseModel):
password: str password: str
class UpdatePasswordForm(BaseModel):
password: str
new_password: str
class SignupForm(BaseModel): class SignupForm(BaseModel):
name: str name: str
email: str email: str
...@@ -109,5 +114,30 @@ class AuthsTable: ...@@ -109,5 +114,30 @@ class AuthsTable:
except: except:
return None return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
try:
query = Auth.update(password=new_password).where(Auth.id == id)
result = query.execute()
return True if result == 1 else False
except:
return False
def delete_auth_by_id(self, id: str) -> bool:
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) Auths = AuthsTable(DB)
...@@ -153,5 +153,14 @@ class ChatTable: ...@@ -153,5 +153,14 @@ class ChatTable:
except: except:
return False 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) Chats = ChatTable(DB)
...@@ -8,6 +8,8 @@ from utils.utils import decode_token ...@@ -8,6 +8,8 @@ from utils.utils import decode_token
from utils.misc import get_gravatar_url from utils.misc import get_gravatar_url
from apps.web.internal.db import DB from apps.web.internal.db import DB
from apps.web.models.chats import Chats
#################### ####################
# User DB Schema # User DB Schema
...@@ -110,5 +112,21 @@ class UsersTable: ...@@ -110,5 +112,21 @@ class UsersTable:
except: except:
return None 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) Users = UsersTable(DB)
...@@ -11,6 +11,7 @@ import uuid ...@@ -11,6 +11,7 @@ import uuid
from apps.web.models.auths import ( from apps.web.models.auths import (
SigninForm, SigninForm,
SignupForm, SignupForm,
UpdatePasswordForm,
UserResponse, UserResponse,
SigninResponse, SigninResponse,
Auths, Auths,
...@@ -53,6 +54,28 @@ async def get_session_user(cred=Depends(bearer_scheme)): ...@@ -53,6 +54,28 @@ async def get_session_user(cred=Depends(bearer_scheme)):
) )
############################
# Update Password
############################
@router.post("/update/password", response_model=bool)
async def update_password(form_data: UpdatePasswordForm, cred=Depends(bearer_scheme)):
token = cred.credentials
session_user = Users.get_user_by_token(token)
if session_user:
user = Auths.authenticate_user(session_user.email, form_data.password)
if user:
hashed = get_password_hash(form_data.new_password)
return Auths.update_user_password_by_id(user.id, hashed)
else:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_PASSWORD)
else:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
############################ ############################
# SignIn # SignIn
############################ ############################
......
...@@ -9,6 +9,8 @@ import time ...@@ -9,6 +9,8 @@ import time
import uuid import uuid
from apps.web.models.users import UserModel, UserRoleUpdateForm, Users from apps.web.models.users import UserModel, UserRoleUpdateForm, Users
from apps.web.models.auths import Auths
from utils.utils import ( from utils.utils import (
get_password_hash, get_password_hash,
...@@ -73,3 +75,42 @@ async def update_user_role(form_data: UserRoleUpdateForm, cred=Depends(bearer_sc ...@@ -73,3 +75,42 @@ async def update_user_role(form_data: UserRoleUpdateForm, cred=Depends(bearer_sc
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN, 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,
)
...@@ -12,6 +12,7 @@ class ERROR_MESSAGES(str, Enum): ...@@ -12,6 +12,7 @@ class ERROR_MESSAGES(str, Enum):
DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}" DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." 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." 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." EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
USERNAME_TAKEN = ( USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username." "Uh-oh! This username is already registered. Please choose another username."
...@@ -20,6 +21,9 @@ class ERROR_MESSAGES(str, Enum): ...@@ -20,6 +21,9 @@ class ERROR_MESSAGES(str, Enum):
"Your session has expired or the token is invalid. Please sign in again." "Your session has expired or the token is invalid. Please sign in again."
) )
INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again." INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
INVALID_PASSWORD = (
"The password provided is incorrect. Please check for typos and try again."
)
UNAUTHORIZED = "401 Unauthorized" UNAUTHORIZED = "401 Unauthorized"
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
ACTION_PROHIBITED = ( ACTION_PROHIBITED = (
...@@ -27,4 +31,5 @@ class ERROR_MESSAGES(str, Enum): ...@@ -27,4 +31,5 @@ class ERROR_MESSAGES(str, Enum):
) )
NOT_FOUND = "We could not find what you're looking for :/" NOT_FOUND = "We could not find what you're looking for :/"
USER_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." MALICIOUS = "Unusual activities detected, please try again in a few minutes."
...@@ -88,3 +88,34 @@ export const userSignUp = async (name: string, email: string, password: string) ...@@ -88,3 +88,34 @@ export const userSignUp = async (name: string, email: string, password: string)
return res; return res;
}; };
export const updateUserPassword = async (token: string, password: string, newPassword: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/update/password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
password: password,
new_password: newPassword
})
})
.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;
};
...@@ -45,8 +45,9 @@ export const getUsers = async (token: string) => { ...@@ -45,8 +45,9 @@ export const getUsers = async (token: string) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
}) })
.catch((error) => { .catch((err) => {
console.log(error); console.log(err);
error = err.detail;
return null; return null;
}); });
...@@ -56,3 +57,30 @@ export const getUsers = async (token: string) => { ...@@ -56,3 +57,30 @@ export const getUsers = async (token: string) => {
return res ? res : []; 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;
};
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
import Advanced from './Settings/Advanced.svelte'; import Advanced from './Settings/Advanced.svelte';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import { updateUserPassword } from '$lib/apis/auths';
export let show = false; export let show = false;
...@@ -118,6 +119,11 @@ ...@@ -118,6 +119,11 @@
let authType = 'Basic'; let authType = 'Basic';
let authContent = ''; let authContent = '';
// Account
let currentPassword = '';
let newPassword = '';
let newPasswordConfirm = '';
// About // About
let ollamaVersion = ''; let ollamaVersion = '';
...@@ -595,6 +601,31 @@ ...@@ -595,6 +601,31 @@
return models; return models;
}; };
const updatePasswordHandler = async () => {
if (newPassword === newPasswordConfirm) {
const res = await updateUserPassword(localStorage.token, currentPassword, newPassword).catch(
(error) => {
toast.error(error);
return null;
}
);
if (res) {
toast.success('Successfully updated.');
}
currentPassword = '';
newPassword = '';
newPasswordConfirm = '';
} else {
toast.error(
`The passwords you entered don't quite match. Please double-check and try again.`
);
newPassword = '';
newPasswordConfirm = '';
}
};
onMount(async () => { onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
console.log(settings); console.log(settings);
...@@ -845,6 +876,32 @@ ...@@ -845,6 +876,32 @@
</button> </button>
{/if} {/if}
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'account'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => {
selectedTab = 'account';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">Account</div>
</button>
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'about' 'about'
...@@ -940,12 +997,12 @@ ...@@ -940,12 +997,12 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Ollama Server URL</div> <div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter URL (e.g. http://localhost:11434/api)" placeholder="Enter URL (e.g. http://localhost:8080/ollama/api)"
bind:value={API_BASE_URL} bind:value={API_BASE_URL}
/> />
</div> </div>
...@@ -971,7 +1028,10 @@ ...@@ -971,7 +1028,10 @@
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Trouble accessing Ollama? <a The field above should be set to <span
class=" text-gray-500 dark:text-gray-300 font-medium">'/ollama/api'</span
>;
<a
class=" text-gray-500 dark:text-gray-300 font-medium" class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://github.com/ollama-webui/ollama-webui#troubleshooting" href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
target="_blank" target="_blank"
...@@ -1817,6 +1877,67 @@ ...@@ -1817,6 +1877,67 @@
</button> </button>
</div> </div>
</form> </form>
{:else if selectedTab === 'account'}
<form
class="flex flex-col h-full text-sm"
on:submit|preventDefault={() => {
updatePasswordHandler();
}}
>
<div class=" mb-2.5 font-medium">Change Password</div>
<div class=" space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Current Password</div>
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
type="password"
bind:value={currentPassword}
autocomplete="current-password"
required
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">New Password</div>
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
type="password"
bind:value={newPassword}
autocomplete="new-password"
required
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
type="password"
bind:value={newPasswordConfirm}
autocomplete="off"
required
/>
</div>
</div>
</div>
<div class="mt-3 flex justify-end">
<button
class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
>
Update password
</button>
</div>
</form>
{:else if selectedTab === 'about'} {:else if selectedTab === 'about'}
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6"> <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
<div class=" space-y-3"> <div class=" space-y-3">
......
...@@ -249,6 +249,17 @@ ...@@ -249,6 +249,17 @@
<br class=" hidden sm:flex" />(version <br class=" hidden sm:flex" />(version
<span class=" dark:text-white font-medium">{REQUIRED_OLLAMA_VERSION} or higher</span <span class=" dark:text-white font-medium">{REQUIRED_OLLAMA_VERSION} or higher</span
>) or check your connection. >) or check your connection.
<div class="mt-1 text-sm">
Trouble accessing Ollama?
<a
class=" text-black dark:text-white font-semibold underline"
href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
target="_blank"
>
Click here for help.
</a>
</div>
</div> </div>
<div class=" mt-6 mx-auto relative group w-fit"> <div class=" mt-6 mx-auto relative group w-fit">
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
import toast from 'svelte-french-toast'; 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 loaded = false;
let users = []; let users = [];
...@@ -22,6 +22,16 @@ ...@@ -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 () => { onMount(async () => {
if ($user?.role !== 'admin') { if ($user?.role !== 'admin') {
await goto('/'); await goto('/');
...@@ -55,7 +65,7 @@ ...@@ -55,7 +65,7 @@
<th scope="col" class="px-6 py-3"> Name </th> <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"> Email </th>
<th scope="col" class="px-6 py-3"> Role </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> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -63,15 +73,16 @@ ...@@ -63,15 +73,16 @@
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700"> <tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th <th
scope="row" 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"> <div class="flex flex-row">
<img <img
class=" rounded-full max-w-[30px] max-h-[30px] object-cover mr-4" class=" rounded-full max-w-[30px] max-h-[30px] object-cover mr-4"
src={user.profile_image_url} 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> </div>
</th> </th>
<td class="px-6 py-4"> {user.email} </td> <td class="px-6 py-4"> {user.email} </td>
...@@ -89,9 +100,29 @@ ...@@ -89,9 +100,29 @@
}}>{user.role}</button }}>{user.role}</button
> >
</td> </td>
<!-- <td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center flex justify-center">
<button class=" text-white underline"> Edit </button> <button
</td> --> 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> </tr>
{/each} {/each}
</tbody> </tbody>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment