forked from open-webui/open-webui
feat: basic RBAC support
This commit is contained in:
parent
921eef03b3
commit
8547b7807d
13 changed files with 266 additions and 44 deletions
|
@ -8,7 +8,7 @@ import json
|
||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
from constants import ERROR_MESSAGES
|
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
|
from config import OLLAMA_API_BASE_URL, OLLAMA_WEBUI_AUTH
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
@ -25,24 +25,37 @@ TARGET_SERVER_URL = OLLAMA_API_BASE_URL
|
||||||
def proxy(path):
|
def proxy(path):
|
||||||
# Combine the base URL of the target server with the requested path
|
# Combine the base URL of the target server with the requested path
|
||||||
target_url = f"{TARGET_SERVER_URL}/{path}"
|
target_url = f"{TARGET_SERVER_URL}/{path}"
|
||||||
print(target_url)
|
print(path)
|
||||||
|
|
||||||
# Get data from the original request
|
# Get data from the original request
|
||||||
data = request.get_data()
|
data = request.get_data()
|
||||||
headers = dict(request.headers)
|
headers = dict(request.headers)
|
||||||
|
|
||||||
|
# Basic RBAC support
|
||||||
if OLLAMA_WEBUI_AUTH:
|
if OLLAMA_WEBUI_AUTH:
|
||||||
if "Authorization" in headers:
|
if "Authorization" in headers:
|
||||||
token = extract_token_from_auth_header(headers["Authorization"])
|
token = extract_token_from_auth_header(headers["Authorization"])
|
||||||
user = Users.get_user_by_token(token)
|
user = Users.get_user_by_token(token)
|
||||||
if user:
|
if user:
|
||||||
print(user)
|
# 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
|
pass
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}),
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), 401
|
||||||
else:
|
else:
|
||||||
return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
|
return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
|
||||||
else:
|
else:
|
||||||
return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
|
return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import uuid
|
||||||
|
|
||||||
|
|
||||||
from apps.web.models.users import UserModel, Users
|
from apps.web.models.users import UserModel, Users
|
||||||
from utils import (
|
from utils.utils import (
|
||||||
verify_password,
|
verify_password,
|
||||||
get_password_hash,
|
get_password_hash,
|
||||||
bearer_scheme,
|
bearer_scheme,
|
||||||
|
@ -43,6 +43,7 @@ class UserResponse(BaseModel):
|
||||||
email: str
|
email: str
|
||||||
name: str
|
name: str
|
||||||
role: str
|
role: str
|
||||||
|
profile_image_url: str
|
||||||
|
|
||||||
|
|
||||||
class SigninResponse(Token, UserResponse):
|
class SigninResponse(Token, UserResponse):
|
||||||
|
@ -66,7 +67,7 @@ class AuthsTable:
|
||||||
self.table = db.auths
|
self.table = db.auths
|
||||||
|
|
||||||
def insert_new_auth(
|
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]:
|
) -> Optional[UserModel]:
|
||||||
print("insert_new_auth")
|
print("insert_new_auth")
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ from typing import List, Union, Optional
|
||||||
from pymongo import ReturnDocument
|
from pymongo import ReturnDocument
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from utils import decode_token
|
from utils.utils import decode_token
|
||||||
|
from utils.misc import get_gravatar_url
|
||||||
|
|
||||||
from config import DB
|
from config import DB
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -15,7 +17,8 @@ class UserModel(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
email: str
|
email: str
|
||||||
role: str = "user"
|
role: str = "pending"
|
||||||
|
profile_image_url: str = "/user.png"
|
||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +33,7 @@ class UsersTable:
|
||||||
self.table = db.users
|
self.table = db.users
|
||||||
|
|
||||||
def insert_new_user(
|
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]:
|
) -> Optional[UserModel]:
|
||||||
user = UserModel(
|
user = UserModel(
|
||||||
**{
|
**{
|
||||||
|
@ -38,6 +41,7 @@ class UsersTable:
|
||||||
"name": name,
|
"name": name,
|
||||||
"email": email,
|
"email": email,
|
||||||
"role": role,
|
"role": role,
|
||||||
|
"profile_image_url": get_gravatar_url(email),
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,12 +9,14 @@ import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from utils import (
|
from utils.utils import (
|
||||||
get_password_hash,
|
get_password_hash,
|
||||||
bearer_scheme,
|
bearer_scheme,
|
||||||
create_token,
|
create_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from utils.misc import get_gravatar_url
|
||||||
|
|
||||||
from apps.web.models.auths import (
|
from apps.web.models.auths import (
|
||||||
SigninForm,
|
SigninForm,
|
||||||
SignupForm,
|
SignupForm,
|
||||||
|
@ -45,10 +47,12 @@ async def get_session_user(cred=Depends(bearer_scheme)):
|
||||||
"email": user.email,
|
"email": user.email,
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
|
"profile_image_url": user.profile_image_url,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,9 +74,10 @@ async def signin(form_data: SigninForm):
|
||||||
"email": user.email,
|
"email": user.email,
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
|
"profile_image_url": user.profile_image_url,
|
||||||
}
|
}
|
||||||
else:
|
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,
|
"email": user.email,
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
|
"profile_image_url": user.profile_image_url,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||||
|
|
|
@ -7,7 +7,12 @@ class MESSAGES(str, Enum):
|
||||||
|
|
||||||
class ERROR_MESSAGES(str, Enum):
|
class ERROR_MESSAGES(str, Enum):
|
||||||
DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
|
DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
|
||||||
|
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"
|
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 :/"
|
NOT_FOUND = "We could not find what you're looking for :/"
|
||||||
USER_NOT_FOUND = "We could not find what you're looking for :/"
|
USER_NOT_FOUND = "We could not find what you're looking for :/"
|
||||||
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
|
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
|
||||||
|
|
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) {
|
if (data.error) {
|
||||||
throw data.error;
|
throw data.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.detail) {
|
||||||
|
throw data.detail;
|
||||||
|
}
|
||||||
if (data.status) {
|
if (data.status) {
|
||||||
if (!data.status.includes('downloading')) {
|
if (!data.status.includes('downloading')) {
|
||||||
toast.success(data.status);
|
toast.success(data.status);
|
||||||
|
@ -206,6 +210,10 @@
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw data.error;
|
throw data.error;
|
||||||
}
|
}
|
||||||
|
if (data.detail) {
|
||||||
|
throw data.detail;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.status) {
|
if (data.status) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -388,17 +388,17 @@
|
||||||
{#if $user !== undefined}
|
{#if $user !== undefined}
|
||||||
<button
|
<button
|
||||||
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
|
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
|
||||||
on:focus={() => {
|
on:click={() => {
|
||||||
showDropdown = true;
|
showDropdown = !showDropdown;
|
||||||
}}
|
}}
|
||||||
on:focusout={() => {
|
on:focusout={() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showDropdown = false;
|
showDropdown = false;
|
||||||
}, 100);
|
}, 150);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-3">
|
<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>
|
||||||
<div class=" self-center font-semibold">{$user.name}</div>
|
<div class=" self-center font-semibold">{$user.name}</div>
|
||||||
</button>
|
</button>
|
||||||
|
@ -406,7 +406,7 @@
|
||||||
{#if showDropdown}
|
{#if showDropdown}
|
||||||
<div
|
<div
|
||||||
id="dropdownDots"
|
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">
|
<div class="py-2 w-full">
|
||||||
<button
|
<button
|
||||||
|
@ -440,14 +440,14 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div class="py-2 w-full">
|
||||||
<button
|
<button
|
||||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
|
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
location.href = '/';
|
location.href = '/auth';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-3">
|
<div class=" self-center mr-3">
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { config, user } from '$lib/stores';
|
import { config, user } from '$lib/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
onMount(async () => {
|
||||||
if ($config && $config.auth && $user === undefined) {
|
if ($config && $config.auth && $user === undefined) {
|
||||||
goto('/auth');
|
await goto('/auth');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
loaded = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $config !== undefined}
|
{#if loaded}
|
||||||
<slot />
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
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';
|
import { config, 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;
|
||||||
|
@ -1224,14 +1224,27 @@
|
||||||
<div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
|
<div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
|
||||||
<div class=" flex w-full">
|
<div class=" flex w-full">
|
||||||
<div class=" mr-4">
|
<div class=" mr-4">
|
||||||
|
{#if message.role === 'user'}
|
||||||
|
{#if $config === null}
|
||||||
<img
|
<img
|
||||||
src="{message.role == 'user'
|
src="{settings.gravatarUrl ? settings.gravatarUrl : '/user'}.png"
|
||||||
? settings.gravatarUrl
|
|
||||||
? settings.gravatarUrl
|
|
||||||
: '/user'
|
|
||||||
: '/favicon'}.png"
|
|
||||||
class=" max-w-[28px] object-cover rounded-full"
|
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>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const webBackendStatus = await fetch(`${WEBUI_API_BASE_URL}/`, {
|
const resBackend = await fetch(`${WEBUI_API_BASE_URL}/`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
@ -26,11 +26,11 @@
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(webBackendStatus);
|
console.log(resBackend);
|
||||||
await config.set(webBackendStatus);
|
await config.set(resBackend);
|
||||||
|
|
||||||
if (webBackendStatus) {
|
if ($config) {
|
||||||
if (webBackendStatus.auth) {
|
if ($config.auth) {
|
||||||
if (localStorage.token) {
|
if (localStorage.token) {
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -49,9 +49,14 @@
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
await user.set(res);
|
await user.set(res);
|
||||||
} else {
|
} else {
|
||||||
goto('/auth');
|
localStorage.removeItem('token');
|
||||||
|
await goto('/auth');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await goto('/auth');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
import { config, user } from '$lib/stores';
|
import { config, user } from '$lib/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import toast from 'svelte-french-toast';
|
import toast from 'svelte-french-toast';
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
let mode = 'signin';
|
let mode = 'signin';
|
||||||
|
|
||||||
let name = '';
|
let name = '';
|
||||||
|
@ -33,7 +35,7 @@
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
console.log(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;
|
localStorage.token = res.token;
|
||||||
await user.set(res);
|
await user.set(res);
|
||||||
goto('/');
|
goto('/');
|
||||||
|
@ -71,12 +73,15 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
if ($config === null || !$config.auth || ($config.auth && $user !== undefined)) {
|
if ($config === null || !$config.auth || ($config.auth && $user !== undefined)) {
|
||||||
goto('/');
|
await goto('/');
|
||||||
}
|
}
|
||||||
|
loaded = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $config && $config.auth}
|
{#if loaded && $config && $config.auth}
|
||||||
<div class="fixed m-10 z-50">
|
<div class="fixed m-10 z-50">
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<div class=" self-center">
|
<div class=" self-center">
|
||||||
|
@ -1065,6 +1070,146 @@
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
<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=" my-auto pb-36 w-full px-4">
|
||||||
<div class=" text-center flex flex-col justify-center">
|
<div class=" text-center flex flex-col justify-center">
|
||||||
<div class=" text-xl md:text-2xl font-bold">Get Started</div>
|
<div class=" text-xl md:text-2xl font-bold">Get Started</div>
|
||||||
|
|
Loading…
Reference in a new issue