Browse Source

Merge pull request #2574 from cheahjs/feat/oauth

feat: experimental SSO support for Google, Microsoft, and OIDC
Timothy Jaeryang Baek 10 months ago
parent
commit
d17dc59246
52 changed files with 633 additions and 13 deletions
  1. 49 0
      backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py
  2. 2 0
      backend/apps/webui/main.py
  3. 4 1
      backend/apps/webui/models/auths.py
  4. 25 0
      backend/apps/webui/models/users.py
  5. 0 1
      backend/apps/webui/routers/auths.py
  6. 139 0
      backend/config.py
  7. 174 2
      backend/main.py
  8. 1 0
      backend/requirements.txt
  9. 1 0
      pyproject.toml
  10. 9 3
      requirements-dev.lock
  11. 9 3
      requirements.lock
  12. 2 0
      src/lib/i18n/locales/ar-BH/translation.json
  13. 2 0
      src/lib/i18n/locales/bg-BG/translation.json
  14. 2 0
      src/lib/i18n/locales/bn-BD/translation.json
  15. 2 0
      src/lib/i18n/locales/ca-ES/translation.json
  16. 2 0
      src/lib/i18n/locales/ceb-PH/translation.json
  17. 2 0
      src/lib/i18n/locales/de-DE/translation.json
  18. 2 0
      src/lib/i18n/locales/dg-DG/translation.json
  19. 2 0
      src/lib/i18n/locales/en-GB/translation.json
  20. 2 0
      src/lib/i18n/locales/en-US/translation.json
  21. 2 0
      src/lib/i18n/locales/es-ES/translation.json
  22. 2 0
      src/lib/i18n/locales/fa-IR/translation.json
  23. 2 0
      src/lib/i18n/locales/fi-FI/translation.json
  24. 2 0
      src/lib/i18n/locales/fr-CA/translation.json
  25. 2 0
      src/lib/i18n/locales/fr-FR/translation.json
  26. 2 0
      src/lib/i18n/locales/he-IL/translation.json
  27. 2 0
      src/lib/i18n/locales/hi-IN/translation.json
  28. 2 0
      src/lib/i18n/locales/hr-HR/translation.json
  29. 2 0
      src/lib/i18n/locales/it-IT/translation.json
  30. 2 0
      src/lib/i18n/locales/ja-JP/translation.json
  31. 2 0
      src/lib/i18n/locales/ka-GE/translation.json
  32. 2 0
      src/lib/i18n/locales/ko-KR/translation.json
  33. 2 0
      src/lib/i18n/locales/lt-LT/translation.json
  34. 2 0
      src/lib/i18n/locales/nb-NO/translation.json
  35. 2 0
      src/lib/i18n/locales/nl-NL/translation.json
  36. 2 0
      src/lib/i18n/locales/pa-IN/translation.json
  37. 2 0
      src/lib/i18n/locales/pl-PL/translation.json
  38. 2 0
      src/lib/i18n/locales/pt-BR/translation.json
  39. 2 0
      src/lib/i18n/locales/pt-PT/translation.json
  40. 2 0
      src/lib/i18n/locales/ru-RU/translation.json
  41. 2 0
      src/lib/i18n/locales/sr-RS/translation.json
  42. 2 0
      src/lib/i18n/locales/sv-SE/translation.json
  43. 2 0
      src/lib/i18n/locales/tk-TW/translation.json
  44. 2 0
      src/lib/i18n/locales/tr-TR/translation.json
  45. 2 0
      src/lib/i18n/locales/uk-UA/translation.json
  46. 2 0
      src/lib/i18n/locales/vi-VN/translation.json
  47. 2 0
      src/lib/i18n/locales/zh-CN/translation.json
  48. 2 0
      src/lib/i18n/locales/zh-TW/translation.json
  49. 5 0
      src/lib/stores/index.ts
  50. 14 0
      src/routes/(app)/admin/+page.svelte
  51. 6 1
      src/routes/+layout.svelte
  52. 121 2
      src/routes/auth/+page.svelte

+ 49 - 0
backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py

@@ -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")

+ 2 - 0
backend/apps/webui/main.py

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

+ 4 - 1
backend/apps/webui/models/auths.py

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

+ 25 - 0
backend/apps/webui/models/users.py

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

+ 0 - 1
backend/apps/webui/routers/auths.py

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

+ 139 - 0
backend/config.py

@@ -305,6 +305,135 @@ JWT_EXPIRES_IN = PersistentConfig(
     "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
     "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
 # Static DIR
 ####################################
 ####################################
@@ -733,6 +862,16 @@ WEBUI_SECRET_KEY = os.environ.get(
     ),  # DEPRECATED: remove at next major version
     ),  # 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 == "":
 if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
     raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
     raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
 
 

+ 174 - 2
backend/main.py

@@ -1,4 +1,9 @@
+import base64
+import uuid
 from contextlib import asynccontextmanager
 from contextlib import asynccontextmanager
+
+from authlib.integrations.starlette_client import OAuth
+from authlib.oidc.core import UserInfo
 from bs4 import BeautifulSoup
 from bs4 import BeautifulSoup
 import json
 import json
 import markdown
 import markdown
@@ -24,7 +29,8 @@ from fastapi.middleware.wsgi import WSGIMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 from starlette.exceptions import HTTPException as StarletteHTTPException
 from starlette.exceptions import HTTPException as StarletteHTTPException
 from starlette.middleware.base import BaseHTTPMiddleware
 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
 from apps.socket.main import app as socket_app
@@ -53,9 +59,11 @@ from apps.webui.main import (
 from pydantic import BaseModel
 from pydantic import BaseModel
 from typing import List, Optional, Iterator, Generator, Union
 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.models import Models, ModelModel
 from apps.webui.models.tools import Tools
 from apps.webui.models.tools import Tools
 from apps.webui.models.functions import Functions
 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
 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_verified_user,
     get_current_user,
     get_current_user,
     get_http_authorization_cred,
     get_http_authorization_cred,
+    get_password_hash,
+    create_token,
 )
 )
 from utils.task import (
 from utils.task import (
     title_generation_template,
     title_generation_template,
@@ -74,6 +84,7 @@ from utils.misc import (
     get_last_user_message,
     get_last_user_message,
     add_or_update_system_message,
     add_or_update_system_message,
     stream_message_template,
     stream_message_template,
+    parse_duration,
 )
 )
 
 
 from apps.rag.utils import get_rag_context, rag_template
 from apps.rag.utils import get_rag_context, rag_template
@@ -106,9 +117,16 @@ from config import (
     SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
     SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
     TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
     TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
     SAFE_MODE,
     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,
     AppConfig,
 )
 )
-from constants import ERROR_MESSAGES
+from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
+from utils.webhook import post_webhook
 
 
 if SAFE_MODE:
 if SAFE_MODE:
     print("SAFE MODE ENABLED")
     print("SAFE MODE ENABLED")
@@ -1725,6 +1743,12 @@ async def get_app_config():
                 "engine": audio_app.state.config.STT_ENGINE,
                 "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")
 @app.get("/manifest.json")
 async def get_manifest_json():
 async def get_manifest_json():
     return {
     return {

+ 1 - 0
backend/requirements.txt

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

+ 1 - 0
pyproject.toml

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

+ 9 - 3
requirements-dev.lock

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

+ 9 - 3
requirements.lock

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

+ 2 - 0
src/lib/i18n/locales/ar-BH/translation.json

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

+ 2 - 0
src/lib/i18n/locales/bg-BG/translation.json

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

+ 2 - 0
src/lib/i18n/locales/bn-BD/translation.json

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

+ 2 - 0
src/lib/i18n/locales/ca-ES/translation.json

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

+ 2 - 0
src/lib/i18n/locales/ceb-PH/translation.json

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

+ 2 - 0
src/lib/i18n/locales/de-DE/translation.json

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

+ 2 - 0
src/lib/i18n/locales/dg-DG/translation.json

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

+ 2 - 0
src/lib/i18n/locales/en-GB/translation.json

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

+ 2 - 0
src/lib/i18n/locales/en-US/translation.json

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

+ 2 - 0
src/lib/i18n/locales/es-ES/translation.json

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

+ 2 - 0
src/lib/i18n/locales/fa-IR/translation.json

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

+ 2 - 0
src/lib/i18n/locales/fi-FI/translation.json

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

+ 2 - 0
src/lib/i18n/locales/fr-CA/translation.json

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

+ 2 - 0
src/lib/i18n/locales/fr-FR/translation.json

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

+ 2 - 0
src/lib/i18n/locales/he-IL/translation.json

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

+ 2 - 0
src/lib/i18n/locales/hi-IN/translation.json

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

+ 2 - 0
src/lib/i18n/locales/hr-HR/translation.json

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

+ 2 - 0
src/lib/i18n/locales/it-IT/translation.json

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

+ 2 - 0
src/lib/i18n/locales/ja-JP/translation.json

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

+ 2 - 0
src/lib/i18n/locales/ka-GE/translation.json

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

+ 2 - 0
src/lib/i18n/locales/ko-KR/translation.json

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

+ 2 - 0
src/lib/i18n/locales/lt-LT/translation.json

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

+ 2 - 0
src/lib/i18n/locales/nb-NO/translation.json

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

+ 2 - 0
src/lib/i18n/locales/nl-NL/translation.json

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

+ 2 - 0
src/lib/i18n/locales/pa-IN/translation.json

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

+ 2 - 0
src/lib/i18n/locales/pl-PL/translation.json

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

+ 2 - 0
src/lib/i18n/locales/pt-BR/translation.json

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

+ 2 - 0
src/lib/i18n/locales/pt-PT/translation.json

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

+ 2 - 0
src/lib/i18n/locales/ru-RU/translation.json

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

+ 2 - 0
src/lib/i18n/locales/sr-RS/translation.json

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

+ 2 - 0
src/lib/i18n/locales/sv-SE/translation.json

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

+ 2 - 0
src/lib/i18n/locales/tk-TW/translation.json

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

+ 2 - 0
src/lib/i18n/locales/tr-TR/translation.json

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

+ 2 - 0
src/lib/i18n/locales/uk-UA/translation.json

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

+ 2 - 0
src/lib/i18n/locales/vi-VN/translation.json

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

+ 2 - 0
src/lib/i18n/locales/zh-CN/translation.json

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

+ 2 - 0
src/lib/i18n/locales/zh-TW/translation.json

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

+ 5 - 0
src/lib/stores/index.ts

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

+ 14 - 0
src/routes/(app)/admin/+page.svelte

@@ -195,6 +195,18 @@
 							<span class="invisible">▲</span>
 							<span class="invisible">▲</span>
 						{/if}
 						{/if}
 					</th>
 					</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
 					<th
 						scope="col"
 						scope="col"
 						class="px-3 py-2 cursor-pointer select-none"
 						class="px-3 py-2 cursor-pointer select-none"
@@ -283,6 +295,8 @@
 						</td>
 						</td>
 						<td class=" px-3 py-2"> {user.email} </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">
 						<td class=" px-3 py-2">
 							{dayjs(user.last_active_at * 1000).fromNow()}
 							{dayjs(user.last_active_at * 1000).fromNow()}
 						</td>
 						</td>

+ 6 - 1
src/routes/+layout.svelte

@@ -18,6 +18,7 @@
 		USAGE_POOL
 		USAGE_POOL
 	} from '$lib/stores';
 	} from '$lib/stores';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
+	import { page } from '$app/stores';
 	import { Toaster, toast } from 'svelte-sonner';
 	import { Toaster, toast } from 'svelte-sonner';
 
 
 	import { getBackendConfig } from '$lib/apis';
 	import { getBackendConfig } from '$lib/apis';
@@ -141,7 +142,11 @@
 						await goto('/auth');
 						await goto('/auth');
 					}
 					}
 				} else {
 				} 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 {
 		} else {

+ 121 - 2
src/routes/auth/+page.svelte

@@ -1,12 +1,13 @@
 <script>
 <script>
 	import { goto } from '$app/navigation';
 	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 Spinner from '$lib/components/common/Spinner.svelte';
 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_NAME, config, user, socket } from '$lib/stores';
 	import { WEBUI_NAME, config, user, socket } from '$lib/stores';
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
 	import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
+	import { page } from '$app/stores';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -21,7 +22,9 @@
 		if (sessionUser) {
 		if (sessionUser) {
 			console.log(sessionUser);
 			console.log(sessionUser);
 			toast.success($i18n.t(`You're now logged in.`));
 			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 } });
 			$socket.emit('user-join', { auth: { token: sessionUser.token } });
 			await user.set(sessionUser);
 			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 () => {
 	onMount(async () => {
 		if ($user !== undefined) {
 		if ($user !== undefined) {
 			await goto('/');
 			await goto('/');
 		}
 		}
+		await checkOauthCallback();
 		loaded = true;
 		loaded = true;
 		if (($config?.features.auth_trusted_header ?? false) || $config?.features.auth === false) {
 		if (($config?.features.auth_trusted_header ?? false) || $config?.features.auth === false) {
 			await signInHandler();
 			await signInHandler();
@@ -219,6 +247,97 @@
 							{/if}
 							{/if}
 						</div>
 						</div>
 					</form>
 					</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>
 				</div>
 			{/if}
 			{/if}
 		</div>
 		</div>