forked from open-webui/open-webui
		
	feat: multi-user support w/ RBAC
This commit is contained in:
		
							parent
							
								
									31e38df0a5
								
							
						
					
					
						commit
						921eef03b3
					
				
					 21 changed files with 1815 additions and 66 deletions
				
			
		|  | @ -1,4 +1,4 @@ | ||||||
| from flask import Flask, request, Response | from flask import Flask, request, Response, jsonify | ||||||
| from flask_cors import CORS | from flask_cors import CORS | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -6,7 +6,10 @@ import requests | ||||||
| import json | import json | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| from config import OLLAMA_API_BASE_URL | from apps.web.models.users import Users | ||||||
|  | from constants import ERROR_MESSAGES | ||||||
|  | from utils import extract_token_from_auth_header | ||||||
|  | from config import OLLAMA_API_BASE_URL, OLLAMA_WEBUI_AUTH | ||||||
| 
 | 
 | ||||||
| app = Flask(__name__) | app = Flask(__name__) | ||||||
| CORS( | CORS( | ||||||
|  | @ -28,6 +31,21 @@ def proxy(path): | ||||||
|     data = request.get_data() |     data = request.get_data() | ||||||
|     headers = dict(request.headers) |     headers = dict(request.headers) | ||||||
| 
 | 
 | ||||||
|  |     if OLLAMA_WEBUI_AUTH: | ||||||
|  |         if "Authorization" in headers: | ||||||
|  |             token = extract_token_from_auth_header(headers["Authorization"]) | ||||||
|  |             user = Users.get_user_by_token(token) | ||||||
|  |             if user: | ||||||
|  |                 print(user) | ||||||
|  |                 pass | ||||||
|  |             else: | ||||||
|  |                 return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 | ||||||
|  |         else: | ||||||
|  |             return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 | ||||||
|  | 
 | ||||||
|  |     else: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|     # Make a request to the target server |     # Make a request to the target server | ||||||
|     target_response = requests.request( |     target_response = requests.request( | ||||||
|         method=request.method, |         method=request.method, | ||||||
|  |  | ||||||
							
								
								
									
										25
									
								
								backend/apps/web/main.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								backend/apps/web/main.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | ||||||
|  | from fastapi import FastAPI, Request, Depends, HTTPException | ||||||
|  | from fastapi.middleware.cors import CORSMiddleware | ||||||
|  | 
 | ||||||
|  | from apps.web.routers import auths | ||||||
|  | from config import OLLAMA_WEBUI_VERSION, OLLAMA_WEBUI_AUTH | ||||||
|  | 
 | ||||||
|  | app = FastAPI() | ||||||
|  | 
 | ||||||
|  | origins = ["*"] | ||||||
|  | 
 | ||||||
|  | app.add_middleware( | ||||||
|  |     CORSMiddleware, | ||||||
|  |     allow_origins=origins, | ||||||
|  |     allow_credentials=True, | ||||||
|  |     allow_methods=["*"], | ||||||
|  |     allow_headers=["*"], | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | app.include_router(auths.router, prefix="/auths", tags=["auths"]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.get("/") | ||||||
|  | async def get_status(): | ||||||
|  |     return {"status": True, "version": OLLAMA_WEBUI_VERSION, "auth": OLLAMA_WEBUI_AUTH} | ||||||
							
								
								
									
										102
									
								
								backend/apps/web/models/auths.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								backend/apps/web/models/auths.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | ||||||
|  | from pydantic import BaseModel | ||||||
|  | from typing import List, Union, Optional | ||||||
|  | import time | ||||||
|  | import uuid | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from apps.web.models.users import UserModel, Users | ||||||
|  | from utils import ( | ||||||
|  |     verify_password, | ||||||
|  |     get_password_hash, | ||||||
|  |     bearer_scheme, | ||||||
|  |     create_token, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | import config | ||||||
|  | 
 | ||||||
|  | DB = config.DB | ||||||
|  | 
 | ||||||
|  | #################### | ||||||
|  | # DB MODEL | ||||||
|  | #################### | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AuthModel(BaseModel): | ||||||
|  |     id: str | ||||||
|  |     email: str | ||||||
|  |     password: str | ||||||
|  |     active: bool = True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #################### | ||||||
|  | # Forms | ||||||
|  | #################### | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Token(BaseModel): | ||||||
|  |     token: str | ||||||
|  |     token_type: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class UserResponse(BaseModel): | ||||||
|  |     id: str | ||||||
|  |     email: str | ||||||
|  |     name: str | ||||||
|  |     role: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SigninResponse(Token, UserResponse): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SigninForm(BaseModel): | ||||||
|  |     email: str | ||||||
|  |     password: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SignupForm(BaseModel): | ||||||
|  |     name: str | ||||||
|  |     email: str | ||||||
|  |     password: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AuthsTable: | ||||||
|  |     def __init__(self, db): | ||||||
|  |         self.db = db | ||||||
|  |         self.table = db.auths | ||||||
|  | 
 | ||||||
|  |     def insert_new_auth( | ||||||
|  |         self, email: str, password: str, name: str, role: str = "user" | ||||||
|  |     ) -> Optional[UserModel]: | ||||||
|  |         print("insert_new_auth") | ||||||
|  | 
 | ||||||
|  |         id = str(uuid.uuid4()) | ||||||
|  | 
 | ||||||
|  |         auth = AuthModel( | ||||||
|  |             **{"id": id, "email": email, "password": password, "active": True} | ||||||
|  |         ) | ||||||
|  |         result = self.table.insert_one(auth.model_dump()) | ||||||
|  |         user = Users.insert_new_user(id, name, email, role) | ||||||
|  | 
 | ||||||
|  |         print(result, user) | ||||||
|  |         if result and user: | ||||||
|  |             return user | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def authenticate_user(self, email: str, password: str) -> Optional[UserModel]: | ||||||
|  |         print("authenticate_user") | ||||||
|  | 
 | ||||||
|  |         auth = self.table.find_one({"email": email, "active": True}) | ||||||
|  | 
 | ||||||
|  |         if auth: | ||||||
|  |             if verify_password(password, auth["password"]): | ||||||
|  |                 user = self.db.users.find_one({"id": auth["id"]}) | ||||||
|  |                 return UserModel(**user) | ||||||
|  |             else: | ||||||
|  |                 return None | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Auths = AuthsTable(DB) | ||||||
							
								
								
									
										76
									
								
								backend/apps/web/models/users.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								backend/apps/web/models/users.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,76 @@ | ||||||
|  | from pydantic import BaseModel | ||||||
|  | from typing import List, Union, Optional | ||||||
|  | from pymongo import ReturnDocument | ||||||
|  | import time | ||||||
|  | 
 | ||||||
|  | from utils import decode_token | ||||||
|  | from config import DB | ||||||
|  | 
 | ||||||
|  | #################### | ||||||
|  | # User DB Schema | ||||||
|  | #################### | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class UserModel(BaseModel): | ||||||
|  |     id: str | ||||||
|  |     name: str | ||||||
|  |     email: str | ||||||
|  |     role: str = "user" | ||||||
|  |     created_at: int  # timestamp in epoch | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #################### | ||||||
|  | # Forms | ||||||
|  | #################### | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class UsersTable: | ||||||
|  |     def __init__(self, db): | ||||||
|  |         self.db = db | ||||||
|  |         self.table = db.users | ||||||
|  | 
 | ||||||
|  |     def insert_new_user( | ||||||
|  |         self, id: str, name: str, email: str, role: str = "user" | ||||||
|  |     ) -> Optional[UserModel]: | ||||||
|  |         user = UserModel( | ||||||
|  |             **{ | ||||||
|  |                 "id": id, | ||||||
|  |                 "name": name, | ||||||
|  |                 "email": email, | ||||||
|  |                 "role": role, | ||||||
|  |                 "created_at": int(time.time()), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         result = self.table.insert_one(user.model_dump()) | ||||||
|  | 
 | ||||||
|  |         if result: | ||||||
|  |             return user | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def get_user_by_email(self, email: str) -> Optional[UserModel]: | ||||||
|  |         user = self.table.find_one({"email": email}, {"_id": False}) | ||||||
|  | 
 | ||||||
|  |         if user: | ||||||
|  |             return UserModel(**user) | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def get_user_by_token(self, token: str) -> Optional[UserModel]: | ||||||
|  |         data = decode_token(token) | ||||||
|  | 
 | ||||||
|  |         if data != None and "email" in data: | ||||||
|  |             return self.get_user_by_email(data["email"]) | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def get_users(self, skip: int = 0, limit: int = 50) -> Optional[UserModel]: | ||||||
|  |         return [ | ||||||
|  |             UserModel(**user) | ||||||
|  |             for user in list(self.table.find({}, {"_id": False})) | ||||||
|  |             .skip(skip) | ||||||
|  |             .limit(limit) | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Users = UsersTable(DB) | ||||||
							
								
								
									
										107
									
								
								backend/apps/web/routers/auths.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								backend/apps/web/routers/auths.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | ||||||
|  | from fastapi import Response | ||||||
|  | from fastapi import Depends, FastAPI, HTTPException, status | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from typing import List, Union | ||||||
|  | 
 | ||||||
|  | from fastapi import APIRouter | ||||||
|  | from pydantic import BaseModel | ||||||
|  | import time | ||||||
|  | import uuid | ||||||
|  | 
 | ||||||
|  | from constants import ERROR_MESSAGES | ||||||
|  | from utils import ( | ||||||
|  |     get_password_hash, | ||||||
|  |     bearer_scheme, | ||||||
|  |     create_token, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | from apps.web.models.auths import ( | ||||||
|  |     SigninForm, | ||||||
|  |     SignupForm, | ||||||
|  |     UserResponse, | ||||||
|  |     SigninResponse, | ||||||
|  |     Auths, | ||||||
|  | ) | ||||||
|  | from apps.web.models.users import Users | ||||||
|  | import config | ||||||
|  | 
 | ||||||
|  | router = APIRouter() | ||||||
|  | 
 | ||||||
|  | DB = config.DB | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # GetSessionUser | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.get("/", response_model=UserResponse) | ||||||
|  | async def get_session_user(cred=Depends(bearer_scheme)): | ||||||
|  |     token = cred.credentials | ||||||
|  |     user = Users.get_user_by_token(token) | ||||||
|  |     if user: | ||||||
|  |         return { | ||||||
|  |             "id": user.id, | ||||||
|  |             "email": user.email, | ||||||
|  |             "name": user.name, | ||||||
|  |             "role": user.role, | ||||||
|  |         } | ||||||
|  |     else: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # SignIn | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.post("/signin", response_model=SigninResponse) | ||||||
|  | async def signin(form_data: SigninForm): | ||||||
|  |     user = Auths.authenticate_user(form_data.email.lower(), form_data.password) | ||||||
|  |     if user: | ||||||
|  |         token = create_token(data={"email": user.email}) | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             "token": token, | ||||||
|  |             "token_type": "Bearer", | ||||||
|  |             "id": user.id, | ||||||
|  |             "email": user.email, | ||||||
|  |             "name": user.name, | ||||||
|  |             "role": user.role, | ||||||
|  |         } | ||||||
|  |     else: | ||||||
|  |         raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # SignUp | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.post("/signup", response_model=SigninResponse) | ||||||
|  | async def signup(form_data: SignupForm): | ||||||
|  |     if not Users.get_user_by_email(form_data.email.lower()): | ||||||
|  |         try: | ||||||
|  |             hashed = get_password_hash(form_data.password) | ||||||
|  |             user = Auths.insert_new_auth(form_data.email, hashed, form_data.name) | ||||||
|  | 
 | ||||||
|  |             if user: | ||||||
|  |                 token = create_token(data={"email": user.email}) | ||||||
|  |                 # response.set_cookie(key='token', value=token, httponly=True) | ||||||
|  | 
 | ||||||
|  |                 return { | ||||||
|  |                     "token": token, | ||||||
|  |                     "token_type": "Bearer", | ||||||
|  |                     "id": user.id, | ||||||
|  |                     "email": user.email, | ||||||
|  |                     "name": user.name, | ||||||
|  |                     "role": user.role, | ||||||
|  |                 } | ||||||
|  |             else: | ||||||
|  |                 raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) | ||||||
|  |         except Exception as err: | ||||||
|  |             raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) | ||||||
|  |     else: | ||||||
|  |         raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT()) | ||||||
|  | @ -1,11 +1,22 @@ | ||||||
| import sys |  | ||||||
| import os |  | ||||||
| from dotenv import load_dotenv, find_dotenv | from dotenv import load_dotenv, find_dotenv | ||||||
|  | from pymongo import MongoClient | ||||||
|  | 
 | ||||||
|  | from secrets import token_bytes | ||||||
|  | from base64 import b64encode | ||||||
|  | import os | ||||||
| 
 | 
 | ||||||
| load_dotenv(find_dotenv()) | load_dotenv(find_dotenv()) | ||||||
| 
 | 
 | ||||||
|  | #################################### | ||||||
|  | # ENV (dev,test,prod) | ||||||
|  | #################################### | ||||||
|  | 
 | ||||||
| ENV = os.environ.get("ENV", "dev") | ENV = os.environ.get("ENV", "dev") | ||||||
| 
 | 
 | ||||||
|  | #################################### | ||||||
|  | # OLLAMA_API_BASE_URL | ||||||
|  | #################################### | ||||||
|  | 
 | ||||||
| OLLAMA_API_BASE_URL = os.environ.get( | OLLAMA_API_BASE_URL = os.environ.get( | ||||||
|     "OLLAMA_API_BASE_URL", "http://localhost:11434/api" |     "OLLAMA_API_BASE_URL", "http://localhost:11434/api" | ||||||
| ) | ) | ||||||
|  | @ -13,3 +24,42 @@ OLLAMA_API_BASE_URL = os.environ.get( | ||||||
| if ENV == "prod": | if ENV == "prod": | ||||||
|     if OLLAMA_API_BASE_URL == "/ollama/api": |     if OLLAMA_API_BASE_URL == "/ollama/api": | ||||||
|         OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" |         OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" | ||||||
|  | 
 | ||||||
|  | #################################### | ||||||
|  | # OLLAMA_WEBUI_VERSION | ||||||
|  | #################################### | ||||||
|  | 
 | ||||||
|  | OLLAMA_WEBUI_VERSION = os.environ.get("OLLAMA_WEBUI_VERSION", "v1.0.0-alpha.9") | ||||||
|  | 
 | ||||||
|  | #################################### | ||||||
|  | # OLLAMA_WEBUI_AUTH | ||||||
|  | #################################### | ||||||
|  | 
 | ||||||
|  | OLLAMA_WEBUI_AUTH = ( | ||||||
|  |     True if os.environ.get("OLLAMA_WEBUI_AUTH", "TRUE") == "TRUE" else False | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if OLLAMA_WEBUI_AUTH: | ||||||
|  |     #################################### | ||||||
|  |     # OLLAMA_WEBUI_DB | ||||||
|  |     #################################### | ||||||
|  | 
 | ||||||
|  |     OLLAMA_WEBUI_DB_URL = os.environ.get( | ||||||
|  |         "OLLAMA_WEBUI_DB_URL", "mongodb://root:root@localhost:27017/" | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     DB_CLIENT = MongoClient(f"{OLLAMA_WEBUI_DB_URL}?authSource=admin") | ||||||
|  |     DB = DB_CLIENT["ollama-webui"] | ||||||
|  | 
 | ||||||
|  |     #################################### | ||||||
|  |     # OLLAMA_WEBUI_JWT_SECRET_KEY | ||||||
|  |     #################################### | ||||||
|  | 
 | ||||||
|  |     OLLAMA_WEBUI_JWT_SECRET_KEY = os.environ.get( | ||||||
|  |         "OLLAMA_WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t" | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     if ENV == "prod": | ||||||
|  |         if OLLAMA_WEBUI_JWT_SECRET_KEY == "": | ||||||
|  |             OLLAMA_WEBUI_JWT_SECRET_KEY = str(b64encode(token_bytes(32)).decode()) | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								backend/constants.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/constants.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | from enum import Enum | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MESSAGES(str, Enum): | ||||||
|  |     DEFAULT = lambda msg="": f"{msg if msg else ''}" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ERROR_MESSAGES(str, Enum): | ||||||
|  |     DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}" | ||||||
|  |     UNAUTHORIZED = "401 Unauthorized" | ||||||
|  |     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." | ||||||
|  | @ -1,16 +1,14 @@ | ||||||
| import time |  | ||||||
| import sys |  | ||||||
| 
 |  | ||||||
| from fastapi import FastAPI, Request | from fastapi import FastAPI, Request | ||||||
| from fastapi.staticfiles import StaticFiles | from fastapi.staticfiles import StaticFiles | ||||||
| 
 |  | ||||||
| from fastapi import HTTPException | from fastapi import HTTPException | ||||||
| from starlette.exceptions import HTTPException as StarletteHTTPException |  | ||||||
| 
 |  | ||||||
| from fastapi.middleware.wsgi import WSGIMiddleware | from fastapi.middleware.wsgi import WSGIMiddleware | ||||||
| from fastapi.middleware.cors import CORSMiddleware | from fastapi.middleware.cors import CORSMiddleware | ||||||
|  | from starlette.exceptions import HTTPException as StarletteHTTPException | ||||||
| 
 | 
 | ||||||
| from apps.ollama.main import app as ollama_app | from apps.ollama.main import app as ollama_app | ||||||
|  | from apps.web.main import app as webui_app | ||||||
|  | 
 | ||||||
|  | import time | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SPAStaticFiles(StaticFiles): | class SPAStaticFiles(StaticFiles): | ||||||
|  | @ -47,5 +45,6 @@ async def check_url(request: Request, call_next): | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | app.mount("/api/v1", webui_app) | ||||||
| app.mount("/ollama/api", WSGIMiddleware(ollama_app)) | app.mount("/ollama/api", WSGIMiddleware(ollama_app)) | ||||||
| app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") | app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") | ||||||
|  |  | ||||||
							
								
								
									
										68
									
								
								backend/utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								backend/utils.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | ||||||
|  | from fastapi.security import HTTPBasicCredentials, HTTPBearer | ||||||
|  | from pydantic import BaseModel | ||||||
|  | from typing import Union, Optional | ||||||
|  | 
 | ||||||
|  | from passlib.context import CryptContext | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | import requests | ||||||
|  | import jwt | ||||||
|  | 
 | ||||||
|  | import config | ||||||
|  | 
 | ||||||
|  | JWT_SECRET_KEY = config.OLLAMA_WEBUI_JWT_SECRET_KEY | ||||||
|  | ALGORITHM = "HS256" | ||||||
|  | 
 | ||||||
|  | ############## | ||||||
|  | # Auth Utils | ||||||
|  | ############## | ||||||
|  | 
 | ||||||
|  | bearer_scheme = HTTPBearer() | ||||||
|  | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def verify_password(plain_password, hashed_password): | ||||||
|  |     return ( | ||||||
|  |         pwd_context.verify(plain_password, hashed_password) if hashed_password else None | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_password_hash(password): | ||||||
|  |     return pwd_context.hash(password) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str: | ||||||
|  |     payload = data.copy() | ||||||
|  | 
 | ||||||
|  |     if expires_delta: | ||||||
|  |         expire = datetime.utcnow() + expires_delta | ||||||
|  |         payload.update({"exp": expire}) | ||||||
|  | 
 | ||||||
|  |     encoded_jwt = jwt.encode(payload, JWT_SECRET_KEY, algorithm=ALGORITHM) | ||||||
|  |     return encoded_jwt | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def decode_token(token: str) -> Optional[dict]: | ||||||
|  |     try: | ||||||
|  |         decoded = jwt.decode(token, JWT_SECRET_KEY, options={"verify_signature": False}) | ||||||
|  |         return decoded | ||||||
|  |     except Exception as e: | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def extract_token_from_auth_header(auth_header: str): | ||||||
|  |     return auth_header[len("Bearer ") :] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def verify_token(request): | ||||||
|  |     try: | ||||||
|  |         bearer = request.headers["authorization"] | ||||||
|  |         if bearer: | ||||||
|  |             token = bearer[len("Bearer ") :] | ||||||
|  |             decoded = jwt.decode( | ||||||
|  |                 token, JWT_SECRET_KEY, options={"verify_signature": False} | ||||||
|  |             ) | ||||||
|  |             return decoded | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  |     except Exception as e: | ||||||
|  |         return None | ||||||
							
								
								
									
										11
									
								
								compose.yaml
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								compose.yaml
									
										
									
									
									
								
							|  | @ -22,6 +22,15 @@ services: | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     image: ollama/ollama:latest |     image: ollama/ollama:latest | ||||||
| 
 | 
 | ||||||
|  |   ollama-webui-db: | ||||||
|  |     image: mongo | ||||||
|  |     container_name: ollama-webui-db | ||||||
|  |     restart: always | ||||||
|  |     # Make sure to change the username/password! | ||||||
|  |     environment: | ||||||
|  |       MONGO_INITDB_ROOT_USERNAME: root | ||||||
|  |       MONGO_INITDB_ROOT_PASSWORD: example | ||||||
|  | 
 | ||||||
|   ollama-webui: |   ollama-webui: | ||||||
|     build: |     build: | ||||||
|       context: . |       context: . | ||||||
|  | @ -32,10 +41,12 @@ services: | ||||||
|     container_name: ollama-webui |     container_name: ollama-webui | ||||||
|     depends_on: |     depends_on: | ||||||
|       - ollama |       - ollama | ||||||
|  |       - ollama-webui-db | ||||||
|     ports: |     ports: | ||||||
|       - 3000:8080 |       - 3000:8080 | ||||||
|     environment: |     environment: | ||||||
|       - "OLLAMA_API_BASE_URL=http://ollama:11434/api" |       - "OLLAMA_API_BASE_URL=http://ollama:11434/api" | ||||||
|  |       - "OLLAMA_WEBUI_DB_URL=mongodb://root:example@ollama-webui-db:27017/" | ||||||
|     extra_hosts: |     extra_hosts: | ||||||
|       - host.docker.internal:host-gateway |       - host.docker.internal:host-gateway | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|  |  | ||||||
|  | @ -4,8 +4,13 @@ | ||||||
| 	font-display: swap; | 	font-display: swap; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @font-face { | ||||||
|  | 	font-family: 'Mona Sans'; | ||||||
|  | 	src: url('/assets/fonts/Mona-Sans.woff2'); | ||||||
|  | 	font-display: swap; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| html { | html { | ||||||
| 	@apply bg-gray-800; |  | ||||||
| 	word-break: break-word; | 	word-break: break-word; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,9 +2,10 @@ | ||||||
| 	import sha256 from 'js-sha256'; | 	import sha256 from 'js-sha256'; | ||||||
| 	import Modal from '../common/Modal.svelte'; | 	import Modal from '../common/Modal.svelte'; | ||||||
| 
 | 
 | ||||||
| 	import { WEB_UI_VERSION, API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants'; | 	import { WEB_UI_VERSION, OLLAMA_API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants'; | ||||||
| 	import toast from 'svelte-french-toast'; | 	import toast from 'svelte-french-toast'; | ||||||
| 	import { onMount } from 'svelte'; | 	import { onMount } from 'svelte'; | ||||||
|  | 	import { config, user } from '$lib/stores'; | ||||||
| 
 | 
 | ||||||
| 	export let show = false; | 	export let show = false; | ||||||
| 	export let saveSettings: Function; | 	export let saveSettings: Function; | ||||||
|  | @ -119,7 +120,8 @@ | ||||||
| 		const res = await fetch(`${API_BASE_URL}/pull`, { | 		const res = await fetch(`${API_BASE_URL}/pull`, { | ||||||
| 			method: 'POST', | 			method: 'POST', | ||||||
| 			headers: { | 			headers: { | ||||||
| 				'Content-Type': 'text/event-stream' | 				'Content-Type': 'text/event-stream', | ||||||
|  | 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||||
| 			}, | 			}, | ||||||
| 			body: JSON.stringify({ | 			body: JSON.stringify({ | ||||||
| 				name: modelTag | 				name: modelTag | ||||||
|  | @ -175,7 +177,8 @@ | ||||||
| 		const res = await fetch(`${API_BASE_URL}/delete`, { | 		const res = await fetch(`${API_BASE_URL}/delete`, { | ||||||
| 			method: 'DELETE', | 			method: 'DELETE', | ||||||
| 			headers: { | 			headers: { | ||||||
| 				'Content-Type': 'text/event-stream' | 				'Content-Type': 'text/event-stream', | ||||||
|  | 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||||
| 			}, | 			}, | ||||||
| 			body: JSON.stringify({ | 			body: JSON.stringify({ | ||||||
| 				name: deleteModelTag | 				name: deleteModelTag | ||||||
|  | @ -992,7 +995,7 @@ | ||||||
| 								<div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div> | 								<div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div> | ||||||
| 								<div class="flex w-full"> | 								<div class="flex w-full"> | ||||||
| 									<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> | 									<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> | ||||||
| 										{WEB_UI_VERSION} | 										{$config && $config.version ? $config.version : WEB_UI_VERSION} | ||||||
| 									</div> | 									</div> | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,6 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | 	import { goto } from '$app/navigation'; | ||||||
|  | 	import { user } from '$lib/stores'; | ||||||
| 	import { onMount } from 'svelte'; | 	import { onMount } from 'svelte'; | ||||||
| 
 | 
 | ||||||
| 	let show = false; | 	let show = false; | ||||||
|  | @ -22,9 +24,9 @@ | ||||||
| 	let chatTitleEditIdx = null; | 	let chatTitleEditIdx = null; | ||||||
| 	let chatTitle = ''; | 	let chatTitle = ''; | ||||||
| 
 | 
 | ||||||
| 	let _chats = chats.map((item, idx) => chats[chats.length - 1 - idx]); | 	let showDropdown = false; | ||||||
| 
 | 
 | ||||||
| 	onMount(() => {}); | 	let _chats = chats.map((item, idx) => chats[chats.length - 1 - idx]); | ||||||
| 
 | 
 | ||||||
| 	$: if (chats) { | 	$: if (chats) { | ||||||
| 		_chats = chats.map((item, idx) => chats[chats.length - 1 - idx]); | 		_chats = chats.map((item, idx) => chats[chats.length - 1 - idx]); | ||||||
|  | @ -98,6 +100,7 @@ | ||||||
| 			<button | 			<button | ||||||
| 				class="flex-grow flex justify-between rounded-md px-3 py-1.5 my-2 hover:bg-gray-900 transition" | 				class="flex-grow flex justify-between rounded-md px-3 py-1.5 my-2 hover:bg-gray-900 transition" | ||||||
| 				on:click={() => { | 				on:click={() => { | ||||||
|  | 					// goto('/'); | ||||||
| 					createNewChat(); | 					createNewChat(); | ||||||
| 				}} | 				}} | ||||||
| 			> | 			> | ||||||
|  | @ -163,6 +166,7 @@ | ||||||
| 							? 'bg-gray-900' | 							? 'bg-gray-900' | ||||||
| 							: ''} transition whitespace-nowrap text-ellipsis" | 							: ''} transition whitespace-nowrap text-ellipsis" | ||||||
| 						on:click={() => { | 						on:click={() => { | ||||||
|  | 							// goto(`/c/${chat.id}`); | ||||||
| 							if (chat.id !== chatTitleEditIdx) { | 							if (chat.id !== chatTitleEditIdx) { | ||||||
| 								chatTitleEditIdx = null; | 								chatTitleEditIdx = null; | ||||||
| 								chatTitle = ''; | 								chatTitle = ''; | ||||||
|  | @ -380,35 +384,127 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class=" self-center">Clear conversations</div> | 					<div class=" self-center">Clear conversations</div> | ||||||
| 				</button> | 				</button> | ||||||
| 				<button | 
 | ||||||
| 					class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition" | 				{#if $user !== undefined} | ||||||
| 					on:click={() => { | 					<button | ||||||
| 						openSettings(); | 						class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition" | ||||||
| 					}} | 						on:focus={() => { | ||||||
| 				> | 							showDropdown = true; | ||||||
| 					<div class=" self-center mr-3"> | 						}} | ||||||
| 						<svg | 						on:focusout={() => { | ||||||
| 							xmlns="http://www.w3.org/2000/svg" | 							setTimeout(() => { | ||||||
| 							fill="none" | 								showDropdown = false; | ||||||
| 							viewBox="0 0 24 24" | 							}, 100); | ||||||
| 							stroke-width="1.5" | 						}} | ||||||
| 							stroke="currentColor" | 					> | ||||||
| 							class="w-5 h-5" | 						<div class=" self-center mr-3"> | ||||||
|  | 							<img src="/user.png" class=" max-w-[30px] object-cover rounded-full" /> | ||||||
|  | 						</div> | ||||||
|  | 						<div class=" self-center font-semibold">{$user.name}</div> | ||||||
|  | 					</button> | ||||||
|  | 
 | ||||||
|  | 					{#if showDropdown} | ||||||
|  | 						<div | ||||||
|  | 							id="dropdownDots" | ||||||
|  | 							class="absolute z-10 bottom-[4.5rem] rounded-lg shadow w-[240px] bg-gray-900" | ||||||
| 						> | 						> | ||||||
| 							<path | 							<div class="py-2 w-full"> | ||||||
| 								stroke-linecap="round" | 								<button | ||||||
| 								stroke-linejoin="round" | 									class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition" | ||||||
| 								d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" | 									on:click={() => { | ||||||
| 							/> | 										openSettings(); | ||||||
| 							<path | 									}} | ||||||
| 								stroke-linecap="round" | 								> | ||||||
| 								stroke-linejoin="round" | 									<div class=" self-center mr-3"> | ||||||
| 								d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" | 										<svg | ||||||
| 							/> | 											xmlns="http://www.w3.org/2000/svg" | ||||||
| 						</svg> | 											fill="none" | ||||||
| 					</div> | 											viewBox="0 0 24 24" | ||||||
| 					<div class=" self-center font-medium">Settings</div> | 											stroke-width="1.5" | ||||||
| 				</button> | 											stroke="currentColor" | ||||||
|  | 											class="w-5 h-5" | ||||||
|  | 										> | ||||||
|  | 											<path | ||||||
|  | 												stroke-linecap="round" | ||||||
|  | 												stroke-linejoin="round" | ||||||
|  | 												d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" | ||||||
|  | 											/> | ||||||
|  | 											<path | ||||||
|  | 												stroke-linecap="round" | ||||||
|  | 												stroke-linejoin="round" | ||||||
|  | 												d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" | ||||||
|  | 											/> | ||||||
|  | 										</svg> | ||||||
|  | 									</div> | ||||||
|  | 									<div class=" self-center font-medium">Settings</div> | ||||||
|  | 								</button> | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<hr class=" dark:border-gray-700 m-0 p-0" /> | ||||||
|  | 
 | ||||||
|  | 							<div class="py-2 w-full"> | ||||||
|  | 								<button | ||||||
|  | 									class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition" | ||||||
|  | 									on:click={() => { | ||||||
|  | 										localStorage.removeItem('token'); | ||||||
|  | 										location.href = '/'; | ||||||
|  | 									}} | ||||||
|  | 								> | ||||||
|  | 									<div class=" self-center mr-3"> | ||||||
|  | 										<svg | ||||||
|  | 											xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 											viewBox="0 0 20 20" | ||||||
|  | 											fill="currentColor" | ||||||
|  | 											class="w-5 h-5" | ||||||
|  | 										> | ||||||
|  | 											<path | ||||||
|  | 												fill-rule="evenodd" | ||||||
|  | 												d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z" | ||||||
|  | 												clip-rule="evenodd" | ||||||
|  | 											/> | ||||||
|  | 											<path | ||||||
|  | 												fill-rule="evenodd" | ||||||
|  | 												d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z" | ||||||
|  | 												clip-rule="evenodd" | ||||||
|  | 											/> | ||||||
|  | 										</svg> | ||||||
|  | 									</div> | ||||||
|  | 									<div class=" self-center font-medium">Sign Out</div> | ||||||
|  | 								</button> | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					{/if} | ||||||
|  | 				{:else} | ||||||
|  | 					<button | ||||||
|  | 						class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition" | ||||||
|  | 						on:click={() => { | ||||||
|  | 							openSettings(); | ||||||
|  | 						}} | ||||||
|  | 					> | ||||||
|  | 						<div class=" self-center mr-3"> | ||||||
|  | 							<svg | ||||||
|  | 								xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 								fill="none" | ||||||
|  | 								viewBox="0 0 24 24" | ||||||
|  | 								stroke-width="1.5" | ||||||
|  | 								stroke="currentColor" | ||||||
|  | 								class="w-5 h-5" | ||||||
|  | 							> | ||||||
|  | 								<path | ||||||
|  | 									stroke-linecap="round" | ||||||
|  | 									stroke-linejoin="round" | ||||||
|  | 									d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" | ||||||
|  | 								/> | ||||||
|  | 								<path | ||||||
|  | 									stroke-linecap="round" | ||||||
|  | 									stroke-linejoin="round" | ||||||
|  | 									d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" | ||||||
|  | 								/> | ||||||
|  | 							</svg> | ||||||
|  | 						</div> | ||||||
|  | 						<div class=" self-center font-medium">Settings</div> | ||||||
|  | 					</button> | ||||||
|  | 				{/if} | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
|  | @ -1,14 +1,16 @@ | ||||||
| import { browser } from '$app/environment'; | import { dev, browser } from '$app/environment'; | ||||||
| import { PUBLIC_API_BASE_URL } from '$env/static/public'; | import { PUBLIC_API_BASE_URL } from '$env/static/public'; | ||||||
| 
 | 
 | ||||||
| export const API_BASE_URL = | export const OLLAMA_API_BASE_URL = | ||||||
| 	PUBLIC_API_BASE_URL === '' | 	PUBLIC_API_BASE_URL === '' | ||||||
| 		? browser | 		? browser | ||||||
| 			? `http://${location.hostname}:11434/api` | 			? `http://${location.hostname}:8080/ollama/api` | ||||||
| 			: `http://localhost:11434/api` | 			: `http://localhost:11434/api` | ||||||
| 		: PUBLIC_API_BASE_URL; | 		: PUBLIC_API_BASE_URL; | ||||||
| 
 | 
 | ||||||
| export const WEB_UI_VERSION = 'v1.0.0-alpha.8'; | export const WEBUI_API_BASE_URL = dev ? `http://${location.hostname}:8080/api/v1` : `/api/v1`; | ||||||
|  | 
 | ||||||
|  | export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; | ||||||
| 
 | 
 | ||||||
| // Source: https://kit.svelte.dev/docs/modules#$env-static-public
 | // Source: https://kit.svelte.dev/docs/modules#$env-static-public
 | ||||||
| // This feature, akin to $env/static/private, exclusively incorporates environment variables
 | // This feature, akin to $env/static/private, exclusively incorporates environment variables
 | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								src/lib/stores/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/lib/stores/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | import { writable } from 'svelte/store'; | ||||||
|  | 
 | ||||||
|  | export const config = writable(undefined); | ||||||
|  | export const user = writable(undefined); | ||||||
							
								
								
									
										12
									
								
								src/routes/(app)/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/routes/(app)/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | <script> | ||||||
|  | 	import { config, user } from '$lib/stores'; | ||||||
|  | 	import { goto } from '$app/navigation'; | ||||||
|  | 
 | ||||||
|  | 	if ($config && $config.auth && $user === undefined) { | ||||||
|  | 		goto('/auth'); | ||||||
|  | 	} | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if $config !== undefined} | ||||||
|  | 	<slot /> | ||||||
|  | {/if} | ||||||
|  | @ -10,17 +10,17 @@ | ||||||
| 	import 'katex/dist/katex.min.css'; | 	import 'katex/dist/katex.min.css'; | ||||||
| 	import toast from 'svelte-french-toast'; | 	import toast from 'svelte-french-toast'; | ||||||
| 
 | 
 | ||||||
| 	import { API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants'; | 	import { OLLAMA_API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants'; | ||||||
| 	import { onMount, tick } from 'svelte'; | 	import { onMount, tick } from 'svelte'; | ||||||
| 
 | 
 | ||||||
| 	import Navbar from '$lib/components/layout/Navbar.svelte'; | 	import Navbar from '$lib/components/layout/Navbar.svelte'; | ||||||
| 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | ||||||
| 	import Suggestions from '$lib/components/chat/Suggestions.svelte'; | 	import Suggestions from '$lib/components/chat/Suggestions.svelte'; | ||||||
|  | 	import { user } from '$lib/stores'; | ||||||
| 
 | 
 | ||||||
| 	let API_BASE_URL = BUILD_TIME_API_BASE_URL; | 	let API_BASE_URL = BUILD_TIME_API_BASE_URL; | ||||||
| 	let db; | 	let db; | ||||||
| 
 | 
 | ||||||
| 	// let selectedModel = ''; |  | ||||||
| 	let selectedModels = ['']; | 	let selectedModels = ['']; | ||||||
| 	let settings = { | 	let settings = { | ||||||
| 		system: null, | 		system: null, | ||||||
|  | @ -619,7 +619,8 @@ | ||||||
| 			headers: { | 			headers: { | ||||||
| 				Accept: 'application/json', | 				Accept: 'application/json', | ||||||
| 				'Content-Type': 'application/json', | 				'Content-Type': 'application/json', | ||||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }) | 				...(settings.authHeader && { Authorization: settings.authHeader }), | ||||||
|  | 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 			.then(async (res) => { | 			.then(async (res) => { | ||||||
|  | @ -628,7 +629,11 @@ | ||||||
| 			}) | 			}) | ||||||
| 			.catch((error) => { | 			.catch((error) => { | ||||||
| 				console.log(error); | 				console.log(error); | ||||||
| 				toast.error('Server connection failed'); | 				if ('detail' in error) { | ||||||
|  | 					toast.error(error.detail); | ||||||
|  | 				} else { | ||||||
|  | 					toast.error('Server connection failed'); | ||||||
|  | 				} | ||||||
| 				return null; | 				return null; | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
|  | @ -687,13 +692,6 @@ | ||||||
| 				} | 				} | ||||||
| 			}) | 			}) | ||||||
| 		); | 		); | ||||||
| 
 |  | ||||||
| 		// if (selectedModel.includes('gpt-')) { |  | ||||||
| 		// 	await sendPromptOpenAI(userPrompt, parentId); |  | ||||||
| 		// } else { |  | ||||||
| 		// 	await sendPromptOllama(userPrompt, parentId); |  | ||||||
| 		// } |  | ||||||
| 
 |  | ||||||
| 		console.log(history); | 		console.log(history); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | @ -724,7 +722,8 @@ | ||||||
| 			method: 'POST', | 			method: 'POST', | ||||||
| 			headers: { | 			headers: { | ||||||
| 				'Content-Type': 'text/event-stream', | 				'Content-Type': 'text/event-stream', | ||||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }) | 				...(settings.authHeader && { Authorization: settings.authHeader }), | ||||||
|  | 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||||
| 			}, | 			}, | ||||||
| 			body: JSON.stringify({ | 			body: JSON.stringify({ | ||||||
| 				model: model, | 				model: model, | ||||||
|  | @ -779,6 +778,8 @@ | ||||||
| 								responseMessage.content += data.response; | 								responseMessage.content += data.response; | ||||||
| 								messages = messages; | 								messages = messages; | ||||||
| 							} | 							} | ||||||
|  | 						} else if ('detail' in data) { | ||||||
|  | 							throw data; | ||||||
| 						} else { | 						} else { | ||||||
| 							responseMessage.done = true; | 							responseMessage.done = true; | ||||||
| 							responseMessage.context = data.context; | 							responseMessage.context = data.context; | ||||||
|  | @ -791,6 +792,10 @@ | ||||||
| 				} | 				} | ||||||
| 			} catch (error) { | 			} catch (error) { | ||||||
| 				console.log(error); | 				console.log(error); | ||||||
|  | 				if ('detail' in error) { | ||||||
|  | 					toast.error(error.detail); | ||||||
|  | 				} | ||||||
|  | 				break; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if (autoScroll) { | 			if (autoScroll) { | ||||||
|  | @ -817,7 +822,7 @@ | ||||||
| 			window.scrollTo({ top: document.body.scrollHeight }); | 			window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (messages.length == 2) { | 		if (messages.length == 2 && messages.at(1).content !== '') { | ||||||
| 			await generateChatTitle(chatId, userPrompt); | 			await generateChatTitle(chatId, userPrompt); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
|  | @ -1034,7 +1039,8 @@ | ||||||
| 			method: 'POST', | 			method: 'POST', | ||||||
| 			headers: { | 			headers: { | ||||||
| 				'Content-Type': 'text/event-stream', | 				'Content-Type': 'text/event-stream', | ||||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }) | 				...(settings.authHeader && { Authorization: settings.authHeader }), | ||||||
|  | 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||||
| 			}, | 			}, | ||||||
| 			body: JSON.stringify({ | 			body: JSON.stringify({ | ||||||
| 				model: selectedModels[0], | 				model: selectedModels[0], | ||||||
|  | @ -1047,6 +1053,9 @@ | ||||||
| 				return res.json(); | 				return res.json(); | ||||||
| 			}) | 			}) | ||||||
| 			.catch((error) => { | 			.catch((error) => { | ||||||
|  | 				if ('detail' in error) { | ||||||
|  | 					toast.error(error.detail); | ||||||
|  | 				} | ||||||
| 				console.log(error); | 				console.log(error); | ||||||
| 				return null; | 				return null; | ||||||
| 			}); | 			}); | ||||||
							
								
								
									
										0
									
								
								src/routes/(app)/c/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/(app)/c/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -1,13 +1,71 @@ | ||||||
| <script> | <script> | ||||||
| 	import { Toaster } from 'svelte-french-toast'; | 	import { onMount, tick } from 'svelte'; | ||||||
|  | 	import { config, user } from '$lib/stores'; | ||||||
|  | 	import { goto } from '$app/navigation'; | ||||||
|  | 	import { WEBUI_API_BASE_URL } from '$lib/constants'; | ||||||
|  | 	import toast, { Toaster } from 'svelte-french-toast'; | ||||||
| 
 | 
 | ||||||
| 	import '../app.css'; | 	import '../app.css'; | ||||||
| 	import '../tailwind.css'; | 	import '../tailwind.css'; | ||||||
|  | 
 | ||||||
|  | 	let loaded = false; | ||||||
|  | 
 | ||||||
|  | 	onMount(async () => { | ||||||
|  | 		const webBackendStatus = await fetch(`${WEBUI_API_BASE_URL}/`, { | ||||||
|  | 			method: 'GET', | ||||||
|  | 			headers: { | ||||||
|  | 				'Content-Type': 'application/json' | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 			.then(async (res) => { | ||||||
|  | 				if (!res.ok) throw await res.json(); | ||||||
|  | 				return res.json(); | ||||||
|  | 			}) | ||||||
|  | 			.catch((error) => { | ||||||
|  | 				console.log(error); | ||||||
|  | 				return null; | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 		console.log(webBackendStatus); | ||||||
|  | 		await config.set(webBackendStatus); | ||||||
|  | 
 | ||||||
|  | 		if (webBackendStatus) { | ||||||
|  | 			if (webBackendStatus.auth) { | ||||||
|  | 				if (localStorage.token) { | ||||||
|  | 					const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, { | ||||||
|  | 						method: 'GET', | ||||||
|  | 						headers: { | ||||||
|  | 							'Content-Type': 'application/json', | ||||||
|  | 							Authorization: `Bearer ${localStorage.token}` | ||||||
|  | 						} | ||||||
|  | 					}) | ||||||
|  | 						.then(async (res) => { | ||||||
|  | 							if (!res.ok) throw await res.json(); | ||||||
|  | 							return res.json(); | ||||||
|  | 						}) | ||||||
|  | 						.catch((error) => { | ||||||
|  | 							console.log(error); | ||||||
|  | 							toast.error(error.detail); | ||||||
|  | 							return null; | ||||||
|  | 						}); | ||||||
|  | 
 | ||||||
|  | 					await user.set(res); | ||||||
|  | 				} else { | ||||||
|  | 					goto('/auth'); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		await tick(); | ||||||
|  | 		loaded = true; | ||||||
|  | 	}); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <svelte:head> | <svelte:head> | ||||||
| 	<title>Ollama</title> | 	<title>Ollama</title> | ||||||
| </svelte:head> | </svelte:head> | ||||||
| 
 |  | ||||||
| <slot /> |  | ||||||
| <Toaster /> | <Toaster /> | ||||||
|  | 
 | ||||||
|  | {#if $config !== undefined && loaded} | ||||||
|  | 	<slot /> | ||||||
|  | {/if} | ||||||
|  |  | ||||||
							
								
								
									
										1091
									
								
								src/routes/auth/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1091
									
								
								src/routes/auth/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/assets/fonts/Mona-Sans.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/assets/fonts/Mona-Sans.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue