diff --git a/README.md b/README.md index bde675256..e99704aba 100644 --- a/README.md +++ b/README.md @@ -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. -- π **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. @@ -71,11 +71,14 @@ Don't forget to explore our sibling project, [OllamaHub](https://ollamahub.com/) ## 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. + - **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 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: diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 59f7049be..339c230fd 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -1,30 +1,32 @@ # 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 -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 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/). +**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 you encounter difficulties connecting to the Ollama server, please follow these steps to diagnose and resolve the issue: +**Troubleshooting Steps**: -**1. Check Ollama URL Format** +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. -Ensure that the Ollama URL is correctly formatted in the application settings. Follow these steps: - -- 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) -- 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. - -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. diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py index c066dbefe..800750c3d 100644 --- a/backend/apps/web/models/auths.py +++ b/backend/apps/web/models/auths.py @@ -64,6 +64,11 @@ class SigninForm(BaseModel): password: str +class UpdatePasswordForm(BaseModel): + password: str + new_password: str + + class SignupForm(BaseModel): name: str email: str @@ -109,5 +114,30 @@ class AuthsTable: except: 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) diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py index 8f075e408..ebc17d9a5 100644 --- a/backend/apps/web/models/chats.py +++ b/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) diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py index 782b7f47e..b7df92ebe 100644 --- a/backend/apps/web/models/users.py +++ b/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) diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 27d6a3b6a..9174865a7 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -11,6 +11,7 @@ import uuid from apps.web.models.auths import ( SigninForm, SignupForm, + UpdatePasswordForm, UserResponse, SigninResponse, Auths, @@ -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 ############################ diff --git a/backend/apps/web/routers/users.py b/backend/apps/web/routers/users.py index 08437bd34..fd0d2d6f0 100644 --- a/backend/apps/web/routers/users.py +++ b/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, + ) diff --git a/backend/constants.py b/backend/constants.py index 06d67eec5..761507f2b 100644 --- a/backend/constants.py +++ b/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." @@ -20,6 +21,9 @@ class ERROR_MESSAGES(str, Enum): "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_PASSWORD = ( + "The password provided is incorrect. Please check for typos and try again." + ) UNAUTHORIZED = "401 Unauthorized" ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." ACTION_PROHIBITED = ( @@ -27,4 +31,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." diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts index 56a4a7a6d..73934055a 100644 --- a/src/lib/apis/auths/index.ts +++ b/src/lib/apis/auths/index.ts @@ -88,3 +88,34 @@ export const userSignUp = async (name: string, email: string, password: string) 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; +}; diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index b1f9e5d9f..3fca8b99a 100644 --- a/src/lib/apis/users/index.ts +++ b/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; +}; diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 47c57d994..5dd57c781 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -18,6 +18,7 @@ import Advanced from './Settings/Advanced.svelte'; import Modal from '../common/Modal.svelte'; + import { updateUserPassword } from '$lib/apis/auths'; export let show = false; @@ -118,6 +119,11 @@ let authType = 'Basic'; let authContent = ''; + // Account + let currentPassword = ''; + let newPassword = ''; + let newPasswordConfirm = ''; + // About let ollamaVersion = ''; @@ -595,6 +601,31 @@ 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 () => { let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); console.log(settings); @@ -845,6 +876,32 @@ {/if} + +