Merge pull request #2574 from cheahjs/feat/oauth

feat: experimental SSO support for Google, Microsoft, and OIDC
This commit is contained in:
Timothy Jaeryang Baek 2024-06-24 19:05:58 -07:00 committed by GitHub
commit d17dc59246
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 633 additions and 13 deletions

View File

@ -0,0 +1,49 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"user",
oauth_sub=pw.TextField(null=True, unique=True),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("user", "oauth_sub")

View File

@ -2,6 +2,8 @@ from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from apps.webui.routers import (
auths,
users,

View File

@ -105,6 +105,7 @@ class AuthsTable:
name: str,
profile_image_url: str = "/user.png",
role: str = "pending",
oauth_sub: Optional[str] = None,
) -> Optional[UserModel]:
log.info("insert_new_auth")
@ -115,7 +116,9 @@ class AuthsTable:
)
result = Auth.create(**auth.model_dump())
user = Users.insert_new_user(id, name, email, profile_image_url, role)
user = Users.insert_new_user(
id, name, email, profile_image_url, role, oauth_sub
)
if result and user:
return user

View File

@ -28,6 +28,8 @@ class User(Model):
settings = JSONField(null=True)
info = JSONField(null=True)
oauth_sub = TextField(null=True, unique=True)
class Meta:
database = DB
@ -53,6 +55,8 @@ class UserModel(BaseModel):
settings: Optional[UserSettings] = None
info: Optional[dict] = None
oauth_sub: Optional[str] = None
####################
# Forms
@ -83,6 +87,7 @@ class UsersTable:
email: str,
profile_image_url: str = "/user.png",
role: str = "pending",
oauth_sub: Optional[str] = None,
) -> Optional[UserModel]:
user = UserModel(
**{
@ -94,6 +99,7 @@ class UsersTable:
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
"oauth_sub": oauth_sub,
}
)
result = User.create(**user.model_dump())
@ -123,6 +129,13 @@ class UsersTable:
except:
return None
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
try:
user = User.get(User.oauth_sub == sub)
return UserModel(**model_to_dict(user))
except:
return None
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
return [
UserModel(**model_to_dict(user))
@ -174,6 +187,18 @@ class UsersTable:
except:
return None
def update_user_oauth_sub_by_id(
self, id: str, oauth_sub: str
) -> Optional[UserModel]:
try:
query = User.update(oauth_sub=oauth_sub).where(User.id == id)
query.execute()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
try:
query = User.update(**updated).where(User.id == id)

View File

@ -10,7 +10,6 @@ import re
import uuid
import csv
from apps.webui.models.auths import (
SigninForm,
SignupForm,

View File

@ -305,6 +305,135 @@ JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
)
####################################
# OAuth config
####################################
ENABLE_OAUTH_SIGNUP = PersistentConfig(
"ENABLE_OAUTH_SIGNUP",
"oauth.enable_signup",
os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true",
)
OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig(
"OAUTH_MERGE_ACCOUNTS_BY_EMAIL",
"oauth.merge_accounts_by_email",
os.environ.get("OAUTH_MERGE_ACCOUNTS_BY_EMAIL", "False").lower() == "true",
)
OAUTH_PROVIDERS = {}
GOOGLE_CLIENT_ID = PersistentConfig(
"GOOGLE_CLIENT_ID",
"oauth.google.client_id",
os.environ.get("GOOGLE_CLIENT_ID", ""),
)
GOOGLE_CLIENT_SECRET = PersistentConfig(
"GOOGLE_CLIENT_SECRET",
"oauth.google.client_secret",
os.environ.get("GOOGLE_CLIENT_SECRET", ""),
)
GOOGLE_OAUTH_SCOPE = PersistentConfig(
"GOOGLE_OAUTH_SCOPE",
"oauth.google.scope",
os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
)
MICROSOFT_CLIENT_ID = PersistentConfig(
"MICROSOFT_CLIENT_ID",
"oauth.microsoft.client_id",
os.environ.get("MICROSOFT_CLIENT_ID", ""),
)
MICROSOFT_CLIENT_SECRET = PersistentConfig(
"MICROSOFT_CLIENT_SECRET",
"oauth.microsoft.client_secret",
os.environ.get("MICROSOFT_CLIENT_SECRET", ""),
)
MICROSOFT_CLIENT_TENANT_ID = PersistentConfig(
"MICROSOFT_CLIENT_TENANT_ID",
"oauth.microsoft.tenant_id",
os.environ.get("MICROSOFT_CLIENT_TENANT_ID", ""),
)
MICROSOFT_OAUTH_SCOPE = PersistentConfig(
"MICROSOFT_OAUTH_SCOPE",
"oauth.microsoft.scope",
os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
)
OAUTH_CLIENT_ID = PersistentConfig(
"OAUTH_CLIENT_ID",
"oauth.oidc.client_id",
os.environ.get("OAUTH_CLIENT_ID", ""),
)
OAUTH_CLIENT_SECRET = PersistentConfig(
"OAUTH_CLIENT_SECRET",
"oauth.oidc.client_secret",
os.environ.get("OAUTH_CLIENT_SECRET", ""),
)
OPENID_PROVIDER_URL = PersistentConfig(
"OPENID_PROVIDER_URL",
"oauth.oidc.provider_url",
os.environ.get("OPENID_PROVIDER_URL", ""),
)
OAUTH_SCOPES = PersistentConfig(
"OAUTH_SCOPES",
"oauth.oidc.scopes",
os.environ.get("OAUTH_SCOPES", "openid email profile"),
)
OAUTH_PROVIDER_NAME = PersistentConfig(
"OAUTH_PROVIDER_NAME",
"oauth.oidc.provider_name",
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
)
def load_oauth_providers():
OAUTH_PROVIDERS.clear()
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
OAUTH_PROVIDERS["google"] = {
"client_id": GOOGLE_CLIENT_ID.value,
"client_secret": GOOGLE_CLIENT_SECRET.value,
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
"scope": GOOGLE_OAUTH_SCOPE.value,
}
if (
MICROSOFT_CLIENT_ID.value
and MICROSOFT_CLIENT_SECRET.value
and MICROSOFT_CLIENT_TENANT_ID.value
):
OAUTH_PROVIDERS["microsoft"] = {
"client_id": MICROSOFT_CLIENT_ID.value,
"client_secret": MICROSOFT_CLIENT_SECRET.value,
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
"scope": MICROSOFT_OAUTH_SCOPE.value,
}
if (
OAUTH_CLIENT_ID.value
and OAUTH_CLIENT_SECRET.value
and OPENID_PROVIDER_URL.value
):
OAUTH_PROVIDERS["oidc"] = {
"client_id": OAUTH_CLIENT_ID.value,
"client_secret": OAUTH_CLIENT_SECRET.value,
"server_metadata_url": OPENID_PROVIDER_URL.value,
"scope": OAUTH_SCOPES.value,
"name": OAUTH_PROVIDER_NAME.value,
}
load_oauth_providers()
####################################
# Static DIR
####################################
@ -733,6 +862,16 @@ WEBUI_SECRET_KEY = os.environ.get(
), # DEPRECATED: remove at next major version
)
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
"WEBUI_SESSION_COOKIE_SAME_SITE",
os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
)
WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
"WEBUI_SESSION_COOKIE_SECURE",
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
)
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)

View File

@ -1,4 +1,9 @@
import base64
import uuid
from contextlib import asynccontextmanager
from authlib.integrations.starlette_client import OAuth
from authlib.oidc.core import UserInfo
from bs4 import BeautifulSoup
import json
import markdown
@ -24,7 +29,8 @@ from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import StreamingResponse, Response
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import StreamingResponse, Response, RedirectResponse
from apps.socket.main import app as socket_app
@ -53,9 +59,11 @@ from apps.webui.main import (
from pydantic import BaseModel
from typing import List, Optional, Iterator, Generator, Union
from apps.webui.models.auths import Auths
from apps.webui.models.models import Models, ModelModel
from apps.webui.models.tools import Tools
from apps.webui.models.functions import Functions
from apps.webui.models.users import Users
from apps.webui.utils import load_toolkit_module_by_id, load_function_module_by_id
@ -64,6 +72,8 @@ from utils.utils import (
get_verified_user,
get_current_user,
get_http_authorization_cred,
get_password_hash,
create_token,
)
from utils.task import (
title_generation_template,
@ -74,6 +84,7 @@ from utils.misc import (
get_last_user_message,
add_or_update_system_message,
stream_message_template,
parse_duration,
)
from apps.rag.utils import get_rag_context, rag_template
@ -106,9 +117,16 @@ from config import (
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
SAFE_MODE,
OAUTH_PROVIDERS,
ENABLE_OAUTH_SIGNUP,
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
WEBUI_SECRET_KEY,
WEBUI_SESSION_COOKIE_SAME_SITE,
WEBUI_SESSION_COOKIE_SECURE,
AppConfig,
)
from constants import ERROR_MESSAGES
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from utils.webhook import post_webhook
if SAFE_MODE:
print("SAFE MODE ENABLED")
@ -1725,6 +1743,12 @@ async def get_app_config():
"engine": audio_app.state.config.STT_ENGINE,
},
},
"oauth": {
"providers": {
name: config.get("name", name)
for name, config in OAUTH_PROVIDERS.items()
}
},
}
@ -1806,6 +1830,154 @@ async def get_app_latest_release_version():
)
############################
# OAuth Login & Callback
############################
oauth = OAuth()
for provider_name, provider_config in OAUTH_PROVIDERS.items():
oauth.register(
name=provider_name,
client_id=provider_config["client_id"],
client_secret=provider_config["client_secret"],
server_metadata_url=provider_config["server_metadata_url"],
client_kwargs={
"scope": provider_config["scope"],
},
)
# SessionMiddleware is used by authlib for oauth
if len(OAUTH_PROVIDERS) > 0:
app.add_middleware(
SessionMiddleware,
secret_key=WEBUI_SECRET_KEY,
session_cookie="oui-session",
same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
https_only=WEBUI_SESSION_COOKIE_SECURE,
)
@app.get("/oauth/{provider}/login")
async def oauth_login(provider: str, request: Request):
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
redirect_uri = request.url_for("oauth_callback", provider=provider)
return await oauth.create_client(provider).authorize_redirect(request, redirect_uri)
# OAuth login logic is as follows:
# 1. Attempt to find a user with matching subject ID, tied to the provider
# 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth
# - This is considered insecure in general, as OAuth providers do not always verify email addresses
# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user
# - Email addresses are considered unique, so we fail registration if the email address is alreayd taken
@app.get("/oauth/{provider}/callback")
async def oauth_callback(provider: str, request: Request, response: Response):
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
client = oauth.create_client(provider)
try:
token = await client.authorize_access_token(request)
except Exception as e:
log.warning(f"OAuth callback error: {e}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
user_data: UserInfo = token["userinfo"]
sub = user_data.get("sub")
if not sub:
log.warning(f"OAuth callback failed, sub is missing: {user_data}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
provider_sub = f"{provider}@{sub}"
email = user_data.get("email", "").lower()
# We currently mandate that email addresses are provided
if not email:
log.warning(f"OAuth callback failed, email is missing: {user_data}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
# Check if the user exists
user = Users.get_user_by_oauth_sub(provider_sub)
if not user:
# If the user does not exist, check if merging is enabled
if OAUTH_MERGE_ACCOUNTS_BY_EMAIL.value:
# Check if the user exists by email
user = Users.get_user_by_email(email)
if user:
# Update the user with the new oauth sub
Users.update_user_oauth_sub_by_id(user.id, provider_sub)
if not user:
# If the user does not exist, check if signups are enabled
if ENABLE_OAUTH_SIGNUP.value:
# Check if an existing user with the same email already exists
existing_user = Users.get_user_by_email(user_data.get("email", "").lower())
if existing_user:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
picture_url = user_data.get("picture", "")
if picture_url:
# Download the profile image into a base64 string
try:
async with aiohttp.ClientSession() as session:
async with session.get(picture_url) as resp:
picture = await resp.read()
base64_encoded_picture = base64.b64encode(picture).decode(
"utf-8"
)
guessed_mime_type = mimetypes.guess_type(picture_url)[0]
if guessed_mime_type is None:
# assume JPG, browsers are tolerant enough of image formats
guessed_mime_type = "image/jpeg"
picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
except Exception as e:
log.error(f"Error downloading profile image '{picture_url}': {e}")
picture_url = ""
if not picture_url:
picture_url = "/user.png"
user = Auths.insert_new_auth(
email=email,
password=get_password_hash(
str(uuid.uuid4())
), # Random password, not used
name=user_data.get("name", "User"),
profile_image_url=picture_url,
role=webui_app.state.config.DEFAULT_USER_ROLE,
oauth_sub=provider_sub,
)
if webui_app.state.config.WEBHOOK_URL:
post_webhook(
webui_app.state.config.WEBHOOK_URL,
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
"action": "signup",
"message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
"user": user.model_dump_json(exclude_none=True),
},
)
else:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
jwt_token = create_token(
data={"id": user.id},
expires_delta=parse_duration(webui_app.state.config.JWT_EXPIRES_IN),
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
# Redirect back to the frontend with the JWT token
redirect_url = f"{request.base_url}auth#token={jwt_token}"
return RedirectResponse(url=redirect_url)
@app.get("/manifest.json")
async def get_manifest_json():
return {

View File

@ -58,6 +58,7 @@ rank-bm25==0.2.2
faster-whisper==1.0.2
PyJWT[crypto]==2.8.0
authlib==1.3.0
black==24.4.2
langfuse==2.33.0

View File

@ -59,6 +59,7 @@ dependencies = [
"faster-whisper==1.0.2",
"PyJWT[crypto]==2.8.0",
"authlib==1.3.0",
"black==24.4.2",
"langfuse==2.33.0",

View File

@ -31,6 +31,8 @@ asgiref==3.8.1
# via opentelemetry-instrumentation-asgi
attrs==23.2.0
# via aiohttp
authlib==1.3.0
# via open-webui
av==11.0.0
# via faster-whisper
backoff==2.2.1
@ -93,6 +95,7 @@ coloredlogs==15.0.1
compressed-rtf==1.0.6
# via extract-msg
cryptography==42.0.7
# via authlib
# via msoffcrypto-tool
# via pyjwt
ctranslate2==4.2.1
@ -395,6 +398,7 @@ pandas==2.2.2
# via open-webui
passlib==1.7.4
# via open-webui
# via passlib
pathspec==0.12.1
# via black
pcodedmp==1.2.6
@ -453,6 +457,7 @@ pygments==2.18.0
# via rich
pyjwt==2.8.0
# via open-webui
# via pyjwt
pymysql==1.1.0
# via open-webui
pypandoc==1.13
@ -554,9 +559,6 @@ scipy==1.13.0
# via sentence-transformers
sentence-transformers==2.7.0
# via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.4
# via rapidocr-onnxruntime
shellingham==1.5.4
@ -651,6 +653,7 @@ uvicorn==0.22.0
# via chromadb
# via fastapi
# via open-webui
# via uvicorn
uvloop==0.19.0
# via uvicorn
validators==0.28.1
@ -678,3 +681,6 @@ youtube-transcript-api==0.6.2
# via open-webui
zipp==3.18.1
# via importlib-metadata
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation

View File

@ -31,6 +31,8 @@ asgiref==3.8.1
# via opentelemetry-instrumentation-asgi
attrs==23.2.0
# via aiohttp
authlib==1.3.0
# via open-webui
av==11.0.0
# via faster-whisper
backoff==2.2.1
@ -93,6 +95,7 @@ coloredlogs==15.0.1
compressed-rtf==1.0.6
# via extract-msg
cryptography==42.0.7
# via authlib
# via msoffcrypto-tool
# via pyjwt
ctranslate2==4.2.1
@ -395,6 +398,7 @@ pandas==2.2.2
# via open-webui
passlib==1.7.4
# via open-webui
# via passlib
pathspec==0.12.1
# via black
pcodedmp==1.2.6
@ -453,6 +457,7 @@ pygments==2.18.0
# via rich
pyjwt==2.8.0
# via open-webui
# via pyjwt
pymysql==1.1.0
# via open-webui
pypandoc==1.13
@ -554,9 +559,6 @@ scipy==1.13.0
# via sentence-transformers
sentence-transformers==2.7.0
# via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.4
# via rapidocr-onnxruntime
shellingham==1.5.4
@ -651,6 +653,7 @@ uvicorn==0.22.0
# via chromadb
# via fastapi
# via open-webui
# via uvicorn
uvloop==0.19.0
# via uvicorn
validators==0.28.1
@ -678,3 +681,6 @@ youtube-transcript-api==0.6.2
# via open-webui
zipp==3.18.1
# via importlib-metadata
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation

View File

@ -126,6 +126,7 @@
"Content": "الاتصال",
"Context Length": "طول السياق",
"Continue Response": "متابعة الرد",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
"Copy": "نسخ",
"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
@ -378,6 +379,7 @@
"Notifications": "إشعارات",
"November": "نوفمبر",
"num_thread (Ollama)": "num_thread (أولاما)",
"OAuth ID": "",
"October": "اكتوبر",
"Off": "أغلاق",
"Okay, Let's Go!": "حسنا دعنا نذهب!",

View File

@ -126,6 +126,7 @@
"Content": "Съдържание",
"Context Length": "Дължина на Контекста",
"Continue Response": "Продължи отговора",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Копирана е връзката за чат!",
"Copy": "Копирай",
"Copy last code block": "Копиране на последен код блок",
@ -378,6 +379,7 @@
"Notifications": "Десктоп Известия",
"November": "Ноември",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Октомври",
"Off": "Изкл.",
"Okay, Let's Go!": "ОК, Нека започваме!",

View File

@ -126,6 +126,7 @@
"Content": "বিষয়বস্তু",
"Context Length": "কনটেক্সটের দৈর্ঘ্য",
"Continue Response": "যাচাই করুন",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!",
"Copy": "অনুলিপি",
"Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন",
@ -378,6 +379,7 @@
"Notifications": "নোটিফিকেশনসমূহ",
"November": "নভেম্বর",
"num_thread (Ollama)": "num_thread (ওলামা)",
"OAuth ID": "",
"October": "অক্টোবর",
"Off": "বন্ধ",
"Okay, Let's Go!": "ঠিক আছে, চলুন যাই!",

View File

@ -126,6 +126,7 @@
"Content": "Contingut",
"Context Length": "Mida del context",
"Continue Response": "Continuar la resposta",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "S'ha copiat l'URL compartida al porta-retalls!",
"Copy": "Copiar",
"Copy last code block": "Copiar l'últim bloc de codi",
@ -378,6 +379,7 @@
"Notifications": "Notificacions",
"November": "Novembre",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Octubre",
"Off": "Desactivat",
"Okay, Let's Go!": "D'acord, som-hi!",

View File

@ -126,6 +126,7 @@
"Content": "Kontento",
"Context Length": "Ang gitas-on sa konteksto",
"Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "",
"Copy": "",
"Copy last code block": "Kopyaha ang katapusang bloke sa code",
@ -378,6 +379,7 @@
"Notifications": "Mga pahibalo sa desktop",
"November": "",
"num_thread (Ollama)": "",
"OAuth ID": "",
"October": "",
"Off": "Napuo",
"Okay, Let's Go!": "Okay, lakaw na!",

View File

@ -126,6 +126,7 @@
"Content": "Info",
"Context Length": "Context Length",
"Continue Response": "Antwort fortsetzen",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Geteilte Chat-URL in die Zwischenablage kopiert!",
"Copy": "Kopieren",
"Copy last code block": "Letzten Codeblock kopieren",
@ -378,6 +379,7 @@
"Notifications": "Desktop-Benachrichtigungen",
"November": "November",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Oktober",
"Off": "Aus",
"Okay, Let's Go!": "Okay, los geht's!",

View File

@ -126,6 +126,7 @@
"Content": "Content",
"Context Length": "Context Length",
"Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "",
"Copy": "",
"Copy last code block": "Copy last code block",
@ -378,6 +379,7 @@
"Notifications": "Notifications",
"November": "",
"num_thread (Ollama)": "",
"OAuth ID": "",
"October": "",
"Off": "Off",
"Okay, Let's Go!": "Okay, Let's Go!",

View File

@ -126,6 +126,7 @@
"Content": "",
"Context Length": "",
"Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "",
"Copy": "",
"Copy last code block": "",
@ -378,6 +379,7 @@
"Notifications": "",
"November": "",
"num_thread (Ollama)": "",
"OAuth ID": "",
"October": "",
"Off": "",
"Okay, Let's Go!": "",

View File

@ -126,6 +126,7 @@
"Content": "",
"Context Length": "",
"Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "",
"Copy": "",
"Copy last code block": "",
@ -378,6 +379,7 @@
"Notifications": "",
"November": "",
"num_thread (Ollama)": "",
"OAuth ID": "",
"October": "",
"Off": "",
"Okay, Let's Go!": "",

View File

@ -126,6 +126,7 @@
"Content": "Contenido",
"Context Length": "Longitud del contexto",
"Continue Response": "Continuar Respuesta",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "¡URL de chat compartido copiado al portapapeles!",
"Copy": "Copiar",
"Copy last code block": "Copia el último bloque de código",
@ -378,6 +379,7 @@
"Notifications": "Notificaciones",
"November": "Noviembre",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Octubre",
"Off": "Desactivado",
"Okay, Let's Go!": "Bien, ¡Vamos!",

View File

@ -126,6 +126,7 @@
"Content": "محتوا",
"Context Length": "طول زمینه",
"Continue Response": "ادامه پاسخ",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "URL چت به کلیپ بورد کپی شد!",
"Copy": "کپی",
"Copy last code block": "کپی آخرین بلوک کد",
@ -378,6 +379,7 @@
"Notifications": "اعلان",
"November": "نوامبر",
"num_thread (Ollama)": "num_thread (اولاما)",
"OAuth ID": "",
"October": "اکتبر",
"Off": "خاموش",
"Okay, Let's Go!": "باشه، بزن بریم!",

View File

@ -126,6 +126,7 @@
"Content": "Sisältö",
"Context Length": "Kontekstin pituus",
"Continue Response": "Jatka vastausta",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Jaettu keskustelulinkki kopioitu leikepöydälle!",
"Copy": "Kopioi",
"Copy last code block": "Kopioi viimeisin koodilohko",
@ -378,6 +379,7 @@
"Notifications": "Ilmoitukset",
"November": "marraskuu",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "lokakuu",
"Off": "Pois",
"Okay, Let's Go!": "Eikun menoksi!",

View File

@ -126,6 +126,7 @@
"Content": "Contenu",
"Context Length": "Longueur du contexte",
"Continue Response": "Continuer la réponse",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "URL de chat partagé copié dans le presse-papier !",
"Copy": "Copier",
"Copy last code block": "Copier le dernier bloc de code",
@ -378,6 +379,7 @@
"Notifications": "Notifications de bureau",
"November": "Novembre",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Octobre",
"Off": "Éteint",
"Okay, Let's Go!": "Okay, Allons-y !",

View File

@ -126,6 +126,7 @@
"Content": "Contenu",
"Context Length": "Longueur du contexte",
"Continue Response": "Continuer la Réponse",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "URL du chat copié dans le presse-papiers !",
"Copy": "Copier",
"Copy last code block": "Copier le dernier bloc de code",
@ -378,6 +379,7 @@
"Notifications": "Notifications de bureau",
"November": "Novembre",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Octobre",
"Off": "Désactivé",
"Okay, Let's Go!": "D'accord, allons-y !",

View File

@ -126,6 +126,7 @@
"Content": "תוכן",
"Context Length": "אורך הקשר",
"Continue Response": "המשך תגובה",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "העתקת כתובת URL של צ'אט משותף ללוח!",
"Copy": "העתק",
"Copy last code block": "העתק את בלוק הקוד האחרון",
@ -378,6 +379,7 @@
"Notifications": "התראות",
"November": "נובמבר",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "אוקטובר",
"Off": "כבוי",
"Okay, Let's Go!": "בסדר, בואו נתחיל!",

View File

@ -126,6 +126,7 @@
"Content": "सामग्री",
"Context Length": "प्रसंग की लंबाई",
"Continue Response": "प्रतिक्रिया जारी रखें",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "साझा चैट URL को क्लिपबोर्ड पर कॉपी किया गया!",
"Copy": "कॉपी",
"Copy last code block": "अंतिम कोड ब्लॉक कॉपी करें",
@ -378,6 +379,7 @@
"Notifications": "सूचनाएं",
"November": "नवंबर",
"num_thread (Ollama)": "num_thread (ओलामा)",
"OAuth ID": "",
"October": "अक्टूबर",
"Off": "बंद",
"Okay, Let's Go!": "ठीक है, चलिए चलते हैं!",

View File

@ -126,6 +126,7 @@
"Content": "Sadržaj",
"Context Length": "Dužina konteksta",
"Continue Response": "Nastavi odgovor",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "URL dijeljenog razgovora kopiran u međuspremnik!",
"Copy": "Kopiraj",
"Copy last code block": "Kopiraj zadnji blok koda",
@ -378,6 +379,7 @@
"Notifications": "Obavijesti",
"November": "Studeni",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Listopad",
"Off": "Isključeno",
"Okay, Let's Go!": "U redu, idemo!",

View File

@ -126,6 +126,7 @@
"Content": "Contenuto",
"Context Length": "Lunghezza contesto",
"Continue Response": "Continua risposta",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "URL della chat condivisa copiato negli appunti!",
"Copy": "Copia",
"Copy last code block": "Copia ultimo blocco di codice",
@ -378,6 +379,7 @@
"Notifications": "Notifiche desktop",
"November": "Novembre",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Ottobre",
"Off": "Disattivato",
"Okay, Let's Go!": "Ok, andiamo!",

View File

@ -126,6 +126,7 @@
"Content": "コンテンツ",
"Context Length": "コンテキストの長さ",
"Continue Response": "続きの応答",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "共有チャットURLをクリップボードにコピーしました",
"Copy": "コピー",
"Copy last code block": "最後のコードブロックをコピー",
@ -378,6 +379,7 @@
"Notifications": "デスクトップ通知",
"November": "11月",
"num_thread (Ollama)": "num_thread(オラマ)",
"OAuth ID": "",
"October": "10月",
"Off": "オフ",
"Okay, Let's Go!": "OK、始めましょう",

View File

@ -126,6 +126,7 @@
"Content": "კონტენტი",
"Context Length": "კონტექსტის სიგრძე",
"Continue Response": "პასუხის გაგრძელება",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "ყავს ჩათის URL-ი კლიპბორდში!",
"Copy": "კოპირება",
"Copy last code block": "ბოლო ბლოკის კოპირება",
@ -378,6 +379,7 @@
"Notifications": "შეტყობინება",
"November": "ნოემბერი",
"num_thread (Ollama)": "num_thread (ოლამა)",
"OAuth ID": "",
"October": "ოქტომბერი",
"Off": "გამორთვა",
"Okay, Let's Go!": "კარგი, წავედით!",

View File

@ -126,6 +126,7 @@
"Content": "내용",
"Context Length": "내용 길이",
"Continue Response": "대화 계속",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "공유 채팅 URL이 클립보드에 복사되었습니다!",
"Copy": "복사",
"Copy last code block": "마지막 코드 블록 복사",
@ -378,6 +379,7 @@
"Notifications": "알림",
"November": "11월",
"num_thread (Ollama)": "num_thread (올라마)",
"OAuth ID": "",
"October": "10월",
"Off": "끄기",
"Okay, Let's Go!": "좋아요, 시작합시다!",

View File

@ -126,6 +126,7 @@
"Content": "Turinys",
"Context Length": "Konteksto ilgis",
"Continue Response": "Tęsti atsakymą",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Nukopijavote pokalbio nuorodą",
"Copy": "Kopijuoti",
"Copy last code block": "Kopijuoti paskutinį kodo bloką",
@ -378,6 +379,7 @@
"Notifications": "Pranešimai",
"November": "lapkritis",
"num_thread (Ollama)": "",
"OAuth ID": "",
"October": "spalis",
"Off": "Išjungta",
"Okay, Let's Go!": "Gerai, važiuojam!",

View File

@ -126,6 +126,7 @@
"Content": "Innhold",
"Context Length": "Kontekstlengde",
"Continue Response": "Fortsett svar",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Kopiert delt chat-URL til utklippstavlen!",
"Copy": "Kopier",
"Copy last code block": "Kopier siste kodeblokk",
@ -378,6 +379,7 @@
"Notifications": "Varsler",
"November": "November",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Oktober",
"Off": "Av",
"Okay, Let's Go!": "Ok, la oss gå!",

View File

@ -126,6 +126,7 @@
"Content": "Inhoud",
"Context Length": "Context Lengte",
"Continue Response": "Doorgaan met Antwoord",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "URL van gedeelde gesprekspagina gekopieerd naar klembord!",
"Copy": "Kopieer",
"Copy last code block": "Kopieer laatste code blok",
@ -378,6 +379,7 @@
"Notifications": "Desktop Notificaties",
"November": "November",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Oktober",
"Off": "Uit",
"Okay, Let's Go!": "Okay, Laten we gaan!",

View File

@ -126,6 +126,7 @@
"Content": "ਸਮੱਗਰੀ",
"Context Length": "ਸੰਦਰਭ ਲੰਬਾਈ",
"Continue Response": "ਜਵਾਬ ਜਾਰੀ ਰੱਖੋ",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "ਸਾਂਝੇ ਕੀਤੇ ਗੱਲਬਾਤ URL ਨੂੰ ਕਲਿੱਪਬੋਰਡ 'ਤੇ ਕਾਪੀ ਕਰ ਦਿੱਤਾ!",
"Copy": "ਕਾਪੀ ਕਰੋ",
"Copy last code block": "ਆਖਰੀ ਕੋਡ ਬਲਾਕ ਨੂੰ ਕਾਪੀ ਕਰੋ",
@ -378,6 +379,7 @@
"Notifications": "ਸੂਚਨਾਵਾਂ",
"November": "ਨਵੰਬਰ",
"num_thread (Ollama)": "num_thread (ਓਲਾਮਾ)",
"OAuth ID": "",
"October": "ਅਕਤੂਬਰ",
"Off": "ਬੰਦ",
"Okay, Let's Go!": "ਠੀਕ ਹੈ, ਚੱਲੋ ਚੱਲੀਏ!",

View File

@ -126,6 +126,7 @@
"Content": "Zawartość",
"Context Length": "Długość kontekstu",
"Continue Response": "Kontynuuj odpowiedź",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Skopiowano URL czatu do schowka!",
"Copy": "Kopiuj",
"Copy last code block": "Skopiuj ostatni blok kodu",
@ -378,6 +379,7 @@
"Notifications": "Powiadomienia",
"November": "Listopad",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Październik",
"Off": "Wyłączony",
"Okay, Let's Go!": "Okej, zaczynamy!",

View File

@ -126,6 +126,7 @@
"Content": "Conteúdo",
"Context Length": "Comprimento do Contexto",
"Continue Response": "Continuar resposta",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "URL de bate-papo compartilhado copiada com sucesso!",
"Copy": "Copiar",
"Copy last code block": "Copiar último bloco de código",
@ -378,6 +379,7 @@
"Notifications": "Notificações da Área de Trabalho",
"November": "Novembro",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Outubro",
"Off": "Desligado",
"Okay, Let's Go!": "Ok, Vamos Lá!",

View File

@ -126,6 +126,7 @@
"Content": "Conteúdo",
"Context Length": "Comprimento do Contexto",
"Continue Response": "Continuar resposta",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "URL de Conversa partilhado copiada com sucesso!",
"Copy": "Copiar",
"Copy last code block": "Copiar último bloco de código",
@ -378,6 +379,7 @@
"Notifications": "Notificações da Área de Trabalho",
"November": "Novembro",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Outubro",
"Off": "Desligado",
"Okay, Let's Go!": "Ok, Vamos Lá!",

View File

@ -126,6 +126,7 @@
"Content": "Содержание",
"Context Length": "Длина контексту",
"Continue Response": "Продолжить ответ",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Копирование общей ссылки чат в буфер обмена!",
"Copy": "Копировать",
"Copy last code block": "Копировать последний блок кода",
@ -378,6 +379,7 @@
"Notifications": "Уведомления на рабочем столе",
"November": "Ноябрь",
"num_thread (Ollama)": "num_thread (Оллама)",
"OAuth ID": "",
"October": "Октябрь",
"Off": "Выключено.",
"Okay, Let's Go!": "Давайте начнём!",

View File

@ -126,6 +126,7 @@
"Content": "Садржај",
"Context Length": "Дужина контекста",
"Continue Response": "Настави одговор",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Адреса дељеног ћаскања ископирана у оставу!",
"Copy": "Копирај",
"Copy last code block": "Копирај последњи блок кода",
@ -378,6 +379,7 @@
"Notifications": "Обавештења",
"November": "Новембар",
"num_thread (Ollama)": "нум _тхреад (Оллама)",
"OAuth ID": "",
"October": "Октобар",
"Off": "Искључено",
"Okay, Let's Go!": "У реду, хајде да кренемо!",

View File

@ -126,6 +126,7 @@
"Content": "Innehåll",
"Context Length": "Kontextlängd",
"Continue Response": "Fortsätt svar",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Kopierad delad chatt-URL till urklipp!",
"Copy": "Kopiera",
"Copy last code block": "Kopiera sista kodblock",
@ -378,6 +379,7 @@
"Notifications": "Notifikationer",
"November": "november",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "oktober",
"Off": "Av",
"Okay, Let's Go!": "Okej, nu kör vi!",

View File

@ -126,6 +126,7 @@
"Content": "",
"Context Length": "",
"Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "",
"Copy": "",
"Copy last code block": "",
@ -378,6 +379,7 @@
"Notifications": "",
"November": "",
"num_thread (Ollama)": "",
"OAuth ID": "",
"October": "",
"Off": "",
"Okay, Let's Go!": "",

View File

@ -126,6 +126,7 @@
"Content": "İçerik",
"Context Length": "Bağlam Uzunluğu",
"Continue Response": "Yanıta Devam Et",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Paylaşılan sohbet URL'si panoya kopyalandı!",
"Copy": "Kopyala",
"Copy last code block": "Son kod bloğunu kopyala",
@ -378,6 +379,7 @@
"Notifications": "Bildirimler",
"November": "Kasım",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Ekim",
"Off": "Kapalı",
"Okay, Let's Go!": "Tamam, Hadi Başlayalım!",

View File

@ -126,6 +126,7 @@
"Content": "Зміст",
"Context Length": "Довжина контексту",
"Continue Response": "Продовжити відповідь",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Скопійовано URL-адресу спільного чату в буфер обміну!",
"Copy": "Копіювати",
"Copy last code block": "Копіювати останній блок коду",
@ -378,6 +379,7 @@
"Notifications": "Сповіщення",
"November": "Листопад",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Жовтень",
"Off": "Вимк",
"Okay, Let's Go!": "Гаразд, давайте почнемо!",

View File

@ -126,6 +126,7 @@
"Content": "Nội dung",
"Context Length": "Độ dài ngữ cảnh (Context Length)",
"Continue Response": "Tiếp tục trả lời",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Đã sao chép link chia sẻ trò chuyện vào clipboard!",
"Copy": "Sao chép",
"Copy last code block": "Sao chép khối mã cuối cùng",
@ -378,6 +379,7 @@
"Notifications": "Thông báo trên máy tính (Notification)",
"November": "Tháng 11",
"num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Tháng 10",
"Off": "Tắt",
"Okay, Let's Go!": "Được rồi, Bắt đầu thôi!",

View File

@ -126,6 +126,7 @@
"Content": "内容",
"Context Length": "上下文长度",
"Continue Response": "继续生成",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "已复制此对话分享链接至剪贴板!",
"Copy": "复制",
"Copy last code block": "复制最后一个代码块中的代码",
@ -378,6 +379,7 @@
"Notifications": "桌面通知",
"November": "十一月",
"num_thread (Ollama)": "num_threadOllama",
"OAuth ID": "",
"October": "十月",
"Off": "关闭",
"Okay, Let's Go!": "确认,开始使用!",

View File

@ -126,6 +126,7 @@
"Content": "內容",
"Context Length": "上下文長度",
"Continue Response": "繼續回答",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "已複製共享聊天連結到剪貼簿!",
"Copy": "複製",
"Copy last code block": "複製最後一個程式碼區塊",
@ -378,6 +379,7 @@
"Notifications": "通知",
"November": "11 月",
"num_thread (Ollama)": "num_threadOllama",
"OAuth ID": "",
"October": "10 月",
"Off": "關閉",
"Okay, Let's Go!": "好的,啟動吧!",

View File

@ -149,6 +149,11 @@ type Config = {
enable_admin_export: boolean;
enable_community_sharing: boolean;
};
oauth: {
providers: {
[key: string]: string;
};
};
};
type PromptSuggestion = {

View File

@ -195,6 +195,18 @@
<span class="invisible"></span>
{/if}
</th>
<th
scope="col"
class="px-3 py-2 cursor-pointer select-none"
on:click={() => setSortKey('oauth_sub')}
>
{$i18n.t('OAuth ID')}
{#if sortKey === 'oauth_sub'}
{sortOrder === 'asc' ? '▲' : '▼'}
{:else}
<span class="invisible"></span>
{/if}
</th>
<th
scope="col"
class="px-3 py-2 cursor-pointer select-none"
@ -283,6 +295,8 @@
</td>
<td class=" px-3 py-2"> {user.email} </td>
<td class=" px-3 py-2"> {user.oauth_sub ?? ''} </td>
<td class=" px-3 py-2">
{dayjs(user.last_active_at * 1000).fromNow()}
</td>

View File

@ -18,6 +18,7 @@
USAGE_POOL
} from '$lib/stores';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { Toaster, toast } from 'svelte-sonner';
import { getBackendConfig } from '$lib/apis';
@ -141,7 +142,11 @@
await goto('/auth');
}
} else {
await goto('/auth');
// Don't redirect if we're already on the auth page
// Needed because we pass in tokens from OAuth logins via URL fragments
if ($page.url.pathname !== '/auth') {
await goto('/auth');
}
}
}
} else {

View File

@ -1,12 +1,13 @@
<script>
import { goto } from '$app/navigation';
import { userSignIn, userSignUp } from '$lib/apis/auths';
import { getSessionUser, userSignIn, userSignUp } from '$lib/apis/auths';
import Spinner from '$lib/components/common/Spinner.svelte';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, config, user, socket } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
import { page } from '$app/stores';
const i18n = getContext('i18n');
@ -21,7 +22,9 @@
if (sessionUser) {
console.log(sessionUser);
toast.success($i18n.t(`You're now logged in.`));
localStorage.token = sessionUser.token;
if (sessionUser.token) {
localStorage.token = sessionUser.token;
}
$socket.emit('user-join', { auth: { token: sessionUser.token } });
await user.set(sessionUser);
@ -57,10 +60,35 @@
}
};
const checkOauthCallback = async () => {
if (!$page.url.hash) {
return;
}
const hash = $page.url.hash.substring(1);
if (!hash) {
return;
}
const params = new URLSearchParams(hash);
const token = params.get('token');
if (!token) {
return;
}
const sessionUser = await getSessionUser(token).catch((error) => {
toast.error(error);
return null;
});
if (!sessionUser) {
return;
}
localStorage.token = token;
await setSessionUser(sessionUser);
};
onMount(async () => {
if ($user !== undefined) {
await goto('/');
}
await checkOauthCallback();
loaded = true;
if (($config?.features.auth_trusted_header ?? false) || $config?.features.auth === false) {
await signInHandler();
@ -219,6 +247,97 @@
{/if}
</div>
</form>
{#if Object.keys($config?.oauth?.providers ?? {}).length > 0}
<div class="inline-flex items-center justify-center w-full">
<hr class="w-64 h-px my-8 bg-gray-200 border-0 dark:bg-gray-700" />
<span
class="absolute px-3 font-medium text-gray-900 -translate-x-1/2 bg-white left-1/2 dark:text-white dark:bg-gray-950"
>{$i18n.t('or')}</span
>
</div>
<div class="flex flex-col space-y-2">
{#if $config?.oauth?.providers?.google}
<button
class="flex items-center px-6 border-2 dark:border-gray-800 duration-300 dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800 w-full rounded-2xl dark:text-white text-sm py-3 transition"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/google/login`;
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" class="size-6 mr-3">
<path
fill="#EA4335"
d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
/><path
fill="#4285F4"
d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
/><path
fill="#FBBC05"
d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
/><path
fill="#34A853"
d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
/><path fill="none" d="M0 0h48v48H0z" />
</svg>
<span>{$i18n.t('Continue with {{provider}}', { provider: 'Google' })}</span>
</button>
{/if}
{#if $config?.oauth?.providers?.microsoft}
<button
class="flex items-center px-6 border-2 dark:border-gray-800 duration-300 dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800 w-full rounded-2xl dark:text-white text-sm py-3 transition"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/microsoft/login`;
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21" class="size-6 mr-3">
<rect x="1" y="1" width="9" height="9" fill="#f25022" /><rect
x="1"
y="11"
width="9"
height="9"
fill="#00a4ef"
/><rect x="11" y="1" width="9" height="9" fill="#7fba00" /><rect
x="11"
y="11"
width="9"
height="9"
fill="#ffb900"
/>
</svg>
<span>{$i18n.t('Continue with {{provider}}', { provider: 'Microsoft' })}</span>
</button>
{/if}
{#if $config?.oauth?.providers?.oidc}
<button
class="flex items-center px-6 border-2 dark:border-gray-800 duration-300 dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800 w-full rounded-2xl dark:text-white text-sm py-3 transition"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/oidc/login`;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mr-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
/>
</svg>
<span
>{$i18n.t('Continue with {{provider}}', {
provider: $config?.oauth?.providers?.oidc ?? 'SSO'
})}</span
>
</button>
{/if}
</div>
{/if}
</div>
{/if}
</div>