mirror of
				https://github.com/open-webui/open-webui
				synced 2025-06-26 18:26:48 +00:00 
			
		
		
		
	feat: basic RBAC support
This commit is contained in:
		
							parent
							
								
									921eef03b3
								
							
						
					
					
						commit
						8547b7807d
					
				@ -8,7 +8,7 @@ import json
 | 
			
		||||
 | 
			
		||||
from apps.web.models.users import Users
 | 
			
		||||
from constants import ERROR_MESSAGES
 | 
			
		||||
from utils import extract_token_from_auth_header
 | 
			
		||||
from utils.utils import extract_token_from_auth_header
 | 
			
		||||
from config import OLLAMA_API_BASE_URL, OLLAMA_WEBUI_AUTH
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
@ -25,24 +25,37 @@ TARGET_SERVER_URL = OLLAMA_API_BASE_URL
 | 
			
		||||
def proxy(path):
 | 
			
		||||
    # Combine the base URL of the target server with the requested path
 | 
			
		||||
    target_url = f"{TARGET_SERVER_URL}/{path}"
 | 
			
		||||
    print(target_url)
 | 
			
		||||
    print(path)
 | 
			
		||||
 | 
			
		||||
    # Get data from the original request
 | 
			
		||||
    data = request.get_data()
 | 
			
		||||
    headers = dict(request.headers)
 | 
			
		||||
 | 
			
		||||
    # Basic RBAC support
 | 
			
		||||
    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
 | 
			
		||||
                # Only user and admin roles can access
 | 
			
		||||
                if user.role in ["user", "admin"]:
 | 
			
		||||
                    if path in ["pull", "delete", "push", "copy", "create"]:
 | 
			
		||||
                        # Only admin role can perform actions above
 | 
			
		||||
                        if user.role == "admin":
 | 
			
		||||
                            pass
 | 
			
		||||
                        else:
 | 
			
		||||
                            return (
 | 
			
		||||
                                jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}),
 | 
			
		||||
                                401,
 | 
			
		||||
                            )
 | 
			
		||||
                    else:
 | 
			
		||||
                        pass
 | 
			
		||||
                else:
 | 
			
		||||
                    return jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), 401
 | 
			
		||||
            else:
 | 
			
		||||
                return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
 | 
			
		||||
        else:
 | 
			
		||||
            return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import uuid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from apps.web.models.users import UserModel, Users
 | 
			
		||||
from utils import (
 | 
			
		||||
from utils.utils import (
 | 
			
		||||
    verify_password,
 | 
			
		||||
    get_password_hash,
 | 
			
		||||
    bearer_scheme,
 | 
			
		||||
@ -43,6 +43,7 @@ class UserResponse(BaseModel):
 | 
			
		||||
    email: str
 | 
			
		||||
    name: str
 | 
			
		||||
    role: str
 | 
			
		||||
    profile_image_url: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SigninResponse(Token, UserResponse):
 | 
			
		||||
@ -66,7 +67,7 @@ class AuthsTable:
 | 
			
		||||
        self.table = db.auths
 | 
			
		||||
 | 
			
		||||
    def insert_new_auth(
 | 
			
		||||
        self, email: str, password: str, name: str, role: str = "user"
 | 
			
		||||
        self, email: str, password: str, name: str, role: str = "pending"
 | 
			
		||||
    ) -> Optional[UserModel]:
 | 
			
		||||
        print("insert_new_auth")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,9 @@ from typing import List, Union, Optional
 | 
			
		||||
from pymongo import ReturnDocument
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from utils import decode_token
 | 
			
		||||
from utils.utils import decode_token
 | 
			
		||||
from utils.misc import get_gravatar_url
 | 
			
		||||
 | 
			
		||||
from config import DB
 | 
			
		||||
 | 
			
		||||
####################
 | 
			
		||||
@ -15,7 +17,8 @@ class UserModel(BaseModel):
 | 
			
		||||
    id: str
 | 
			
		||||
    name: str
 | 
			
		||||
    email: str
 | 
			
		||||
    role: str = "user"
 | 
			
		||||
    role: str = "pending"
 | 
			
		||||
    profile_image_url: str = "/user.png"
 | 
			
		||||
    created_at: int  # timestamp in epoch
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -30,7 +33,7 @@ class UsersTable:
 | 
			
		||||
        self.table = db.users
 | 
			
		||||
 | 
			
		||||
    def insert_new_user(
 | 
			
		||||
        self, id: str, name: str, email: str, role: str = "user"
 | 
			
		||||
        self, id: str, name: str, email: str, role: str = "pending"
 | 
			
		||||
    ) -> Optional[UserModel]:
 | 
			
		||||
        user = UserModel(
 | 
			
		||||
            **{
 | 
			
		||||
@ -38,6 +41,7 @@ class UsersTable:
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "email": email,
 | 
			
		||||
                "role": role,
 | 
			
		||||
                "profile_image_url": get_gravatar_url(email),
 | 
			
		||||
                "created_at": int(time.time()),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -9,12 +9,14 @@ import time
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from constants import ERROR_MESSAGES
 | 
			
		||||
from utils import (
 | 
			
		||||
from utils.utils import (
 | 
			
		||||
    get_password_hash,
 | 
			
		||||
    bearer_scheme,
 | 
			
		||||
    create_token,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from utils.misc import get_gravatar_url
 | 
			
		||||
 | 
			
		||||
from apps.web.models.auths import (
 | 
			
		||||
    SigninForm,
 | 
			
		||||
    SignupForm,
 | 
			
		||||
@ -45,10 +47,12 @@ async def get_session_user(cred=Depends(bearer_scheme)):
 | 
			
		||||
            "email": user.email,
 | 
			
		||||
            "name": user.name,
 | 
			
		||||
            "role": user.role,
 | 
			
		||||
            "profile_image_url": user.profile_image_url,
 | 
			
		||||
        }
 | 
			
		||||
    else:
 | 
			
		||||
        raise HTTPException(
 | 
			
		||||
            status_code=status.HTTP_401_UNAUTHORIZED,
 | 
			
		||||
            detail=ERROR_MESSAGES.INVALID_TOKEN,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -70,9 +74,10 @@ async def signin(form_data: SigninForm):
 | 
			
		||||
            "email": user.email,
 | 
			
		||||
            "name": user.name,
 | 
			
		||||
            "role": user.role,
 | 
			
		||||
            "profile_image_url": user.profile_image_url,
 | 
			
		||||
        }
 | 
			
		||||
    else:
 | 
			
		||||
        raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT())
 | 
			
		||||
        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
############################
 | 
			
		||||
@ -98,6 +103,7 @@ async def signup(form_data: SignupForm):
 | 
			
		||||
                    "email": user.email,
 | 
			
		||||
                    "name": user.name,
 | 
			
		||||
                    "role": user.role,
 | 
			
		||||
                    "profile_image_url": user.profile_image_url,
 | 
			
		||||
                }
 | 
			
		||||
            else:
 | 
			
		||||
                raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,12 @@ class MESSAGES(str, Enum):
 | 
			
		||||
 | 
			
		||||
class ERROR_MESSAGES(str, Enum):
 | 
			
		||||
    DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
 | 
			
		||||
    INVALID_TOKEN = (
 | 
			
		||||
        "Your session has expired or the token is invalid. Please sign in again."
 | 
			
		||||
    )
 | 
			
		||||
    INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
 | 
			
		||||
    UNAUTHORIZED = "401 Unauthorized"
 | 
			
		||||
    ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
 | 
			
		||||
    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."
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								backend/utils/misc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/utils/misc.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import hashlib
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_gravatar_url(email):
 | 
			
		||||
    # Trim leading and trailing whitespace from
 | 
			
		||||
    # an email address and force all characters
 | 
			
		||||
    # to lower case
 | 
			
		||||
    address = str(email).strip().lower()
 | 
			
		||||
 | 
			
		||||
    # Create a SHA256 hash of the final string
 | 
			
		||||
    hash_object = hashlib.sha256(address.encode())
 | 
			
		||||
    hash_hex = hash_object.hexdigest()
 | 
			
		||||
 | 
			
		||||
    # Grab the actual image URL
 | 
			
		||||
    return f"https://www.gravatar.com/avatar/{hash_hex}"
 | 
			
		||||
@ -149,6 +149,10 @@
 | 
			
		||||
						if (data.error) {
 | 
			
		||||
							throw data.error;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						if (data.detail) {
 | 
			
		||||
							throw data.detail;
 | 
			
		||||
						}
 | 
			
		||||
						if (data.status) {
 | 
			
		||||
							if (!data.status.includes('downloading')) {
 | 
			
		||||
								toast.success(data.status);
 | 
			
		||||
@ -206,6 +210,10 @@
 | 
			
		||||
						if (data.error) {
 | 
			
		||||
							throw data.error;
 | 
			
		||||
						}
 | 
			
		||||
						if (data.detail) {
 | 
			
		||||
							throw data.detail;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						if (data.status) {
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
 | 
			
		||||
@ -388,17 +388,17 @@
 | 
			
		||||
				{#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:click={() => {
 | 
			
		||||
							showDropdown = !showDropdown;
 | 
			
		||||
						}}
 | 
			
		||||
						on:focusout={() => {
 | 
			
		||||
							setTimeout(() => {
 | 
			
		||||
								showDropdown = false;
 | 
			
		||||
							}, 100);
 | 
			
		||||
							}, 150);
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<div class=" self-center mr-3">
 | 
			
		||||
							<img src="/user.png" class=" max-w-[30px] object-cover rounded-full" />
 | 
			
		||||
							<img src={$user.profile_image_url} class=" max-w-[30px] object-cover rounded-full" />
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class=" self-center font-semibold">{$user.name}</div>
 | 
			
		||||
					</button>
 | 
			
		||||
@ -406,7 +406,7 @@
 | 
			
		||||
					{#if showDropdown}
 | 
			
		||||
						<div
 | 
			
		||||
							id="dropdownDots"
 | 
			
		||||
							class="absolute z-10 bottom-[4.5rem] rounded-lg shadow w-[240px] bg-gray-900"
 | 
			
		||||
							class="absolute z-10 bottom-[70px] 4.5rem rounded-lg shadow w-[240px] bg-gray-900"
 | 
			
		||||
						>
 | 
			
		||||
							<div class="py-2 w-full">
 | 
			
		||||
								<button
 | 
			
		||||
@ -440,14 +440,14 @@
 | 
			
		||||
								</button>
 | 
			
		||||
							</div>
 | 
			
		||||
 | 
			
		||||
							<hr class=" dark:border-gray-700 m-0 p-0" />
 | 
			
		||||
							<hr class=" 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 = '/';
 | 
			
		||||
										location.href = '/auth';
 | 
			
		||||
									}}
 | 
			
		||||
								>
 | 
			
		||||
									<div class=" self-center mr-3">
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,19 @@
 | 
			
		||||
<script>
 | 
			
		||||
	import { config, user } from '$lib/stores';
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
	import { onMount, tick } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	if ($config && $config.auth && $user === undefined) {
 | 
			
		||||
		goto('/auth');
 | 
			
		||||
	}
 | 
			
		||||
	let loaded = false;
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		if ($config && $config.auth && $user === undefined) {
 | 
			
		||||
			await goto('/auth');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await tick();
 | 
			
		||||
		loaded = true;
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $config !== undefined}
 | 
			
		||||
{#if loaded}
 | 
			
		||||
	<slot />
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
	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';
 | 
			
		||||
	import { config, user } from '$lib/stores';
 | 
			
		||||
 | 
			
		||||
	let API_BASE_URL = BUILD_TIME_API_BASE_URL;
 | 
			
		||||
	let db;
 | 
			
		||||
@ -1224,14 +1224,27 @@
 | 
			
		||||
								<div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
 | 
			
		||||
									<div class=" flex w-full">
 | 
			
		||||
										<div class=" mr-4">
 | 
			
		||||
											<img
 | 
			
		||||
												src="{message.role == 'user'
 | 
			
		||||
													? settings.gravatarUrl
 | 
			
		||||
														? settings.gravatarUrl
 | 
			
		||||
														: '/user'
 | 
			
		||||
													: '/favicon'}.png"
 | 
			
		||||
												class=" max-w-[28px] object-cover rounded-full"
 | 
			
		||||
											/>
 | 
			
		||||
											{#if message.role === 'user'}
 | 
			
		||||
												{#if $config === null}
 | 
			
		||||
													<img
 | 
			
		||||
														src="{settings.gravatarUrl ? settings.gravatarUrl : '/user'}.png"
 | 
			
		||||
														class=" max-w-[28px] object-cover rounded-full"
 | 
			
		||||
														alt="User profile"
 | 
			
		||||
													/>
 | 
			
		||||
												{:else}
 | 
			
		||||
													<img
 | 
			
		||||
														src={$user.profile_image_url}
 | 
			
		||||
														class=" max-w-[28px] object-cover rounded-full"
 | 
			
		||||
														alt="User profile"
 | 
			
		||||
													/>
 | 
			
		||||
												{/if}
 | 
			
		||||
											{:else}
 | 
			
		||||
												<img
 | 
			
		||||
													src="/favicon.png"
 | 
			
		||||
													class=" max-w-[28px] object-cover rounded-full"
 | 
			
		||||
													alt="Ollama profile"
 | 
			
		||||
												/>
 | 
			
		||||
											{/if}
 | 
			
		||||
										</div>
 | 
			
		||||
 | 
			
		||||
										<div class="w-full">
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
	let loaded = false;
 | 
			
		||||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		const webBackendStatus = await fetch(`${WEBUI_API_BASE_URL}/`, {
 | 
			
		||||
		const resBackend = await fetch(`${WEBUI_API_BASE_URL}/`, {
 | 
			
		||||
			method: 'GET',
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'application/json'
 | 
			
		||||
@ -26,11 +26,11 @@
 | 
			
		||||
				return null;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
		console.log(webBackendStatus);
 | 
			
		||||
		await config.set(webBackendStatus);
 | 
			
		||||
		console.log(resBackend);
 | 
			
		||||
		await config.set(resBackend);
 | 
			
		||||
 | 
			
		||||
		if (webBackendStatus) {
 | 
			
		||||
			if (webBackendStatus.auth) {
 | 
			
		||||
		if ($config) {
 | 
			
		||||
			if ($config.auth) {
 | 
			
		||||
				if (localStorage.token) {
 | 
			
		||||
					const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
 | 
			
		||||
						method: 'GET',
 | 
			
		||||
@ -49,9 +49,14 @@
 | 
			
		||||
							return null;
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
					await user.set(res);
 | 
			
		||||
					if (res) {
 | 
			
		||||
						await user.set(res);
 | 
			
		||||
					} else {
 | 
			
		||||
						localStorage.removeItem('token');
 | 
			
		||||
						await goto('/auth');
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					goto('/auth');
 | 
			
		||||
					await goto('/auth');
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,10 @@
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
	import { WEBUI_API_BASE_URL } from '$lib/constants';
 | 
			
		||||
	import { config, user } from '$lib/stores';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import toast from 'svelte-french-toast';
 | 
			
		||||
 | 
			
		||||
	let loaded = false;
 | 
			
		||||
	let mode = 'signin';
 | 
			
		||||
 | 
			
		||||
	let name = '';
 | 
			
		||||
@ -33,7 +35,7 @@
 | 
			
		||||
 | 
			
		||||
		if (res) {
 | 
			
		||||
			console.log(res);
 | 
			
		||||
			toast.success(`You're now logged in. Redirecting you to the main page."`);
 | 
			
		||||
			toast.success(`You're now logged in. Redirecting you to the main page.`);
 | 
			
		||||
			localStorage.token = res.token;
 | 
			
		||||
			await user.set(res);
 | 
			
		||||
			goto('/');
 | 
			
		||||
@ -71,12 +73,15 @@
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	if ($config === null || !$config.auth || ($config.auth && $user !== undefined)) {
 | 
			
		||||
		goto('/');
 | 
			
		||||
	}
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		if ($config === null || !$config.auth || ($config.auth && $user !== undefined)) {
 | 
			
		||||
			await goto('/');
 | 
			
		||||
		}
 | 
			
		||||
		loaded = true;
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $config && $config.auth}
 | 
			
		||||
{#if loaded && $config && $config.auth}
 | 
			
		||||
	<div class="fixed m-10 z-50">
 | 
			
		||||
		<div class="flex space-x-2">
 | 
			
		||||
			<div class=" self-center">
 | 
			
		||||
@ -1065,6 +1070,146 @@
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class=" my-auto pb-36 w-full px-4">
 | 
			
		||||
		<div class=" text-center flex flex-col justify-center">
 | 
			
		||||
			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
 | 
			
		||||
 | 
			
		||||
			<div
 | 
			
		||||
				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
 | 
			
		||||
			>
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Log in
 | 
			
		||||
				</button>
 | 
			
		||||
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Sign up
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div> -->
 | 
			
		||||
	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
 | 
			
		||||
	<div class=" mt-6 mx-6">
 | 
			
		||||
		<div class="flex space-x-2">
 | 
			
		||||
			<div class=" self-center text-2xl font-semibold">Ollama</div>
 | 
			
		||||
			<div class=" self-center">
 | 
			
		||||
				<img src="/ollama.png" class=" w-5" />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class=" my-auto pb-36 w-full px-4">
 | 
			
		||||
		<div class=" text-center flex flex-col justify-center">
 | 
			
		||||
			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
 | 
			
		||||
 | 
			
		||||
			<div
 | 
			
		||||
				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
 | 
			
		||||
			>
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Log in
 | 
			
		||||
				</button>
 | 
			
		||||
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Sign up
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div> -->
 | 
			
		||||
	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
 | 
			
		||||
	<div class=" mt-6 mx-6">
 | 
			
		||||
		<div class="flex space-x-2">
 | 
			
		||||
			<div class=" self-center text-2xl font-semibold">Ollama</div>
 | 
			
		||||
			<div class=" self-center">
 | 
			
		||||
				<img src="/ollama.png" class=" w-5" />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class=" my-auto pb-36 w-full px-4">
 | 
			
		||||
		<div class=" text-center flex flex-col justify-center">
 | 
			
		||||
			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
 | 
			
		||||
 | 
			
		||||
			<div
 | 
			
		||||
				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
 | 
			
		||||
			>
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Log in
 | 
			
		||||
				</button>
 | 
			
		||||
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Sign up
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div> -->
 | 
			
		||||
	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
 | 
			
		||||
	<div class=" mt-6 mx-6">
 | 
			
		||||
		<div class="flex space-x-2">
 | 
			
		||||
			<div class=" self-center text-2xl font-semibold">Ollama</div>
 | 
			
		||||
			<div class=" self-center">
 | 
			
		||||
				<img src="/ollama.png" class=" w-5" />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class=" my-auto pb-36 w-full px-4">
 | 
			
		||||
		<div class=" text-center flex flex-col justify-center">
 | 
			
		||||
			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
 | 
			
		||||
 | 
			
		||||
			<div
 | 
			
		||||
				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
 | 
			
		||||
			>
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Log in
 | 
			
		||||
				</button>
 | 
			
		||||
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Sign up
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div> -->
 | 
			
		||||
	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
 | 
			
		||||
	<div class=" mt-6 mx-6">
 | 
			
		||||
		<div class="flex space-x-2">
 | 
			
		||||
			<div class=" self-center text-2xl font-semibold">Ollama</div>
 | 
			
		||||
			<div class=" self-center">
 | 
			
		||||
				<img src="/ollama.png" class=" w-5" />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class=" my-auto pb-36 w-full px-4">
 | 
			
		||||
		<div class=" text-center flex flex-col justify-center">
 | 
			
		||||
			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
 | 
			
		||||
 | 
			
		||||
			<div
 | 
			
		||||
				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
 | 
			
		||||
			>
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Log in
 | 
			
		||||
				</button>
 | 
			
		||||
 | 
			
		||||
				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
 | 
			
		||||
					Sign up
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div> -->
 | 
			
		||||
	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
 | 
			
		||||
	<div class=" mt-6 mx-6">
 | 
			
		||||
		<div class="flex space-x-2">
 | 
			
		||||
			<div class=" self-center text-2xl font-semibold">Ollama</div>
 | 
			
		||||
			<div class=" self-center">
 | 
			
		||||
				<img src="/ollama.png" class=" w-5" />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class=" my-auto pb-36 w-full px-4">
 | 
			
		||||
		<div class=" text-center flex flex-col justify-center">
 | 
			
		||||
			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user