mirror of
				https://github.com/open-webui/open-webui
				synced 2025-06-26 18:26:48 +00:00 
			
		
		
		
	feat: multi-user support w/ RBAC
This commit is contained in:
		
							parent
							
								
									31e38df0a5
								
							
						
					
					
						commit
						921eef03b3
					
				| @ -1,4 +1,4 @@ | ||||
| from flask import Flask, request, Response | ||||
| from flask import Flask, request, Response, jsonify | ||||
| from flask_cors import CORS | ||||
| 
 | ||||
| 
 | ||||
| @ -6,7 +6,10 @@ import requests | ||||
| 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__) | ||||
| CORS( | ||||
| @ -28,6 +31,21 @@ def proxy(path): | ||||
|     data = request.get_data() | ||||
|     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 | ||||
|     target_response = requests.request( | ||||
|         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 pymongo import MongoClient | ||||
| 
 | ||||
| from secrets import token_bytes | ||||
| from base64 import b64encode | ||||
| import os | ||||
| 
 | ||||
| load_dotenv(find_dotenv()) | ||||
| 
 | ||||
| #################################### | ||||
| # ENV (dev,test,prod) | ||||
| #################################### | ||||
| 
 | ||||
| ENV = os.environ.get("ENV", "dev") | ||||
| 
 | ||||
| #################################### | ||||
| # OLLAMA_API_BASE_URL | ||||
| #################################### | ||||
| 
 | ||||
| OLLAMA_API_BASE_URL = os.environ.get( | ||||
|     "OLLAMA_API_BASE_URL", "http://localhost:11434/api" | ||||
| ) | ||||
| @ -13,3 +24,42 @@ OLLAMA_API_BASE_URL = os.environ.get( | ||||
| if ENV == "prod": | ||||
|     if OLLAMA_API_BASE_URL == "/ollama/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.staticfiles import StaticFiles | ||||
| 
 | ||||
| from fastapi import HTTPException | ||||
| from starlette.exceptions import HTTPException as StarletteHTTPException | ||||
| 
 | ||||
| from fastapi.middleware.wsgi import WSGIMiddleware | ||||
| from fastapi.middleware.cors import CORSMiddleware | ||||
| from starlette.exceptions import HTTPException as StarletteHTTPException | ||||
| 
 | ||||
| from apps.ollama.main import app as ollama_app | ||||
| from apps.web.main import app as webui_app | ||||
| 
 | ||||
| import time | ||||
| 
 | ||||
| 
 | ||||
| class SPAStaticFiles(StaticFiles): | ||||
| @ -47,5 +45,6 @@ async def check_url(request: Request, call_next): | ||||
|     return response | ||||
| 
 | ||||
| 
 | ||||
| app.mount("/api/v1", webui_app) | ||||
| app.mount("/ollama/api", WSGIMiddleware(ollama_app)) | ||||
| 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 | ||||
|     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: | ||||
|     build: | ||||
|       context: . | ||||
| @ -32,10 +41,12 @@ services: | ||||
|     container_name: ollama-webui | ||||
|     depends_on: | ||||
|       - ollama | ||||
|       - ollama-webui-db | ||||
|     ports: | ||||
|       - 3000:8080 | ||||
|     environment: | ||||
|       - "OLLAMA_API_BASE_URL=http://ollama:11434/api" | ||||
|       - "OLLAMA_WEBUI_DB_URL=mongodb://root:example@ollama-webui-db:27017/" | ||||
|     extra_hosts: | ||||
|       - host.docker.internal:host-gateway | ||||
|     restart: unless-stopped | ||||
|  | ||||
| @ -4,8 +4,13 @@ | ||||
| 	font-display: swap; | ||||
| } | ||||
| 
 | ||||
| @font-face { | ||||
| 	font-family: 'Mona Sans'; | ||||
| 	src: url('/assets/fonts/Mona-Sans.woff2'); | ||||
| 	font-display: swap; | ||||
| } | ||||
| 
 | ||||
| html { | ||||
| 	@apply bg-gray-800; | ||||
| 	word-break: break-word; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -2,9 +2,10 @@ | ||||
| 	import sha256 from 'js-sha256'; | ||||
| 	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 { onMount } from 'svelte'; | ||||
| 	import { config, user } from '$lib/stores'; | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 	export let saveSettings: Function; | ||||
| @ -119,7 +120,8 @@ | ||||
| 		const res = await fetch(`${API_BASE_URL}/pull`, { | ||||
| 			method: 'POST', | ||||
| 			headers: { | ||||
| 				'Content-Type': 'text/event-stream' | ||||
| 				'Content-Type': 'text/event-stream', | ||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 			}, | ||||
| 			body: JSON.stringify({ | ||||
| 				name: modelTag | ||||
| @ -175,7 +177,8 @@ | ||||
| 		const res = await fetch(`${API_BASE_URL}/delete`, { | ||||
| 			method: 'DELETE', | ||||
| 			headers: { | ||||
| 				'Content-Type': 'text/event-stream' | ||||
| 				'Content-Type': 'text/event-stream', | ||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 			}, | ||||
| 			body: JSON.stringify({ | ||||
| 				name: deleteModelTag | ||||
| @ -992,7 +995,7 @@ | ||||
| 								<div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div> | ||||
| 								<div class="flex w-full"> | ||||
| 									<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> | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| <script lang="ts"> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { user } from '$lib/stores'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	let show = false; | ||||
| @ -22,9 +24,9 @@ | ||||
| 	let chatTitleEditIdx = null; | ||||
| 	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) { | ||||
| 		_chats = chats.map((item, idx) => chats[chats.length - 1 - idx]); | ||||
| @ -98,6 +100,7 @@ | ||||
| 			<button | ||||
| 				class="flex-grow flex justify-between rounded-md px-3 py-1.5 my-2 hover:bg-gray-900 transition" | ||||
| 				on:click={() => { | ||||
| 					// goto('/'); | ||||
| 					createNewChat(); | ||||
| 				}} | ||||
| 			> | ||||
| @ -163,6 +166,7 @@ | ||||
| 							? 'bg-gray-900' | ||||
| 							: ''} transition whitespace-nowrap text-ellipsis" | ||||
| 						on:click={() => { | ||||
| 							// goto(`/c/${chat.id}`); | ||||
| 							if (chat.id !== chatTitleEditIdx) { | ||||
| 								chatTitleEditIdx = null; | ||||
| 								chatTitle = ''; | ||||
| @ -380,35 +384,127 @@ | ||||
| 					</div> | ||||
| 					<div class=" self-center">Clear conversations</div> | ||||
| 				</button> | ||||
| 				<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" | ||||
| 
 | ||||
| 				{#if $user !== undefined} | ||||
| 					<button | ||||
| 						class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition" | ||||
| 						on:focus={() => { | ||||
| 							showDropdown = true; | ||||
| 						}} | ||||
| 						on:focusout={() => { | ||||
| 							setTimeout(() => { | ||||
| 								showDropdown = false; | ||||
| 							}, 100); | ||||
| 						}} | ||||
| 					> | ||||
| 						<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 | ||||
| 								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 class="py-2 w-full"> | ||||
| 								<button | ||||
| 									class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 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> | ||||
| 							</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> | ||||
|  | ||||
| @ -1,14 +1,16 @@ | ||||
| import { browser } from '$app/environment'; | ||||
| import { dev, browser } from '$app/environment'; | ||||
| import { PUBLIC_API_BASE_URL } from '$env/static/public'; | ||||
| 
 | ||||
| export const API_BASE_URL = | ||||
| export const OLLAMA_API_BASE_URL = | ||||
| 	PUBLIC_API_BASE_URL === '' | ||||
| 		? browser | ||||
| 			? `http://${location.hostname}:11434/api` | ||||
| 			? `http://${location.hostname}:8080/ollama/api` | ||||
| 			: `http://localhost:11434/api` | ||||
| 		: 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
 | ||||
| // 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 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 Navbar from '$lib/components/layout/Navbar.svelte'; | ||||
| 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | ||||
| 	import Suggestions from '$lib/components/chat/Suggestions.svelte'; | ||||
| 	import { user } from '$lib/stores'; | ||||
| 
 | ||||
| 	let API_BASE_URL = BUILD_TIME_API_BASE_URL; | ||||
| 	let db; | ||||
| 
 | ||||
| 	// let selectedModel = ''; | ||||
| 	let selectedModels = ['']; | ||||
| 	let settings = { | ||||
| 		system: null, | ||||
| @ -619,7 +619,8 @@ | ||||
| 			headers: { | ||||
| 				Accept: 'application/json', | ||||
| 				'Content-Type': 'application/json', | ||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }) | ||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }), | ||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 			} | ||||
| 		}) | ||||
| 			.then(async (res) => { | ||||
| @ -628,7 +629,11 @@ | ||||
| 			}) | ||||
| 			.catch((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; | ||||
| 			}); | ||||
| 
 | ||||
| @ -687,13 +692,6 @@ | ||||
| 				} | ||||
| 			}) | ||||
| 		); | ||||
| 
 | ||||
| 		// if (selectedModel.includes('gpt-')) { | ||||
| 		// 	await sendPromptOpenAI(userPrompt, parentId); | ||||
| 		// } else { | ||||
| 		// 	await sendPromptOllama(userPrompt, parentId); | ||||
| 		// } | ||||
| 
 | ||||
| 		console.log(history); | ||||
| 	}; | ||||
| 
 | ||||
| @ -724,7 +722,8 @@ | ||||
| 			method: 'POST', | ||||
| 			headers: { | ||||
| 				'Content-Type': 'text/event-stream', | ||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }) | ||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }), | ||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 			}, | ||||
| 			body: JSON.stringify({ | ||||
| 				model: model, | ||||
| @ -779,6 +778,8 @@ | ||||
| 								responseMessage.content += data.response; | ||||
| 								messages = messages; | ||||
| 							} | ||||
| 						} else if ('detail' in data) { | ||||
| 							throw data; | ||||
| 						} else { | ||||
| 							responseMessage.done = true; | ||||
| 							responseMessage.context = data.context; | ||||
| @ -791,6 +792,10 @@ | ||||
| 				} | ||||
| 			} catch (error) { | ||||
| 				console.log(error); | ||||
| 				if ('detail' in error) { | ||||
| 					toast.error(error.detail); | ||||
| 				} | ||||
| 				break; | ||||
| 			} | ||||
| 
 | ||||
| 			if (autoScroll) { | ||||
| @ -817,7 +822,7 @@ | ||||
| 			window.scrollTo({ top: document.body.scrollHeight }); | ||||
| 		} | ||||
| 
 | ||||
| 		if (messages.length == 2) { | ||||
| 		if (messages.length == 2 && messages.at(1).content !== '') { | ||||
| 			await generateChatTitle(chatId, userPrompt); | ||||
| 		} | ||||
| 	}; | ||||
| @ -1034,7 +1039,8 @@ | ||||
| 			method: 'POST', | ||||
| 			headers: { | ||||
| 				'Content-Type': 'text/event-stream', | ||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }) | ||||
| 				...(settings.authHeader && { Authorization: settings.authHeader }), | ||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 			}, | ||||
| 			body: JSON.stringify({ | ||||
| 				model: selectedModels[0], | ||||
| @ -1047,6 +1053,9 @@ | ||||
| 				return res.json(); | ||||
| 			}) | ||||
| 			.catch((error) => { | ||||
| 				if ('detail' in error) { | ||||
| 					toast.error(error.detail); | ||||
| 				} | ||||
| 				console.log(error); | ||||
| 				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> | ||||
| 	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 '../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> | ||||
| 
 | ||||
| <svelte:head> | ||||
| 	<title>Ollama</title> | ||||
| </svelte:head> | ||||
| 
 | ||||
| <slot /> | ||||
| <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…
	
		Reference in New Issue
	
	Block a user