diff --git a/.cursor/rules/db-migrations.mdc b/.cursor/rules/db-migrations.mdc index fe6b0d4733..b8d537bc72 100644 --- a/.cursor/rules/db-migrations.mdc +++ b/.cursor/rules/db-migrations.mdc @@ -5,7 +5,22 @@ alwaysApply: false # Database Migrations Guide -## Defensive Programming - Use Idempotent Clauses +## Step1: Generate migrations: + +```bash +bun run db:generate +``` + +this step will generate or update the following files: + +- packages/database/migrations/0046_xxx.sql +- packages/database/migrations/meta/\_journal.json + +## Step2: optimize the migration sql fileName + +the migration sql file name is randomly generated, we need to optimize the file name to make it more readable and meaningful. For example, `0046_xxx.sql` -> `0046_better_auth.sql` + +## Step3: Defensive Programming - Use Idempotent Clauses Always use defensive clauses to make migrations idempotent: diff --git a/.cursor/rules/project-introduce.mdc b/.cursor/rules/project-introduce.mdc index c24d8908b5..5f7719d26d 100644 --- a/.cursor/rules/project-introduce.mdc +++ b/.cursor/rules/project-introduce.mdc @@ -16,7 +16,7 @@ logo emoji: 🤯 ## Project Technologies Stack -- Next.js 15 +- Next.js 16 - react 19 - TypeScript - `@lobehub/ui`, antd for component framework diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc index 05b6e1be44..22b20c0af9 100644 --- a/.cursor/rules/project-structure.mdc +++ b/.cursor/rules/project-structure.mdc @@ -16,17 +16,28 @@ lobe-chat/ ├── apps/ │ └── desktop/ ├── docs/ +│ ├── changelog/ +│ ├── development/ +│ ├── self-hosting/ +│ └── usage/ ├── locales/ │ ├── en-US/ │ └── zh-CN/ ├── packages/ +│ ├── agent-runtime/ │ ├── const/ │ ├── context-engine/ +│ ├── conversation-flow/ │ ├── database/ │ │ ├── src/ │ │ │ ├── models/ │ │ │ ├── schemas/ │ │ │ └── repositories/ +│ ├── electron-client-ipc/ +│ ├── electron-server-ipc/ +│ ├── fetch-sse/ +│ ├── file-loaders/ +│ ├── memory-extract/ │ ├── model-bank/ │ │ └── src/ │ │ └── aiModels/ @@ -34,11 +45,16 @@ lobe-chat/ │ │ └── src/ │ │ ├── core/ │ │ └── providers/ +│ ├── obervability-otel/ +│ ├── prompts/ +│ ├── python-interpreter/ +│ ├── ssrf-safe-fetch/ │ ├── types/ │ │ └── src/ │ │ ├── message/ │ │ └── user/ -│ └── utils/ +│ ├── utils/ +│ └── web-crawler/ ├── public/ ├── scripts/ ├── src/ @@ -68,7 +84,9 @@ lobe-chat/ │ │ ├── AuthProvider/ │ │ └── GlobalProvider/ │ ├── libs/ -│ │ └── oidc-provider/ +│ │ ├── better-auth/ +│ │ ├── oidc-provider/ +│ │ └── trpc/ │ ├── locales/ │ │ └── default/ │ ├── server/ diff --git a/.env.example b/.env.example index 7675bc50c9..46260b964c 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,9 @@ # Specify your API Key selection method, currently supporting `random` and `turn`. # API_KEY_SELECT_MODE=random -######################################## -########### Security Settings ########### -######################################## +# ####################################### +# ########## Security Settings ########### +# ####################################### # Control Content Security Policy headers # Set to '1' to enable X-Frame-Options and Content-Security-Policy headers @@ -24,11 +24,11 @@ # Example: Allow specific internal servers while keeping SSRF protection # SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50 -######################################## -########## AI Provider Service ######### -######################################## +# ####################################### +# ######### AI Provider Service ######### +# ####################################### -### OpenAI ### +# ## OpenAI ### # you openai api key OPENAI_API_KEY=sk-xxxxxxxxx @@ -40,7 +40,7 @@ OPENAI_API_KEY=sk-xxxxxxxxx # OPENAI_MODEL_LIST=gpt-3.5-turbo -### Azure OpenAI ### +# ## Azure OpenAI ### # you can learn azure OpenAI Service on https://learn.microsoft.com/en-us/azure/ai-services/openai/overview # use Azure OpenAI Service by uncomment the following line @@ -55,7 +55,7 @@ OPENAI_API_KEY=sk-xxxxxxxxx # AZURE_API_VERSION=2024-10-21 -### Anthropic Service #### +# ## Anthropic Service #### # ANTHROPIC_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -63,19 +63,19 @@ OPENAI_API_KEY=sk-xxxxxxxxx # ANTHROPIC_PROXY_URL=https://api.anthropic.com -### Google AI #### +# ## Google AI #### # GOOGLE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### AWS Bedrock ### +# ## AWS Bedrock ### # AWS_REGION=us-east-1 # AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxx # AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### Ollama AI #### +# ## Ollama AI #### # You can use ollama to get and run LLM locally, learn more about it via https://github.com/ollama/ollama @@ -85,132 +85,132 @@ OPENAI_API_KEY=sk-xxxxxxxxx # OLLAMA_MODEL_LIST=your_ollama_model_names -### OpenRouter Service ### +# ## OpenRouter Service ### # OPENROUTER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx # OPENROUTER_MODEL_LIST=model1,model2,model3 -### Mistral AI ### +# ## Mistral AI ### # MISTRAL_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### Perplexity Service ### +# ## Perplexity Service ### # PERPLEXITY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### Groq Service #### +# ## Groq Service #### # GROQ_API_KEY=gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -#### 01.AI Service #### +# ### 01.AI Service #### # ZEROONE_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### TogetherAI Service ### +# ## TogetherAI Service ### # TOGETHERAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### ZhiPu AI ### +# ## ZhiPu AI ### # ZHIPU_API_KEY=xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxx -### Moonshot AI #### +# ## Moonshot AI #### # MOONSHOT_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### Minimax AI #### +# ## Minimax AI #### # MINIMAX_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### DeepSeek AI #### +# ## DeepSeek AI #### # DEEPSEEK_PROXY_URL=https://api.deepseek.com/v1 # DEEPSEEK_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### Qiniu AI #### +# ## Qiniu AI #### # QINIU_PROXY_URL=https://api.qnaigc.com/v1 # QINIU_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### Qwen AI #### +# ## Qwen AI #### # QWEN_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### Cloudflare Workers AI #### +# ## Cloudflare Workers AI #### # CLOUDFLARE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx # CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### SiliconCloud AI #### +# ## SiliconCloud AI #### # SILICONCLOUD_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### TencentCloud AI #### +# ## TencentCloud AI #### # TENCENT_CLOUD_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### PPIO #### +# ## PPIO #### # PPIO_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### INFINI-AI ### +# ## INFINI-AI ### # INFINIAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### 302.AI ### +# ## 302.AI ### # AI302_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### ModelScope ### +# ## ModelScope ### # MODELSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### AiHubMix ### +# ## AiHubMix ### # AIHUBMIX_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### BFL ### +# ## BFL ### # BFL_API_KEY=bfl-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### FAL ### +# ## FAL ### # FAL_API_KEY=fal-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -######################################## -######### AI Image Settings ############ -######################################## +# ####################################### +# ######## AI Image Settings ############ +# ####################################### # Default image generation count (range: 1-20, default: 4) # AI_IMAGE_DEFAULT_IMAGE_NUM=4 -### Nebius ### +# ## Nebius ### # NEBIUS_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -### NewAPI Service ### +# ## NewAPI Service ### # NEWAPI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # NEWAPI_PROXY_URL=https://your-newapi-server.com -### Vercel AI Gateway ### +# ## Vercel AI Gateway ### # VERCELAIGATEWAY_API_KEY=your_vercel_ai_gateway_api_key -######################################## -############ Market Service ############ -######################################## +# ####################################### +# ########### Market Service ############ +# ####################################### # The LobeChat agents market index url # AGENTS_INDEX_URL=https://chat-agents.lobehub.com -######################################## -############ Plugin Service ############ -######################################## +# ####################################### +# ########### Plugin Service ############ +# ####################################### # The LobeChat plugins store index url # PLUGINS_INDEX_URL=https://chat-plugins.lobehub.com @@ -219,9 +219,9 @@ OPENAI_API_KEY=sk-xxxxxxxxx # the format is `plugin-identifier:key1=value1;key2=value2`, multiple settings fields are separated by semicolons `;`, multiple plugin settings are separated by commas `,`. # PLUGIN_SETTINGS=search-engine:SERPAPI_API_KEY=xxxxx -######################################## -####### Doc / Changelog Service ######## -######################################## +# ####################################### +# ###### Doc / Changelog Service ######## +# ####################################### # Use in Changelog / Document service cdn url prefix # DOC_S3_PUBLIC_DOMAIN=https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -231,9 +231,9 @@ OPENAI_API_KEY=sk-xxxxxxxxx # DOC_S3_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx -######################################## -##### S3 Object Storage Service ######## -######################################## +# ####################################### +# #### S3 Object Storage Service ######## +# ####################################### # S3 keys # S3_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -253,19 +253,19 @@ OPENAI_API_KEY=sk-xxxxxxxxx # S3_REGION=us-west-1 -######################################## -############ Auth Service ############## -######################################## +# ####################################### +# ########### Auth Service ############## +# ####################################### # Clerk related configurations # Clerk public key and secret key -#NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx -#CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx +# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx +# CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx # you need to config the clerk webhook secret key if you want to use the clerk with database -#CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx +# CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx # Clear allow origin https://clerk.com/docs/guides/dashboard/dns-domains/satellite-domains # Authentication across different domains , use,to splite different origin @@ -280,23 +280,106 @@ OPENAI_API_KEY=sk-xxxxxxxxx # AUTH_AUTH0_SECRET= # AUTH_AUTH0_ISSUER=https://your-domain.auth0.com -######################################## -########## Server Database ############# -######################################## +# Better-Auth related configurations +# NEXT_PUBLIC_ENABLE_BETTER_AUTH=1 + +# Auth Secret (use `openssl rand -base64 32` to generate) +# Shared between Better-Auth and Next-Auth +# AUTH_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Auth URL (accessible from browser, optional if same domain) +# NEXT_PUBLIC_AUTH_URL=http://localhost:3210 + +# Require email verification before allowing users to sign in (default: false) +# Set to '1' to force users to verify their email before signing in +# NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION=0 + +# SSO Providers Configuration (for Better-Auth) +# Comma-separated list of enabled OAuth providers +# Supported providers: auth0, authelia, authentik, casdoor, cloudflare-zero-trust, cognito, generic-oidc, github, google, keycloak, logto, microsoft, microsoft-entra-id, okta, zitadel +# Example: AUTH_SSO_PROVIDERS=google,github,auth0,microsoft-entra-id +# AUTH_SSO_PROVIDERS= + +# Google OAuth Configuration (for Better-Auth) +# Get credentials from: https://console.cloud.google.com/apis/credentials +# Authorized redirect URIs: +# - Development: http://localhost:3210/api/auth/callback/google +# - Production: https://yourdomain.com/api/auth/callback/google +# GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com +# GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxx + +# GitHub OAuth Configuration (for Better-Auth) +# Get credentials from: https://github.com/settings/developers +# Create a new OAuth App with: +# Authorized callback URL: +# - Development: http://localhost:3210/api/auth/callback/github +# - Production: https://yourdomain.com/api/auth/callback/github +# GITHUB_CLIENT_ID=Ov23xxxxxxxxxxxxx +# GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# AWS Cognito OAuth Configuration (for Better-Auth) +# Get credentials from: https://console.aws.amazon.com/cognito +# Setup steps: +# 1. Create a User Pool with App Client +# 2. Configure Hosted UI domain +# 3. Enable "Authorization code grant" OAuth flow +# 4. Set OAuth scopes: openid, profile, email +# Authorized callback URL: +# - Development: http://localhost:3210/api/auth/callback/cognito +# - Production: https://yourdomain.com/api/auth/callback/cognito +# COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxx +# COGNITO_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# COGNITO_DOMAIN=your-app.auth.us-east-1.amazoncognito.com +# COGNITO_REGION=us-east-1 +# COGNITO_USERPOOL_ID=us-east-1_xxxxxxxxx + +# Microsoft OAuth Configuration (for Better-Auth) +# Get credentials from: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade +# Create a new App Registration in Microsoft Entra ID (Azure AD) +# Authorized redirect URL: +# - Development: http://localhost:3210/api/auth/callback/microsoft +# - Production: https://yourdomain.com/api/auth/callback/microsoft +# MICROSOFT_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +# MICROSOFT_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# ####################################### +# ########## Email Service ############## +# ####################################### + +# SMTP Server Configuration (required for email verification with Better-Auth) + +# SMTP server hostname (e.g., smtp.gmail.com, smtp.office365.com) +# SMTP_HOST=smtp.example.com + +# SMTP server port (usually 587 for TLS, or 465 for SSL) +# SMTP_PORT=587 + +# Use secure connection (set to 'true' for port 465, 'false' for port 587) +# SMTP_SECURE=false + +# SMTP authentication username (usually your email address) +# SMTP_USER=your-email@example.com + +# SMTP authentication password (use app-specific password for Gmail) +# SMTP_PASS=your-password-or-app-specific-password + +# ####################################### +# ######### Server Database ############# +# ####################################### # Postgres database URL # DATABASE_URL=postgres://username:password@host:port/database # use `openssl rand -base64 32` to generate a key for the encryption of the database # we use this key to encrypt the user api key and proxy url -#KEY_VAULTS_SECRET=xxxxx/xxxxxxxxxxxxxx= +# KEY_VAULTS_SECRET=xxxxx/xxxxxxxxxxxxxx= # Specify the Embedding model and Reranker model(unImplemented) # DEFAULT_FILES_CONFIG="embedding_model=openai/embedding-text-3-small,reranker_model=cohere/rerank-english-v3.0,query_mode=full_text" -######################################## -########## MCP Service Config ########## -######################################## +# ####################################### +# ######### MCP Service Config ########## +# ####################################### # MCP tool call timeout (milliseconds) # MCP_TOOL_TIMEOUT=60000 diff --git a/.env.example.development b/.env.example.development index 72c275f9ae..6471d67610 100644 --- a/.env.example.development +++ b/.env.example.development @@ -32,19 +32,17 @@ DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432/${LOBE_DB DATABASE_DRIVER=node # Authentication Configuration -# Enable NextAuth authentication -NEXT_PUBLIC_ENABLE_NEXT_AUTH=1 +# Enable Better Auth authentication +NEXT_PUBLIC_ENABLE_BETTER_AUTH=1 -# NextAuth secret for JWT signing (generate with: openssl rand -base64 32) -NEXT_AUTH_SECRET=${UNSAFE_SECRET} - -NEXTAUTH_URL=${APP_URL} +# Better Auth secret for JWT signing (generate with: openssl rand -base64 32) +AUTH_SECRET=${UNSAFE_SECRET} # Authentication URL -AUTH_URL=${APP_URL}/api/auth +NEXT_PUBLIC_AUTH_URL=${APP_URL} # SSO providers configuration - using Casdoor for development -NEXT_AUTH_SSO_PROVIDERS=casdoor +AUTH_SSO_PROVIDERS=casdoor # Casdoor Configuration # Casdoor service port diff --git a/.gitignore b/.gitignore index 2b39a5c46f..89e29f8fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -115,6 +115,4 @@ CLAUDE.local.md *.doc* *.xls* -prd -GEMINI.md e2e/reports diff --git a/AGENTS.md b/AGENTS.md index 7057b0bf3c..4263993a51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,13 +6,12 @@ This document serves as a comprehensive guide for all team members when developi Built with modern technologies: -- **Frontend**: Next.js 15, React 19, TypeScript +- **Frontend**: Next.js 16, React 19, TypeScript - **UI Components**: Ant Design, @lobehub/ui, antd-style - **State Management**: Zustand, SWR - **Database**: PostgreSQL, PGLite, Drizzle ORM - **Testing**: Vitest, Testing Library - **Package Manager**: pnpm (monorepo structure) -- **Build Tools**: Next.js (Turbopack in dev, Webpack in prod) ## Directory Structure @@ -39,7 +38,6 @@ The project follows a well-organized monorepo structure: - Use `pnpm` as the primary package manager - Use `bun` to run npm scripts - Use `bunx` to run executable npm packages -- Navigate to specific packages using `cd packages/` ### Code Style Guidelines diff --git a/Dockerfile b/Dockerfile index b06d24c645..230d8cdf87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ FROM base AS builder ARG USE_CN_MIRROR ARG NEXT_PUBLIC_BASE_PATH -ARG NEXT_PUBLIC_ENABLE_NEXT_AUTH +ARG NEXT_PUBLIC_ENABLE_BETTER_AUTH ARG NEXT_PUBLIC_ENABLE_CLERK_AUTH ARG NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ARG NEXT_PUBLIC_SENTRY_DSN @@ -52,7 +52,7 @@ ARG FEATURE_FLAGS ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}" \ FEATURE_FLAGS="${FEATURE_FLAGS}" -ENV NEXT_PUBLIC_ENABLE_NEXT_AUTH="${NEXT_PUBLIC_ENABLE_NEXT_AUTH:-1}" \ +ENV NEXT_PUBLIC_ENABLE_BETTER_AUTH="${NEXT_PUBLIC_ENABLE_BETTER_AUTH:-1}" \ NEXT_PUBLIC_ENABLE_CLERK_AUTH="${NEXT_PUBLIC_ENABLE_CLERK_AUTH:-0}" \ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}" \ CLERK_WEBHOOK_SECRET="whsec_xxx" \ @@ -177,10 +177,10 @@ ENV KEY_VAULTS_SECRET="" \ DATABASE_DRIVER="node" \ DATABASE_URL="" -# Next Auth -ENV NEXT_AUTH_SECRET="" \ - NEXT_AUTH_SSO_PROVIDERS="" \ - NEXTAUTH_URL="" +# Better Auth +ENV AUTH_SECRET="" \ + AUTH_SSO_PROVIDERS="" \ + NEXT_PUBLIC_AUTH_URL="" # Clerk ENV CLERK_SECRET_KEY="" \ diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000000..bb7083dc03 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,63 @@ +# GEMINI.md + +This document serves as a shared guideline for all team members when using Gemini CLI in this repository. + +## Tech Stack + +read @.cursor/rules/project-introduce.mdc + +## Directory Structure + +read @.cursor/rules/project-structure.mdc + +## Development + +### Git Workflow + +- use rebase for git pull +- git commit message should prefix with gitmoji +- git branch name format example: tj/feat/feature-name +- use .github/PULL_REQUEST_TEMPLATE.md to generate pull request description + +### Package Management + +This repository adopts a monorepo structure. + +- Use `pnpm` as the primary package manager for dependency management +- Use `bun` to run npm scripts +- Use `bunx` to run executable npm packages + +### TypeScript Code Style Guide + +see @.cursor/rules/typescript.mdc + +### Testing + +- **Required Rule**: read `@.cursor/rules/testing-guide/testing-guide.mdc` before writing tests +- **Command**: + - web: `bunx vitest run --silent='passed-only' '[file-path-pattern]'` + - packages(eg: database): `cd packages/database && bunx vitest run --silent='passed-only' '[file-path-pattern]'` + +**Important**: + +- wrap the file path in single quotes to avoid shell expansion +- Never run `bun run test` etc to run tests, this will run all tests and cost about 10mins +- If trying to fix the same test twice, but still failed, stop and ask for help. + +### Typecheck + +- use `bun run type-check` to check type errors. + +### i18n + +- **Keys**: Add to `src/locales/default/namespace.ts` +- **Dev**: Translate `locales/zh-CN/namespace.json` and `locales/en-US/namespace.json` locales file only for dev preview +- DON'T run `pnpm i18n`, let CI auto handle it + +## 🚨 Quality Checks + +**MANDATORY**: After completing code changes, always run `mcp__vscode-mcp__get_diagnostics` on the modified files to identify any errors introduced by your changes and fix them. + +## Rules Index + +Some useful project rules are listed in @.cursor/rules/rules-index.mdc diff --git a/docs/development/database-schema.dbml b/docs/development/database-schema.dbml index 34efc57e8b..dd2dcbc229 100644 --- a/docs/development/database-schema.dbml +++ b/docs/development/database-schema.dbml @@ -137,6 +137,42 @@ table async_tasks { updated_at "timestamp with time zone" [not null, default: `now()`] } +table accounts { + access_token text + access_token_expires_at timestamp + account_id text [not null] + created_at timestamp [not null, default: `now()`] + id text [pk, not null] + id_token text + password text + provider_id text [not null] + refresh_token text + refresh_token_expires_at timestamp + scope text + updated_at timestamp [not null] + user_id text [not null] +} + +table auth_sessions { + created_at timestamp [not null, default: `now()`] + expires_at timestamp [not null] + id text [pk, not null] + ip_address text + token text [not null, unique] + updated_at timestamp [not null] + user_agent text + user_id text [not null] +} + +table verifications { + created_at timestamp [not null, default: `now()`] + expires_at timestamp [not null] + id text [pk, not null] + identifier text [not null] + updated_at timestamp [not null, default: `now()`] + value text [not null] +} + table chat_groups { id text [pk, not null] title text @@ -981,6 +1017,7 @@ table users { full_name text is_onboarded boolean [default: false] clerk_created_at "timestamp with time zone" + email_verified boolean [not null, default: false] email_verified_at "timestamp with time zone" preference jsonb accessed_at "timestamp with time zone" [not null, default: `now()`] diff --git a/docs/self-hosting/advanced/auth.mdx b/docs/self-hosting/advanced/auth.mdx index 7738e46dd9..ce81b83a4b 100644 --- a/docs/self-hosting/advanced/auth.mdx +++ b/docs/self-hosting/advanced/auth.mdx @@ -1,10 +1,11 @@ --- title: LobeChat Authentication Service Configuration description: >- - Learn how to configure external authentication services using Clerk or Next Auth for centralized user authorization management. Supported authentication services include Auth0, Azure ID, etc. + Learn how to configure external authentication services using Better Auth, Clerk, or Next Auth for centralized user authorization management. Supported authentication services include Auth0, Azure ID, etc. tags: - Authentication Service + - Better Auth - Next Auth - SSO - Clerk @@ -12,7 +13,7 @@ tags: # Authentication Service -LobeChat supports the configuration of external authentication services using Clerk or Next Auth for internal use within enterprises/organizations to centrally manage user authorization. +LobeChat supports the configuration of external authentication services using Better Auth, Clerk, or Next Auth for internal use within enterprises/organizations to centrally manage user authorization. ## Clerk @@ -22,6 +23,78 @@ LobeChat has deeply integrated with Clerk to provide users with a more secure an By setting the environment variables `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` in LobeChat's environment, you can enable and use Clerk. +## Better Auth + +[Better Auth](https://www.better-auth.com) is a modern, framework-agnostic authentication library designed to provide comprehensive, secure, and flexible authentication solutions. It supports various authentication methods including email/password, magic links, and multiple OAuth/SSO providers. + +### Key Features + +- **Email/Password Authentication**: Built-in support for traditional email and password login with secure password hashing +- **Email Verification**: Optional email verification flow with customizable email templates +- **Magic Link Login**: Passwordless authentication via email magic links +- **OAuth/SSO Support**: Integration with popular identity providers including Google, GitHub, Microsoft, AWS Cognito, and more +- **Generic OIDC/OAuth**: Support for any OpenID Connect or OAuth 2.0 compliant provider + +### Getting Started + +To enable Better Auth in LobeChat, set the following environment variables: + +| Environment Variable | Type | Description | +| -------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | +| `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | Required | Set to `1` to enable Better Auth service | +| `AUTH_SECRET` | Required | Key used to encrypt session tokens. Generate using: `openssl rand -base64 32` | +| `NEXT_PUBLIC_AUTH_URL` | Optional | The URL accessible from the browser for Better Auth callbacks. Only set this if the default generated URL is incorrect | +| `AUTH_SSO_PROVIDERS` | Optional | Comma-separated list of enabled SSO providers, e.g., `google,github,microsoft` | + +### Supported SSO Providers + +| Provider | Value | Environment Variables | +| --------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- | +| Google | `google` | `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET` | +| GitHub | `github` | `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET` | +| Microsoft | `microsoft` | `AUTH_MICROSOFT_ID`, `AUTH_MICROSOFT_SECRET` | +| AWS Cognito | `cognito` | `AUTH_COGNITO_ID`, `AUTH_COGNITO_SECRET`, `AUTH_COGNITO_ISSUER` | +| Auth0 | `auth0` | `AUTH_AUTH0_ID`, `AUTH_AUTH0_SECRET`, `AUTH_AUTH0_ISSUER` | +| Authelia | `authelia` | `AUTH_AUTHELIA_ID`, `AUTH_AUTHELIA_SECRET`, `AUTH_AUTHELIA_ISSUER` | +| Authentik | `authentik` | `AUTH_AUTHENTIK_ID`, `AUTH_AUTHENTIK_SECRET`, `AUTH_AUTHENTIK_ISSUER` | +| Casdoor | `casdoor` | `AUTH_CASDOOR_ID`, `AUTH_CASDOOR_SECRET`, `AUTH_CASDOOR_ISSUER` | +| Cloudflare Zero Trust | `cloudflare-zero-trust` | `AUTH_CLOUDFLARE_ZERO_TRUST_ID`, `AUTH_CLOUDFLARE_ZERO_TRUST_SECRET`, `AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER` | +| Keycloak | `keycloak` | `AUTH_KEYCLOAK_ID`, `AUTH_KEYCLOAK_SECRET`, `AUTH_KEYCLOAK_ISSUER` | +| Logto | `logto` | `AUTH_LOGTO_ID`, `AUTH_LOGTO_SECRET`, `AUTH_LOGTO_ISSUER` | +| Okta | `okta` | `AUTH_OKTA_ID`, `AUTH_OKTA_SECRET`, `AUTH_OKTA_ISSUER` | +| ZITADEL | `zitadel` | `AUTH_ZITADEL_ID`, `AUTH_ZITADEL_SECRET`, `AUTH_ZITADEL_ISSUER` | +| Generic OIDC | `generic-oidc` | `AUTH_GENERIC_OIDC_ID`, `AUTH_GENERIC_OIDC_SECRET`, `AUTH_GENERIC_OIDC_ISSUER` | +| Feishu | `feishu` | `AUTH_FEISHU_APP_ID`, `AUTH_FEISHU_APP_SECRET` | +| WeChat | `wechat` | `AUTH_WECHAT_ID`, `AUTH_WECHAT_SECRET` | + +### Callback URL Format + +When configuring OAuth providers, use the following callback URL format: + +- **Development**: `http://localhost:3210/api/auth/callback/{provider}` +- **Production**: `https://yourdomain.com/api/auth/callback/{provider}` + +### Email Service Configuration + +If you want to enable email verification or password reset features, you need to configure SMTP settings: + +| Environment Variable | Type | Description | +| ------------------------------------- | -------- | ----------------------------------------------------------------- | +| `NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION` | Optional | Set to `1` to require email verification before users can sign in | +| `SMTP_HOST` | Required | SMTP server hostname (e.g., `smtp.gmail.com`) | +| `SMTP_PORT` | Required | SMTP server port (usually `587` for TLS, `465` for SSL) | +| `SMTP_SECURE` | Optional | Set to `true` for SSL (port 465), `false` for TLS (port 587) | +| `SMTP_USER` | Required | SMTP authentication username | +| `SMTP_PASS` | Required | SMTP authentication password | + + + For detailed provider configuration, refer to the [Next Auth provider documentation](/docs/self-hosting/advanced/auth/next-auth) as most configurations are compatible, or visit the official [Better Auth documentation](https://www.better-auth.com/docs/introduction). + + + + Go to [📘 Environment Variables](/docs/self-hosting/environment-variables/auth#better-auth) for detailed information on all Better Auth variables. + + ## Next Auth Before using NextAuth, please set the following variables in LobeChat's environment variables: diff --git a/docs/self-hosting/advanced/auth.zh-CN.mdx b/docs/self-hosting/advanced/auth.zh-CN.mdx index aa041bfbb5..ae3e4c032f 100644 --- a/docs/self-hosting/advanced/auth.zh-CN.mdx +++ b/docs/self-hosting/advanced/auth.zh-CN.mdx @@ -1,8 +1,9 @@ --- title: LobeChat 身份验证服务配置 -description: 了解如何使用 Clerk 或 Next Auth 配置外部身份验证服务,以统一管理用户授权。支持的身份验证服务包括 Auth0、 Azure ID 等。 +description: 了解如何使用 Better Auth、Clerk 或 Next Auth 配置外部身份验证服务,以统一管理用户授权。支持的身份验证服务包括 Auth0、 Azure ID 等。 tags: - 身份验证服务 + - Better Auth - LobeChat - SSO - Clerk @@ -10,7 +11,7 @@ tags: # 身份验证服务 -LobeChat 支持使用 Clerk 或者 Next Auth 配置外部身份验证服务,供企业 / 组织内部使用,统一管理用户授权。 +LobeChat 支持使用 Better Auth、Clerk 或者 Next Auth 配置外部身份验证服务,供企业 / 组织内部使用,统一管理用户授权。 ## Clerk @@ -20,6 +21,78 @@ LobeChat 与 Clerk 做了深度集成,能够为用户提供一个更加安全 在 LobeChat 的环境变量中设置 `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` 和 `CLERK_SECRET_KEY`,即可开启和使用 Clerk。 +## Better Auth + +[Better Auth](https://www.better-auth.com) 是一个现代化、框架无关的身份验证库,旨在提供全面、安全、灵活的身份验证解决方案。它支持多种认证方式,包括邮箱 / 密码登录、魔法链接登录以及多种 OAuth/SSO 提供商。 + +### 主要特性 + +- **邮箱 / 密码认证**:内置支持传统的邮箱和密码登录,采用安全的密码哈希算法 +- **邮箱验证**:可选的邮箱验证流程,支持自定义邮件模板 +- **魔法链接登录**:通过邮件魔法链接实现无密码认证 +- **OAuth/SSO 支持**:集成 Google、GitHub、Microsoft、AWS Cognito 等主流身份提供商 +- **通用 OIDC/OAuth**:支持任何符合 OpenID Connect 或 OAuth 2.0 标准的提供商 + +### 快速开始 + +要在 LobeChat 中启用 Better Auth,请设置以下环境变量: + +| 环境变量 | 类型 | 描述 | +| -------------------------------- | -- | ------------------------------------------------ | +| `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | 必选 | 设置为 `1` 以启用 Better Auth 服务 | +| `AUTH_SECRET` | 必选 | 用于加密会话令牌的密钥。使用以下命令生成:`openssl rand -base64 32` | +| `NEXT_PUBLIC_AUTH_URL` | 可选 | 浏览器可访问的 Better Auth 回调 URL。仅在默认生成的 URL 不正确时设置 | +| `AUTH_SSO_PROVIDERS` | 可选 | 启用的 SSO 提供商列表,以逗号分隔,例如 `google,github,microsoft` | + +### 支持的 SSO 提供商 + +| 提供商 | 值 | 环境变量 | +| --------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- | +| Google | `google` | `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET` | +| GitHub | `github` | `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET` | +| Microsoft | `microsoft` | `AUTH_MICROSOFT_ID`, `AUTH_MICROSOFT_SECRET` | +| AWS Cognito | `cognito` | `AUTH_COGNITO_ID`, `AUTH_COGNITO_SECRET`, `AUTH_COGNITO_ISSUER` | +| Auth0 | `auth0` | `AUTH_AUTH0_ID`, `AUTH_AUTH0_SECRET`, `AUTH_AUTH0_ISSUER` | +| Authelia | `authelia` | `AUTH_AUTHELIA_ID`, `AUTH_AUTHELIA_SECRET`, `AUTH_AUTHELIA_ISSUER` | +| Authentik | `authentik` | `AUTH_AUTHENTIK_ID`, `AUTH_AUTHENTIK_SECRET`, `AUTH_AUTHENTIK_ISSUER` | +| Casdoor | `casdoor` | `AUTH_CASDOOR_ID`, `AUTH_CASDOOR_SECRET`, `AUTH_CASDOOR_ISSUER` | +| Cloudflare Zero Trust | `cloudflare-zero-trust` | `AUTH_CLOUDFLARE_ZERO_TRUST_ID`, `AUTH_CLOUDFLARE_ZERO_TRUST_SECRET`, `AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER` | +| Keycloak | `keycloak` | `AUTH_KEYCLOAK_ID`, `AUTH_KEYCLOAK_SECRET`, `AUTH_KEYCLOAK_ISSUER` | +| Logto | `logto` | `AUTH_LOGTO_ID`, `AUTH_LOGTO_SECRET`, `AUTH_LOGTO_ISSUER` | +| Okta | `okta` | `AUTH_OKTA_ID`, `AUTH_OKTA_SECRET`, `AUTH_OKTA_ISSUER` | +| ZITADEL | `zitadel` | `AUTH_ZITADEL_ID`, `AUTH_ZITADEL_SECRET`, `AUTH_ZITADEL_ISSUER` | +| Generic OIDC | `generic-oidc` | `AUTH_GENERIC_OIDC_ID`, `AUTH_GENERIC_OIDC_SECRET`, `AUTH_GENERIC_OIDC_ISSUER` | +| 飞书 | `feishu` | `AUTH_FEISHU_APP_ID`, `AUTH_FEISHU_APP_SECRET` | +| 微信 | `wechat` | `AUTH_WECHAT_ID`, `AUTH_WECHAT_SECRET` | + +### 回调 URL 格式 + +配置 OAuth 提供商时,请使用以下回调 URL 格式: + +- **开发环境**:`http://localhost:3210/api/auth/callback/{provider}` +- **生产环境**:`https://yourdomain.com/api/auth/callback/{provider}` + +### 邮件服务配置 + +如果需要启用邮箱验证或密码重置功能,需要配置 SMTP 设置: + +| 环境变量 | 类型 | 描述 | +| ------------------------------------- | -- | ---------------------------------------------- | +| `NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION` | 可选 | 设置为 `1` 以要求用户在登录前验证邮箱 | +| `SMTP_HOST` | 必选 | SMTP 服务器主机名(例如 `smtp.gmail.com`) | +| `SMTP_PORT` | 必选 | SMTP 服务器端口(TLS 通常为 `587`,SSL 为 `465`) | +| `SMTP_SECURE` | 可选 | SSL 设置为 `true`(端口 465),TLS 设置为 `false`(端口 587) | +| `SMTP_USER` | 必选 | SMTP 认证用户名 | +| `SMTP_PASS` | 必选 | SMTP 认证密码 | + + + 详细的提供商配置可参考 [Next Auth 提供商文档](/zh/docs/self-hosting/advanced/auth/next-auth)(大部分配置兼容),或访问官方 [Better Auth 文档](https://www.better-auth.com/docs/introduction)。 + + + + 前往 [📘 环境变量](/zh/docs/self-hosting/environment-variables/auth#better-auth) 可查阅所有 Better Auth 相关变量详情。 + + ## Next Auth 在使用 NextAuth 之前,请先在 LobeChat 的环境变量中设置以下变量: diff --git a/docs/self-hosting/environment-variables/auth.mdx b/docs/self-hosting/environment-variables/auth.mdx index fc3b857551..1267e66322 100644 --- a/docs/self-hosting/environment-variables/auth.mdx +++ b/docs/self-hosting/environment-variables/auth.mdx @@ -1,11 +1,12 @@ --- title: LobeChat Authentication Service Environment Variables description: >- - Explore the essential environment variables for configuring authentication services in LobeChat, including OAuth SSO, NextAuth settings, and provider-specific details. + Explore the essential environment variables for configuring authentication services in LobeChat, including Better Auth, OAuth SSO, NextAuth settings, and provider-specific details. tags: - Authentication Service + - Better Auth - OAuth SSO - Clerk - NextAuth @@ -15,6 +16,191 @@ tags: LobeChat provides a complete authentication service capability when deployed. The following are the relevant environment variables. You can use these environment variables to easily define the identity verification services that need to be enabled in LobeChat. +## Better Auth + +### General Settings + +#### `NEXT_PUBLIC_ENABLE_BETTER_AUTH` + +- Type: Required +- Description: Set to `1` to enable Better Auth service. When enabled, Better Auth will be used for authentication instead of Next Auth or Clerk. +- Default: `-` +- Example: `1` + +#### `AUTH_SECRET` + +- Type: Required +- Description: Key used to encrypt session tokens. Shared between Better Auth and Next Auth. You can generate the key using the command: `openssl rand -base64 32`. +- Default: `-` +- Example: `Tfhi2t2pelSMEA8eaV61KaqPNEndFFdMIxDaJnS1CUI=` + +#### `NEXT_PUBLIC_AUTH_URL` + +- Type: Optional +- Description: The URL accessible from the browser for Better Auth callbacks. Only set this if the default generated URL is incorrect. +- Default: `-` +- Example: `https://example.com` + +#### `NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION` + +- Type: Optional +- Description: Set to `1` to require email verification before users can sign in. Users must verify their email address after registration. +- Default: `0` +- Example: `1` + +#### `AUTH_SSO_PROVIDERS` + +- Type: Optional +- Description: Comma-separated list of enabled SSO providers. The order determines the display order of providers on the login page. +- Default: `-` +- Example: `google,github,microsoft,cognito` + +### Email Service (SMTP) + +These settings are required for email verification and password reset features. + +#### `SMTP_HOST` + +- Type: Required (for email features) +- Description: SMTP server hostname. +- Default: `-` +- Example: `smtp.gmail.com` + +#### `SMTP_PORT` + +- Type: Required (for email features) +- Description: SMTP server port. Usually `587` for TLS or `465` for SSL. +- Default: `-` +- Example: `587` + +#### `SMTP_SECURE` + +- Type: Optional +- Description: Use secure connection. Set to `true` for port 465 (SSL), `false` for port 587 (TLS). +- Default: `false` +- Example: `false` + +#### `SMTP_USER` + +- Type: Required (for email features) +- Description: SMTP authentication username, usually your email address. +- Default: `-` +- Example: `your-email@example.com` + +#### `SMTP_PASS` + +- Type: Required (for email features) +- Description: SMTP authentication password. For Gmail, use an app-specific password. +- Default: `-` +- Example: `your-app-specific-password` + +### Google + +#### `AUTH_GOOGLE_ID` + +- Type: Required +- Description: Client ID of the Google OAuth application. Get it from [Google Cloud Console](https://console.cloud.google.com/apis/credentials). +- Default: `-` +- Example: `123456789.apps.googleusercontent.com` + +#### `AUTH_GOOGLE_SECRET` + +- Type: Required +- Description: Client Secret of the Google OAuth application. +- Default: `-` +- Example: `GOCSPX-xxxxxxxxxxxxxxxxxxxx` + +### GitHub + +#### `AUTH_GITHUB_ID` + +- Type: Required +- Description: Client ID of the GitHub OAuth application. Get it from [GitHub Developer Settings](https://github.com/settings/developers). +- Default: `-` +- Example: `Ov23xxxxxxxxxxxxx` + +#### `AUTH_GITHUB_SECRET` + +- Type: Required +- Description: Client Secret of the GitHub OAuth application. +- Default: `-` +- Example: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +### Microsoft + +#### `AUTH_MICROSOFT_ID` + +- Type: Required +- Description: Client ID of the Microsoft Entra ID (Azure AD) application. Get it from [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade). +- Default: `-` +- Example: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` + +#### `AUTH_MICROSOFT_SECRET` + +- Type: Required +- Description: Client Secret of the Microsoft Entra ID application. +- Default: `-` +- Example: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +### AWS Cognito + +#### `AUTH_COGNITO_ID` + +- Type: Required +- Description: Client ID of the AWS Cognito User Pool App Client. Get it from [AWS Cognito Console](https://console.aws.amazon.com/cognito). +- Default: `-` +- Example: `xxxxxxxxxxxxxxxxxxxxx` + +#### `AUTH_COGNITO_SECRET` + +- Type: Required +- Description: Client Secret of the AWS Cognito App Client. +- Default: `-` +- Example: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +#### `AUTH_COGNITO_ISSUER` + +- Type: Required +- Description: The Cognito User Pool issuer URL. Format: `https://cognito-idp.{region}.amazonaws.com/{userPoolId}` +- Default: `-` +- Example: `https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxxxxxxx` + +### Feishu + +#### `AUTH_FEISHU_APP_ID` + +- Type: Required +- Description: App ID of the Feishu application. Get it from [Feishu Open Platform](https://open.feishu.cn/app). +- Default: `-` +- Example: `cli_xxxxxxxxxxxxxxxx` + +#### `AUTH_FEISHU_APP_SECRET` + +- Type: Required +- Description: App Secret of the Feishu application. +- Default: `-` +- Example: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +### WeChat + +#### `AUTH_WECHAT_ID` + +- Type: Required +- Description: App ID of the WeChat Open Platform application. Get it from [WeChat Open Platform](https://open.weixin.qq.com/). +- Default: `-` +- Example: `wxxxxxxxxxxxxxxxxxxx` + +#### `AUTH_WECHAT_SECRET` + +- Type: Required +- Description: App Secret of the WeChat application. +- Default: `-` +- Example: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + + + For other OIDC-based providers (Auth0, Authelia, Authentik, Casdoor, Cloudflare Zero Trust, Keycloak, Logto, Okta, ZITADEL, Generic OIDC), the environment variables follow the same pattern as Next Auth. See the [Next Auth section](#next-auth) below for details. + + ## Next Auth ### General Settings diff --git a/docs/self-hosting/environment-variables/auth.zh-CN.mdx b/docs/self-hosting/environment-variables/auth.zh-CN.mdx index 805a22fd42..a0918726b6 100644 --- a/docs/self-hosting/environment-variables/auth.zh-CN.mdx +++ b/docs/self-hosting/environment-variables/auth.zh-CN.mdx @@ -1,9 +1,10 @@ --- title: LobeChat 身份验证服务设置 -description: 了解如何配置 LobeChat 的身份验证服务环境变量。 +description: 了解如何配置 LobeChat 的身份验证服务环境变量,包括 Better Auth、OAuth SSO、NextAuth 设置等。 tags: - LobeChat - 身份验证服务 + - Better Auth - 单点登录 - Next Auth - Clerk @@ -13,6 +14,191 @@ tags: LobeChat 在部署时提供了完善的身份验证服务能力,以下是相关的环境变量,你可以使用这些环境变量轻松定义需要在 LobeChat 中开启的身份验证服务。 +## Better Auth + +### 通用设置 + +#### `NEXT_PUBLIC_ENABLE_BETTER_AUTH` + +- 类型:必选 +- 描述:设置为 `1` 以启用 Better Auth 服务。启用后,将使用 Better Auth 进行身份验证,而非 Next Auth 或 Clerk。 +- 默认值:`-` +- 示例:`1` + +#### `AUTH_SECRET` + +- 类型:必选 +- 描述:用于加密会话令牌的密钥,Better Auth 和 Next Auth 共享。使用以下命令生成:`openssl rand -base64 32` +- 默认值:`-` +- 示例:`Tfhi2t2pelSMEA8eaV61KaqPNEndFFdMIxDaJnS1CUI=` + +#### `NEXT_PUBLIC_AUTH_URL` + +- 类型:可选 +- 描述:浏览器可访问的 Better Auth 回调 URL。仅在默认生成的 URL 不正确时设置。 +- 默认值:`-` +- 示例:`https://example.com` + +#### `NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION` + +- 类型:可选 +- 描述:设置为 `1` 以要求用户在登录前验证邮箱。用户注册后必须验证邮箱地址。 +- 默认值:`0` +- 示例:`1` + +#### `AUTH_SSO_PROVIDERS` + +- 类型:可选 +- 描述:启用的 SSO 提供商列表,以逗号分隔。顺序决定了登录页面上提供商的显示顺序。 +- 默认值:`-` +- 示例:`google,github,microsoft,cognito` + +### 邮件服务(SMTP) + +启用邮箱验证和密码重置功能需要配置以下设置。 + +#### `SMTP_HOST` + +- 类型:必选(用于邮件功能) +- 描述:SMTP 服务器主机名。 +- 默认值:`-` +- 示例:`smtp.gmail.com` + +#### `SMTP_PORT` + +- 类型:必选(用于邮件功能) +- 描述:SMTP 服务器端口。TLS 通常为 `587`,SSL 为 `465`。 +- 默认值:`-` +- 示例:`587` + +#### `SMTP_SECURE` + +- 类型:可选 +- 描述:是否使用安全连接。端口 465(SSL)设置为 `true`,端口 587(TLS)设置为 `false`。 +- 默认值:`false` +- 示例:`false` + +#### `SMTP_USER` + +- 类型:必选(用于邮件功能) +- 描述:SMTP 认证用户名,通常是您的邮箱地址。 +- 默认值:`-` +- 示例:`your-email@example.com` + +#### `SMTP_PASS` + +- 类型:必选(用于邮件功能) +- 描述:SMTP 认证密码。Gmail 需使用应用专用密码。 +- 默认值:`-` +- 示例:`your-app-specific-password` + +### Google + +#### `AUTH_GOOGLE_ID` + +- 类型:必选 +- 描述:Google OAuth 应用的 Client ID。在 [Google Cloud Console](https://console.cloud.google.com/apis/credentials) 获取。 +- 默认值:`-` +- 示例:`123456789.apps.googleusercontent.com` + +#### `AUTH_GOOGLE_SECRET` + +- 类型:必选 +- 描述:Google OAuth 应用的 Client Secret。 +- 默认值:`-` +- 示例:`GOCSPX-xxxxxxxxxxxxxxxxxxxx` + +### GitHub + +#### `AUTH_GITHUB_ID` + +- 类型:必选 +- 描述:GitHub OAuth 应用的 Client ID。在 [GitHub Developer Settings](https://github.com/settings/developers) 获取。 +- 默认值:`-` +- 示例:`Ov23xxxxxxxxxxxxx` + +#### `AUTH_GITHUB_SECRET` + +- 类型:必选 +- 描述:GitHub OAuth 应用的 Client Secret。 +- 默认值:`-` +- 示例:`xxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +### Microsoft + +#### `AUTH_MICROSOFT_ID` + +- 类型:必选 +- 描述:Microsoft Entra ID(Azure AD)应用的 Client ID。在 [Azure 门户](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) 获取。 +- 默认值:`-` +- 示例:`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` + +#### `AUTH_MICROSOFT_SECRET` + +- 类型:必选 +- 描述:Microsoft Entra ID 应用的 Client Secret。 +- 默认值:`-` +- 示例:`xxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +### AWS Cognito + +#### `AUTH_COGNITO_ID` + +- 类型:必选 +- 描述:AWS Cognito 用户池应用客户端的 Client ID。在 [AWS Cognito 控制台](https://console.aws.amazon.com/cognito) 获取。 +- 默认值:`-` +- 示例:`xxxxxxxxxxxxxxxxxxxxx` + +#### `AUTH_COGNITO_SECRET` + +- 类型:必选 +- 描述:AWS Cognito 应用客户端的 Client Secret。 +- 默认值:`-` +- 示例:`xxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +#### `AUTH_COGNITO_ISSUER` + +- 类型:必选 +- 描述:Cognito 用户池的颁发者 URL。格式:`https://cognito-idp.{region}.amazonaws.com/{userPoolId}` +- 默认值:`-` +- 示例:`https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxxxxxxx` + +### 飞书 + +#### `AUTH_FEISHU_APP_ID` + +- 类型:必选 +- 描述:飞书应用的 App ID。在 [飞书开放平台](https://open.feishu.cn/app) 获取。 +- 默认值:`-` +- 示例:`cli_xxxxxxxxxxxxxxxx` + +#### `AUTH_FEISHU_APP_SECRET` + +- 类型:必选 +- 描述:飞书应用的 App Secret。 +- 默认值:`-` +- 示例:`xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +### 微信 + +#### `AUTH_WECHAT_ID` + +- 类型:必选 +- 描述:微信开放平台应用的 App ID。在 [微信开放平台](https://open.weixin.qq.com/) 获取。 +- 默认值:`-` +- 示例:`wxxxxxxxxxxxxxxxxxxx` + +#### `AUTH_WECHAT_SECRET` + +- 类型:必选 +- 描述:微信应用的 App Secret。 +- 默认值:`-` +- 示例:`xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` + + + 其他基于 OIDC 的提供商(Auth0、Authelia、Authentik、Casdoor、Cloudflare Zero Trust、Keycloak、Logto、Okta、ZITADEL、Generic OIDC)的环境变量配置与 Next Auth 相同。详情请参阅下方的 [Next Auth 章节](#next-auth)。 + + ## Next Auth ### 通用设置 diff --git a/locales/en-US/auth.json b/locales/en-US/auth.json index c464d8c4a2..543f58ec38 100644 --- a/locales/en-US/auth.json +++ b/locales/en-US/auth.json @@ -52,6 +52,84 @@ "required": "This field cannot be empty" } }, + "betterAuth": { + "errors": { + "emailInvalid": "Please enter a valid email address", + "emailNotRegistered": "This email is not registered", + "emailNotVerified": "Email not verified, please verify your email first", + "emailRequired": "Please enter your email address", + "firstNameRequired": "Please enter your first name", + "lastNameRequired": "Please enter your last name", + "loginFailed": "Login failed, please check your email and password", + "passwordFormat": "Password must contain both letters and numbers", + "passwordMaxLength": "Password must not exceed 64 characters", + "passwordMinLength": "Password must be at least 8 characters", + "passwordRequired": "Please enter your password", + "usernameRequired": "Please enter your username" + }, + "signin": { + "backToEmail": "Back to change email", + "continueWithCognito": "Continue with AWS Cognito", + "continueWithGithub": "Continue with GitHub", + "continueWithGoogle": "Continue with Google", + "continueWithMicrosoft": "Continue with Microsoft", + "emailPlaceholder": "Enter your email address", + "emailStep": { + "subtitle": "Enter your email address to continue", + "title": "Sign In" + }, + "error": "Sign in failed, please check your email and password", + "forgotPassword": "Forgot password?", + "forgotPasswordError": "Failed to send password reset link", + "forgotPasswordSent": "Password reset link sent, please check your email", + "nextStep": "Next", + "noAccount": "Don't have an account?", + "orContinueWith": "OR", + "passwordPlaceholder": "Enter your password", + "passwordStep": { + "subtitle": "Enter your password to continue" + }, + "signupLink": "Sign up now", + "socialError": "Social sign in failed, please try again", + "magicLinkButton": "Send sign-in link", + "magicLinkSent": "Sign-in link sent, please check your email", + "magicLinkError": "Failed to send sign-in link, please try again later", + "submit": "Sign In" + }, + "signup": { + "emailPlaceholder": "Enter your email address", + "error": "Sign up failed, please try again", + "firstNamePlaceholder": "First Name", + "hasAccount": "Already have an account?", + "lastNamePlaceholder": "Last Name", + "passwordPlaceholder": "Enter your password", + "signinLink": "Sign in now", + "submit": "Sign Up", + "success": "Sign up successful! Please check your email for verification", + "subtitle": "Join LobeChat Community", + "title": "Create Account", + "usernamePlaceholder": "Enter your username" + }, + "verifyEmail": { + "backToSignIn": "Back to Sign In", + "checkSpam": "If you don't receive the email, please check your spam folder", + "description": "We've sent a verification email to {{email}}", + "title": "Verify Your Email" + }, + "resetPassword": { + "backToSignIn": "Back to Sign In", + "confirmPasswordPlaceholder": "Confirm new password", + "confirmPasswordRequired": "Please confirm your new password", + "description": "Please enter your new password", + "error": "Failed to reset password, please try again", + "invalidToken": "Invalid or expired reset link", + "newPasswordPlaceholder": "Enter new password", + "passwordMismatch": "Passwords do not match", + "submit": "Reset Password", + "success": "Password reset successful, please sign in with your new password", + "title": "Reset Password" + } + }, "date": { "prevMonth": "Last Month", "recent30Days": "Last 30 Days" @@ -86,8 +164,23 @@ "loginOrSignup": "Log In / Sign Up", "profile": { "avatar": "Avatar", + "cancel": "Cancel", + "changePassword": "Reset password", "email": "Email Address", + "fullName": "Fullname", + "fullNameInputHint": "Please enter your new fullname", + "password": "Password", + "resetPasswordError": "Failed to send password reset link", + "resetPasswordSent": "Password reset link sent, please check your email", + "save": "Save", + "title": "Profile Details", + "updateAvatar": "Update avatar", + "updateFullName": "Update fullname", "sso": { + "link": { + "button": "Connect Account", + "success": "Account linked successfully" + }, "loading": "Loading linked third-party accounts", "providers": "Connected Accounts", "unlink": { diff --git a/locales/zh-CN/auth.json b/locales/zh-CN/auth.json index d9683f8b2a..5e0e831dcc 100644 --- a/locales/zh-CN/auth.json +++ b/locales/zh-CN/auth.json @@ -52,6 +52,97 @@ "required": "内容不得为空" } }, + "betterAuth": { + "errors": { + "emailInvalid": "请输入有效的邮箱地址", + "emailNotRegistered": "该邮箱尚未注册", + "emailNotVerified": "邮箱尚未验证,请先验证邮箱", + "emailRequired": "请输入邮箱地址", + "firstNameRequired": "请输入名字", + "lastNameRequired": "请输入姓氏", + "loginFailed": "登录失败,请检查邮箱和密码", + "passwordFormat": "密码必须同时包含字母和数字", + "passwordMaxLength": "密码最多不超过 64 个字符", + "passwordMinLength": "密码至少需要 8 个字符", + "passwordRequired": "请输入密码", + "usernameRequired": "请输入用户名" + }, + "signin": { + "backToEmail": "返回修改邮箱", + "continueWithCognito": "使用 AWS Cognito 登录", + "continueWithGithub": "使用 GitHub 登录", + "continueWithGoogle": "使用 Google 登录", + "continueWithMicrosoft": "使用 Microsoft 登录", + "continueWithAuth0": "使用 Auth0 登录", + "continueWithAuthelia": "使用 Authelia 登录", + "continueWithAuthentik": "使用 Authentik 登录", + "continueWithCasdoor": "使用 Casdoor 登录", + "continueWithCloudflareZeroTrust": "使用 Cloudflare Zero Trust 登录", + "continueWithOIDC": "使用 OIDC 登录", + "continueWithKeycloak": "使用 Keycloak 登录", + "continueWithLogto": "使用 Logto 登录", + "continueWithOkta": "使用 Okta 登录", + "continueWithZitadel": "使用 Zitadel 登录", + "continueWithFeishu": "使用飞书登录", + "continueWithWechat": "使用微信登录", + "emailPlaceholder": "请输入邮箱地址", + "emailStep": { + "subtitle": "请输入您的邮箱地址以继续", + "title": "登录" + }, + "error": "登录失败,请检查邮箱和密码", + "forgotPassword": "忘记密码?", + "forgotPasswordError": "发送重置密码链接失败", + "forgotPasswordSent": "重置密码链接已发送,请检查邮箱", + "nextStep": "下一步", + "noAccount": "还没有账号?", + "orContinueWith": "或", + "passwordPlaceholder": "请输入密码", + "passwordStep": { + "subtitle": "请输入密码以继续" + }, + "signupLink": "立即注册", + "socialError": "社交登录失败,请重试", + "socialOnlyHint": "该邮箱使用社交账号注册,请使用社交账号登录", + "magicLinkButton": "发送登录链接", + "magicLinkSent": "登录链接已发送,请检查邮箱", + "magicLinkError": "发送登录链接失败,请稍后再试", + "submit": "登录" + }, + "signup": { + "emailPlaceholder": "请输入邮箱地址", + "error": "注册失败,请重试", + "firstNamePlaceholder": "名字", + "hasAccount": "已有账号?", + "lastNamePlaceholder": "姓氏", + "passwordPlaceholder": "请输入密码", + "signinLink": "立即登录", + "submit": "注册", + "success": "注册成功!请检查您的邮箱验证邮件", + "subtitle": "加入 LobeChat 社区", + "title": "创建账号", + "usernamePlaceholder": "请输入用户名" + }, + "verifyEmail": { + "backToSignIn": "返回登录", + "checkSpam": "如果没有收到邮件,请检查垃圾邮件文件夹", + "description": "我们已向 {{email}} 发送了验证邮件", + "title": "验证您的邮箱" + }, + "resetPassword": { + "backToSignIn": "返回登录", + "confirmPasswordPlaceholder": "确认新密码", + "confirmPasswordRequired": "请确认新密码", + "description": "请输入您的新密码", + "error": "重置密码失败,请重试", + "invalidToken": "无效或已过期的重置链接", + "newPasswordPlaceholder": "输入新密码", + "passwordMismatch": "两次输入的密码不一致", + "submit": "重置密码", + "success": "密码重置成功,请使用新密码登录", + "title": "重置密码" + } + }, "date": { "prevMonth": "上个月", "recent30Days": "最近30天" @@ -86,12 +177,27 @@ "loginOrSignup": "登录 / 注册", "profile": { "avatar": "头像", + "cancel": "取消", + "changePassword": "重置密码", "email": "电子邮件地址", + "fullName": "全名", + "fullNameInputHint": "请输入新的全名", + "password": "密码", + "resetPasswordError": "发送密码重置链接失败", + "resetPasswordSent": "密码重置链接已发送,请检查邮箱", + "save": "保存", + "title": "个人资料详情", + "updateAvatar": "更新头像", + "updateFullName": "更新全名", "sso": { + "link": { + "button": "连接帐户", + "success": "账户关联成功" + }, "loading": "正在加载已绑定的第三方账户", "providers": "连接的帐户", "unlink": { - "description": "解绑后,您将无法使用 {{provider}} 账户“{{providerAccountId}}”登录。如果您需要重新绑定 {{provider}} 账户到当前账户,请确保 {{provider}} 账户的邮件地址为 {{email}} ,我们会在登陆时为你自动绑定到当前登录账户。", + "description": "解绑后,您将无法使用 {{provider}} 账户 「{{providerAccountId}}」 登录。如果您需要重新绑定 {{provider}} 账户到当前账户,请确保 {{provider}} 账户的邮件地址为 {{email}} ,我们会在登陆时为你自动绑定到当前登录账户。", "forbidden": "您至少需要保留一个第三方账户绑定。", "title": "是否解绑该第三方账户 {{provider}} ?" } diff --git a/package.json b/package.json index 428095d246..85df9fb3f5 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "prepare": "husky", "prettier": "prettier -c --write \"**/**\"", "pull": "git pull", - "reinstall": "rm -rf pnpm-lock.yaml && rm -rf node_modules && pnpm -r exec rm -rf node_modules && pnpm install", + "reinstall": "rm -rf .next && rm -rf node_modules && pnpm -r exec rm -rf node_modules && pnpm install", "reinstall:desktop": "rm -rf pnpm-lock.yaml && rm -rf node_modules && pnpm -r exec rm -rf node_modules && pnpm install --node-linker=hoisted", "release": "semantic-release", "self-hosting:docker": "docker build -t lobehub:local .", @@ -199,6 +199,7 @@ "ahooks": "^3.9.6", "antd": "^5.28.1", "antd-style": "^3.7.1", + "better-auth": "^1.4.1", "brotli-wasm": "^3.0.1", "chroma-js": "^3.1.2", "cmdk": "^1.1.1", @@ -240,6 +241,7 @@ "next-mdx-remote": "^5.0.0", "nextjs-toploader": "^3.9.17", "node-machine-id": "^1.1.12", + "nodemailer": "^7.0.10", "numeral": "^2.0.6", "nuqs": "^2.7.3", "officeparser": "5.1.1", @@ -334,6 +336,7 @@ "@types/lodash": "^4.17.20", "@types/lodash-es": "^4.17.12", "@types/node": "^24.10.1", + "@types/nodemailer": "^7.0.3", "@types/numeral": "^2.0.5", "@types/oidc-provider": "^9.5.0", "@types/pdfkit": "^0.17.3", diff --git a/packages/const/src/auth.ts b/packages/const/src/auth.ts index c177394e73..6e676ee30f 100644 --- a/packages/const/src/auth.ts +++ b/packages/const/src/auth.ts @@ -1,6 +1,7 @@ export const enableClerk = !!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; +export const enableBetterAuth = process.env.NEXT_PUBLIC_ENABLE_BETTER_AUTH === '1'; export const enableNextAuth = process.env.NEXT_PUBLIC_ENABLE_NEXT_AUTH === '1'; -export const enableAuth = enableClerk || enableNextAuth || false; +export const enableAuth = enableClerk || enableBetterAuth || enableNextAuth || false; export const LOBE_CHAT_AUTH_HEADER = 'X-lobe-chat-auth'; export const LOBE_CHAT_OIDC_AUTH_HEADER = 'Oidc-Auth'; diff --git a/packages/database/migrations/0049_better_auth.sql b/packages/database/migrations/0049_better_auth.sql new file mode 100644 index 0000000000..aa42af6b53 --- /dev/null +++ b/packages/database/migrations/0049_better_auth.sql @@ -0,0 +1,49 @@ +CREATE TABLE IF NOT EXISTS "accounts" ( + "access_token" text, + "access_token_expires_at" timestamp, + "account_id" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "id" text PRIMARY KEY NOT NULL, + "id_token" text, + "password" text, + "provider_id" text NOT NULL, + "refresh_token" text, + "refresh_token_expires_at" timestamp, + "scope" text, + "updated_at" timestamp NOT NULL, + "user_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "auth_sessions" ( + "created_at" timestamp DEFAULT now() NOT NULL, + "expires_at" timestamp NOT NULL, + "id" text PRIMARY KEY NOT NULL, + "ip_address" text, + "token" text NOT NULL, + "updated_at" timestamp NOT NULL, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "auth_sessions_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "verifications" ( + "created_at" timestamp DEFAULT now() NOT NULL, + "expires_at" timestamp NOT NULL, + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "value" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "email_verified" boolean DEFAULT false NOT NULL;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/packages/database/migrations/meta/0048_snapshot.json b/packages/database/migrations/meta/0048_snapshot.json index 8def9d94ba..b4cb33be56 100644 --- a/packages/database/migrations/meta/0048_snapshot.json +++ b/packages/database/migrations/meta/0048_snapshot.json @@ -1,8 +1,17 @@ { - "id": "183c9871-d705-4138-9879-929fc92dc2c1", - "prevId": "20a4c30c-04f0-4993-b493-0ae6a3144f9e", - "version": "7", + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, "dialect": "postgresql", + "enums": {}, + "id": "183c9871-d705-4138-9879-929fc92dc2c1", + "policies": {}, + "prevId": "20a4c30c-04f0-4993-b493-0ae6a3144f9e", + "roles": {}, + "schemas": {}, + "sequences": {}, "tables": { "public.agents": { "name": "agents", @@ -225,12 +234,8 @@ "name": "agents_user_id_users_id_fk", "tableFrom": "agents", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -240,9 +245,7 @@ "agents_slug_unique": { "name": "agents_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -322,12 +325,8 @@ "name": "agents_files_file_id_files_id_fk", "tableFrom": "agents_files", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -335,12 +334,8 @@ "name": "agents_files_agent_id_agents_id_fk", "tableFrom": "agents_files", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -348,12 +343,8 @@ "name": "agents_files_user_id_users_id_fk", "tableFrom": "agents_files", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -361,11 +352,7 @@ "compositePrimaryKeys": { "agents_files_file_id_agent_id_user_id_pk": { "name": "agents_files_file_id_agent_id_user_id_pk", - "columns": [ - "file_id", - "agent_id", - "user_id" - ] + "columns": ["file_id", "agent_id", "user_id"] } }, "uniqueConstraints": {}, @@ -446,12 +433,8 @@ "name": "agents_knowledge_bases_agent_id_agents_id_fk", "tableFrom": "agents_knowledge_bases", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -459,12 +442,8 @@ "name": "agents_knowledge_bases_knowledge_base_id_knowledge_bases_id_fk", "tableFrom": "agents_knowledge_bases", "tableTo": "knowledge_bases", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -472,12 +451,8 @@ "name": "agents_knowledge_bases_user_id_users_id_fk", "tableFrom": "agents_knowledge_bases", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -485,10 +460,7 @@ "compositePrimaryKeys": { "agents_knowledge_bases_agent_id_knowledge_base_id_pk": { "name": "agents_knowledge_bases_agent_id_knowledge_base_id_pk", - "columns": [ - "agent_id", - "knowledge_base_id" - ] + "columns": ["agent_id", "knowledge_base_id"] } }, "uniqueConstraints": {}, @@ -634,12 +606,8 @@ "name": "ai_models_user_id_users_id_fk", "tableFrom": "ai_models", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -647,11 +615,7 @@ "compositePrimaryKeys": { "ai_models_id_provider_id_user_id_pk": { "name": "ai_models_id_provider_id_user_id_pk", - "columns": [ - "id", - "provider_id", - "user_id" - ] + "columns": ["id", "provider_id", "user_id"] } }, "uniqueConstraints": {}, @@ -769,12 +733,8 @@ "name": "ai_providers_user_id_users_id_fk", "tableFrom": "ai_providers", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -782,10 +742,7 @@ "compositePrimaryKeys": { "ai_providers_id_user_id_pk": { "name": "ai_providers_id_user_id_pk", - "columns": [ - "id", - "user_id" - ] + "columns": ["id", "user_id"] } }, "uniqueConstraints": {}, @@ -879,12 +836,8 @@ "name": "api_keys_user_id_users_id_fk", "tableFrom": "api_keys", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -894,9 +847,7 @@ "api_keys_key_unique": { "name": "api_keys_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -972,12 +923,8 @@ "name": "async_tasks_user_id_users_id_fk", "tableFrom": "async_tasks", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1091,12 +1038,8 @@ "name": "chat_groups_user_id_users_id_fk", "tableFrom": "chat_groups", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1104,12 +1047,8 @@ "name": "chat_groups_group_id_session_groups_id_fk", "tableFrom": "chat_groups", "tableTo": "session_groups", - "columnsFrom": [ - "group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["group_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1191,12 +1130,8 @@ "name": "chat_groups_agents_chat_group_id_chat_groups_id_fk", "tableFrom": "chat_groups_agents", "tableTo": "chat_groups", - "columnsFrom": [ - "chat_group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_group_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1204,12 +1139,8 @@ "name": "chat_groups_agents_agent_id_agents_id_fk", "tableFrom": "chat_groups_agents", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1217,12 +1148,8 @@ "name": "chat_groups_agents_user_id_users_id_fk", "tableFrom": "chat_groups_agents", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1230,10 +1157,7 @@ "compositePrimaryKeys": { "chat_groups_agents_chat_group_id_agent_id_pk": { "name": "chat_groups_agents_chat_group_id_agent_id_pk", - "columns": [ - "chat_group_id", - "agent_id" - ] + "columns": ["chat_group_id", "agent_id"] } }, "uniqueConstraints": {}, @@ -1479,12 +1403,8 @@ "name": "documents_file_id_files_id_fk", "tableFrom": "documents", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -1492,12 +1412,8 @@ "name": "documents_parent_id_documents_id_fk", "tableFrom": "documents", "tableTo": "documents", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -1505,12 +1421,8 @@ "name": "documents_user_id_users_id_fk", "tableFrom": "documents", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1683,12 +1595,8 @@ "name": "files_user_id_users_id_fk", "tableFrom": "files", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1696,12 +1604,8 @@ "name": "files_file_hash_global_files_hash_id_fk", "tableFrom": "files", "tableTo": "global_files", - "columnsFrom": [ - "file_hash" - ], - "columnsTo": [ - "hash_id" - ], + "columnsFrom": ["file_hash"], + "columnsTo": ["hash_id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1709,12 +1613,8 @@ "name": "files_parent_id_documents_id_fk", "tableFrom": "files", "tableTo": "documents", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -1722,12 +1622,8 @@ "name": "files_chunk_task_id_async_tasks_id_fk", "tableFrom": "files", "tableTo": "async_tasks", - "columnsFrom": [ - "chunk_task_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chunk_task_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -1735,12 +1631,8 @@ "name": "files_embedding_task_id_async_tasks_id_fk", "tableFrom": "files", "tableTo": "async_tasks", - "columnsFrom": [ - "embedding_task_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["embedding_task_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1812,12 +1704,8 @@ "name": "global_files_creator_users_id_fk", "tableFrom": "global_files", "tableTo": "users", - "columnsFrom": [ - "creator" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1864,12 +1752,8 @@ "name": "knowledge_base_files_knowledge_base_id_knowledge_bases_id_fk", "tableFrom": "knowledge_base_files", "tableTo": "knowledge_bases", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1877,12 +1761,8 @@ "name": "knowledge_base_files_file_id_files_id_fk", "tableFrom": "knowledge_base_files", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1890,12 +1770,8 @@ "name": "knowledge_base_files_user_id_users_id_fk", "tableFrom": "knowledge_base_files", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1903,10 +1779,7 @@ "compositePrimaryKeys": { "knowledge_base_files_knowledge_base_id_file_id_pk": { "name": "knowledge_base_files_knowledge_base_id_file_id_pk", - "columns": [ - "knowledge_base_id", - "file_id" - ] + "columns": ["knowledge_base_id", "file_id"] } }, "uniqueConstraints": {}, @@ -2023,12 +1896,8 @@ "name": "knowledge_bases_user_id_users_id_fk", "tableFrom": "knowledge_bases", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2131,12 +2000,8 @@ "name": "generation_batches_user_id_users_id_fk", "tableFrom": "generation_batches", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2144,12 +2009,8 @@ "name": "generation_batches_generation_topic_id_generation_topics_id_fk", "tableFrom": "generation_batches", "tableTo": "generation_topics", - "columnsFrom": [ - "generation_topic_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["generation_topic_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2216,12 +2077,8 @@ "name": "generation_topics_user_id_users_id_fk", "tableFrom": "generation_topics", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2306,12 +2163,8 @@ "name": "generations_user_id_users_id_fk", "tableFrom": "generations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2319,12 +2172,8 @@ "name": "generations_generation_batch_id_generation_batches_id_fk", "tableFrom": "generations", "tableTo": "generation_batches", - "columnsFrom": [ - "generation_batch_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["generation_batch_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2332,12 +2181,8 @@ "name": "generations_async_task_id_async_tasks_id_fk", "tableFrom": "generations", "tableTo": "async_tasks", - "columnsFrom": [ - "async_task_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["async_task_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -2345,12 +2190,8 @@ "name": "generations_file_id_files_id_fk", "tableFrom": "generations", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2390,12 +2231,8 @@ "name": "message_chunks_message_id_messages_id_fk", "tableFrom": "message_chunks", "tableTo": "messages", - "columnsFrom": [ - "message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2403,12 +2240,8 @@ "name": "message_chunks_chunk_id_chunks_id_fk", "tableFrom": "message_chunks", "tableTo": "chunks", - "columnsFrom": [ - "chunk_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2416,12 +2249,8 @@ "name": "message_chunks_user_id_users_id_fk", "tableFrom": "message_chunks", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2429,10 +2258,7 @@ "compositePrimaryKeys": { "message_chunks_chunk_id_message_id_pk": { "name": "message_chunks_chunk_id_message_id_pk", - "columns": [ - "chunk_id", - "message_id" - ] + "columns": ["chunk_id", "message_id"] } }, "uniqueConstraints": {}, @@ -2557,12 +2383,8 @@ "name": "message_groups_topic_id_topics_id_fk", "tableFrom": "message_groups", "tableTo": "topics", - "columnsFrom": [ - "topic_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2570,12 +2392,8 @@ "name": "message_groups_user_id_users_id_fk", "tableFrom": "message_groups", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2583,12 +2401,8 @@ "name": "message_groups_parent_group_id_message_groups_id_fk", "tableFrom": "message_groups", "tableTo": "message_groups", - "columnsFrom": [ - "parent_group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_group_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2596,12 +2410,8 @@ "name": "message_groups_parent_message_id_messages_id_fk", "tableFrom": "message_groups", "tableTo": "messages", - "columnsFrom": [ - "parent_message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_message_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2712,12 +2522,8 @@ "name": "message_plugins_id_messages_id_fk", "tableFrom": "message_plugins", "tableTo": "messages", - "columnsFrom": [ - "id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2725,12 +2531,8 @@ "name": "message_plugins_user_id_users_id_fk", "tableFrom": "message_plugins", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2817,12 +2619,8 @@ "name": "message_queries_message_id_messages_id_fk", "tableFrom": "message_queries", "tableTo": "messages", - "columnsFrom": [ - "message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2830,12 +2628,8 @@ "name": "message_queries_user_id_users_id_fk", "tableFrom": "message_queries", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2843,12 +2637,8 @@ "name": "message_queries_embeddings_id_embeddings_id_fk", "tableFrom": "message_queries", "tableTo": "embeddings", - "columnsFrom": [ - "embeddings_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["embeddings_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2900,12 +2690,8 @@ "name": "message_query_chunks_id_messages_id_fk", "tableFrom": "message_query_chunks", "tableTo": "messages", - "columnsFrom": [ - "id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2913,12 +2699,8 @@ "name": "message_query_chunks_query_id_message_queries_id_fk", "tableFrom": "message_query_chunks", "tableTo": "message_queries", - "columnsFrom": [ - "query_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["query_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2926,12 +2708,8 @@ "name": "message_query_chunks_chunk_id_chunks_id_fk", "tableFrom": "message_query_chunks", "tableTo": "chunks", - "columnsFrom": [ - "chunk_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2939,12 +2717,8 @@ "name": "message_query_chunks_user_id_users_id_fk", "tableFrom": "message_query_chunks", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2952,11 +2726,7 @@ "compositePrimaryKeys": { "message_query_chunks_chunk_id_id_query_id_pk": { "name": "message_query_chunks_chunk_id_id_query_id_pk", - "columns": [ - "chunk_id", - "id", - "query_id" - ] + "columns": ["chunk_id", "id", "query_id"] } }, "uniqueConstraints": {}, @@ -3033,12 +2803,8 @@ "name": "message_tts_id_messages_id_fk", "tableFrom": "message_tts", "tableTo": "messages", - "columnsFrom": [ - "id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3046,12 +2812,8 @@ "name": "message_tts_file_id_files_id_fk", "tableFrom": "message_tts", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3059,12 +2821,8 @@ "name": "message_tts_user_id_users_id_fk", "tableFrom": "message_tts", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3144,12 +2902,8 @@ "name": "message_translates_id_messages_id_fk", "tableFrom": "message_translates", "tableTo": "messages", - "columnsFrom": [ - "id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3157,12 +2911,8 @@ "name": "message_translates_user_id_users_id_fk", "tableFrom": "message_translates", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3492,12 +3242,8 @@ "name": "messages_user_id_users_id_fk", "tableFrom": "messages", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3505,12 +3251,8 @@ "name": "messages_session_id_sessions_id_fk", "tableFrom": "messages", "tableTo": "sessions", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3518,12 +3260,8 @@ "name": "messages_topic_id_topics_id_fk", "tableFrom": "messages", "tableTo": "topics", - "columnsFrom": [ - "topic_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3531,12 +3269,8 @@ "name": "messages_thread_id_threads_id_fk", "tableFrom": "messages", "tableTo": "threads", - "columnsFrom": [ - "thread_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["thread_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3544,12 +3278,8 @@ "name": "messages_parent_id_messages_id_fk", "tableFrom": "messages", "tableTo": "messages", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -3557,12 +3287,8 @@ "name": "messages_quota_id_messages_id_fk", "tableFrom": "messages", "tableTo": "messages", - "columnsFrom": [ - "quota_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["quota_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -3570,12 +3296,8 @@ "name": "messages_agent_id_agents_id_fk", "tableFrom": "messages", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -3583,12 +3305,8 @@ "name": "messages_group_id_chat_groups_id_fk", "tableFrom": "messages", "tableTo": "chat_groups", - "columnsFrom": [ - "group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["group_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -3596,12 +3314,8 @@ "name": "messages_message_group_id_message_groups_id_fk", "tableFrom": "messages", "tableTo": "message_groups", - "columnsFrom": [ - "message_group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_group_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3641,12 +3355,8 @@ "name": "messages_files_file_id_files_id_fk", "tableFrom": "messages_files", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3654,12 +3364,8 @@ "name": "messages_files_message_id_messages_id_fk", "tableFrom": "messages_files", "tableTo": "messages", - "columnsFrom": [ - "message_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["message_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3667,12 +3373,8 @@ "name": "messages_files_user_id_users_id_fk", "tableFrom": "messages_files", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3680,10 +3382,7 @@ "compositePrimaryKeys": { "messages_files_file_id_message_id_pk": { "name": "messages_files_file_id_message_id_pk", - "columns": [ - "file_id", - "message_id" - ] + "columns": ["file_id", "message_id"] } }, "uniqueConstraints": {}, @@ -3768,12 +3467,8 @@ "name": "nextauth_accounts_userId_users_id_fk", "tableFrom": "nextauth_accounts", "tableTo": "users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3781,10 +3476,7 @@ "compositePrimaryKeys": { "nextauth_accounts_provider_providerAccountId_pk": { "name": "nextauth_accounts_provider_providerAccountId_pk", - "columns": [ - "provider", - "providerAccountId" - ] + "columns": ["provider", "providerAccountId"] } }, "uniqueConstraints": {}, @@ -3851,12 +3543,8 @@ "name": "nextauth_authenticators_userId_users_id_fk", "tableFrom": "nextauth_authenticators", "tableTo": "users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3864,19 +3552,14 @@ "compositePrimaryKeys": { "nextauth_authenticators_userId_credentialID_pk": { "name": "nextauth_authenticators_userId_credentialID_pk", - "columns": [ - "userId", - "credentialID" - ] + "columns": ["userId", "credentialID"] } }, "uniqueConstraints": { "nextauth_authenticators_credentialID_unique": { "name": "nextauth_authenticators_credentialID_unique", "nullsNotDistinct": false, - "columns": [ - "credentialID" - ] + "columns": ["credentialID"] } }, "policies": {}, @@ -3912,12 +3595,8 @@ "name": "nextauth_sessions_userId_users_id_fk", "tableFrom": "nextauth_sessions", "tableTo": "users", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3956,10 +3635,7 @@ "compositePrimaryKeys": { "nextauth_verificationtokens_identifier_token_pk": { "name": "nextauth_verificationtokens_identifier_token_pk", - "columns": [ - "identifier", - "token" - ] + "columns": ["identifier", "token"] } }, "uniqueConstraints": {}, @@ -4093,12 +3769,8 @@ "name": "oidc_access_tokens_user_id_users_id_fk", "tableFrom": "oidc_access_tokens", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4183,12 +3855,8 @@ "name": "oidc_authorization_codes_user_id_users_id_fk", "tableFrom": "oidc_authorization_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4380,12 +4048,8 @@ "name": "oidc_consents_user_id_users_id_fk", "tableFrom": "oidc_consents", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4393,12 +4057,8 @@ "name": "oidc_consents_client_id_oidc_clients_id_fk", "tableFrom": "oidc_consents", "tableTo": "oidc_clients", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["client_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4406,10 +4066,7 @@ "compositePrimaryKeys": { "oidc_consents_user_id_client_id_pk": { "name": "oidc_consents_user_id_client_id_pk", - "columns": [ - "user_id", - "client_id" - ] + "columns": ["user_id", "client_id"] } }, "uniqueConstraints": {}, @@ -4497,12 +4154,8 @@ "name": "oidc_device_codes_user_id_users_id_fk", "tableFrom": "oidc_device_codes", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4581,12 +4234,8 @@ "name": "oidc_grants_user_id_users_id_fk", "tableFrom": "oidc_grants", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4723,12 +4372,8 @@ "name": "oidc_refresh_tokens_user_id_users_id_fk", "tableFrom": "oidc_refresh_tokens", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4795,12 +4440,8 @@ "name": "oidc_sessions_user_id_users_id_fk", "tableFrom": "oidc_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4929,12 +4570,8 @@ "name": "chunks_user_id_users_id_fk", "tableFrom": "chunks", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4987,12 +4624,8 @@ "name": "document_chunks_document_id_documents_id_fk", "tableFrom": "document_chunks", "tableTo": "documents", - "columnsFrom": [ - "document_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["document_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5000,12 +4633,8 @@ "name": "document_chunks_chunk_id_chunks_id_fk", "tableFrom": "document_chunks", "tableTo": "chunks", - "columnsFrom": [ - "chunk_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5013,12 +4642,8 @@ "name": "document_chunks_user_id_users_id_fk", "tableFrom": "document_chunks", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5026,10 +4651,7 @@ "compositePrimaryKeys": { "document_chunks_document_id_chunk_id_pk": { "name": "document_chunks_document_id_chunk_id_pk", - "columns": [ - "document_id", - "chunk_id" - ] + "columns": ["document_id", "chunk_id"] } }, "uniqueConstraints": {}, @@ -5122,12 +4744,8 @@ "name": "embeddings_chunk_id_chunks_id_fk", "tableFrom": "embeddings", "tableTo": "chunks", - "columnsFrom": [ - "chunk_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5135,12 +4753,8 @@ "name": "embeddings_user_id_users_id_fk", "tableFrom": "embeddings", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5150,9 +4764,7 @@ "embeddings_chunk_id_unique": { "name": "embeddings_chunk_id_unique", "nullsNotDistinct": false, - "columns": [ - "chunk_id" - ] + "columns": ["chunk_id"] } }, "policies": {}, @@ -5274,12 +4886,8 @@ "name": "unstructured_chunks_composite_id_chunks_id_fk", "tableFrom": "unstructured_chunks", "tableTo": "chunks", - "columnsFrom": [ - "composite_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["composite_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5287,12 +4895,8 @@ "name": "unstructured_chunks_user_id_users_id_fk", "tableFrom": "unstructured_chunks", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5300,12 +4904,8 @@ "name": "unstructured_chunks_file_id_files_id_fk", "tableFrom": "unstructured_chunks", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5401,12 +5001,8 @@ "name": "rag_eval_dataset_records_dataset_id_rag_eval_datasets_id_fk", "tableFrom": "rag_eval_dataset_records", "tableTo": "rag_eval_datasets", - "columnsFrom": [ - "dataset_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["dataset_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5414,12 +5010,8 @@ "name": "rag_eval_dataset_records_user_id_users_id_fk", "tableFrom": "rag_eval_dataset_records", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5503,12 +5095,8 @@ "name": "rag_eval_datasets_knowledge_base_id_knowledge_bases_id_fk", "tableFrom": "rag_eval_datasets", "tableTo": "knowledge_bases", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5516,12 +5104,8 @@ "name": "rag_eval_datasets_user_id_users_id_fk", "tableFrom": "rag_eval_datasets", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5641,12 +5225,8 @@ "name": "rag_eval_evaluations_dataset_id_rag_eval_datasets_id_fk", "tableFrom": "rag_eval_evaluations", "tableTo": "rag_eval_datasets", - "columnsFrom": [ - "dataset_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["dataset_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5654,12 +5234,8 @@ "name": "rag_eval_evaluations_knowledge_base_id_knowledge_bases_id_fk", "tableFrom": "rag_eval_evaluations", "tableTo": "knowledge_bases", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5667,12 +5243,8 @@ "name": "rag_eval_evaluations_user_id_users_id_fk", "tableFrom": "rag_eval_evaluations", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5810,12 +5382,8 @@ "name": "rag_eval_evaluation_records_question_embedding_id_embeddings_id_fk", "tableFrom": "rag_eval_evaluation_records", "tableTo": "embeddings", - "columnsFrom": [ - "question_embedding_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["question_embedding_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -5823,12 +5391,8 @@ "name": "rag_eval_evaluation_records_dataset_record_id_rag_eval_dataset_records_id_fk", "tableFrom": "rag_eval_evaluation_records", "tableTo": "rag_eval_dataset_records", - "columnsFrom": [ - "dataset_record_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["dataset_record_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5836,12 +5400,8 @@ "name": "rag_eval_evaluation_records_evaluation_id_rag_eval_evaluations_id_fk", "tableFrom": "rag_eval_evaluation_records", "tableTo": "rag_eval_evaluations", - "columnsFrom": [ - "evaluation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["evaluation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5849,12 +5409,8 @@ "name": "rag_eval_evaluation_records_user_id_users_id_fk", "tableFrom": "rag_eval_evaluation_records", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5946,9 +5502,7 @@ "rbac_permissions_code_unique": { "name": "rbac_permissions_code_unique", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -6016,12 +5570,8 @@ "name": "rbac_role_permissions_role_id_rbac_roles_id_fk", "tableFrom": "rbac_role_permissions", "tableTo": "rbac_roles", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6029,12 +5579,8 @@ "name": "rbac_role_permissions_permission_id_rbac_permissions_id_fk", "tableFrom": "rbac_role_permissions", "tableTo": "rbac_permissions", - "columnsFrom": [ - "permission_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6042,10 +5588,7 @@ "compositePrimaryKeys": { "rbac_role_permissions_role_id_permission_id_pk": { "name": "rbac_role_permissions_role_id_permission_id_pk", - "columns": [ - "role_id", - "permission_id" - ] + "columns": ["role_id", "permission_id"] } }, "uniqueConstraints": {}, @@ -6142,9 +5685,7 @@ "rbac_roles_name_unique": { "name": "rbac_roles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -6218,12 +5759,8 @@ "name": "rbac_user_roles_user_id_users_id_fk", "tableFrom": "rbac_user_roles", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6231,12 +5768,8 @@ "name": "rbac_user_roles_role_id_rbac_roles_id_fk", "tableFrom": "rbac_user_roles", "tableTo": "rbac_roles", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6244,10 +5777,7 @@ "compositePrimaryKeys": { "rbac_user_roles_user_id_role_id_pk": { "name": "rbac_user_roles_user_id_role_id_pk", - "columns": [ - "user_id", - "role_id" - ] + "columns": ["user_id", "role_id"] } }, "uniqueConstraints": {}, @@ -6315,12 +5845,8 @@ "name": "agents_to_sessions_agent_id_agents_id_fk", "tableFrom": "agents_to_sessions", "tableTo": "agents", - "columnsFrom": [ - "agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6328,12 +5854,8 @@ "name": "agents_to_sessions_session_id_sessions_id_fk", "tableFrom": "agents_to_sessions", "tableTo": "sessions", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6341,12 +5863,8 @@ "name": "agents_to_sessions_user_id_users_id_fk", "tableFrom": "agents_to_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6354,10 +5872,7 @@ "compositePrimaryKeys": { "agents_to_sessions_agent_id_session_id_pk": { "name": "agents_to_sessions_agent_id_session_id_pk", - "columns": [ - "agent_id", - "session_id" - ] + "columns": ["agent_id", "session_id"] } }, "uniqueConstraints": {}, @@ -6401,12 +5916,8 @@ "name": "file_chunks_file_id_files_id_fk", "tableFrom": "file_chunks", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6414,12 +5925,8 @@ "name": "file_chunks_chunk_id_chunks_id_fk", "tableFrom": "file_chunks", "tableTo": "chunks", - "columnsFrom": [ - "chunk_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6427,12 +5934,8 @@ "name": "file_chunks_user_id_users_id_fk", "tableFrom": "file_chunks", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6440,10 +5943,7 @@ "compositePrimaryKeys": { "file_chunks_file_id_chunk_id_pk": { "name": "file_chunks_file_id_chunk_id_pk", - "columns": [ - "file_id", - "chunk_id" - ] + "columns": ["file_id", "chunk_id"] } }, "uniqueConstraints": {}, @@ -6480,12 +5980,8 @@ "name": "files_to_sessions_file_id_files_id_fk", "tableFrom": "files_to_sessions", "tableTo": "files", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["file_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6493,12 +5989,8 @@ "name": "files_to_sessions_session_id_sessions_id_fk", "tableFrom": "files_to_sessions", "tableTo": "sessions", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6506,12 +5998,8 @@ "name": "files_to_sessions_user_id_users_id_fk", "tableFrom": "files_to_sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6519,10 +6007,7 @@ "compositePrimaryKeys": { "files_to_sessions_file_id_session_id_pk": { "name": "files_to_sessions_file_id_session_id_pk", - "columns": [ - "file_id", - "session_id" - ] + "columns": ["file_id", "session_id"] } }, "uniqueConstraints": {}, @@ -6614,12 +6099,8 @@ "name": "session_groups_user_id_users_id_fk", "tableFrom": "session_groups", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6845,12 +6326,8 @@ "name": "sessions_user_id_users_id_fk", "tableFrom": "sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6858,12 +6335,8 @@ "name": "sessions_group_id_session_groups_id_fk", "tableFrom": "sessions", "tableTo": "session_groups", - "columnsFrom": [ - "group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["group_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7005,12 +6478,8 @@ "name": "threads_topic_id_topics_id_fk", "tableFrom": "threads", "tableTo": "topics", - "columnsFrom": [ - "topic_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7018,12 +6487,8 @@ "name": "threads_parent_thread_id_threads_id_fk", "tableFrom": "threads", "tableTo": "threads", - "columnsFrom": [ - "parent_thread_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_thread_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -7031,12 +6496,8 @@ "name": "threads_user_id_users_id_fk", "tableFrom": "threads", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7083,12 +6544,8 @@ "name": "topic_documents_document_id_documents_id_fk", "tableFrom": "topic_documents", "tableTo": "documents", - "columnsFrom": [ - "document_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["document_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7096,12 +6553,8 @@ "name": "topic_documents_topic_id_topics_id_fk", "tableFrom": "topic_documents", "tableTo": "topics", - "columnsFrom": [ - "topic_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7109,12 +6562,8 @@ "name": "topic_documents_user_id_users_id_fk", "tableFrom": "topic_documents", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7122,10 +6571,7 @@ "compositePrimaryKeys": { "topic_documents_document_id_topic_id_pk": { "name": "topic_documents_document_id_topic_id_pk", - "columns": [ - "document_id", - "topic_id" - ] + "columns": ["document_id", "topic_id"] } }, "uniqueConstraints": {}, @@ -7308,12 +6754,8 @@ "name": "topics_session_id_sessions_id_fk", "tableFrom": "topics", "tableTo": "sessions", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["session_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7321,12 +6763,8 @@ "name": "topics_group_id_chat_groups_id_fk", "tableFrom": "topics", "tableTo": "chat_groups", - "columnsFrom": [ - "group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["group_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7334,12 +6772,8 @@ "name": "topics_user_id_users_id_fk", "tableFrom": "topics", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7418,12 +6852,8 @@ "name": "user_installed_plugins_user_id_users_id_fk", "tableFrom": "user_installed_plugins", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7431,10 +6861,7 @@ "compositePrimaryKeys": { "user_installed_plugins_user_id_identifier_pk": { "name": "user_installed_plugins_user_id_identifier_pk", - "columns": [ - "user_id", - "identifier" - ] + "columns": ["user_id", "identifier"] } }, "uniqueConstraints": {}, @@ -7513,12 +6940,8 @@ "name": "user_settings_id_users_id_fk", "tableFrom": "user_settings", "tableTo": "users", - "columnsFrom": [ - "id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7635,9 +7058,7 @@ "users_username_unique": { "name": "users_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] } }, "policies": {}, @@ -7800,12 +7221,8 @@ "name": "user_memories_user_id_users_id_fk", "tableFrom": "user_memories", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7988,12 +7405,8 @@ "name": "user_memories_contexts_user_id_users_id_fk", "tableFrom": "user_memories_contexts", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8191,12 +7604,8 @@ "name": "user_memories_experiences_user_id_users_id_fk", "tableFrom": "user_memories_experiences", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8204,12 +7613,8 @@ "name": "user_memories_experiences_user_memory_id_user_memories_id_fk", "tableFrom": "user_memories_experiences", "tableTo": "user_memories", - "columnsFrom": [ - "user_memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8350,12 +7755,8 @@ "name": "user_memories_identities_user_id_users_id_fk", "tableFrom": "user_memories_identities", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8363,12 +7764,8 @@ "name": "user_memories_identities_user_memory_id_user_memories_id_fk", "tableFrom": "user_memories_identities", "tableTo": "user_memories", - "columnsFrom": [ - "user_memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8489,12 +7886,8 @@ "name": "user_memories_preferences_user_id_users_id_fk", "tableFrom": "user_memories_preferences", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8502,12 +7895,8 @@ "name": "user_memories_preferences_user_memory_id_user_memories_id_fk", "tableFrom": "user_memories_preferences", "tableTo": "user_memories", - "columnsFrom": [ - "user_memory_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_memory_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8519,15 +7908,6 @@ "isRLSEnabled": false } }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "version": "7", + "views": {} +} diff --git a/packages/database/migrations/meta/0049_snapshot.json b/packages/database/migrations/meta/0049_snapshot.json new file mode 100644 index 0000000000..153dcf4b4b --- /dev/null +++ b/packages/database/migrations/meta/0049_snapshot.json @@ -0,0 +1,8151 @@ +{ + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "dialect": "postgresql", + "enums": {}, + "id": "89373417-57ba-4332-afc9-90cb9ffc9938", + "policies": {}, + "prevId": "183c9871-d705-4138-9879-929fc92dc2c1", + "roles": {}, + "schemas": {}, + "sequences": {}, + "tables": { + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "editor_data": { + "name": "editor_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "background_color": { + "name": "background_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "market_identifier": { + "name": "market_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plugins": { + "name": "plugins", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_config": { + "name": "chat_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "few_shots": { + "name": "few_shots", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_role": { + "name": "system_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tts": { + "name": "tts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "virtual": { + "name": "virtual", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "opening_message": { + "name": "opening_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "opening_questions": { + "name": "opening_questions", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "client_id_user_id_unique": { + "name": "client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_title_idx": { + "name": "agents_title_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_description_idx": { + "name": "agents_description_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_user_id_users_id_fk": { + "name": "agents_user_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents_files": { + "name": "agents_files", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_files_agent_id_idx": { + "name": "agents_files_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_files_file_id_files_id_fk": { + "name": "agents_files_file_id_files_id_fk", + "tableFrom": "agents_files", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agents_files_agent_id_agents_id_fk": { + "name": "agents_files_agent_id_agents_id_fk", + "tableFrom": "agents_files", + "tableTo": "agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agents_files_user_id_users_id_fk": { + "name": "agents_files_user_id_users_id_fk", + "tableFrom": "agents_files", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agents_files_file_id_agent_id_user_id_pk": { + "name": "agents_files_file_id_agent_id_user_id_pk", + "columns": ["file_id", "agent_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents_knowledge_bases": { + "name": "agents_knowledge_bases", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_knowledge_bases_agent_id_idx": { + "name": "agents_knowledge_bases_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_knowledge_bases_agent_id_agents_id_fk": { + "name": "agents_knowledge_bases_agent_id_agents_id_fk", + "tableFrom": "agents_knowledge_bases", + "tableTo": "agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agents_knowledge_bases_knowledge_base_id_knowledge_bases_id_fk": { + "name": "agents_knowledge_bases_knowledge_base_id_knowledge_bases_id_fk", + "tableFrom": "agents_knowledge_bases", + "tableTo": "knowledge_bases", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agents_knowledge_bases_user_id_users_id_fk": { + "name": "agents_knowledge_bases_user_id_users_id_fk", + "tableFrom": "agents_knowledge_bases", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agents_knowledge_bases_agent_id_knowledge_base_id_pk": { + "name": "agents_knowledge_bases_agent_id_knowledge_base_id_pk", + "columns": ["agent_id", "knowledge_base_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_models": { + "name": "ai_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization": { + "name": "organization", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'chat'" + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pricing": { + "name": "pricing", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "abilities": { + "name": "abilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "context_window_tokens": { + "name": "context_window_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ai_models_user_id_users_id_fk": { + "name": "ai_models_user_id_users_id_fk", + "tableFrom": "ai_models", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_models_id_provider_id_user_id_pk": { + "name": "ai_models_id_provider_id_user_id_pk", + "columns": ["id", "provider_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "fetch_on_client": { + "name": "fetch_on_client", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "check_model": { + "name": "check_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_vaults": { + "name": "key_vaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_providers_id_user_id_pk": { + "name": "ai_providers_id_user_id_pk", + "columns": ["id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "api_keys_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_unique": { + "name": "api_keys_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.async_tasks": { + "name": "async_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "async_tasks_user_id_users_id_fk": { + "name": "async_tasks_user_id_users_id_fk", + "tableFrom": "async_tasks", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_sessions": { + "name": "auth_sessions", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "auth_sessions_user_id_users_id_fk": { + "name": "auth_sessions_user_id_users_id_fk", + "tableFrom": "auth_sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_sessions_token_unique": { + "name": "auth_sessions_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_groups": { + "name": "chat_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_groups_client_id_user_id_unique": { + "name": "chat_groups_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_groups_user_id_users_id_fk": { + "name": "chat_groups_user_id_users_id_fk", + "tableFrom": "chat_groups", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_groups_group_id_session_groups_id_fk": { + "name": "chat_groups_group_id_session_groups_id_fk", + "tableFrom": "chat_groups", + "tableTo": "session_groups", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_groups_agents": { + "name": "chat_groups_agents", + "schema": "", + "columns": { + "chat_group_id": { + "name": "chat_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'participant'" + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_groups_agents_chat_group_id_chat_groups_id_fk": { + "name": "chat_groups_agents_chat_group_id_chat_groups_id_fk", + "tableFrom": "chat_groups_agents", + "tableTo": "chat_groups", + "columnsFrom": ["chat_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_groups_agents_agent_id_agents_id_fk": { + "name": "chat_groups_agents_agent_id_agents_id_fk", + "tableFrom": "chat_groups_agents", + "tableTo": "agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_groups_agents_user_id_users_id_fk": { + "name": "chat_groups_agents_user_id_users_id_fk", + "tableFrom": "chat_groups_agents", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_groups_agents_chat_group_id_agent_id_pk": { + "name": "chat_groups_agents_chat_group_id_agent_id_pk", + "columns": ["chat_group_id", "agent_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_char_count": { + "name": "total_char_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_line_count": { + "name": "total_line_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pages": { + "name": "pages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "editor_data": { + "name": "editor_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_source_idx": { + "name": "documents_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_file_type_idx": { + "name": "documents_file_type_idx", + "columns": [ + { + "expression": "file_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_file_id_idx": { + "name": "documents_file_id_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_parent_id_idx": { + "name": "documents_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_client_id_user_id_unique": { + "name": "documents_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_slug_user_id_unique": { + "name": "documents_slug_user_id_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"documents\".\"slug\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_file_id_files_id_fk": { + "name": "documents_file_id_files_id_fk", + "tableFrom": "documents", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_parent_id_documents_id_fk": { + "name": "documents_parent_id_documents_id_fk", + "tableFrom": "documents", + "tableTo": "documents", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_user_id_users_id_fk": { + "name": "documents_user_id_users_id_fk", + "tableFrom": "documents", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chunk_task_id": { + "name": "chunk_task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "embedding_task_id": { + "name": "embedding_task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_hash_idx": { + "name": "file_hash_idx", + "columns": [ + { + "expression": "file_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "files_parent_id_idx": { + "name": "files_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "files_client_id_user_id_unique": { + "name": "files_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "files_user_id_users_id_fk": { + "name": "files_user_id_users_id_fk", + "tableFrom": "files", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_file_hash_global_files_hash_id_fk": { + "name": "files_file_hash_global_files_hash_id_fk", + "tableFrom": "files", + "tableTo": "global_files", + "columnsFrom": ["file_hash"], + "columnsTo": ["hash_id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_parent_id_documents_id_fk": { + "name": "files_parent_id_documents_id_fk", + "tableFrom": "files", + "tableTo": "documents", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "files_chunk_task_id_async_tasks_id_fk": { + "name": "files_chunk_task_id_async_tasks_id_fk", + "tableFrom": "files", + "tableTo": "async_tasks", + "columnsFrom": ["chunk_task_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "files_embedding_task_id_async_tasks_id_fk": { + "name": "files_embedding_task_id_async_tasks_id_fk", + "tableFrom": "files", + "tableTo": "async_tasks", + "columnsFrom": ["embedding_task_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.global_files": { + "name": "global_files", + "schema": "", + "columns": { + "hash_id": { + "name": "hash_id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "global_files_creator_users_id_fk": { + "name": "global_files_creator_users_id_fk", + "tableFrom": "global_files", + "tableTo": "users", + "columnsFrom": ["creator"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_files": { + "name": "knowledge_base_files", + "schema": "", + "columns": { + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "knowledge_base_files_knowledge_base_id_knowledge_bases_id_fk": { + "name": "knowledge_base_files_knowledge_base_id_knowledge_bases_id_fk", + "tableFrom": "knowledge_base_files", + "tableTo": "knowledge_bases", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_files_file_id_files_id_fk": { + "name": "knowledge_base_files_file_id_files_id_fk", + "tableFrom": "knowledge_base_files", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_files_user_id_users_id_fk": { + "name": "knowledge_base_files_user_id_users_id_fk", + "tableFrom": "knowledge_base_files", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_base_files_knowledge_base_id_file_id_pk": { + "name": "knowledge_base_files_knowledge_base_id_file_id_pk", + "columns": ["knowledge_base_id", "file_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_bases": { + "name": "knowledge_bases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "knowledge_bases_client_id_user_id_unique": { + "name": "knowledge_bases_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_bases_user_id_users_id_fk": { + "name": "knowledge_bases_user_id_users_id_fk", + "tableFrom": "knowledge_bases", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.generation_batches": { + "name": "generation_batches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_topic_id": { + "name": "generation_topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ratio": { + "name": "ratio", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "generation_batches_user_id_users_id_fk": { + "name": "generation_batches_user_id_users_id_fk", + "tableFrom": "generation_batches", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generation_batches_generation_topic_id_generation_topics_id_fk": { + "name": "generation_batches_generation_topic_id_generation_topics_id_fk", + "tableFrom": "generation_batches", + "tableTo": "generation_topics", + "columnsFrom": ["generation_topic_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.generation_topics": { + "name": "generation_topics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "generation_topics_user_id_users_id_fk": { + "name": "generation_topics_user_id_users_id_fk", + "tableFrom": "generation_topics", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.generations": { + "name": "generations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_batch_id": { + "name": "generation_batch_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "async_task_id": { + "name": "async_task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "seed": { + "name": "seed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "asset": { + "name": "asset", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "generations_user_id_users_id_fk": { + "name": "generations_user_id_users_id_fk", + "tableFrom": "generations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generations_generation_batch_id_generation_batches_id_fk": { + "name": "generations_generation_batch_id_generation_batches_id_fk", + "tableFrom": "generations", + "tableTo": "generation_batches", + "columnsFrom": ["generation_batch_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generations_async_task_id_async_tasks_id_fk": { + "name": "generations_async_task_id_async_tasks_id_fk", + "tableFrom": "generations", + "tableTo": "async_tasks", + "columnsFrom": ["async_task_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "generations_file_id_files_id_fk": { + "name": "generations_file_id_files_id_fk", + "tableFrom": "generations", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_chunks": { + "name": "message_chunks", + "schema": "", + "columns": { + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "message_chunks_message_id_messages_id_fk": { + "name": "message_chunks_message_id_messages_id_fk", + "tableFrom": "message_chunks", + "tableTo": "messages", + "columnsFrom": ["message_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_chunks_chunk_id_chunks_id_fk": { + "name": "message_chunks_chunk_id_chunks_id_fk", + "tableFrom": "message_chunks", + "tableTo": "chunks", + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_chunks_user_id_users_id_fk": { + "name": "message_chunks_user_id_users_id_fk", + "tableFrom": "message_chunks", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_chunks_chunk_id_message_id_pk": { + "name": "message_chunks_chunk_id_message_id_pk", + "columns": ["chunk_id", "message_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_groups": { + "name": "message_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_group_id": { + "name": "parent_group_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_groups_client_id_user_id_unique": { + "name": "message_groups_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_groups_topic_id_idx": { + "name": "message_groups_topic_id_idx", + "columns": [ + { + "expression": "topic_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_groups_topic_id_topics_id_fk": { + "name": "message_groups_topic_id_topics_id_fk", + "tableFrom": "message_groups", + "tableTo": "topics", + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_groups_user_id_users_id_fk": { + "name": "message_groups_user_id_users_id_fk", + "tableFrom": "message_groups", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_groups_parent_group_id_message_groups_id_fk": { + "name": "message_groups_parent_group_id_message_groups_id_fk", + "tableFrom": "message_groups", + "tableTo": "message_groups", + "columnsFrom": ["parent_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_groups_parent_message_id_messages_id_fk": { + "name": "message_groups_parent_message_id_messages_id_fk", + "tableFrom": "message_groups", + "tableTo": "messages", + "columnsFrom": ["parent_message_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_plugins": { + "name": "message_plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "intervention": { + "name": "intervention", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "api_name": { + "name": "api_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "arguments": { + "name": "arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "message_plugins_client_id_user_id_unique": { + "name": "message_plugins_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_plugins_id_messages_id_fk": { + "name": "message_plugins_id_messages_id_fk", + "tableFrom": "message_plugins", + "tableTo": "messages", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_plugins_user_id_users_id_fk": { + "name": "message_plugins_user_id_users_id_fk", + "tableFrom": "message_plugins", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_queries": { + "name": "message_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rewrite_query": { + "name": "rewrite_query", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embeddings_id": { + "name": "embeddings_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_queries_client_id_user_id_unique": { + "name": "message_queries_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_queries_message_id_messages_id_fk": { + "name": "message_queries_message_id_messages_id_fk", + "tableFrom": "message_queries", + "tableTo": "messages", + "columnsFrom": ["message_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_queries_user_id_users_id_fk": { + "name": "message_queries_user_id_users_id_fk", + "tableFrom": "message_queries", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_queries_embeddings_id_embeddings_id_fk": { + "name": "message_queries_embeddings_id_embeddings_id_fk", + "tableFrom": "message_queries", + "tableTo": "embeddings", + "columnsFrom": ["embeddings_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_query_chunks": { + "name": "message_query_chunks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "query_id": { + "name": "query_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "similarity": { + "name": "similarity", + "type": "numeric(6, 5)", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "message_query_chunks_id_messages_id_fk": { + "name": "message_query_chunks_id_messages_id_fk", + "tableFrom": "message_query_chunks", + "tableTo": "messages", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_query_chunks_query_id_message_queries_id_fk": { + "name": "message_query_chunks_query_id_message_queries_id_fk", + "tableFrom": "message_query_chunks", + "tableTo": "message_queries", + "columnsFrom": ["query_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_query_chunks_chunk_id_chunks_id_fk": { + "name": "message_query_chunks_chunk_id_chunks_id_fk", + "tableFrom": "message_query_chunks", + "tableTo": "chunks", + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_query_chunks_user_id_users_id_fk": { + "name": "message_query_chunks_user_id_users_id_fk", + "tableFrom": "message_query_chunks", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_query_chunks_chunk_id_id_query_id_pk": { + "name": "message_query_chunks_chunk_id_id_query_id_pk", + "columns": ["chunk_id", "id", "query_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_tts": { + "name": "message_tts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_md5": { + "name": "content_md5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "voice": { + "name": "voice", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "message_tts_client_id_user_id_unique": { + "name": "message_tts_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_tts_id_messages_id_fk": { + "name": "message_tts_id_messages_id_fk", + "tableFrom": "message_tts", + "tableTo": "messages", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_tts_file_id_files_id_fk": { + "name": "message_tts_file_id_files_id_fk", + "tableFrom": "message_tts", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_tts_user_id_users_id_fk": { + "name": "message_tts_user_id_users_id_fk", + "tableFrom": "message_tts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_translates": { + "name": "message_translates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "message_translates_client_id_user_id_unique": { + "name": "message_translates_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_translates_id_messages_id_fk": { + "name": "message_translates_id_messages_id_fk", + "tableFrom": "message_translates", + "tableTo": "messages", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_translates_user_id_users_id_fk": { + "name": "message_translates_user_id_users_id_fk", + "tableFrom": "message_translates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoning": { + "name": "reasoning", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "search": { + "name": "search", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favorite": { + "name": "favorite", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "observation_id": { + "name": "observation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quota_id": { + "name": "quota_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_group_id": { + "name": "message_group_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_created_at_idx": { + "name": "messages_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_client_id_user_unique": { + "name": "message_client_id_user_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_topic_id_idx": { + "name": "messages_topic_id_idx", + "columns": [ + { + "expression": "topic_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_parent_id_idx": { + "name": "messages_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_quota_id_idx": { + "name": "messages_quota_id_idx", + "columns": [ + { + "expression": "quota_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_user_id_idx": { + "name": "messages_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_thread_id_idx": { + "name": "messages_thread_id_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_agent_id_idx": { + "name": "messages_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_user_id_users_id_fk": { + "name": "messages_user_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_session_id_sessions_id_fk": { + "name": "messages_session_id_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_topic_id_topics_id_fk": { + "name": "messages_topic_id_topics_id_fk", + "tableFrom": "messages", + "tableTo": "topics", + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_thread_id_threads_id_fk": { + "name": "messages_thread_id_threads_id_fk", + "tableFrom": "messages", + "tableTo": "threads", + "columnsFrom": ["thread_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_parent_id_messages_id_fk": { + "name": "messages_parent_id_messages_id_fk", + "tableFrom": "messages", + "tableTo": "messages", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_quota_id_messages_id_fk": { + "name": "messages_quota_id_messages_id_fk", + "tableFrom": "messages", + "tableTo": "messages", + "columnsFrom": ["quota_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_agent_id_agents_id_fk": { + "name": "messages_agent_id_agents_id_fk", + "tableFrom": "messages", + "tableTo": "agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_group_id_chat_groups_id_fk": { + "name": "messages_group_id_chat_groups_id_fk", + "tableFrom": "messages", + "tableTo": "chat_groups", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_message_group_id_message_groups_id_fk": { + "name": "messages_message_group_id_message_groups_id_fk", + "tableFrom": "messages", + "tableTo": "message_groups", + "columnsFrom": ["message_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages_files": { + "name": "messages_files", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "messages_files_file_id_files_id_fk": { + "name": "messages_files_file_id_files_id_fk", + "tableFrom": "messages_files", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_files_message_id_messages_id_fk": { + "name": "messages_files_message_id_messages_id_fk", + "tableFrom": "messages_files", + "tableTo": "messages", + "columnsFrom": ["message_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_files_user_id_users_id_fk": { + "name": "messages_files_user_id_users_id_fk", + "tableFrom": "messages_files", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "messages_files_file_id_message_id_pk": { + "name": "messages_files_file_id_message_id_pk", + "columns": ["file_id", "message_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nextauth_accounts": { + "name": "nextauth_accounts", + "schema": "", + "columns": { + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "nextauth_accounts_userId_users_id_fk": { + "name": "nextauth_accounts_userId_users_id_fk", + "tableFrom": "nextauth_accounts", + "tableTo": "users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "nextauth_accounts_provider_providerAccountId_pk": { + "name": "nextauth_accounts_provider_providerAccountId_pk", + "columns": ["provider", "providerAccountId"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nextauth_authenticators": { + "name": "nextauth_authenticators", + "schema": "", + "columns": { + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credentialBackedUp": { + "name": "credentialBackedUp", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "credentialDeviceType": { + "name": "credentialDeviceType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credentialPublicKey": { + "name": "credentialPublicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "nextauth_authenticators_userId_users_id_fk": { + "name": "nextauth_authenticators_userId_users_id_fk", + "tableFrom": "nextauth_authenticators", + "tableTo": "users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "nextauth_authenticators_userId_credentialID_pk": { + "name": "nextauth_authenticators_userId_credentialID_pk", + "columns": ["userId", "credentialID"] + } + }, + "uniqueConstraints": { + "nextauth_authenticators_credentialID_unique": { + "name": "nextauth_authenticators_credentialID_unique", + "nullsNotDistinct": false, + "columns": ["credentialID"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nextauth_sessions": { + "name": "nextauth_sessions", + "schema": "", + "columns": { + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "nextauth_sessions_userId_users_id_fk": { + "name": "nextauth_sessions_userId_users_id_fk", + "tableFrom": "nextauth_sessions", + "tableTo": "users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nextauth_verificationtokens": { + "name": "nextauth_verificationtokens", + "schema": "", + "columns": { + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "nextauth_verificationtokens_identifier_token_pk": { + "name": "nextauth_verificationtokens_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_handoffs": { + "name": "oauth_handoffs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client": { + "name": "client", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_access_tokens": { + "name": "oidc_access_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oidc_access_tokens_user_id_users_id_fk": { + "name": "oidc_access_tokens_user_id_users_id_fk", + "tableFrom": "oidc_access_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_authorization_codes": { + "name": "oidc_authorization_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oidc_authorization_codes_user_id_users_id_fk": { + "name": "oidc_authorization_codes_user_id_users_id_fk", + "tableFrom": "oidc_authorization_codes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_clients": { + "name": "oidc_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "grants": { + "name": "grants", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "application_type": { + "name": "application_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "client_uri": { + "name": "client_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_uri": { + "name": "logo_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy_uri": { + "name": "policy_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tos_uri": { + "name": "tos_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_first_party": { + "name": "is_first_party", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_consents": { + "name": "oidc_consents", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oidc_consents_user_id_users_id_fk": { + "name": "oidc_consents_user_id_users_id_fk", + "tableFrom": "oidc_consents", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_consents_client_id_oidc_clients_id_fk": { + "name": "oidc_consents_client_id_oidc_clients_id_fk", + "tableFrom": "oidc_consents", + "tableTo": "oidc_clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_consents_user_id_client_id_pk": { + "name": "oidc_consents_user_id_client_id_pk", + "columns": ["user_id", "client_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_device_codes": { + "name": "oidc_device_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "user_code": { + "name": "user_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oidc_device_codes_user_id_users_id_fk": { + "name": "oidc_device_codes_user_id_users_id_fk", + "tableFrom": "oidc_device_codes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_grants": { + "name": "oidc_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_interactions": { + "name": "oidc_interactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_sessions": { + "name": "oidc_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oidc_sessions_user_id_users_id_fk": { + "name": "oidc_sessions_user_id_users_id_fk", + "tableFrom": "oidc_sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chunks": { + "name": "chunks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "abstract": { + "name": "abstract", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chunks_client_id_user_id_unique": { + "name": "chunks_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chunks_user_id_idx": { + "name": "chunks_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chunks_user_id_users_id_fk": { + "name": "chunks_user_id_users_id_fk", + "tableFrom": "chunks", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_chunks": { + "name": "document_chunks", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "page_index": { + "name": "page_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_chunks_document_id_documents_id_fk": { + "name": "document_chunks_document_id_documents_id_fk", + "tableFrom": "document_chunks", + "tableTo": "documents", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_chunks_chunk_id_chunks_id_fk": { + "name": "document_chunks_chunk_id_chunks_id_fk", + "tableFrom": "document_chunks", + "tableTo": "chunks", + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_chunks_user_id_users_id_fk": { + "name": "document_chunks_user_id_users_id_fk", + "tableFrom": "document_chunks", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "document_chunks_document_id_chunk_id_pk": { + "name": "document_chunks_document_id_chunk_id_pk", + "columns": ["document_id", "chunk_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embeddings": { + "name": "embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "embeddings": { + "name": "embeddings", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "embeddings_client_id_user_id_unique": { + "name": "embeddings_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embeddings_chunk_id_idx": { + "name": "embeddings_chunk_id_idx", + "columns": [ + { + "expression": "chunk_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "embeddings_chunk_id_chunks_id_fk": { + "name": "embeddings_chunk_id_chunks_id_fk", + "tableFrom": "embeddings", + "tableTo": "chunks", + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embeddings_user_id_users_id_fk": { + "name": "embeddings_user_id_users_id_fk", + "tableFrom": "embeddings", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "embeddings_chunk_id_unique": { + "name": "embeddings_chunk_id_unique", + "nullsNotDistinct": false, + "columns": ["chunk_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.unstructured_chunks": { + "name": "unstructured_chunks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "parent_id": { + "name": "parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "composite_id": { + "name": "composite_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unstructured_chunks_client_id_user_id_unique": { + "name": "unstructured_chunks_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "unstructured_chunks_composite_id_chunks_id_fk": { + "name": "unstructured_chunks_composite_id_chunks_id_fk", + "tableFrom": "unstructured_chunks", + "tableTo": "chunks", + "columnsFrom": ["composite_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "unstructured_chunks_user_id_users_id_fk": { + "name": "unstructured_chunks_user_id_users_id_fk", + "tableFrom": "unstructured_chunks", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "unstructured_chunks_file_id_files_id_fk": { + "name": "unstructured_chunks_file_id_files_id_fk", + "tableFrom": "unstructured_chunks", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rag_eval_dataset_records": { + "name": "rag_eval_dataset_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "rag_eval_dataset_records_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "dataset_id": { + "name": "dataset_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ideal": { + "name": "ideal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_files": { + "name": "reference_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rag_eval_dataset_records_dataset_id_rag_eval_datasets_id_fk": { + "name": "rag_eval_dataset_records_dataset_id_rag_eval_datasets_id_fk", + "tableFrom": "rag_eval_dataset_records", + "tableTo": "rag_eval_datasets", + "columnsFrom": ["dataset_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rag_eval_dataset_records_user_id_users_id_fk": { + "name": "rag_eval_dataset_records_user_id_users_id_fk", + "tableFrom": "rag_eval_dataset_records", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rag_eval_datasets": { + "name": "rag_eval_datasets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "rag_eval_datasets_id_seq", + "schema": "public", + "increment": "1", + "startWith": "30000", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rag_eval_datasets_knowledge_base_id_knowledge_bases_id_fk": { + "name": "rag_eval_datasets_knowledge_base_id_knowledge_bases_id_fk", + "tableFrom": "rag_eval_datasets", + "tableTo": "knowledge_bases", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rag_eval_datasets_user_id_users_id_fk": { + "name": "rag_eval_datasets_user_id_users_id_fk", + "tableFrom": "rag_eval_datasets", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rag_eval_evaluations": { + "name": "rag_eval_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "rag_eval_evaluations_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eval_records_url": { + "name": "eval_records_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dataset_id": { + "name": "dataset_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language_model": { + "name": "language_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rag_eval_evaluations_dataset_id_rag_eval_datasets_id_fk": { + "name": "rag_eval_evaluations_dataset_id_rag_eval_datasets_id_fk", + "tableFrom": "rag_eval_evaluations", + "tableTo": "rag_eval_datasets", + "columnsFrom": ["dataset_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rag_eval_evaluations_knowledge_base_id_knowledge_bases_id_fk": { + "name": "rag_eval_evaluations_knowledge_base_id_knowledge_bases_id_fk", + "tableFrom": "rag_eval_evaluations", + "tableTo": "knowledge_bases", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rag_eval_evaluations_user_id_users_id_fk": { + "name": "rag_eval_evaluations_user_id_users_id_fk", + "tableFrom": "rag_eval_evaluations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rag_eval_evaluation_records": { + "name": "rag_eval_evaluation_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "rag_eval_evaluation_records_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "ideal": { + "name": "ideal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "language_model": { + "name": "language_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "question_embedding_id": { + "name": "question_embedding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "dataset_record_id": { + "name": "dataset_record_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "evaluation_id": { + "name": "evaluation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rag_eval_evaluation_records_question_embedding_id_embeddings_id_fk": { + "name": "rag_eval_evaluation_records_question_embedding_id_embeddings_id_fk", + "tableFrom": "rag_eval_evaluation_records", + "tableTo": "embeddings", + "columnsFrom": ["question_embedding_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "rag_eval_evaluation_records_dataset_record_id_rag_eval_dataset_records_id_fk": { + "name": "rag_eval_evaluation_records_dataset_record_id_rag_eval_dataset_records_id_fk", + "tableFrom": "rag_eval_evaluation_records", + "tableTo": "rag_eval_dataset_records", + "columnsFrom": ["dataset_record_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rag_eval_evaluation_records_evaluation_id_rag_eval_evaluations_id_fk": { + "name": "rag_eval_evaluation_records_evaluation_id_rag_eval_evaluations_id_fk", + "tableFrom": "rag_eval_evaluation_records", + "tableTo": "rag_eval_evaluations", + "columnsFrom": ["evaluation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rag_eval_evaluation_records_user_id_users_id_fk": { + "name": "rag_eval_evaluation_records_user_id_users_id_fk", + "tableFrom": "rag_eval_evaluation_records", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_permissions": { + "name": "rbac_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "rbac_permissions_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rbac_permissions_code_unique": { + "name": "rbac_permissions_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_role_permissions": { + "name": "rbac_role_permissions", + "schema": "", + "columns": { + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rbac_role_permissions_role_id_idx": { + "name": "rbac_role_permissions_role_id_idx", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rbac_role_permissions_permission_id_idx": { + "name": "rbac_role_permissions_permission_id_idx", + "columns": [ + { + "expression": "permission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rbac_role_permissions_role_id_rbac_roles_id_fk": { + "name": "rbac_role_permissions_role_id_rbac_roles_id_fk", + "tableFrom": "rbac_role_permissions", + "tableTo": "rbac_roles", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rbac_role_permissions_permission_id_rbac_permissions_id_fk": { + "name": "rbac_role_permissions_permission_id_rbac_permissions_id_fk", + "tableFrom": "rbac_role_permissions", + "tableTo": "rbac_permissions", + "columnsFrom": ["permission_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "rbac_role_permissions_role_id_permission_id_pk": { + "name": "rbac_role_permissions_role_id_permission_id_pk", + "columns": ["role_id", "permission_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_roles": { + "name": "rbac_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "rbac_roles_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rbac_roles_name_unique": { + "name": "rbac_roles_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_user_roles": { + "name": "rbac_user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "rbac_user_roles_user_id_idx": { + "name": "rbac_user_roles_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rbac_user_roles_role_id_idx": { + "name": "rbac_user_roles_role_id_idx", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rbac_user_roles_user_id_users_id_fk": { + "name": "rbac_user_roles_user_id_users_id_fk", + "tableFrom": "rbac_user_roles", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rbac_user_roles_role_id_rbac_roles_id_fk": { + "name": "rbac_user_roles_role_id_rbac_roles_id_fk", + "tableFrom": "rbac_user_roles", + "tableTo": "rbac_roles", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "rbac_user_roles_user_id_role_id_pk": { + "name": "rbac_user_roles_user_id_role_id_pk", + "columns": ["user_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents_to_sessions": { + "name": "agents_to_sessions", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agents_to_sessions_session_id_idx": { + "name": "agents_to_sessions_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_to_sessions_agent_id_idx": { + "name": "agents_to_sessions_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_to_sessions_agent_id_agents_id_fk": { + "name": "agents_to_sessions_agent_id_agents_id_fk", + "tableFrom": "agents_to_sessions", + "tableTo": "agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agents_to_sessions_session_id_sessions_id_fk": { + "name": "agents_to_sessions_session_id_sessions_id_fk", + "tableFrom": "agents_to_sessions", + "tableTo": "sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agents_to_sessions_user_id_users_id_fk": { + "name": "agents_to_sessions_user_id_users_id_fk", + "tableFrom": "agents_to_sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agents_to_sessions_agent_id_session_id_pk": { + "name": "agents_to_sessions_agent_id_session_id_pk", + "columns": ["agent_id", "session_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_chunks": { + "name": "file_chunks", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "file_chunks_file_id_files_id_fk": { + "name": "file_chunks_file_id_files_id_fk", + "tableFrom": "file_chunks", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_chunks_chunk_id_chunks_id_fk": { + "name": "file_chunks_chunk_id_chunks_id_fk", + "tableFrom": "file_chunks", + "tableTo": "chunks", + "columnsFrom": ["chunk_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_chunks_user_id_users_id_fk": { + "name": "file_chunks_user_id_users_id_fk", + "tableFrom": "file_chunks", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_chunks_file_id_chunk_id_pk": { + "name": "file_chunks_file_id_chunk_id_pk", + "columns": ["file_id", "chunk_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files_to_sessions": { + "name": "files_to_sessions", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "files_to_sessions_file_id_files_id_fk": { + "name": "files_to_sessions_file_id_files_id_fk", + "tableFrom": "files_to_sessions", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_to_sessions_session_id_sessions_id_fk": { + "name": "files_to_sessions_session_id_sessions_id_fk", + "tableFrom": "files_to_sessions", + "tableTo": "sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_to_sessions_user_id_users_id_fk": { + "name": "files_to_sessions_user_id_users_id_fk", + "tableFrom": "files_to_sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "files_to_sessions_file_id_session_id_pk": { + "name": "files_to_sessions_file_id_session_id_pk", + "columns": ["file_id", "session_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_groups": { + "name": "session_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_groups_client_id_user_id_unique": { + "name": "session_groups_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_groups_user_id_users_id_fk": { + "name": "session_groups_user_id_users_id_fk", + "tableFrom": "session_groups", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "background_color": { + "name": "background_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'agent'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slug_user_id_unique": { + "name": "slug_user_id_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_client_id_user_id_unique": { + "name": "sessions_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_id_user_id_idx": { + "name": "sessions_id_user_id_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_id_updated_at_idx": { + "name": "sessions_user_id_updated_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_group_id_idx": { + "name": "sessions_group_id_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_group_id_session_groups_id_fk": { + "name": "sessions_group_id_session_groups_id_fk", + "tableFrom": "sessions", + "tableTo": "session_groups", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.threads": { + "name": "threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_message_id": { + "name": "source_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_thread_id": { + "name": "parent_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "threads_client_id_user_id_unique": { + "name": "threads_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "threads_topic_id_idx": { + "name": "threads_topic_id_idx", + "columns": [ + { + "expression": "topic_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "threads_topic_id_topics_id_fk": { + "name": "threads_topic_id_topics_id_fk", + "tableFrom": "threads", + "tableTo": "topics", + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "threads_parent_thread_id_threads_id_fk": { + "name": "threads_parent_thread_id_threads_id_fk", + "tableFrom": "threads", + "tableTo": "threads", + "columnsFrom": ["parent_thread_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_user_id_users_id_fk": { + "name": "threads_user_id_users_id_fk", + "tableFrom": "threads", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.topic_documents": { + "name": "topic_documents", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "topic_documents_document_id_documents_id_fk": { + "name": "topic_documents_document_id_documents_id_fk", + "tableFrom": "topic_documents", + "tableTo": "documents", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "topic_documents_topic_id_topics_id_fk": { + "name": "topic_documents_topic_id_topics_id_fk", + "tableFrom": "topic_documents", + "tableTo": "topics", + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "topic_documents_user_id_users_id_fk": { + "name": "topic_documents_user_id_users_id_fk", + "tableFrom": "topic_documents", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "topic_documents_document_id_topic_id_pk": { + "name": "topic_documents_document_id_topic_id_pk", + "columns": ["document_id", "topic_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.topics": { + "name": "topics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favorite": { + "name": "favorite", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "history_summary": { + "name": "history_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "topics_client_id_user_id_unique": { + "name": "topics_client_id_user_id_unique", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_user_id_idx": { + "name": "topics_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_id_user_id_idx": { + "name": "topics_id_user_id_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_session_id_idx": { + "name": "topics_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_group_id_idx": { + "name": "topics_group_id_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "topics_session_id_sessions_id_fk": { + "name": "topics_session_id_sessions_id_fk", + "tableFrom": "topics", + "tableTo": "sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "topics_group_id_chat_groups_id_fk": { + "name": "topics_group_id_chat_groups_id_fk", + "tableFrom": "topics", + "tableTo": "chat_groups", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "topics_user_id_users_id_fk": { + "name": "topics_user_id_users_id_fk", + "tableFrom": "topics", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_installed_plugins": { + "name": "user_installed_plugins", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_params": { + "name": "custom_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_installed_plugins_user_id_users_id_fk": { + "name": "user_installed_plugins_user_id_users_id_fk", + "tableFrom": "user_installed_plugins", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_installed_plugins_user_id_identifier_pk": { + "name": "user_installed_plugins_user_id_identifier_pk", + "columns": ["user_id", "identifier"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tts": { + "name": "tts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hotkey": { + "name": "hotkey", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "key_vaults": { + "name": "key_vaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "language_model": { + "name": "language_model", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "system_agent": { + "name": "system_agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "default_agent": { + "name": "default_agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tool": { + "name": "tool", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_id_users_id_fk": { + "name": "user_settings_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_onboarded": { + "name": "is_onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "clerk_created_at": { + "name": "clerk_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "preference": { + "name": "preference", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": ["username"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_memories": { + "name": "user_memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memory_category": { + "name": "memory_category", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "memory_layer": { + "name": "memory_layer", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "memory_type": { + "name": "memory_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_vector_1024": { + "name": "summary_vector_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details_vector_1024": { + "name": "details_vector_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accessed_count": { + "name": "accessed_count", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_memories_summary_vector_1024_index": { + "name": "user_memories_summary_vector_1024_index", + "columns": [ + { + "expression": "summary_vector_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "user_memories_details_vector_1024_index": { + "name": "user_memories_details_vector_1024_index", + "columns": [ + { + "expression": "details_vector_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "user_memories_user_id_users_id_fk": { + "name": "user_memories_user_id_users_id_fk", + "tableFrom": "user_memories", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_memories_contexts": { + "name": "user_memories_contexts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_memory_ids": { + "name": "user_memory_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "associated_objects": { + "name": "associated_objects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "associated_subjects": { + "name": "associated_subjects", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title_vector": { + "name": "title_vector", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description_vector": { + "name": "description_vector", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "current_status": { + "name": "current_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "score_impact": { + "name": "score_impact", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "score_urgency": { + "name": "score_urgency", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_memories_contexts_title_vector_index": { + "name": "user_memories_contexts_title_vector_index", + "columns": [ + { + "expression": "title_vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "user_memories_contexts_description_vector_index": { + "name": "user_memories_contexts_description_vector_index", + "columns": [ + { + "expression": "description_vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "user_memories_contexts_type_index": { + "name": "user_memories_contexts_type_index", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_memories_contexts_user_id_users_id_fk": { + "name": "user_memories_contexts_user_id_users_id_fk", + "tableFrom": "user_memories_contexts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_memories_experiences": { + "name": "user_memories_experiences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_memory_id": { + "name": "user_memory_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "situation": { + "name": "situation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "situation_vector": { + "name": "situation_vector", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "possible_outcome": { + "name": "possible_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_vector": { + "name": "action_vector", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "key_learning": { + "name": "key_learning", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_learning_vector": { + "name": "key_learning_vector", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "score_confidence": { + "name": "score_confidence", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_memories_experiences_situation_vector_index": { + "name": "user_memories_experiences_situation_vector_index", + "columns": [ + { + "expression": "situation_vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "user_memories_experiences_action_vector_index": { + "name": "user_memories_experiences_action_vector_index", + "columns": [ + { + "expression": "action_vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "user_memories_experiences_key_learning_vector_index": { + "name": "user_memories_experiences_key_learning_vector_index", + "columns": [ + { + "expression": "key_learning_vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "user_memories_experiences_type_index": { + "name": "user_memories_experiences_type_index", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_memories_experiences_user_id_users_id_fk": { + "name": "user_memories_experiences_user_id_users_id_fk", + "tableFrom": "user_memories_experiences", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_memories_experiences_user_memory_id_user_memories_id_fk": { + "name": "user_memories_experiences_user_memory_id_user_memories_id_fk", + "tableFrom": "user_memories_experiences", + "tableTo": "user_memories", + "columnsFrom": ["user_memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_memories_identities": { + "name": "user_memories_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_memory_id": { + "name": "user_memory_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description_vector": { + "name": "description_vector", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "episodic_date": { + "name": "episodic_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "relationship": { + "name": "relationship", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_memories_identities_description_vector_index": { + "name": "user_memories_identities_description_vector_index", + "columns": [ + { + "expression": "description_vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "user_memories_identities_type_index": { + "name": "user_memories_identities_type_index", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_memories_identities_user_id_users_id_fk": { + "name": "user_memories_identities_user_id_users_id_fk", + "tableFrom": "user_memories_identities", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_memories_identities_user_memory_id_user_memories_id_fk": { + "name": "user_memories_identities_user_memory_id_user_memories_id_fk", + "tableFrom": "user_memories_identities", + "tableTo": "user_memories", + "columnsFrom": ["user_memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_memories_preferences": { + "name": "user_memories_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_memory_id": { + "name": "user_memory_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "conclusion_directives": { + "name": "conclusion_directives", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conclusion_directives_vector": { + "name": "conclusion_directives_vector", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "suggestions": { + "name": "suggestions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "score_priority": { + "name": "score_priority", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_memories_preferences_conclusion_directives_vector_index": { + "name": "user_memories_preferences_conclusion_directives_vector_index", + "columns": [ + { + "expression": "conclusion_directives_vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "user_memories_preferences_user_id_users_id_fk": { + "name": "user_memories_preferences_user_id_users_id_fk", + "tableFrom": "user_memories_preferences", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_memories_preferences_user_memory_id_user_memories_id_fk": { + "name": "user_memories_preferences_user_memory_id_user_memories_id_fk", + "tableFrom": "user_memories_preferences", + "tableTo": "user_memories", + "columnsFrom": ["user_memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "version": "7", + "views": {} +} diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index ac48c9ba96..f638fd582f 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -343,7 +343,14 @@ "when": 1764215503726, "tag": "0048_add_editor_data", "breakpoints": true + }, + { + "idx": 49, + "version": "7", + "when": 1764229953081, + "tag": "0049_better_auth", + "breakpoints": true } ], "version": "6" -} \ No newline at end of file +} diff --git a/packages/database/src/core/migrations.json b/packages/database/src/core/migrations.json index f63f39b2ee..12ba936544 100644 --- a/packages/database/src/core/migrations.json +++ b/packages/database/src/core/migrations.json @@ -803,5 +803,18 @@ "bps": true, "folderMillis": 1764215503726, "hash": "4188893a9083b3c7baebdbad0dd3f9d9400ede7584ca2394f5c64305dc9ec7b0" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"accounts\" (\n\t\"access_token\" text,\n\t\"access_token_expires_at\" timestamp,\n\t\"account_id\" text NOT NULL,\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"id_token\" text,\n\t\"password\" text,\n\t\"provider_id\" text NOT NULL,\n\t\"refresh_token\" text,\n\t\"refresh_token_expires_at\" timestamp,\n\t\"scope\" text,\n\t\"updated_at\" timestamp NOT NULL,\n\t\"user_id\" text NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"auth_sessions\" (\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"expires_at\" timestamp NOT NULL,\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"ip_address\" text,\n\t\"token\" text NOT NULL,\n\t\"updated_at\" timestamp NOT NULL,\n\t\"user_agent\" text,\n\t\"user_id\" text NOT NULL,\n\tCONSTRAINT \"auth_sessions_token_unique\" UNIQUE(\"token\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"verifications\" (\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"expires_at\" timestamp NOT NULL,\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"updated_at\" timestamp DEFAULT now() NOT NULL,\n\t\"value\" text NOT NULL\n);\n", + "\nALTER TABLE \"users\" ADD COLUMN IF NOT EXISTS \"email_verified\" boolean DEFAULT false NOT NULL;", + "\nDO $$ BEGIN\n ALTER TABLE \"accounts\" ADD CONSTRAINT \"accounts_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"auth_sessions\" ADD CONSTRAINT \"auth_sessions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1764229953081, + "hash": "1532ebceae7b70550bc9c230fb0a65090aaa773bc7b873eefbc2ce2a815997e2" } ] diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index b38ebc9a19..e6411a528a 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -1 +1,2 @@ +export * from './core/db-adaptor'; export * from './type'; diff --git a/packages/database/src/models/__tests__/session.test.ts b/packages/database/src/models/__tests__/session.test.ts index 2a20720dfc..68fb2444f4 100644 --- a/packages/database/src/models/__tests__/session.test.ts +++ b/packages/database/src/models/__tests__/session.test.ts @@ -1,9 +1,8 @@ +import { DEFAULT_AGENT_CONFIG } from '@lobechat/const'; import { and, eq, inArray } from 'drizzle-orm'; import { LLMParams } from 'model-bank'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_AGENT_CONFIG } from '@/const/settings'; - import { NewSession, SessionItem, diff --git a/packages/database/src/models/user.ts b/packages/database/src/models/user.ts index 4826c4eea6..ed23ac92d8 100644 --- a/packages/database/src/models/user.ts +++ b/packages/database/src/models/user.ts @@ -1,8 +1,13 @@ -import { UserGuide, UserKeyVaults, UserPreference, UserSettings } from '@lobechat/types'; +import { + SSOProvider, + UserGuide, + UserKeyVaults, + UserPreference, + UserSettings, +} from '@lobechat/types'; import { TRPCError } from '@trpc/server'; import dayjs from 'dayjs'; import { eq } from 'drizzle-orm'; -import type { AdapterAccount } from 'next-auth/adapters'; import type { PartialDeep } from 'type-fest'; import { merge } from '@/utils/merge'; @@ -126,19 +131,15 @@ export class UserModel { }; }; - getUserSSOProviders = async () => { - const result = await this.db + getUserSSOProviders = async (): Promise => { + return this.db .select({ expiresAt: nextauthAccounts.expires_at, provider: nextauthAccounts.provider, providerAccountId: nextauthAccounts.providerAccountId, - scope: nextauthAccounts.scope, - type: nextauthAccounts.type, - userId: nextauthAccounts.userId, }) .from(nextauthAccounts) .where(eq(nextauthAccounts.userId, this.userId)); - return result as unknown as AdapterAccount[]; }; getUserSettings = async () => { diff --git a/packages/database/src/repositories/tableViewer/index.test.ts b/packages/database/src/repositories/tableViewer/index.test.ts index 88956485c2..cd992b38d9 100644 --- a/packages/database/src/repositories/tableViewer/index.test.ts +++ b/packages/database/src/repositories/tableViewer/index.test.ts @@ -23,8 +23,8 @@ describe('TableViewerRepo', () => { it('should return all tables with counts', async () => { const result = await repo.getAllTables(); - expect(result.length).toEqual(68); - expect(result[0]).toEqual({ name: 'agents', count: 0, type: 'BASE TABLE' }); + expect(result.length).toEqual(71); + expect(result[0]).toEqual({ name: 'accounts', count: 0, type: 'BASE TABLE' }); }); it('should handle custom schema', async () => { diff --git a/packages/database/src/schemas/betterAuth.ts b/packages/database/src/schemas/betterAuth.ts new file mode 100644 index 0000000000..3d46b4b039 --- /dev/null +++ b/packages/database/src/schemas/betterAuth.ts @@ -0,0 +1,63 @@ +import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'; + +import { users } from './user'; + +// export const user = pgTable('betterauth_user', { +// createdAt: timestamp('created_at').defaultNow().notNull(), +// email: text('email').notNull().unique(), +// emailVerified: boolean('email_verified').default(false).notNull(), +// id: text('id').primaryKey(), +// image: text('image'), +// name: text('name').notNull(), +// updatedAt: timestamp('updated_at') +// .defaultNow() +// .$onUpdate(() => /* @__PURE__ */ new Date()) +// .notNull(), +// }); + +export const session = pgTable('auth_sessions', { + createdAt: timestamp('created_at').defaultNow().notNull(), + expiresAt: timestamp('expires_at').notNull(), + id: text('id').primaryKey(), + ipAddress: text('ip_address'), + token: text('token').notNull().unique(), + updatedAt: timestamp('updated_at') + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + userAgent: text('user_agent'), + userId: text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), +}); + +export const account = pgTable('accounts', { + accessToken: text('access_token'), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + accountId: text('account_id').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + id: text('id').primaryKey(), + idToken: text('id_token'), + password: text('password'), + providerId: text('provider_id').notNull(), + refreshToken: text('refresh_token'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + scope: text('scope'), + updatedAt: timestamp('updated_at') + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + userId: text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), +}); + +export const verification = pgTable('verifications', { + createdAt: timestamp('created_at').defaultNow().notNull(), + expiresAt: timestamp('expires_at').notNull(), + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + value: text('value').notNull(), +}); diff --git a/packages/database/src/schemas/index.ts b/packages/database/src/schemas/index.ts index d34c87c44b..a003980244 100644 --- a/packages/database/src/schemas/index.ts +++ b/packages/database/src/schemas/index.ts @@ -2,6 +2,7 @@ export * from './agent'; export * from './aiInfra'; export * from './apiKey'; export * from './asyncTask'; +export * from './betterAuth'; export * from './chatGroup'; export * from './file'; export * from './generation'; diff --git a/packages/database/src/schemas/ragEvals.ts b/packages/database/src/schemas/ragEvals.ts index fc334bee1f..08fac47ec2 100644 --- a/packages/database/src/schemas/ragEvals.ts +++ b/packages/database/src/schemas/ragEvals.ts @@ -1,9 +1,8 @@ /* eslint-disable sort-keys-fix/sort-keys-fix */ +import { DEFAULT_MODEL } from '@lobechat/const'; import { EvalEvaluationStatus } from '@lobechat/types'; import { integer, jsonb, pgTable, text, uuid } from 'drizzle-orm/pg-core'; -import { DEFAULT_MODEL } from '@/const/settings'; - import { timestamps } from './_helpers'; import { knowledgeBases } from './file'; import { embeddings } from './rag'; diff --git a/packages/database/src/schemas/user.ts b/packages/database/src/schemas/user.ts index 90a69d84d2..d1c948e591 100644 --- a/packages/database/src/schemas/user.ts +++ b/packages/database/src/schemas/user.ts @@ -1,10 +1,9 @@ /* eslint-disable sort-keys-fix/sort-keys-fix */ +import { DEFAULT_PREFERENCE } from '@lobechat/const'; import type { CustomPluginParams } from '@lobechat/types'; import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; import { boolean, jsonb, pgTable, primaryKey, text } from 'drizzle-orm/pg-core'; -import { DEFAULT_PREFERENCE } from '@/const/user'; - import { timestamps, timestamptz } from './_helpers'; export const users = pgTable('users', { @@ -22,6 +21,8 @@ export const users = pgTable('users', { // Time user was created in Clerk clerkCreatedAt: timestamptz('clerk_created_at'), + // Required by better-auth + emailVerified: boolean('email_verified').default(false).notNull(), // Required by nextauth, all null allowed emailVerifiedAt: timestamptz('email_verified_at'), diff --git a/packages/database/src/server/models/__tests__/user.test.ts b/packages/database/src/server/models/__tests__/user.test.ts index 2bf3c74fbc..8e80759047 100644 --- a/packages/database/src/server/models/__tests__/user.test.ts +++ b/packages/database/src/server/models/__tests__/user.test.ts @@ -1,10 +1,10 @@ +import { INBOX_SESSION_ID } from '@lobechat/const'; import type { UserGuide, UserPreference } from '@lobechat/types'; import { TRPCError } from '@trpc/server'; import dayjs from 'dayjs'; import { count, eq } from 'drizzle-orm'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { INBOX_SESSION_ID } from '@/const/session'; import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt'; import { getTestDBInstance } from '../../../core/dbForTest'; @@ -428,9 +428,6 @@ describe('UserModel', () => { expect(result[0]).toMatchObject({ provider: 'github', providerAccountId: '123456', - type: 'oauth', - userId, - scope: 'user:email', }); expect(result[0].expiresAt).toBeDefined(); }); diff --git a/packages/types/src/user/preference.ts b/packages/types/src/user/preference.ts index 8568dbe196..fef9c59065 100644 --- a/packages/types/src/user/preference.ts +++ b/packages/types/src/user/preference.ts @@ -89,6 +89,17 @@ export const NextAuthAccountSchame = z.object({ providerAccountId: z.string(), }); +/** + * SSO Provider info displayed in profile page + */ +export interface SSOProvider { + email?: string; + /** Expiration time - Date for better-auth, number (Unix timestamp) for next-auth */ + expiresAt?: Date | number | null; + provider: string; + providerAccountId: string; +} + export const UserPreferenceSchema = z .object({ guide: UserGuideSchema.optional(), diff --git a/packages/utils/src/server/__tests__/auth.test.ts b/packages/utils/src/server/__tests__/auth.test.ts index b819f91a71..48bfd95aaf 100644 --- a/packages/utils/src/server/__tests__/auth.test.ts +++ b/packages/utils/src/server/__tests__/auth.test.ts @@ -3,10 +3,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { extractBearerToken, getUserAuth } from '../auth'; // Mock auth constants +let mockEnableBetterAuth = false; let mockEnableClerk = false; let mockEnableNextAuth = false; vi.mock('@/const/auth', () => ({ + get enableBetterAuth() { + return mockEnableBetterAuth; + }, get enableClerk() { return mockEnableClerk; }, @@ -38,9 +42,26 @@ vi.mock('@/libs/next-auth', () => ({ }, })); +vi.mock('next/headers', () => ({ + headers: vi.fn(() => new Headers()), +})); + +vi.mock('@/auth', () => ({ + auth: { + api: { + getSession: vi.fn().mockResolvedValue({ + user: { + id: 'better-auth-user-id', + }, + }), + }, + }, +})); + describe('getUserAuth', () => { beforeEach(() => { vi.clearAllMocks(); + mockEnableBetterAuth = false; mockEnableClerk = false; mockEnableNextAuth = false; }); @@ -92,6 +113,37 @@ describe('getUserAuth', () => { userId: 'clerk-user-id', }); }); + + it('should return better auth when better auth is enabled', async () => { + mockEnableBetterAuth = true; + + const auth = await getUserAuth(); + + expect(auth).toEqual({ + betterAuth: { + user: { + id: 'better-auth-user-id', + }, + }, + userId: 'better-auth-user-id', + }); + }); + + it('should prioritize better auth over next auth when both are enabled', async () => { + mockEnableBetterAuth = true; + mockEnableNextAuth = true; + + const auth = await getUserAuth(); + + expect(auth).toEqual({ + betterAuth: { + user: { + id: 'better-auth-user-id', + }, + }, + userId: 'better-auth-user-id', + }); + }); }); describe('extractBearerToken', () => { diff --git a/packages/utils/src/server/auth.ts b/packages/utils/src/server/auth.ts index 496ff0ad97..8ab26f5e7c 100644 --- a/packages/utils/src/server/auth.ts +++ b/packages/utils/src/server/auth.ts @@ -1,4 +1,6 @@ -import { enableClerk, enableNextAuth } from '@/const/auth'; +import { headers } from 'next/headers'; + +import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth'; import { DESKTOP_USER_ID } from '@/const/desktop'; import { isDesktop } from '@/const/version'; @@ -11,6 +13,21 @@ export const getUserAuth = async () => { return await clerkAuth.getAuth(); } + if (enableBetterAuth) { + const { auth: betterAuth } = await import('@/auth'); + + const currentHeaders = await headers(); + const requestHeaders = Object.fromEntries(currentHeaders.entries()); + + const session = await betterAuth.api.getSession({ + headers: requestHeaders, + }); + + const userId = session?.user?.id; + + return { betterAuth: session, userId }; + } + if (enableNextAuth) { const { default: NextAuth } = await import('@/libs/next-auth'); diff --git a/src/app/(backend)/api/auth/[...all]/route.ts b/src/app/(backend)/api/auth/[...all]/route.ts new file mode 100644 index 0000000000..b3b5f654c8 --- /dev/null +++ b/src/app/(backend)/api/auth/[...all]/route.ts @@ -0,0 +1,19 @@ +import { enableBetterAuth, enableNextAuth } from '@lobechat/const'; +import { toNextJsHandler } from 'better-auth/next-js'; + +import { auth } from '@/auth'; +import NextAuthNode from '@/libs/next-auth'; + +const betterAuthHandler = toNextJsHandler(auth); + +export const GET = enableBetterAuth + ? betterAuthHandler.GET + : enableNextAuth + ? NextAuthNode.handlers.GET + : undefined; + +export const POST = enableBetterAuth + ? betterAuthHandler.POST + : enableNextAuth + ? NextAuthNode.handlers.POST + : undefined; diff --git a/src/app/(backend)/api/auth/[...nextauth]/route.ts b/src/app/(backend)/api/auth/[...nextauth]/route.ts deleted file mode 100644 index b70b3b0990..0000000000 --- a/src/app/(backend)/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -import NextAuthNode from '@/libs/next-auth'; - -export const { GET, POST } = NextAuthNode.handlers; diff --git a/src/app/(backend)/api/auth/check-user/route.ts b/src/app/(backend)/api/auth/check-user/route.ts new file mode 100644 index 0000000000..331b422028 --- /dev/null +++ b/src/app/(backend)/api/auth/check-user/route.ts @@ -0,0 +1,62 @@ +import { and, eq } from 'drizzle-orm'; +import { NextRequest, NextResponse } from 'next/server'; + +import { account } from '@/database/schemas/betterAuth'; +import { users } from '@/database/schemas/user'; +import { serverDB } from '@/database/server'; + +/** + * Check if a user exists by email + * @param req - POST request with { email: string } + * @returns { exists: boolean, emailVerified?: boolean } + */ +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { email } = body; + + if (!email || typeof email !== 'string') { + return NextResponse.json({ error: 'Email is required', exists: false }, { status: 400 }); + } + + // Query database for user with this email + const [user] = await serverDB + .select({ + emailVerified: users.emailVerified, + id: users.id, + }) + .from(users) + .where(eq(users.email, email.toLowerCase().trim())) + .limit(1); + + if (!user) { + return NextResponse.json({ exists: false }); + } + + const accounts = await serverDB + .select({ + password: account.password, + providerId: account.providerId, + }) + .from(account) + .where(and(eq(account.userId, user.id))); + + const providers = Array.from(new Set(accounts.map((a) => a.providerId).filter(Boolean))); + const hasPassword = accounts.some( + (a) => + a.providerId === 'credential' && typeof a.password === 'string' && a.password.length > 0, + ); + + return NextResponse.json({ + emailVerified: user.emailVerified, + exists: true, + hasPassword, + providers, + }); + } catch (error) { + console.error('Error checking user existence:', error); + return NextResponse.json({ error: 'Internal server error', exists: false }, { status: 500 }); + } +} + +export const runtime = 'nodejs'; diff --git a/src/app/(backend)/middleware/auth/index.ts b/src/app/(backend)/middleware/auth/index.ts index 002b18994e..41230c70f0 100644 --- a/src/app/(backend)/middleware/auth/index.ts +++ b/src/app/(backend)/middleware/auth/index.ts @@ -12,6 +12,7 @@ import { LOBE_CHAT_AUTH_HEADER, LOBE_CHAT_OIDC_AUTH_HEADER, OAUTH_AUTHORIZED, + enableBetterAuth, enableClerk, } from '@/const/auth'; import { ClerkAuth } from '@/libs/clerk-auth'; @@ -49,6 +50,18 @@ export const checkAuth = // get Authorization from header const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER); const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED); + let betterAuthAuthorized = false; + + // better auth handler + if (enableBetterAuth) { + const { auth: betterAuth } = await import('@/auth'); + + const session = await betterAuth.api.getSession({ + headers: req.headers, + }); + + betterAuthAuthorized = !!session?.user?.id; + } if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized); @@ -81,6 +94,7 @@ export const checkAuth = checkAuthMethod({ accessCode: jwtPayload.accessCode, apiKey: jwtPayload.apiKey, + betterAuthAuthorized, clerkAuth, nextAuthAuthorized: oauthAuthorized, }); diff --git a/src/app/(backend)/middleware/auth/utils.test.ts b/src/app/(backend)/middleware/auth/utils.test.ts index 4944220cff..47f0223f88 100644 --- a/src/app/(backend)/middleware/auth/utils.test.ts +++ b/src/app/(backend)/middleware/auth/utils.test.ts @@ -7,6 +7,7 @@ import { checkAuthMethod } from './utils'; let enableClerkMock = false; let enableNextAuthMock = false; +let enableBetterAuthMock = false; vi.mock('@/const/auth', async (importOriginal) => { const data = await importOriginal(); @@ -16,6 +17,9 @@ vi.mock('@/const/auth', async (importOriginal) => { get enableClerk() { return enableClerkMock; }, + get enableBetterAuth() { + return enableBetterAuthMock; + }, get enableNextAuth() { return enableNextAuthMock; }, @@ -67,6 +71,18 @@ describe('checkAuthMethod', () => { enableNextAuthMock = false; }); + it('should pass with valid Better Auth session', () => { + enableBetterAuthMock = true; + + expect(() => + checkAuthMethod({ + betterAuthAuthorized: true, + }), + ).not.toThrow(); + + enableBetterAuthMock = false; + }); + it('should pass with valid API key', () => { expect(() => checkAuthMethod({ diff --git a/src/app/(backend)/middleware/auth/utils.ts b/src/app/(backend)/middleware/auth/utils.ts index 972396af47..4522a6c837 100644 --- a/src/app/(backend)/middleware/auth/utils.ts +++ b/src/app/(backend)/middleware/auth/utils.ts @@ -2,29 +2,29 @@ import { type AuthObject } from '@clerk/backend'; import { AgentRuntimeError } from '@lobechat/model-runtime'; import { ChatErrorType } from '@lobechat/types'; -import { enableClerk, enableNextAuth } from '@/const/auth'; +import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth'; import { getAppConfig } from '@/envs/app'; interface CheckAuthParams { accessCode?: string; apiKey?: string; + betterAuthAuthorized?: boolean; clerkAuth?: AuthObject; nextAuthAuthorized?: boolean; } /** * Check if the provided access code is valid, a user API key should be used or the OAuth 2 header is provided. * - * @param {string} accessCode - The access code to check. - * @param {string} apiKey - The user API key. - * @param {boolean} oauthAuthorized - Whether the OAuth 2 header is provided. + * @param {CheckAuthParams} params - Authentication parameters extracted from headers. + * @param {string} [params.accessCode] - The access code to check. + * @param {string} [params.apiKey] - The user API key. + * @param {boolean} [params.betterAuthAuthorized] - Whether the Better Auth session exists. + * @param {AuthObject} [params.clerkAuth] - Clerk authentication payload from middleware. + * @param {boolean} [params.nextAuthAuthorized] - Whether the OAuth 2 header is provided. * @throws {AgentRuntimeError} If the access code is invalid and no user API key is provided. */ -export const checkAuthMethod = ({ - apiKey, - nextAuthAuthorized, - accessCode, - clerkAuth, -}: CheckAuthParams) => { +export const checkAuthMethod = (params: CheckAuthParams) => { + const { apiKey, betterAuthAuthorized, nextAuthAuthorized, accessCode, clerkAuth } = params; // clerk auth handler if (enableClerk) { // if there is no userId, means the use is not login, just throw error @@ -34,6 +34,9 @@ export const checkAuthMethod = ({ else return; } + // if better auth session exists + if (enableBetterAuth && betterAuthAuthorized) return; + // if next auth handler is provided if (enableNextAuth && nextAuthAuthorized) return; diff --git a/src/app/(backend)/webapi/chat/[provider]/route.test.ts b/src/app/(backend)/webapi/chat/[provider]/route.test.ts index 31d6cda8d1..46f6893cf6 100644 --- a/src/app/(backend)/webapi/chat/[provider]/route.test.ts +++ b/src/app/(backend)/webapi/chat/[provider]/route.test.ts @@ -135,6 +135,7 @@ describe('POST handler', () => { expect(checkAuthMethod).toBeCalledWith({ accessCode: 'test-access-code', apiKey: 'test-api-key', + betterAuthAuthorized: false, clerkAuth: {}, nextAuthAuthorized: true, }); diff --git a/src/app/[variants]/(auth)/reset-password/layout.tsx b/src/app/[variants]/(auth)/reset-password/layout.tsx new file mode 100644 index 0000000000..bfe28ec565 --- /dev/null +++ b/src/app/[variants]/(auth)/reset-password/layout.tsx @@ -0,0 +1,12 @@ +import { notFound } from 'next/navigation'; +import { PropsWithChildren } from 'react'; + +import { enableBetterAuth } from '@/const/auth'; + +const Layout = ({ children }: PropsWithChildren) => { + if (!enableBetterAuth) return notFound(); + + return children; +}; + +export default Layout; diff --git a/src/app/[variants]/(auth)/reset-password/page.tsx b/src/app/[variants]/(auth)/reset-password/page.tsx new file mode 100644 index 0000000000..1b1f4d3191 --- /dev/null +++ b/src/app/[variants]/(auth)/reset-password/page.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { Button } from '@lobehub/ui'; +import { LobeHub } from '@lobehub/ui/brand'; +import { Form, Input } from 'antd'; +import { createStyles, useTheme } from 'antd-style'; +import { ArrowLeft, KeyRound, Lock } from 'lucide-react'; +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Center, Flexbox } from 'react-layout-kit'; + +import { message } from '@/components/AntdStaticMethods'; +import { resetPassword } from '@/libs/better-auth/auth-client'; + +const useStyles = createStyles(({ css, token }) => ({ + backLink: css` + display: inline-flex; + gap: 6px; + align-items: center; + + font-size: 14px; + color: ${token.colorTextSecondary}; + text-decoration: none; + + transition: color 0.2s ease; + + &:hover { + color: ${token.colorText}; + } + `, + container: css` + max-width: 400px; + padding: 2rem; + text-align: center; + `, + description: css` + font-size: 14px; + line-height: 1.6; + color: ${token.colorTextSecondary}; + `, + email: css` + font-weight: 500; + color: ${token.colorText}; + `, + iconWrapper: css` + display: inline-flex; + align-items: center; + justify-content: center; + + width: 80px; + height: 80px; + border-radius: 50%; + + background: linear-gradient( + 135deg, + ${token.colorPrimaryBg} 0%, + ${token.colorPrimaryBgHover} 100% + ); + `, + title: css` + margin-block: 0; + font-size: 24px; + font-weight: 600; + color: ${token.colorTextHeading}; + `, +})); + +interface ResetPasswordFormValues { + confirmPassword: string; + newPassword: string; +} + +export default function ResetPasswordPage() { + const { styles } = useStyles(); + const theme = useTheme(); + const { t } = useTranslation('auth'); + const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams.get('token'); + const email = searchParams.get('email'); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + const handleResetPassword = async (values: ResetPasswordFormValues) => { + if (!token) { + message.error(t('betterAuth.resetPassword.invalidToken')); + return; + } + + setLoading(true); + try { + const result = await resetPassword({ + newPassword: values.newPassword, + token, + }); + + if (result.error) { + message.error(result.error.message || t('betterAuth.resetPassword.error')); + return; + } + + message.success(t('betterAuth.resetPassword.success')); + router.push(email ? `/signin?email=${encodeURIComponent(email)}` : '/signin'); + } catch (error) { + console.error('Reset password error:', error); + message.error(t('betterAuth.resetPassword.error')); + } finally { + setLoading(false); + } + }; + + // Show error if no token + if (!token) { + return ( +
+ + +

{t('betterAuth.resetPassword.title')}

+

{t('betterAuth.resetPassword.invalidToken')}

+ + + {t('betterAuth.resetPassword.backToSignIn')} + +
+
+ ); + } + + return ( +
+ + + +

{t('betterAuth.resetPassword.title')}

+ +
+ +
+ +

+ {t('betterAuth.resetPassword.description')} + {email && ( + <> +
+ {email} + + )} +

+ +
+ + } + size="large" + /> + + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error(t('betterAuth.resetPassword.passwordMismatch'))); + }, + }), + ]} + > + } + size="large" + /> + + + + + +
+ + + + {t('betterAuth.resetPassword.backToSignIn')} + +
+
+ ); +} diff --git a/src/app/[variants]/(auth)/signin/layout.tsx b/src/app/[variants]/(auth)/signin/layout.tsx new file mode 100644 index 0000000000..bfe28ec565 --- /dev/null +++ b/src/app/[variants]/(auth)/signin/layout.tsx @@ -0,0 +1,12 @@ +import { notFound } from 'next/navigation'; +import { PropsWithChildren } from 'react'; + +import { enableBetterAuth } from '@/const/auth'; + +const Layout = ({ children }: PropsWithChildren) => { + if (!enableBetterAuth) return notFound(); + + return children; +}; + +export default Layout; diff --git a/src/app/[variants]/(auth)/signin/page.tsx b/src/app/[variants]/(auth)/signin/page.tsx new file mode 100644 index 0000000000..3ad3f94487 --- /dev/null +++ b/src/app/[variants]/(auth)/signin/page.tsx @@ -0,0 +1,448 @@ +'use client'; + +import { ActionIcon, Button } from '@lobehub/ui'; +import { LobeHub } from '@lobehub/ui/brand'; +import { Form, Input, type InputRef } from 'antd'; +import { createStyles, useTheme } from 'antd-style'; +import { ChevronLeft, ChevronRight, Lock, Mail } from 'lucide-react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { message } from '@/components/AntdStaticMethods'; +import AuthIcons from '@/components/NextAuth/AuthIcons'; +import { getAuthConfig } from '@/envs/auth'; +import { requestPasswordReset, signIn } from '@/libs/better-auth/auth-client'; +import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client'; +import { useUserStore } from '@/store/user'; + +const useStyles = createStyles(({ css, token }) => ({ + backButton: css` + cursor: pointer; + font-size: 14px; + color: ${token.colorPrimary}; + + &:hover { + color: ${token.colorPrimaryHover}; + } + `, + card: css` + padding-block: 2.5rem; + padding-inline: 2rem; + `, + container: css` + width: 360px; + border: 1px solid ${token.colorBorder}; + border-radius: ${token.borderRadiusLG}px; + `, + divider: css` + flex: 1; + height: 1px; + background: ${token.colorBorder}; + `, + dividerText: css` + font-size: 14px; + color: ${token.colorTextSecondary}; + `, + emailDisplay: css` + font-size: 14px; + color: ${token.colorTextSecondary}; + text-align: center; + `, + footer: css` + padding: 1rem; + border-block-start: 1px solid ${token.colorBorder}; + + font-size: 14px; + color: ${token.colorTextDescription}; + text-align: center; + + background: ${token.colorBgElevated}; + `, + subtitle: css` + margin-block-start: 0.5rem; + font-size: 14px; + color: ${token.colorTextSecondary}; + text-align: center; + `, + title: css` + margin-block-start: 1rem; + + font-size: 24px; + font-weight: 600; + color: ${token.colorTextHeading}; + text-align: center; + `, +})); + +type Step = 'email' | 'password'; + +interface SignInFormValues { + email: string; + password: string; +} + +export default function SignInPage() { + const { styles } = useStyles(); + const theme = useTheme(); + const { t } = useTranslation('auth'); + const router = useRouter(); + const searchParams = useSearchParams(); + const { NEXT_PUBLIC_ENABLE_MAGIC_LINK: enableMagicLink } = getAuthConfig(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [socialLoading, setSocialLoading] = useState(null); + const [step, setStep] = useState('email'); + const [email, setEmail] = useState(''); + const emailInputRef = useRef(null); + const passwordInputRef = useRef(null); + const oAuthSSOProviders = useUserStore((s) => s.oAuthSSOProviders || []); + + // Auto-focus input when step changes + useEffect(() => { + if (step === 'email') { + emailInputRef.current?.focus(); + } else if (step === 'password') { + passwordInputRef.current?.focus(); + } + }, [step]); + + // Pre-fill email from URL params + useEffect(() => { + const emailParam = searchParams.get('email'); + if (emailParam) { + form.setFieldValue('email', emailParam); + } + }, [searchParams, form]); + + const handleSendMagicLink = async (targetEmail?: string) => { + try { + const emailValue = + targetEmail || + (await form + .validateFields(['email']) + .then((v) => v.email as string) + .catch(() => null)); + + if (!emailValue) { + return; + } + + const callbackUrl = searchParams.get('callbackUrl') || '/'; + const { error } = await signIn.magicLink({ + callbackURL: callbackUrl, + email: emailValue, + }); + + if (error) { + message.error(error.message || t('betterAuth.signin.magicLinkError')); + return; + } + + message.success(t('betterAuth.signin.magicLinkSent')); + } catch (error) { + // validation errors are surfaced by antd form; only log unexpected errors + if (!(error as any)?.errorFields) { + console.error('Magic link error:', error); + message.error(t('betterAuth.signin.magicLinkError')); + } + } finally { + // no-op + } + }; + + // Check if user exists + const handleCheckUser = async (values: Pick) => { + setLoading(true); + try { + const response = await fetch('/api/auth/check-user', { + body: JSON.stringify({ email: values.email }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }); + + const data = await response.json(); + + if (!data.exists) { + // User not found, redirect to signup page with email pre-filled + const callbackUrl = searchParams.get('callbackUrl') || '/'; + router.push( + `/signup?email=${encodeURIComponent(values.email)}&callbackUrl=${encodeURIComponent(callbackUrl)}`, + ); + return; + } + + setEmail(values.email); + + if (enableMagicLink) { + await handleSendMagicLink(values.email); + return; + } + + if (data.hasPassword) { + setStep('password'); + return; + } + + message.info(t('betterAuth.signin.socialOnlyHint')); + } catch (error) { + console.error('Error checking user:', error); + message.error(t('betterAuth.signin.error')); + } finally { + setLoading(false); + } + }; + + // Sign in with email and password + const handleSignIn = async (values: SignInFormValues) => { + setLoading(true); + try { + const callbackUrl = searchParams.get('callbackUrl') || '/'; + + const result = await signIn.email( + { + callbackURL: callbackUrl, + email: email, + password: values.password, + }, + { + onError: (ctx) => { + console.error('Sign in error:', ctx.error); + // Check if error is due to unverified email (403 status) + if (ctx.error.status === 403) { + // Redirect to verify-email page instead of showing error + router.push( + `/verify-email?email=${encodeURIComponent(email)}&callbackUrl=${encodeURIComponent(callbackUrl)}`, + ); + return; + } + }, + onSuccess: () => { + router.push(callbackUrl); + }, + }, + ); + + // Only show error if not already handled in onError callback + if (result.error && result.error.status !== 403) { + message.error(result.error.message || t('betterAuth.signin.error')); + return; + } + } catch (error) { + console.error('Sign in error:', error); + message.error(t('betterAuth.signin.error')); + } finally { + setLoading(false); + } + }; + + const handleBackToEmail = () => { + setStep('email'); + setEmail(''); + }; + + const handleGoToSignup = () => { + const currentEmail = form.getFieldValue('email'); + const callbackUrl = searchParams.get('callbackUrl') || '/'; + const params = new URLSearchParams(); + if (currentEmail) { + params.set('email', currentEmail); + } + params.set('callbackUrl', callbackUrl); + router.push(`/signup?${params.toString()}`); + }; + + const getProviderLabel = (provider: string) => { + const normalized = normalizeProviderId(provider); + const normalizedKey = normalized + .replaceAll(/(^|[_-])([a-z])/g, (_, __, c) => c.toUpperCase()) + .replaceAll(/[^\dA-Za-z]/g, ''); + const key = `betterAuth.signin.continueWith${normalizedKey}`; + return t(key, { defaultValue: `Continue with ${normalized}` }); + }; + + const handleSocialSignIn = async (provider: string) => { + setSocialLoading(provider); + const normalizedProvider = normalizeProviderId(provider); + + try { + const callbackUrl = searchParams.get('callbackUrl') || '/'; + const result = isBuiltinProvider(normalizedProvider) + ? await signIn.social({ + callbackURL: callbackUrl, + provider: normalizedProvider, + }) + : await signIn.oauth2({ + callbackURL: callbackUrl, + providerId: normalizedProvider, + }); + + if (result?.error) { + throw result.error; + } + } catch (error) { + console.error(`${normalizedProvider} sign in error:`, error); + message.error(t('betterAuth.signin.socialError')); + } finally { + setSocialLoading(null); + } + }; + + return ( + +
+
+ + + + +

{t('betterAuth.signin.emailStep.title')}

+ + {step === 'email' && ( + <> +

{t('betterAuth.signin.emailStep.subtitle')}

+ + {/* Social Login Section */} + {oAuthSSOProviders.length > 0 && ( + + {oAuthSSOProviders.map((provider) => ( + + ))} + + {/* Divider */} + +
+ + {t('betterAuth.signin.orContinueWith')} + +
+ + + )} + +
+ + } + ref={emailInputRef} + size="large" + suffix={ + form.submit()} + size={{ blockSize: 32, size: 16 }} + style={{ color: theme.colorPrimary }} + title={t('betterAuth.signin.nextStep')} + /> + } + /> + +
+ + )} + + {step === 'password' && ( + <> +

{email}

+
+ + {t('betterAuth.signin.backToEmail')} +
+

+ {t('betterAuth.signin.passwordStep.subtitle')} +

+ +
+ + } + ref={passwordInputRef} + size="large" + suffix={ + form.submit()} + size={{ blockSize: 32, size: 16 }} + style={{ color: theme.colorPrimary }} + title={t('betterAuth.signin.submit')} + /> + } + /> + +
+ +
{ + try { + await requestPasswordReset({ + email, + redirectTo: `/reset-password?email=${encodeURIComponent(email)}`, + }); + message.success(t('betterAuth.signin.forgotPasswordSent')); + } catch { + message.error(t('betterAuth.signin.forgotPasswordError')); + } + }} + style={{ marginTop: '1rem', textAlign: 'center' }} + > + {t('betterAuth.signin.forgotPassword')} +
+ + )} +
+ +
+ {t('betterAuth.signin.noAccount')}{' '} + + {t('betterAuth.signin.signupLink')} + +
+
+
+ ); +} diff --git a/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx b/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx new file mode 100644 index 0000000000..07e3d7fe7e --- /dev/null +++ b/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx @@ -0,0 +1,192 @@ +'use client'; + +import { Button } from '@lobehub/ui'; +import { LobeHub } from '@lobehub/ui/brand'; +import { Form, Input } from 'antd'; +import { createStyles } from 'antd-style'; +import { ChevronRight, Lock, Mail } from 'lucide-react'; +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { message } from '@/components/AntdStaticMethods'; +import { authEnv } from '@/envs/auth'; +import { signUp } from '@/libs/better-auth/auth-client'; + +const useStyles = createStyles(({ css, token }) => ({ + card: css` + padding-block: 2.5rem; + padding-inline: 2rem; + `, + container: css` + width: 360px; + border: 1px solid ${token.colorBorder}; + border-radius: ${token.borderRadiusLG}px; + `, + footer: css` + padding: 1rem; + border-block-start: 1px solid ${token.colorBorder}; + + font-size: 14px; + color: ${token.colorTextDescription}; + text-align: center; + + background: ${token.colorBgElevated}; + `, + subtitle: css` + margin-block-start: 0.5rem; + font-size: 14px; + color: ${token.colorTextSecondary}; + text-align: center; + `, + title: css` + margin-block-start: 1rem; + + font-size: 24px; + font-weight: 600; + color: ${token.colorTextHeading}; + text-align: center; + `, +})); + +interface SignUpFormValues { + email: string; + password: string; +} + +export default function BetterAuthSignUpForm() { + const { styles } = useStyles(); + const { t } = useTranslation('auth'); + const router = useRouter(); + const searchParams = useSearchParams(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + // Pre-fill email from query params (from signin page redirect) + useEffect(() => { + const email = searchParams.get('email'); + if (email) { + form.setFieldsValue({ email }); + } + }, [searchParams, form]); + + const handleSignUp = async (values: SignUpFormValues) => { + setLoading(true); + try { + const callbackUrl = searchParams.get('callbackUrl') || '/'; + + // Generate username from email (use the part before @) + const username = values.email.split('@')[0]; + + const { error } = await signUp.email({ + callbackURL: callbackUrl, + email: values.email, + name: username, + password: values.password, + }); + + if (error) { + message.error(error.message || t('betterAuth.signup.error')); + return; + } + + // Redirect based on email verification requirement + if (authEnv.NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION) { + // Email verification required, redirect to verification notice page + // callbackURL is already passed to signUp.email for verification link + router.push( + `/verify-email?email=${encodeURIComponent(values.email)}&callbackUrl=${encodeURIComponent(callbackUrl)}`, + ); + } else { + // Email verification not required, user is already logged in (autoSignIn: true) + // Redirect to callback URL or home + router.push(callbackUrl); + } + } catch { + message.error(t('betterAuth.signup.error')); + } finally { + setLoading(false); + } + }; + + return ( + +
+
+ + + + +

{t('betterAuth.signup.title')}

+

{t('betterAuth.signup.subtitle')}

+ +
+ + } + size="large" + /> + + + { + if (!value) return Promise.resolve(); + const hasLetter = /[A-Za-z]/.test(value); + const hasNumber = /\d/.test(value); + if (hasLetter && hasNumber) { + return Promise.resolve(); + } + return Promise.reject(); + }, + }, + ]} + > + } + size="large" + /> + + + + + +
+
+ +
+ {t('betterAuth.signup.hasAccount')}{' '} + + {t('betterAuth.signup.signinLink')} + +
+
+
+ ); +} diff --git a/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx b/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx index a45995817d..acd6d3c367 100644 --- a/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +++ b/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx @@ -1,26 +1,51 @@ import { SignUp } from '@clerk/nextjs'; import { notFound } from 'next/navigation'; -import { enableClerk } from '@/const/auth'; +import { enableBetterAuth, enableClerk } from '@/const/auth'; import { metadataModule } from '@/server/metadata'; import { translation } from '@/server/translation'; import { DynamicLayoutProps } from '@/types/next'; import { RouteVariants } from '@/utils/server/routeVariants'; +import BetterAuthSignUpForm from './BetterAuthSignUpForm'; + export const generateMetadata = async (props: DynamicLayoutProps) => { const locale = await RouteVariants.getLocale(props); - const { t } = await translation('clerk', locale); + + if (enableClerk) { + const { t } = await translation('clerk', locale); + return metadataModule.generate({ + description: t('signUp.start.subtitle'), + title: t('signUp.start.title'), + url: '/signup', + }); + } + + if (enableBetterAuth) { + const { t } = await translation('auth', locale); + return metadataModule.generate({ + description: t('betterAuth.signup.subtitle'), + title: t('betterAuth.signup.title'), + url: '/signup', + }); + } + return metadataModule.generate({ - description: t('signUp.start.subtitle'), - title: t('signUp.start.title'), + title: 'Sign Up', url: '/signup', }); }; const Page = () => { - if (!enableClerk) return notFound(); + if (enableClerk) { + return ; + } - return ; + if (enableBetterAuth) { + return ; + } + + return notFound(); }; Page.displayName = 'SignUp'; diff --git a/src/app/[variants]/(auth)/verify-email/layout.tsx b/src/app/[variants]/(auth)/verify-email/layout.tsx new file mode 100644 index 0000000000..bfe28ec565 --- /dev/null +++ b/src/app/[variants]/(auth)/verify-email/layout.tsx @@ -0,0 +1,12 @@ +import { notFound } from 'next/navigation'; +import { PropsWithChildren } from 'react'; + +import { enableBetterAuth } from '@/const/auth'; + +const Layout = ({ children }: PropsWithChildren) => { + if (!enableBetterAuth) return notFound(); + + return children; +}; + +export default Layout; diff --git a/src/app/[variants]/(auth)/verify-email/page.tsx b/src/app/[variants]/(auth)/verify-email/page.tsx new file mode 100644 index 0000000000..78fef89053 --- /dev/null +++ b/src/app/[variants]/(auth)/verify-email/page.tsx @@ -0,0 +1,164 @@ +'use client'; + +import { Button } from '@lobehub/ui'; +import { LobeHub } from '@lobehub/ui/brand'; +import { createStyles, useTheme } from 'antd-style'; +import { ArrowLeft, Mail, RefreshCw } from 'lucide-react'; +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Center, Flexbox } from 'react-layout-kit'; + +import { message } from '@/components/AntdStaticMethods'; +import { sendVerificationEmail } from '@/libs/better-auth/auth-client'; + +const useStyles = createStyles(({ css, token }) => ({ + backLink: css` + display: inline-flex; + gap: 6px; + align-items: center; + + font-size: 14px; + color: ${token.colorTextSecondary}; + text-decoration: none; + + transition: color 0.2s ease; + + &:hover { + color: ${token.colorText}; + } + `, + container: css` + max-width: 480px; + padding: 2rem; + text-align: center; + `, + description: css` + font-size: 16px; + line-height: 1.6; + color: ${token.colorText}; + `, + hint: css` + margin-block-start: 0.5rem; + font-size: 14px; + color: ${token.colorTextTertiary}; + `, + iconWrapper: css` + display: inline-flex; + align-items: center; + justify-content: center; + + width: 96px; + height: 96px; + border-radius: 50%; + + background: linear-gradient( + 135deg, + ${token.colorPrimaryBg} 0%, + ${token.colorPrimaryBgHover} 100% + ); + `, + mailLink: css` + font-weight: 500; + color: ${token.colorPrimary}; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + `, + resendButton: css` + margin-block-start: 0.5rem; + `, + textGroup: css` + display: flex; + flex-direction: column; + gap: 0.5rem; + `, + title: css` + margin-block: 0; + font-size: 28px; + font-weight: 600; + color: ${token.colorTextHeading}; + `, +})); + +export default function VerifyEmailPage() { + const { styles } = useStyles(); + const theme = useTheme(); + const { t } = useTranslation('auth'); + const searchParams = useSearchParams(); + const email = searchParams.get('email'); + const [resending, setResending] = useState(false); + + const handleResendEmail = async () => { + if (!email) { + message.error(t('betterAuth.verifyEmail.resend.noEmail')); + return; + } + + setResending(true); + try { + const callbackUrl = searchParams.get('callbackUrl') || '/'; + + const result = await sendVerificationEmail({ + callbackURL: callbackUrl, + email, + }); + + if (result.error) { + message.error(result.error.message || t('betterAuth.verifyEmail.resend.error')); + return; + } + + message.success(t('betterAuth.verifyEmail.resend.success')); + } catch (error) { + console.error('Error resending verification email:', error); + message.error(t('betterAuth.verifyEmail.resend.error')); + } finally { + setResending(false); + } + }; + + return ( +
+ + + +

{t('betterAuth.verifyEmail.title')}

+ +
+ +
+ +
+

+ {t('betterAuth.verifyEmail.descriptionPrefix')}{' '} + + {email} + {' '} + {t('betterAuth.verifyEmail.descriptionSuffix')} +

+

{t('betterAuth.verifyEmail.checkSpam')}

+
+ + + + + + {t('betterAuth.verifyEmail.backToSignIn')} + +
+
+ ); +} diff --git a/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx b/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx index 4006377b49..4d45ab94da 100644 --- a/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +++ b/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx @@ -31,22 +31,24 @@ vi.mock('@/const/version', () => ({ isDesktop: false, })); -// 定义一个变量来存储 enableAuth 的值 -let enableAuth = true; -let enableClerk = false; +// Use vi.hoisted to ensure variables exist before vi.mock factory executes +const { enableAuth, enableClerk } = vi.hoisted(() => ({ + enableAuth: { value: true }, + enableClerk: { value: false }, +})); -// 模拟 @/const/auth 模块 vi.mock('@/const/auth', () => ({ get enableAuth() { - return enableAuth; + return enableAuth.value; }, get enableClerk() { - return enableClerk; + return enableClerk.value; }, })); afterEach(() => { - enableAuth = true; + enableAuth.value = true; + enableClerk.value = false; mockNavigate.mockReset(); }); @@ -55,7 +57,7 @@ describe('UserBanner', () => { act(() => { useUserStore.setState({ isSignedIn: false }); }); - enableAuth = false; + enableAuth.value = false; render(); @@ -69,7 +71,7 @@ describe('UserBanner', () => { useUserStore.setState({ isSignedIn: true }); }); - enableClerk = true; + enableClerk.value = true; render(); @@ -82,7 +84,7 @@ describe('UserBanner', () => { act(() => { useUserStore.setState({ isSignedIn: false }); }); - enableClerk = true; + enableClerk.value = true; render(); diff --git a/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx b/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx index 3861d424cc..3625ba1fb1 100644 --- a/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +++ b/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx @@ -22,16 +22,18 @@ vi.mock('react-i18next', () => ({ })), })); -// 定义一个变量来存储 enableAuth 的值 -let enableAuth = true; -let enableClerk = true; -// 模拟 @/const/auth 模块 +// Use vi.hoisted to ensure variables exist before vi.mock factory executes +const { enableAuth, enableClerk } = vi.hoisted(() => ({ + enableAuth: { value: true }, + enableClerk: { value: true }, +})); + vi.mock('@/const/auth', () => ({ get enableAuth() { - return enableAuth; + return enableAuth.value; }, get enableClerk() { - return enableClerk; + return enableClerk.value; }, })); @@ -45,8 +47,8 @@ vi.mock('@/const/version', async (importOriginal) => { }); afterEach(() => { - enableAuth = true; - enableClerk = true; + enableAuth.value = true; + enableClerk.value = true; mockNavigate.mockReset(); }); @@ -55,8 +57,8 @@ describe('useCategory', () => { act(() => { useUserStore.setState({ isSignedIn: true }); }); - enableAuth = true; - enableClerk = false; + enableAuth.value = true; + enableClerk.value = false; const { result } = renderHook(() => useCategory(), { wrapper }); @@ -74,7 +76,7 @@ describe('useCategory', () => { act(() => { useUserStore.setState({ isSignedIn: false }); }); - enableAuth = true; + enableAuth.value = true; const { result } = renderHook(() => useCategory(), { wrapper }); diff --git a/src/app/[variants]/(main)/profile/(home)/Client.tsx b/src/app/[variants]/(main)/profile/(home)/Client.tsx index 3d57c56dc2..c060ba2a56 100644 --- a/src/app/[variants]/(main)/profile/(home)/Client.tsx +++ b/src/app/[variants]/(main)/profile/(home)/Client.tsx @@ -1,35 +1,280 @@ 'use client'; -import { Form, type FormGroupItemType, Input } from '@lobehub/ui'; -import { Skeleton } from 'antd'; -import { memo } from 'react'; +import { LoadingOutlined } from '@ant-design/icons'; +import { Button, Divider, Input, Skeleton, Spin, Typography, Upload } from 'antd'; +import { AnimatePresence, motion } from 'framer-motion'; +import { CSSProperties, ReactNode, memo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; +import { notification } from '@/components/AntdStaticMethods'; +import { fetchErrorNotification } from '@/components/Error/fetchErrorNotification'; import { enableAuth } from '@/const/auth'; -import { FORM_STYLE } from '@/const/layoutTokens'; -import AvatarWithUpload from '@/features/AvatarWithUpload'; import UserAvatar from '@/features/User/UserAvatar'; +import { requestPasswordReset } from '@/libs/better-auth/auth-client'; import { useUserStore } from '@/store/user'; import { authSelectors, userProfileSelectors } from '@/store/user/selectors'; +import { imageToBase64 } from '@/utils/imageToBase64'; +import { createUploadImageHandler } from '@/utils/uploadFIle'; import SSOProvidersList from './features/SSOProvidersList'; +interface ProfileRowProps { + action?: ReactNode; + children: ReactNode; + label: string; +} + +const rowStyle: CSSProperties = { + minHeight: 48, + padding: '16px 0', +}; + +const labelStyle: CSSProperties = { + flexShrink: 0, + width: 160, +}; + +const ProfileRow = memo(({ label, children, action }) => ( + + + {label} + {children} + + {action && {action}} + +)); + +const AvatarRow = memo(() => { + const { t } = useTranslation('auth'); + const isLogin = useUserStore(authSelectors.isLogin); + const updateAvatar = useUserStore((s) => s.updateAvatar); + const [uploading, setUploading] = useState(false); + + const handleUploadAvatar = useCallback( + createUploadImageHandler(async (avatar) => { + try { + setUploading(true); + const img = new Image(); + img.src = avatar; + + await new Promise((resolve, reject) => { + img.addEventListener('load', resolve); + img.addEventListener('error', reject); + }); + + const webpBase64 = imageToBase64({ img, size: 256 }); + await updateAvatar(webpBase64); + setUploading(false); + } catch (error) { + console.error('Failed to upload avatar:', error); + setUploading(false); + + fetchErrorNotification.error({ + errorMessage: error instanceof Error ? error.message : String(error), + status: 500, + }); + } + }), + [updateAvatar], + ); + + const canUpload = !enableAuth || isLogin; + + return ( + + + {t('profile.avatar')} + + {canUpload ? ( + } spinning={uploading}> + void 0} maxCount={1}> + + + + ) : ( + + )} + + + {canUpload && ( + void 0} maxCount={1}> + + {t('profile.updateAvatar')} + + + )} + + ); +}); + +const FullNameRow = memo(() => { + const { t } = useTranslation('auth'); + const fullName = useUserStore(userProfileSelectors.fullName); + const updateFullName = useUserStore((s) => s.updateFullName); + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(''); + const [saving, setSaving] = useState(false); + + const handleStartEdit = () => { + setEditValue(fullName || ''); + setIsEditing(true); + }; + + const handleCancel = () => { + setIsEditing(false); + setEditValue(''); + }; + + const handleSave = useCallback(async () => { + if (!editValue.trim()) return; + + try { + setSaving(true); + await updateFullName(editValue.trim()); + setIsEditing(false); + } catch (error) { + console.error('Failed to update fullName:', error); + fetchErrorNotification.error({ + errorMessage: error instanceof Error ? error.message : String(error), + status: 500, + }); + } finally { + setSaving(false); + } + }, [editValue, updateFullName]); + + return ( + + {t('profile.fullName')} + + + {isEditing ? ( + + + {t('profile.fullNameInputHint')} + setEditValue(e.target.value)} + onPressEnter={handleSave} + placeholder={t('profile.fullName')} + value={editValue} + /> + + + + + + + ) : ( + + + {fullName || '--'} + + {t('profile.updateFullName')} + + + + )} + + + + ); +}); + +const PasswordRow = memo(() => { + const { t } = useTranslation('auth'); + const userProfile = useUserStore(userProfileSelectors.userProfile); + const [sending, setSending] = useState(false); + + const handleChangePassword = useCallback(async () => { + if (!userProfile?.email) return; + + try { + setSending(true); + await requestPasswordReset({ + email: userProfile.email, + redirectTo: `/reset-password?email=${encodeURIComponent(userProfile.email)}`, + }); + notification.success({ + message: t('profile.resetPasswordSent'), + }); + } catch (error) { + console.error('Failed to send reset password email:', error); + notification.error({ + message: t('profile.resetPasswordError'), + }); + } finally { + setSending(false); + } + }, [userProfile?.email, t]); + + return ( + + {t('profile.changePassword')} + + } + label={t('profile.password')} + > + •••••• + + ); +}); + const Client = memo<{ mobile?: boolean }>(({ mobile }) => { - const [isLoginWithNextAuth, isLogin] = useUserStore((s) => [ + const [isLoginWithNextAuth, isLoginWithBetterAuth] = useUserStore((s) => [ authSelectors.isLoginWithNextAuth(s), - authSelectors.isLogin(s), + authSelectors.isLoginWithBetterAuth(s), ]); - const [nickname, username, userProfile, loading] = useUserStore((s) => [ - userProfileSelectors.nickName(s), + const [username, userProfile, isUserLoaded] = useUserStore((s) => [ userProfileSelectors.username(s), userProfileSelectors.userProfile(s), - !s.isLoaded, + s.isLoaded, ]); + const isEmailPasswordAuth = useUserStore(authSelectors.isEmailPasswordAuth); + const isLoadedAuthProviders = useUserStore(authSelectors.isLoadedAuthProviders); + const fetchAuthProviders = useUserStore((s) => s.fetchAuthProviders); + + const isLoginWithAuth = isLoginWithNextAuth || isLoginWithBetterAuth; + const isLoading = !isUserLoaded || (isLoginWithAuth && !isLoadedAuthProviders); + + useEffect(() => { + if (isLoginWithAuth) { + fetchAuthProviders(); + } + }, [isLoginWithAuth, fetchAuthProviders]); - const [form] = Form.useForm(); const { t } = useTranslation('auth'); - if (loading) + if (isLoading) return ( (({ mobile }) => { /> ); - const profile: FormGroupItemType = { - children: [ - { - children: enableAuth && !isLogin ? : , - label: t('profile.avatar'), - layout: 'horizontal', - minWidth: undefined, - }, - { - children: , - label: t('profile.username'), - name: 'username', - }, - { - children: , - hidden: !isLoginWithNextAuth || !userProfile?.email, - label: t('profile.email'), - name: 'email', - }, - { - children: , - hidden: !isLoginWithNextAuth, - label: t('profile.sso.providers'), - layout: 'vertical', - minWidth: undefined, - }, - ], - title: t('tab.profile'), - }; return ( -
+ + + {t('profile.title')} + + + + + {/* Avatar Row - Editable */} + + + + + {/* Full Name Row - Editable */} + + + + + {/* Username Row - Read Only */} + + {username || '--'} + + + + + {/* Password Row - Only for Better Auth users with credential login */} + {isLoginWithBetterAuth && isEmailPasswordAuth && ( + <> + + + + )} + + {/* Email Row - Read Only */} + {isLoginWithAuth && userProfile?.email && ( + <> + + {userProfile.email} + + + + )} + + {/* SSO Providers Row */} + {isLoginWithAuth && ( + + + + )} + ); }); diff --git a/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx b/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx index 21f802a7ef..45d058d906 100644 --- a/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +++ b/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx @@ -1,38 +1,48 @@ -import { ActionIcon, CopyButton, List } from '@lobehub/ui'; -import { RotateCw, Unlink } from 'lucide-react'; -import { CSSProperties, memo, useState } from 'react'; +import { ActionIcon } from '@lobehub/ui'; +import { Dropdown, type MenuProps, Typography } from 'antd'; +import { ArrowRight, Plus, Unlink } from 'lucide-react'; +import { CSSProperties, memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; import { modal, notification } from '@/components/AntdStaticMethods'; import AuthIcons from '@/components/NextAuth/AuthIcons'; -import { useOnlyFetchOnceSWR } from '@/libs/swr'; +import { linkSocial, unlinkAccount } from '@/libs/better-auth/auth-client'; import { userService } from '@/services/user'; +import { useServerConfigStore } from '@/store/serverConfig'; +import { serverConfigSelectors } from '@/store/serverConfig/selectors'; import { useUserStore } from '@/store/user'; -import { userProfileSelectors } from '@/store/user/selectors'; - -const { Item } = List; +import { authSelectors, userProfileSelectors } from '@/store/user/selectors'; const providerNameStyle: CSSProperties = { textTransform: 'capitalize', }; export const SSOProvidersList = memo(() => { - const [userProfile] = useUserStore((s) => [userProfileSelectors.userProfile(s)]); + const userProfile = useUserStore(userProfileSelectors.userProfile); + const isLoginWithBetterAuth = useUserStore(authSelectors.isLoginWithBetterAuth); + const providers = useUserStore(authSelectors.authProviders); + const isEmailPasswordAuth = useUserStore(authSelectors.isEmailPasswordAuth); + const refreshAuthProviders = useUserStore((s) => s.refreshAuthProviders); + const oAuthSSOProviders = useServerConfigStore(serverConfigSelectors.oAuthSSOProviders); const { t } = useTranslation('auth'); - const [allowUnlink, setAllowUnlink] = useState(false); - const [hoveredIndex, setHoveredIndex] = useState(null); + // Allow unlink if user has multiple SSO providers OR has email/password login + const allowUnlink = providers.length > 1 || isEmailPasswordAuth; - const { data, isLoading, mutate } = useOnlyFetchOnceSWR('profile-sso-providers', async () => { - const list = await userService.getUserSSOProviders(); - setAllowUnlink(list?.length > 1); - return list; - }); + // Get linked provider IDs for filtering + const linkedProviderIds = useMemo(() => { + return new Set(providers.map((item) => item.provider)); + }, [providers]); + + // Get available providers for linking (filter out already linked) + const availableProviders = useMemo(() => { + return (oAuthSSOProviders || []).filter((provider) => !linkedProviderIds.has(provider)); + }, [oAuthSSOProviders, linkedProviderIds]); const handleUnlinkSSO = async (provider: string, providerAccountId: string) => { - if (data?.length === 1 || !data) { - // At least one SSO provider should be linked + // Prevent unlink if this is the only login method + if (!allowUnlink) { notification.error({ message: t('profile.sso.unlink.forbidden'), }); @@ -48,43 +58,75 @@ export const SSOProvidersList = memo(() => { danger: true, }, onOk: async () => { - await userService.unlinkSSOProvider(provider, providerAccountId); - mutate(); + if (isLoginWithBetterAuth) { + // Use better-auth native API + await unlinkAccount({ providerId: provider }); + } else { + // Fallback for NextAuth + await userService.unlinkSSOProvider(provider, providerAccountId); + } + refreshAuthProviders(); }, title: {t('profile.sso.unlink.title', { provider })}, }); }; - return isLoading ? ( - - - {t('profile.sso.loading')} - - ) : ( - - {data?.map((item, index) => ( - - - handleUnlinkSSO(item.provider, item.providerAccountId)} - size={'small'} - /> - - } - avatar={AuthIcons(item.provider)} - date={item.expires_at} - description={item.providerAccountId} + const handleLinkSSO = async (provider: string) => { + if (isLoginWithBetterAuth) { + // Use better-auth native linkSocial API + await linkSocial({ + callbackURL: '/profile', + provider: provider as any, + }); + } + }; + + // Dropdown menu items for linking new providers + const linkMenuItems: MenuProps['items'] = availableProviders.map((provider) => ({ + icon: AuthIcons(provider, 16), + key: provider, + label: {provider}, + onClick: () => handleLinkSSO(provider), + })); + + return ( + + {providers.map((item) => ( + setHoveredIndex(index)} - onMouseLeave={() => setHoveredIndex(null)} - showAction={hoveredIndex === index} - title={{item.provider}} - /> + > + + {AuthIcons(item.provider, 16)} + {item.provider} + {item.email && ( + + · {item.email} + + )} + + handleUnlinkSSO(item.provider, item.providerAccountId)} + size={'small'} + /> + ))} + + {/* Link Account Button - Only show for Better-Auth users with available providers */} + {isLoginWithBetterAuth && availableProviders.length > 0 && ( + + + + {t('profile.sso.link.button')} + + + + )} ); }); diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000000..3847874b4f --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,118 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */ +import { serverDB } from '@lobechat/database'; +import { betterAuth } from 'better-auth'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { genericOAuth, magicLink } from 'better-auth/plugins'; + +import { authEnv } from '@/envs/auth'; +import { + getMagicLinkEmailTemplate, + getResetPasswordEmailTemplate, + getVerificationEmailTemplate, +} from '@/libs/better-auth/email-templates'; +import { initBetterAuthSSOProviders } from '@/libs/better-auth/sso'; +import { EmailService } from '@/server/services/email'; + +// Email verification link expiration time (in seconds) +// Default is 1 hour (3600 seconds) as per Better Auth documentation +const VERIFICATION_LINK_EXPIRES_IN = 3600; +const MAGIC_LINK_EXPIRES_IN = 900; +const enableMagicLink = authEnv.NEXT_PUBLIC_ENABLE_MAGIC_LINK; + +const { socialProviders, genericOAuthProviders } = initBetterAuthSSOProviders(); + +export const auth = betterAuth({ + account: { + accountLinking: { + allowDifferentEmails: true, + enabled: true, + }, + }, + + // Use renamed env vars (fallback to next-auth vars is handled in src/envs/auth.ts) + baseURL: authEnv.NEXT_PUBLIC_AUTH_URL, + secret: authEnv.AUTH_SECRET, + + database: drizzleAdapter(serverDB, { + provider: 'pg', + }), + + emailAndPassword: { + autoSignIn: true, + enabled: true, + maxPasswordLength: 64, + minPasswordLength: 8, + requireEmailVerification: authEnv.NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION, + + sendResetPassword: async ({ user, url }) => { + const template = getResetPasswordEmailTemplate({ url }); + + const emailService = new EmailService(); + await emailService.sendMail({ + to: user.email, + ...template, + }); + }, + }, + emailVerification: { + autoSignInAfterVerification: true, + expiresIn: VERIFICATION_LINK_EXPIRES_IN, + sendVerificationEmail: async ({ user, url }) => { + const template = getVerificationEmailTemplate({ + expiresInSeconds: VERIFICATION_LINK_EXPIRES_IN, + url, + userName: user.name, + }); + + const emailService = new EmailService(); + await emailService.sendMail({ + to: user.email, + ...template, + }); + }, + }, + + plugins: [ + ...(genericOAuthProviders.length > 0 + ? [ + genericOAuth({ + config: genericOAuthProviders, + }), + ] + : []), + ...(enableMagicLink + ? [ + magicLink({ + expiresIn: MAGIC_LINK_EXPIRES_IN, + sendMagicLink: async ({ email, url }) => { + const template = getMagicLinkEmailTemplate({ + expiresInSeconds: MAGIC_LINK_EXPIRES_IN, + url, + }); + + const emailService = new EmailService(); + await emailService.sendMail({ + to: email, + ...template, + }); + }, + }), + ] + : []), + ], + socialProviders, + + user: { + additionalFields: { + fullName: { + required: false, + type: 'string', + }, + }, + fields: { + image: 'avatar', + name: 'username', + }, + modelName: 'users', + }, +}); diff --git a/src/components/NextAuth/AuthIcons.tsx b/src/components/NextAuth/AuthIcons.tsx index 6346c4a7de..0695700e90 100644 --- a/src/components/NextAuth/AuthIcons.tsx +++ b/src/components/NextAuth/AuthIcons.tsx @@ -1,4 +1,4 @@ -import { Google } from '@lobehub/icons'; +import { Aws, Google, Microsoft } from '@lobehub/icons'; import { Auth0, Authelia, @@ -19,10 +19,12 @@ const iconComponents: { [key: string]: React.ElementType } = { 'authentik': Authentik.Color, 'casdoor': Casdoor.Color, 'cloudflare': Cloudflare.Color, + 'cognito': Aws.Color, 'default': NextAuth.Color, 'github': Github, 'google': Google.Color, 'logto': Logto.Color, + 'microsoft': Microsoft.Color, 'microsoft-entra-id': MicrosoftEntra.Color, 'zitadel': Zitadel.Color, }; diff --git a/src/envs/auth.ts b/src/envs/auth.ts index 8562f286ac..61e6d73c32 100644 --- a/src/envs/auth.ts +++ b/src/envs/auth.ts @@ -11,6 +11,12 @@ declare global { CLERK_SECRET_KEY?: string; CLERK_WEBHOOK_SECRET?: string; + // ===== Auth (shared by Better Auth / Next Auth) ===== // + AUTH_SECRET?: string; + NEXT_PUBLIC_AUTH_URL?: string; + NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION?: string; + AUTH_SSO_PROVIDERS?: string; + // ===== Next Auth ===== // NEXT_AUTH_SECRET?: string; @@ -20,11 +26,78 @@ declare global { NEXT_AUTH_SSO_SESSION_STRATEGY?: string; - // Github - GITHUB_CLIENT_ID?: string; - GITHUB_CLIENT_SECRET?: string; + // ===== Next Auth Provider Credentials ===== // + AUTH_GOOGLE_ID?: string; + AUTH_GOOGLE_SECRET?: string; + + AUTH_GITHUB_ID?: string; + AUTH_GITHUB_SECRET?: string; + + AUTH_COGNITO_ID?: string; + AUTH_COGNITO_SECRET?: string; + AUTH_COGNITO_ISSUER?: string; + AUTH_COGNITO_DOMAIN?: string; + AUTH_COGNITO_REGION?: string; + AUTH_COGNITO_USERPOOL_ID?: string; + + AUTH_MICROSOFT_ID?: string; + AUTH_MICROSOFT_SECRET?: string; + + AUTH_AUTH0_ID?: string; + AUTH_AUTH0_SECRET?: string; + AUTH_AUTH0_ISSUER?: string; + + AUTH_AUTHELIA_ID?: string; + AUTH_AUTHELIA_SECRET?: string; + AUTH_AUTHELIA_ISSUER?: string; + + AUTH_AUTHENTIK_ID?: string; + AUTH_AUTHENTIK_SECRET?: string; + AUTH_AUTHENTIK_ISSUER?: string; + + AUTH_CASDOOR_ID?: string; + AUTH_CASDOOR_SECRET?: string; + AUTH_CASDOOR_ISSUER?: string; + + AUTH_CLOUDFLARE_ZERO_TRUST_ID?: string; + AUTH_CLOUDFLARE_ZERO_TRUST_SECRET?: string; + AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER?: string; + + AUTH_FEISHU_APP_ID?: string; + AUTH_FEISHU_APP_SECRET?: string; + + AUTH_GENERIC_OIDC_ID?: string; + AUTH_GENERIC_OIDC_SECRET?: string; + AUTH_GENERIC_OIDC_ISSUER?: string; + + AUTH_KEYCLOAK_ID?: string; + AUTH_KEYCLOAK_SECRET?: string; + AUTH_KEYCLOAK_ISSUER?: string; + + AUTH_LOGTO_ID?: string; + AUTH_LOGTO_SECRET?: string; + AUTH_LOGTO_ISSUER?: string; + + AUTH_MICROSOFT_ENTRA_ID_ID?: string; + AUTH_MICROSOFT_ENTRA_ID_SECRET?: string; + AUTH_MICROSOFT_ENTRA_ID_TENANT_ID?: string; + AUTH_MICROSOFT_ENTRA_ID_BASE_URL?: string; + + AUTH_OKTA_ID?: string; + AUTH_OKTA_SECRET?: string; + AUTH_OKTA_ISSUER?: string; + + AUTH_WECHAT_ID?: string; + AUTH_WECHAT_SECRET?: string; + + AUTH_ZITADEL_ID?: string; + AUTH_ZITADEL_SECRET?: string; + AUTH_ZITADEL_ISSUER?: string; + + AUTH_AZURE_AD_ID?: string; + AUTH_AZURE_AD_SECRET?: string; + AUTH_AZURE_AD_TENANT_ID?: string; - // Azure AD AZURE_AD_CLIENT_ID?: string; AZURE_AD_CLIENT_SECRET?: string; AZURE_AD_TENANT_ID?: string; @@ -40,30 +113,113 @@ declare global { export const getAuthConfig = () => { return createEnv({ client: { + // ---------------------------------- clerk ---------------------------------- + NEXT_PUBLIC_ENABLE_CLERK_AUTH: z.boolean().optional().default(false), NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().optional(), - /** - * whether to enabled clerk - */ - NEXT_PUBLIC_ENABLE_CLERK_AUTH: z.boolean().optional(), + // ---------------------------------- better auth ---------------------------------- + NEXT_PUBLIC_ENABLE_BETTER_AUTH: z.boolean().optional(), + NEXT_PUBLIC_AUTH_URL: z.string().optional(), + NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION: z.boolean().optional().default(false), + NEXT_PUBLIC_ENABLE_MAGIC_LINK: z.boolean().optional().default(false), + + // ---------------------------------- next auth ---------------------------------- NEXT_PUBLIC_ENABLE_NEXT_AUTH: z.boolean().optional(), }, server: { - // Clerk + // ---------------------------------- clerk ---------------------------------- CLERK_SECRET_KEY: z.string().optional(), CLERK_WEBHOOK_SECRET: z.string().optional(), - // NEXT-AUTH + // ---------------------------------- better auth ---------------------------------- + AUTH_SECRET: z.string().optional(), + AUTH_SSO_PROVIDERS: z.string().optional().default(''), + + // ---------------------------------- next auth ---------------------------------- NEXT_AUTH_SECRET: z.string().optional(), NEXT_AUTH_SSO_PROVIDERS: z.string().optional().default('auth0'), NEXT_AUTH_DEBUG: z.boolean().optional().default(false), NEXT_AUTH_SSO_SESSION_STRATEGY: z.enum(['jwt', 'database']).optional().default('jwt'), - // Azure AD + AUTH_GOOGLE_ID: z.string().optional(), + AUTH_GOOGLE_SECRET: z.string().optional(), + + AUTH_GITHUB_ID: z.string().optional(), + AUTH_GITHUB_SECRET: z.string().optional(), + + AUTH_COGNITO_ID: z.string().optional(), + AUTH_COGNITO_SECRET: z.string().optional(), + AUTH_COGNITO_ISSUER: z.string().optional(), + AUTH_COGNITO_DOMAIN: z.string().optional(), + AUTH_COGNITO_REGION: z.string().optional(), + AUTH_COGNITO_USERPOOL_ID: z.string().optional(), + + AUTH_MICROSOFT_ID: z.string().optional(), + AUTH_MICROSOFT_SECRET: z.string().optional(), + + AUTH_AUTH0_ID: z.string().optional(), + AUTH_AUTH0_SECRET: z.string().optional(), + AUTH_AUTH0_ISSUER: z.string().optional(), + + AUTH_AUTHELIA_ID: z.string().optional(), + AUTH_AUTHELIA_SECRET: z.string().optional(), + AUTH_AUTHELIA_ISSUER: z.string().optional(), + + AUTH_AUTHENTIK_ID: z.string().optional(), + AUTH_AUTHENTIK_SECRET: z.string().optional(), + AUTH_AUTHENTIK_ISSUER: z.string().optional(), + + AUTH_CASDOOR_ID: z.string().optional(), + AUTH_CASDOOR_SECRET: z.string().optional(), + AUTH_CASDOOR_ISSUER: z.string().optional(), + + AUTH_CLOUDFLARE_ZERO_TRUST_ID: z.string().optional(), + AUTH_CLOUDFLARE_ZERO_TRUST_SECRET: z.string().optional(), + AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER: z.string().optional(), + + AUTH_FEISHU_APP_ID: z.string().optional(), + AUTH_FEISHU_APP_SECRET: z.string().optional(), + + AUTH_GENERIC_OIDC_ID: z.string().optional(), + AUTH_GENERIC_OIDC_SECRET: z.string().optional(), + AUTH_GENERIC_OIDC_ISSUER: z.string().optional(), + + AUTH_KEYCLOAK_ID: z.string().optional(), + AUTH_KEYCLOAK_SECRET: z.string().optional(), + AUTH_KEYCLOAK_ISSUER: z.string().optional(), + + AUTH_LOGTO_ID: z.string().optional(), + AUTH_LOGTO_SECRET: z.string().optional(), + AUTH_LOGTO_ISSUER: z.string().optional(), + + AUTH_MICROSOFT_ENTRA_ID_ID: z.string().optional(), + AUTH_MICROSOFT_ENTRA_ID_SECRET: z.string().optional(), + AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: z.string().optional(), + AUTH_MICROSOFT_ENTRA_ID_BASE_URL: z.string().optional(), + + AUTH_OKTA_ID: z.string().optional(), + AUTH_OKTA_SECRET: z.string().optional(), + AUTH_OKTA_ISSUER: z.string().optional(), + + AUTH_WECHAT_ID: z.string().optional(), + AUTH_WECHAT_SECRET: z.string().optional(), + + AUTH_ZITADEL_ID: z.string().optional(), + AUTH_ZITADEL_SECRET: z.string().optional(), + AUTH_ZITADEL_ISSUER: z.string().optional(), + + AUTH_AZURE_AD_ID: z.string().optional(), + AUTH_AZURE_AD_SECRET: z.string().optional(), + AUTH_AZURE_AD_TENANT_ID: z.string().optional(), + AZURE_AD_CLIENT_ID: z.string().optional(), AZURE_AD_CLIENT_SECRET: z.string().optional(), AZURE_AD_TENANT_ID: z.string().optional(), + ZITADEL_CLIENT_ID: z.string().optional(), + ZITADEL_CLIENT_SECRET: z.string().optional(), + ZITADEL_ISSUER: z.string().optional(), + LOGTO_WEBHOOK_SIGNING_KEY: z.string().optional(), // Casdoor @@ -77,18 +233,109 @@ export const getAuthConfig = () => { CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, CLERK_WEBHOOK_SECRET: process.env.CLERK_WEBHOOK_SECRET, - // Next Auth + // ---------------------------------- better auth ---------------------------------- + NEXT_PUBLIC_ENABLE_BETTER_AUTH: process.env.NEXT_PUBLIC_ENABLE_BETTER_AUTH === '1', + // Fallback to NEXTAUTH_URL origin for seamless migration from next-auth + NEXT_PUBLIC_AUTH_URL: + process.env.NEXT_PUBLIC_AUTH_URL ?? + (process.env.NEXTAUTH_URL ? new URL(process.env.NEXTAUTH_URL).origin : undefined), + NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION: process.env.NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION === '1', + NEXT_PUBLIC_ENABLE_MAGIC_LINK: process.env.NEXT_PUBLIC_ENABLE_MAGIC_LINK === '1', + // Fallback to NEXT_AUTH_SECRET for seamless migration from next-auth + AUTH_SECRET: process.env.AUTH_SECRET ?? process.env.NEXT_AUTH_SECRET, + // Fallback to NEXT_AUTH_SSO_PROVIDERS for seamless migration from next-auth + AUTH_SSO_PROVIDERS: process.env.AUTH_SSO_PROVIDERS ?? process.env.NEXT_AUTH_SSO_PROVIDERS, + + // better-auth env for Cognito provider is different from next-auth's one + AUTH_COGNITO_DOMAIN: process.env.AUTH_COGNITO_DOMAIN, + AUTH_COGNITO_REGION: process.env.AUTH_COGNITO_REGION, + AUTH_COGNITO_USERPOOL_ID: process.env.AUTH_COGNITO_USERPOOL_ID, + + // ---------------------------------- next auth ---------------------------------- NEXT_PUBLIC_ENABLE_NEXT_AUTH: process.env.NEXT_PUBLIC_ENABLE_NEXT_AUTH === '1', NEXT_AUTH_SSO_PROVIDERS: process.env.NEXT_AUTH_SSO_PROVIDERS, NEXT_AUTH_SECRET: process.env.NEXT_AUTH_SECRET, NEXT_AUTH_DEBUG: !!process.env.NEXT_AUTH_DEBUG, NEXT_AUTH_SSO_SESSION_STRATEGY: process.env.NEXT_AUTH_SSO_SESSION_STRATEGY || 'jwt', - // Azure AD + // Next Auth Provider Credentials + AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, + AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, + + AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID, + AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET, + + AUTH_MICROSOFT_ID: process.env.AUTH_MICROSOFT_ID, + AUTH_MICROSOFT_SECRET: process.env.AUTH_MICROSOFT_SECRET, + + AUTH_COGNITO_ID: process.env.AUTH_COGNITO_ID, + AUTH_COGNITO_SECRET: process.env.AUTH_COGNITO_SECRET, + AUTH_COGNITO_ISSUER: process.env.AUTH_COGNITO_ISSUER, + + AUTH_AUTH0_ID: process.env.AUTH_AUTH0_ID, + AUTH_AUTH0_SECRET: process.env.AUTH_AUTH0_SECRET, + AUTH_AUTH0_ISSUER: process.env.AUTH_AUTH0_ISSUER, + + AUTH_AUTHELIA_ID: process.env.AUTH_AUTHELIA_ID, + AUTH_AUTHELIA_SECRET: process.env.AUTH_AUTHELIA_SECRET, + AUTH_AUTHELIA_ISSUER: process.env.AUTH_AUTHELIA_ISSUER, + + AUTH_AUTHENTIK_ID: process.env.AUTH_AUTHENTIK_ID, + AUTH_AUTHENTIK_SECRET: process.env.AUTH_AUTHENTIK_SECRET, + AUTH_AUTHENTIK_ISSUER: process.env.AUTH_AUTHENTIK_ISSUER, + + AUTH_CASDOOR_ID: process.env.AUTH_CASDOOR_ID, + AUTH_CASDOOR_SECRET: process.env.AUTH_CASDOOR_SECRET, + AUTH_CASDOOR_ISSUER: process.env.AUTH_CASDOOR_ISSUER, + + AUTH_CLOUDFLARE_ZERO_TRUST_ID: process.env.AUTH_CLOUDFLARE_ZERO_TRUST_ID, + AUTH_CLOUDFLARE_ZERO_TRUST_SECRET: process.env.AUTH_CLOUDFLARE_ZERO_TRUST_SECRET, + AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER: process.env.AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER, + + AUTH_FEISHU_APP_ID: process.env.AUTH_FEISHU_APP_ID, + AUTH_FEISHU_APP_SECRET: process.env.AUTH_FEISHU_APP_SECRET, + + AUTH_GENERIC_OIDC_ID: process.env.AUTH_GENERIC_OIDC_ID, + AUTH_GENERIC_OIDC_SECRET: process.env.AUTH_GENERIC_OIDC_SECRET, + AUTH_GENERIC_OIDC_ISSUER: process.env.AUTH_GENERIC_OIDC_ISSUER, + + AUTH_KEYCLOAK_ID: process.env.AUTH_KEYCLOAK_ID, + AUTH_KEYCLOAK_SECRET: process.env.AUTH_KEYCLOAK_SECRET, + AUTH_KEYCLOAK_ISSUER: process.env.AUTH_KEYCLOAK_ISSUER, + + AUTH_LOGTO_ID: process.env.AUTH_LOGTO_ID, + AUTH_LOGTO_SECRET: process.env.AUTH_LOGTO_SECRET, + AUTH_LOGTO_ISSUER: process.env.AUTH_LOGTO_ISSUER, + + AUTH_MICROSOFT_ENTRA_ID_ID: process.env.AUTH_MICROSOFT_ENTRA_ID_ID, + AUTH_MICROSOFT_ENTRA_ID_SECRET: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET, + AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID, + AUTH_MICROSOFT_ENTRA_ID_BASE_URL: process.env.AUTH_MICROSOFT_ENTRA_ID_BASE_URL, + + AUTH_OKTA_ID: process.env.AUTH_OKTA_ID, + AUTH_OKTA_SECRET: process.env.AUTH_OKTA_SECRET, + AUTH_OKTA_ISSUER: process.env.AUTH_OKTA_ISSUER, + + AUTH_WECHAT_ID: process.env.AUTH_WECHAT_ID, + AUTH_WECHAT_SECRET: process.env.AUTH_WECHAT_SECRET, + + AUTH_ZITADEL_ID: process.env.AUTH_ZITADEL_ID, + AUTH_ZITADEL_SECRET: process.env.AUTH_ZITADEL_SECRET, + AUTH_ZITADEL_ISSUER: process.env.AUTH_ZITADEL_ISSUER, + + AUTH_AZURE_AD_ID: process.env.AUTH_AZURE_AD_ID, + AUTH_AZURE_AD_SECRET: process.env.AUTH_AZURE_AD_SECRET, + AUTH_AZURE_AD_TENANT_ID: process.env.AUTH_AZURE_AD_TENANT_ID, + + // legacy Azure AD envs for backward compatibility AZURE_AD_CLIENT_ID: process.env.AZURE_AD_CLIENT_ID, AZURE_AD_CLIENT_SECRET: process.env.AZURE_AD_CLIENT_SECRET, AZURE_AD_TENANT_ID: process.env.AZURE_AD_TENANT_ID, + ZITADEL_CLIENT_ID: process.env.ZITADEL_CLIENT_ID, + ZITADEL_CLIENT_SECRET: process.env.ZITADEL_CLIENT_SECRET, + ZITADEL_ISSUER: process.env.ZITADEL_ISSUER, + // LOGTO LOGTO_WEBHOOK_SIGNING_KEY: process.env.LOGTO_WEBHOOK_SIGNING_KEY, diff --git a/src/envs/email.ts b/src/envs/email.ts new file mode 100644 index 0000000000..d1e6860994 --- /dev/null +++ b/src/envs/email.ts @@ -0,0 +1,37 @@ +/* eslint-disable sort-keys-fix/sort-keys-fix */ +import { createEnv } from '@t3-oss/env-nextjs'; +import { z } from 'zod'; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace NodeJS { + interface ProcessEnv { + SMTP_HOST?: string; + SMTP_PASS?: string; + SMTP_PORT?: string; + SMTP_SECURE?: string; + SMTP_USER?: string; + } + } +} + +export const getEmailConfig = () => { + return createEnv({ + server: { + SMTP_HOST: z.string().optional(), + SMTP_PORT: z.coerce.number().optional(), + SMTP_SECURE: z.boolean().optional(), + SMTP_USER: z.string().optional(), + SMTP_PASS: z.string().optional(), + }, + runtimeEnv: { + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : undefined, + SMTP_SECURE: process.env.SMTP_SECURE === 'true', + SMTP_USER: process.env.SMTP_USER, + SMTP_PASS: process.env.SMTP_PASS, + }, + }); +}; + +export const emailEnv = getEmailConfig(); diff --git a/src/features/User/UserPanel/PanelContent.tsx b/src/features/User/UserPanel/PanelContent.tsx index e5dd0b1fea..8b31095fe2 100644 --- a/src/features/User/UserPanel/PanelContent.tsx +++ b/src/features/User/UserPanel/PanelContent.tsx @@ -1,10 +1,12 @@ -import { Link } from 'react-router-dom'; +import { enableBetterAuth, enableNextAuth } from '@lobechat/const'; import { useRouter } from 'next/navigation'; import { memo } from 'react'; import { Flexbox } from 'react-layout-kit'; +import { Link } from 'react-router-dom'; import BrandWatermark from '@/components/BrandWatermark'; import Menu from '@/components/Menu'; +import { isDesktop } from '@/const/version'; import { useUserStore } from '@/store/user'; import { authSelectors } from '@/store/user/selectors'; @@ -14,8 +16,6 @@ import UserLoginOrSignup from '../UserLoginOrSignup'; import LangButton from './LangButton'; import ThemeButton from './ThemeButton'; import { useMenu } from './useMenu'; -import { enableNextAuth } from '@/const/auth'; -import { isDesktop } from '@/const/version'; const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => { const router = useRouter(); @@ -31,8 +31,9 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => { const handleSignOut = () => { signOut(); closePopover(); - // NextAuth doesn't need to redirect to login page - if (enableNextAuth) return; + // NextAuth and Better Auth handle redirect in their own signOut methods + if (enableNextAuth || enableBetterAuth) return; + // Clerk uses /login page router.push('/login'); }; diff --git a/src/features/User/__tests__/PanelContent.test.tsx b/src/features/User/__tests__/PanelContent.test.tsx index bb4c5599c9..36e280c7a7 100644 --- a/src/features/User/__tests__/PanelContent.test.tsx +++ b/src/features/User/__tests__/PanelContent.test.tsx @@ -1,6 +1,6 @@ import { act, render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { useUserStore } from '@/store/user'; @@ -67,13 +67,22 @@ vi.mock('@/const/version', () => ({ isDesktop: false, })); -// 定义一个变量来存储 enableAuth 的值 -let enableAuth = true; +// Use vi.hoisted to ensure variables exist before vi.mock factory executes +const { enableAuth, enableClerk, enableNextAuth } = vi.hoisted(() => ({ + enableAuth: { value: true }, + enableClerk: { value: false }, + enableNextAuth: { value: false }, +})); -// 模拟 @/const/auth 模块 vi.mock('@/const/auth', () => ({ get enableAuth() { - return enableAuth; + return enableAuth.value; + }, + get enableClerk() { + return enableClerk.value; + }, + get enableNextAuth() { + return enableNextAuth.value; }, })); @@ -145,7 +154,7 @@ describe('PanelContent', () => { }); it('should render BrandWatermark when disable auth', () => { - enableAuth = false; + enableAuth.value = false; act(() => { useUserStore.setState({ isSignedIn: false }); diff --git a/src/features/User/__tests__/UserAvatar.test.tsx b/src/features/User/__tests__/UserAvatar.test.tsx index ce22e63036..25342d63a2 100644 --- a/src/features/User/__tests__/UserAvatar.test.tsx +++ b/src/features/User/__tests__/UserAvatar.test.tsx @@ -9,18 +9,29 @@ import UserAvatar from '../UserAvatar'; vi.mock('zustand/traditional'); -// 定义一个变量来存储 enableAuth 的值 -let enableAuth = true; +// Use vi.hoisted to ensure variables exist before vi.mock factory executes +const { enableAuth, enableClerk, enableNextAuth } = vi.hoisted(() => ({ + enableAuth: { value: true }, + enableClerk: { value: false }, + enableNextAuth: { value: false }, +})); -// 模拟 @/const/auth 模块 vi.mock('@/const/auth', () => ({ get enableAuth() { - return enableAuth; + return enableAuth.value; + }, + get enableClerk() { + return enableClerk.value; + }, + get enableNextAuth() { + return enableNextAuth.value; }, })); afterEach(() => { - enableAuth = true; + enableAuth.value = true; + enableClerk.value = false; + enableNextAuth.value = false; }); describe('UserAvatar', () => { @@ -71,7 +82,7 @@ describe('UserAvatar', () => { describe('disable Auth', () => { it('should show LobeChat and default avatar when the user is not logged in and disabled auth', () => { - enableAuth = false; + enableAuth.value = false; act(() => { useUserStore.setState({ enableAuth: () => false, isSignedIn: false, user: undefined }); }); diff --git a/src/features/User/__tests__/useMenu.test.tsx b/src/features/User/__tests__/useMenu.test.tsx index e7374d3153..98704c9510 100644 --- a/src/features/User/__tests__/useMenu.test.tsx +++ b/src/features/User/__tests__/useMenu.test.tsx @@ -48,22 +48,24 @@ vi.mock('./useNewVersion', () => ({ useNewVersion: vi.fn(() => false), })); -// 定义一个变量来存储 enableAuth 的值 -let enableAuth = true; -let enableClerk = true; -// 模拟 @/const/auth 模块 +// Use vi.hoisted to ensure variables exist before vi.mock factory executes +const { enableAuth, enableClerk } = vi.hoisted(() => ({ + enableAuth: { value: true }, + enableClerk: { value: true }, +})); + vi.mock('@/const/auth', () => ({ get enableAuth() { - return enableAuth; + return enableAuth.value; }, get enableClerk() { - return enableClerk; + return enableClerk.value; }, })); afterEach(() => { - enableAuth = true; - enableClerk = true; + enableAuth.value = true; + enableClerk.value = true; }); describe('useMenu', () => { @@ -71,8 +73,8 @@ describe('useMenu', () => { act(() => { useUserStore.setState({ isSignedIn: true, enableAuth: () => true }); }); - enableAuth = true; - enableClerk = false; + enableAuth.value = true; + enableClerk.value = false; const { result } = renderHook(() => useMenu(), { wrapper }); @@ -90,7 +92,7 @@ describe('useMenu', () => { act(() => { useUserStore.setState({ isSignedIn: false, enableAuth: () => false }); }); - enableAuth = false; + enableAuth.value = false; const { result } = renderHook(() => useMenu(), { wrapper }); @@ -108,7 +110,7 @@ describe('useMenu', () => { act(() => { useUserStore.setState({ isSignedIn: false, enableAuth: () => true }); }); - enableAuth = true; + enableAuth.value = true; const { result } = renderHook(() => useMenu(), { wrapper }); diff --git a/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx b/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx new file mode 100644 index 0000000000..830bc26d75 --- /dev/null +++ b/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { memo, useEffect } from 'react'; +import { createStoreUpdater } from 'zustand-utils'; + +import { useSession } from '@/libs/better-auth/auth-client'; +import { useUserStore } from '@/store/user'; +import { LobeUser } from '@/types/user'; + +/** + * Sync Better-Auth session state to Zustand store + */ +const UserUpdater = memo(() => { + const { data: session, isPending, error } = useSession(); + + const isLoaded = !isPending; + const isSignedIn = !!session?.user && !error; + + const betterAuthUser = session?.user; + const useStoreUpdater = createStoreUpdater(useUserStore); + + useStoreUpdater('isLoaded', isLoaded); + useStoreUpdater('isSignedIn', isSignedIn); + + // Sync user data from Better-Auth session to Zustand store + useEffect(() => { + if (betterAuthUser) { + const userAvatar = useUserStore.getState().user?.avatar; + + const lobeUser = { + // Preserve avatar from settings, don't override with auth provider value + avatar: userAvatar || '', + email: betterAuthUser.email, + fullName: betterAuthUser.fullName, + id: betterAuthUser.id, + username: betterAuthUser.name, + } as LobeUser; + + // Update user data in store + useUserStore.setState({ user: lobeUser }); + return; + } + + // Clear user data when session becomes unavailable + useUserStore.setState({ user: undefined }); + }, [betterAuthUser]); + + return null; +}); + +export default UserUpdater; diff --git a/src/layout/AuthProvider/BetterAuth/index.tsx b/src/layout/AuthProvider/BetterAuth/index.tsx new file mode 100644 index 0000000000..a52d2dce56 --- /dev/null +++ b/src/layout/AuthProvider/BetterAuth/index.tsx @@ -0,0 +1,14 @@ +import { PropsWithChildren } from 'react'; + +import UserUpdater from './UserUpdater'; + +const BetterAuth = ({ children }: PropsWithChildren) => { + return ( + <> + {children} + + + ); +}; + +export default BetterAuth; diff --git a/src/layout/AuthProvider/index.tsx b/src/layout/AuthProvider/index.tsx index e138f01978..51d379b8b8 100644 --- a/src/layout/AuthProvider/index.tsx +++ b/src/layout/AuthProvider/index.tsx @@ -3,6 +3,7 @@ import { PropsWithChildren } from 'react'; import { isDesktop } from '@/const/version'; import { authEnv } from '@/envs/auth'; +import BetterAuth from './BetterAuth'; import Clerk from './Clerk'; import { MarketAuthProvider } from './MarketAuth'; import NextAuth from './NextAuth'; @@ -13,6 +14,8 @@ const AuthProvider = ({ children }: PropsWithChildren) => { let InnerAuthProvider; if (authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH) { InnerAuthProvider = ({ children }: PropsWithChildren) => {children}; + } else if (authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH) { + InnerAuthProvider = ({ children }: PropsWithChildren) => {children}; } else if (authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH) { InnerAuthProvider = ({ children }: PropsWithChildren) => {children}; } else { diff --git a/src/libs/better-auth/auth-client.ts b/src/libs/better-auth/auth-client.ts new file mode 100644 index 0000000000..9a40e28d3d --- /dev/null +++ b/src/libs/better-auth/auth-client.ts @@ -0,0 +1,34 @@ +import { + genericOAuthClient, + inferAdditionalFields, + magicLinkClient, +} from 'better-auth/client/plugins'; +import { createAuthClient } from 'better-auth/react'; + +import type { auth } from '@/auth'; +import { getAuthConfig } from '@/envs/auth'; + +const { NEXT_PUBLIC_AUTH_URL } = getAuthConfig(); +const enableMagicLink = getAuthConfig().NEXT_PUBLIC_ENABLE_MAGIC_LINK; + +export const { + linkSocial, + accountInfo, + listAccounts, + requestPasswordReset, + resetPassword, + sendVerificationEmail, + signIn, + signOut, + signUp, + unlinkAccount, + useSession, +} = createAuthClient({ + /** The base URL of the server (optional if you're using the same domain) */ + baseURL: NEXT_PUBLIC_AUTH_URL, + plugins: [ + inferAdditionalFields(), + genericOAuthClient(), + ...(enableMagicLink ? [magicLinkClient()] : []), + ], +}); diff --git a/src/libs/better-auth/constants.ts b/src/libs/better-auth/constants.ts new file mode 100644 index 0000000000..5b0aade45d --- /dev/null +++ b/src/libs/better-auth/constants.ts @@ -0,0 +1,13 @@ +/** + * Canonical IDs of Better-Auth built-in social providers. + * Keep this list in sync with provider definitions in `src/libs/better-auth/sso/providers`. + */ +export const BUILTIN_BETTER_AUTH_PROVIDERS = ['google', 'github', 'cognito', 'microsoft'] as const; + +/** + * Provider alias → canonical ID mapping. + * This is used on the client to normalize configured provider keys. + */ +export const PROVIDER_ALIAS_MAP: Record = { + 'microsoft-entra-id': 'microsoft', +}; diff --git a/src/libs/better-auth/email-templates/index.ts b/src/libs/better-auth/email-templates/index.ts new file mode 100644 index 0000000000..15c25c4d14 --- /dev/null +++ b/src/libs/better-auth/email-templates/index.ts @@ -0,0 +1,3 @@ +export { getMagicLinkEmailTemplate } from './magic-link'; +export { getResetPasswordEmailTemplate } from './reset-password'; +export { getVerificationEmailTemplate } from './verification'; diff --git a/src/libs/better-auth/email-templates/magic-link.ts b/src/libs/better-auth/email-templates/magic-link.ts new file mode 100644 index 0000000000..1e2945cb36 --- /dev/null +++ b/src/libs/better-auth/email-templates/magic-link.ts @@ -0,0 +1,98 @@ +/** + * Magic link sign-in email template + * Sent when user requests passwordless login + */ +export const getMagicLinkEmailTemplate = (params: { expiresInSeconds: number; url: string }) => { + const { url, expiresInSeconds } = params; + + const expiresInMinutes = Math.round(expiresInSeconds / 60); + const expirationText = + expiresInMinutes >= 1 + ? `${expiresInMinutes} minute${expiresInMinutes > 1 ? 's' : ''}` + : `${expiresInSeconds} seconds`; + + return { + html: ` + + + + + + Sign in to LobeChat + + + +
+ + +
+
+ 🤯 + LobeChat +
+
+ + +
+ + +
+

+ Sign in to LobeChat +

+

+ Click the link below to sign in to your account. +

+
+ + +
+ + + + + +
+

+ ⏰ This link will expire in ${expirationText}. +

+
+ +

+ If you didn't request this email, you can safely ignore it. +

+
+ + +
+ + +
+

+ Button not working? Copy and paste this link into your browser: +

+ + ${url} + +
+
+ + +
+

+ © ${new Date().getFullYear()} LobeChat. All rights reserved. +

+
+
+ + + `, + subject: 'Your LobeChat sign-in link', + text: `Use this link to sign in: ${url}\n\nThis link expires in ${expirationText}.`, + }; +}; diff --git a/src/libs/better-auth/email-templates/reset-password.ts b/src/libs/better-auth/email-templates/reset-password.ts new file mode 100644 index 0000000000..2f7baed417 --- /dev/null +++ b/src/libs/better-auth/email-templates/reset-password.ts @@ -0,0 +1,91 @@ +/** + * Password reset email template + * Sent to users when they request a password reset + */ +export const getResetPasswordEmailTemplate = (params: { url: string }) => { + const { url } = params; + + return { + html: ` + + + + + + Reset your password + + + +
+ + +
+
+ 🤯 + LobeChat +
+
+ + +
+ + +
+

+ Reset Your Password +

+

+ No worries, we'll help you get back on track. +

+
+ + +
+

+ You recently requested to reset your password for your LobeChat account. Click the button below to proceed. +

+ + + + + +
+

+ 🔒 If you did not request a password reset, please ignore this email or contact support if you have concerns. +

+
+
+ + +
+ + +
+

+ Trouble clicking the button? Copy and paste the URL below: +

+ + ${url} + +
+
+ + +
+

+ © ${new Date().getFullYear()} LobeChat. All rights reserved. +

+
+
+ + + `, + subject: 'Reset Your Password - LobeChat', + text: `Reset your password by clicking this link: ${url}`, + }; +}; diff --git a/src/libs/better-auth/email-templates/verification.ts b/src/libs/better-auth/email-templates/verification.ts new file mode 100644 index 0000000000..22d4049871 --- /dev/null +++ b/src/libs/better-auth/email-templates/verification.ts @@ -0,0 +1,108 @@ +/** + * Email verification template + * Sent to users when they sign up to verify their email address + */ +export const getVerificationEmailTemplate = (params: { + expiresInSeconds: number; + url: string; + userName?: string | null; +}) => { + const { url, userName, expiresInSeconds } = params; + + // Format expiration time in a human-readable way + const expiresInHours = expiresInSeconds / 3600; + const expirationText = + expiresInHours >= 1 + ? `${expiresInHours} hour${expiresInHours > 1 ? 's' : ''}` + : `${expiresInSeconds / 60} minutes`; + + return { + html: ` + + + + + + Verify your email + + + +
+ + +
+
+ 🤯 + LobeChat +
+
+ + +
+ + +
+

+ Verify your email address +

+

+ Let's get you signed in. +

+
+ + +
+ ${userName ? `

Hi ${userName},

` : ''} + +

+ Thanks for creating an account with LobeChat. To access your account, please verify your email address by clicking the button below. +

+ + + + + +
+

+ ⏰ This link will expire in ${expirationText}. +

+
+ +

+ If you didn't create an account, you can safely ignore this email. +

+
+ + +
+ + +
+

+ Button not working? Copy and paste this link into your browser: +

+ + ${url} + +
+
+ + +
+

+ © 2025 LobeChat. All rights reserved. +

+
+
+ + + `, + subject: 'Verify Your Email - LobeChat', + text: `Please verify your email by clicking this link: ${url}\n\nThis link will expire in ${expirationText}.`, + }; +}; diff --git a/src/libs/better-auth/sso/helpers.ts b/src/libs/better-auth/sso/helpers.ts new file mode 100644 index 0000000000..ddea07e187 --- /dev/null +++ b/src/libs/better-auth/sso/helpers.ts @@ -0,0 +1,61 @@ +import type { GenericOAuthConfig } from 'better-auth/plugins'; + +export const DEFAULT_OIDC_SCOPES = ['openid', 'email', 'profile']; + +export const pickEnv = (...values: (string | undefined | null)[]) => { + for (const value of values) { + const trimmed = value?.trim(); + if (trimmed) { + return trimmed; + } + } + + return undefined; +}; + +const createDiscoveryUrl = (issuer: string) => { + const normalized = issuer.replace(/\/$/, ''); + return normalized.includes('/.well-known/') + ? normalized + : `${normalized}/.well-known/openid-configuration`; +}; + +type OIDCProviderInput = { + clientId?: string; + clientSecret?: string; + issuer?: string; + overrides?: Partial; + pkce?: boolean; + providerId: string; + scopes?: string[]; +}; + +export const buildOidcConfig = ({ + providerId, + clientId, + clientSecret, + issuer, + scopes = DEFAULT_OIDC_SCOPES, + pkce = true, + overrides, +}: OIDCProviderInput): GenericOAuthConfig => { + const sanitizedIssuer = issuer?.trim(); + + if (!clientId || !clientSecret || !sanitizedIssuer) { + throw new Error(`[Better-Auth] ${providerId} OAuth enabled but missing credentials`); + } + + const normalizedIssuer = sanitizedIssuer.replace(/\/$/, ''); + const discoveryUrl = createDiscoveryUrl(normalizedIssuer); + + return { + clientId, + clientSecret, + discoveryUrl, + pkce, + providerId, + scopes, + // ...fallbackEndpoints, + ...overrides, + } satisfies GenericOAuthConfig; +}; diff --git a/src/libs/better-auth/sso/index.ts b/src/libs/better-auth/sso/index.ts new file mode 100644 index 0000000000..00eab43042 --- /dev/null +++ b/src/libs/better-auth/sso/index.ts @@ -0,0 +1,113 @@ +import type { GenericOAuthConfig } from 'better-auth/plugins'; +import type { SocialProviders } from 'better-auth/social-providers'; + +import { authEnv } from '@/envs/auth'; +import { BUILTIN_BETTER_AUTH_PROVIDERS } from '@/libs/better-auth/constants'; +import { parseSSOProviders } from '@/libs/better-auth/utils/server'; + +import Auth0 from './providers/auth0'; +import Authelia from './providers/authelia'; +import Authentik from './providers/authentik'; +import Casdoor from './providers/casdoor'; +import CloudflareZeroTrust from './providers/cloudflare-zero-trust'; +import Cognito from './providers/cognito'; +import Feishu from './providers/feishu'; +import GenericOIDC from './providers/generic-oidc'; +import Github from './providers/github'; +import Google from './providers/google'; +import Keycloak from './providers/keycloak'; +import Logto from './providers/logto'; +import Microsoft from './providers/microsoft'; +import Okta from './providers/okta'; +import Wechat from './providers/wechat'; +import Zitadel from './providers/zitadel'; + +const providerDefinitions = [ + Google, + Github, + Cognito, + Microsoft, + Auth0, + Authelia, + Authentik, + Casdoor, + CloudflareZeroTrust, + GenericOIDC, + Keycloak, + Logto, + Okta, + Zitadel, + Feishu, + Wechat, +] as const; + +const builtInProviderIds = new Set(BUILTIN_BETTER_AUTH_PROVIDERS); + +for (const definition of providerDefinitions) { + if (definition.type === 'builtin' && !builtInProviderIds.has(definition.id)) { + throw new Error( + `[Better-Auth] Built-in provider "${definition.id}" is not registered in BUILTIN_BETTER_AUTH_PROVIDERS (src/libs/better-auth/constants.ts). Please update the constant to keep them in sync.`, + ); + } +} + +const providerRegistry = new Map(); + +for (const definition of providerDefinitions) { + providerRegistry.set(definition.id, definition); + definition.aliases?.forEach((alias) => providerRegistry.set(alias, definition)); +} + +export const initBetterAuthSSOProviders = () => { + const enabledProviders = parseSSOProviders(authEnv.AUTH_SSO_PROVIDERS); + + const socialProviders: SocialProviders = {}; + const genericOAuthProviders: GenericOAuthConfig[] = []; + + for (const rawProvider of enabledProviders) { + const definition = providerRegistry.get(rawProvider); + + if (!definition) { + throw new Error(`[Better-Auth] Unknown SSO provider: ${rawProvider}`); + } + + /** + * Providers expose checkEnvs predicates so we can fail fast when credentials are missing instead + * of encountering harder-to-trace errors later in the Better-Auth pipeline. + */ + const env = definition.checkEnvs(); + if (!env) { + throw new Error( + `[Better-Auth] ${rawProvider} SSO provider environment variables are not set correctly!`, + ); + } + + if (definition.type === 'builtin') { + const providerId = definition.id; + if (socialProviders[providerId]) { + throw new Error(`[Better-Auth] Duplicate SSO provider: ${providerId}`); + } + + // @ts-expect-error - build expects specific env type, but we use union definition type + const config = definition.build(env); + if (config) { + // @ts-expect-error hard to type + socialProviders[providerId] = config; + } + + continue; + } + + // @ts-expect-error - build expects specific env type, but we use union definition type + const config = definition.build(env); + + if (config) { + genericOAuthProviders.push(config); + } + } + + return { + genericOAuthProviders, + socialProviders: socialProviders, + }; +}; diff --git a/src/libs/better-auth/sso/providers/auth0.ts b/src/libs/better-auth/sso/providers/auth0.ts new file mode 100644 index 0000000000..11eccf80c4 --- /dev/null +++ b/src/libs/better-auth/sso/providers/auth0.ts @@ -0,0 +1,33 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_AUTH0_ID: string; + AUTH_AUTH0_ISSUER: string; + AUTH_AUTH0_SECRET: string; +}> = { + build: (env) => { + const config = buildOidcConfig({ + clientId: env.AUTH_AUTH0_ID, + clientSecret: env.AUTH_AUTH0_SECRET, + issuer: env.AUTH_AUTH0_ISSUER, + providerId: 'auth0', + }); + return config; + }, + checkEnvs: () => { + return !!(authEnv.AUTH_AUTH0_ID && authEnv.AUTH_AUTH0_SECRET && authEnv.AUTH_AUTH0_ISSUER) + ? { + AUTH_AUTH0_ID: authEnv.AUTH_AUTH0_ID, + AUTH_AUTH0_ISSUER: authEnv.AUTH_AUTH0_ISSUER, + AUTH_AUTH0_SECRET: authEnv.AUTH_AUTH0_SECRET, + } + : false; + }, + id: 'auth0', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/authelia.ts b/src/libs/better-auth/sso/providers/authelia.ts new file mode 100644 index 0000000000..b97884bc4f --- /dev/null +++ b/src/libs/better-auth/sso/providers/authelia.ts @@ -0,0 +1,35 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_AUTHELIA_ID: string; + AUTH_AUTHELIA_ISSUER: string; + AUTH_AUTHELIA_SECRET: string; +}> = { + build: (env) => + buildOidcConfig({ + clientId: env.AUTH_AUTHELIA_ID, + clientSecret: env.AUTH_AUTHELIA_SECRET, + issuer: env.AUTH_AUTHELIA_ISSUER, + providerId: 'authelia', + }), + checkEnvs: () => { + return !!( + authEnv.AUTH_AUTHELIA_ID && + authEnv.AUTH_AUTHELIA_SECRET && + authEnv.AUTH_AUTHELIA_ISSUER + ) + ? { + AUTH_AUTHELIA_ID: authEnv.AUTH_AUTHELIA_ID, + AUTH_AUTHELIA_ISSUER: authEnv.AUTH_AUTHELIA_ISSUER, + AUTH_AUTHELIA_SECRET: authEnv.AUTH_AUTHELIA_SECRET, + } + : false; + }, + id: 'authelia', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/authentik.ts b/src/libs/better-auth/sso/providers/authentik.ts new file mode 100644 index 0000000000..84beefccd0 --- /dev/null +++ b/src/libs/better-auth/sso/providers/authentik.ts @@ -0,0 +1,35 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_AUTHENTIK_ID: string; + AUTH_AUTHENTIK_ISSUER: string; + AUTH_AUTHENTIK_SECRET: string; +}> = { + build: (env) => + buildOidcConfig({ + clientId: env.AUTH_AUTHENTIK_ID, + clientSecret: env.AUTH_AUTHENTIK_SECRET, + issuer: env.AUTH_AUTHENTIK_ISSUER, + providerId: 'authentik', + }), + checkEnvs: () => { + return !!( + authEnv.AUTH_AUTHENTIK_ID && + authEnv.AUTH_AUTHENTIK_SECRET && + authEnv.AUTH_AUTHENTIK_ISSUER + ) + ? { + AUTH_AUTHENTIK_ID: authEnv.AUTH_AUTHENTIK_ID, + AUTH_AUTHENTIK_ISSUER: authEnv.AUTH_AUTHENTIK_ISSUER, + AUTH_AUTHENTIK_SECRET: authEnv.AUTH_AUTHENTIK_SECRET, + } + : false; + }, + id: 'authentik', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/casdoor.ts b/src/libs/better-auth/sso/providers/casdoor.ts new file mode 100644 index 0000000000..d039535184 --- /dev/null +++ b/src/libs/better-auth/sso/providers/casdoor.ts @@ -0,0 +1,48 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_CASDOOR_ID: string; + AUTH_CASDOOR_ISSUER: string; + AUTH_CASDOOR_SECRET: string; +}> = { + build: (env) => + buildOidcConfig({ + clientId: env.AUTH_CASDOOR_ID, + clientSecret: env.AUTH_CASDOOR_SECRET, + issuer: env.AUTH_CASDOOR_ISSUER, + overrides: { + mapProfileToUser: (profile) => { + const composedName = [profile.firstName, profile.lastName] + .filter(Boolean) + .join(' ') + .trim(); + const fallbackName = composedName.length > 0 ? composedName : undefined; + + return { + email: profile.email, + emailVerified: Boolean(profile.emailVerified), + image: profile.avatar ?? profile.permanentAvatar, + name: + profile.displayName ?? fallbackName ?? profile.name ?? profile.email ?? profile.id, + }; + }, + }, + providerId: 'casdoor', + }), + checkEnvs: () => { + return !!(authEnv.AUTH_CASDOOR_ID && authEnv.AUTH_CASDOOR_SECRET && authEnv.AUTH_CASDOOR_ISSUER) + ? { + AUTH_CASDOOR_ID: authEnv.AUTH_CASDOOR_ID, + AUTH_CASDOOR_ISSUER: authEnv.AUTH_CASDOOR_ISSUER, + AUTH_CASDOOR_SECRET: authEnv.AUTH_CASDOOR_SECRET, + } + : false; + }, + id: 'casdoor', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts b/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts new file mode 100644 index 0000000000..1cbf58928a --- /dev/null +++ b/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts @@ -0,0 +1,41 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_CLOUDFLARE_ZERO_TRUST_ID: string; + AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER: string; + AUTH_CLOUDFLARE_ZERO_TRUST_SECRET: string; +}> = { + build: (env) => + buildOidcConfig({ + clientId: env.AUTH_CLOUDFLARE_ZERO_TRUST_ID, + clientSecret: env.AUTH_CLOUDFLARE_ZERO_TRUST_SECRET, + issuer: env.AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER, + overrides: { + mapProfileToUser: (profile) => ({ + email: profile.email, + name: profile.name ?? profile.email ?? profile.sub, + }), + }, + providerId: 'cloudflare-zero-trust', + }), + checkEnvs: () => { + return !!( + authEnv.AUTH_CLOUDFLARE_ZERO_TRUST_ID && + authEnv.AUTH_CLOUDFLARE_ZERO_TRUST_SECRET && + authEnv.AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER + ) + ? { + AUTH_CLOUDFLARE_ZERO_TRUST_ID: authEnv.AUTH_CLOUDFLARE_ZERO_TRUST_ID, + AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER: authEnv.AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER, + AUTH_CLOUDFLARE_ZERO_TRUST_SECRET: authEnv.AUTH_CLOUDFLARE_ZERO_TRUST_SECRET, + } + : false; + }, + id: 'cloudflare-zero-trust', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/cognito.ts b/src/libs/better-auth/sso/providers/cognito.ts new file mode 100644 index 0000000000..4bce703f25 --- /dev/null +++ b/src/libs/better-auth/sso/providers/cognito.ts @@ -0,0 +1,45 @@ +import { authEnv } from '@/envs/auth'; + +import type { BuiltinProviderDefinition } from '../types'; + +const provider: BuiltinProviderDefinition< + { + AUTH_COGNITO_DOMAIN: string; + AUTH_COGNITO_ID: string; + AUTH_COGNITO_REGION: string; + AUTH_COGNITO_SECRET: string; + AUTH_COGNITO_USERPOOL_ID: string; + }, + 'cognito' +> = { + build: (env) => { + return { + clientId: env.AUTH_COGNITO_ID, + clientSecret: env.AUTH_COGNITO_SECRET, + domain: env.AUTH_COGNITO_DOMAIN, + region: env.AUTH_COGNITO_REGION, + userPoolId: env.AUTH_COGNITO_USERPOOL_ID, + }; + }, + checkEnvs: () => { + return !!( + authEnv.AUTH_COGNITO_ID && + authEnv.AUTH_COGNITO_SECRET && + authEnv.AUTH_COGNITO_DOMAIN && + authEnv.AUTH_COGNITO_REGION && + authEnv.AUTH_COGNITO_USERPOOL_ID + ) + ? { + AUTH_COGNITO_DOMAIN: authEnv.AUTH_COGNITO_DOMAIN, + AUTH_COGNITO_ID: authEnv.AUTH_COGNITO_ID, + AUTH_COGNITO_REGION: authEnv.AUTH_COGNITO_REGION, + AUTH_COGNITO_SECRET: authEnv.AUTH_COGNITO_SECRET, + AUTH_COGNITO_USERPOOL_ID: authEnv.AUTH_COGNITO_USERPOOL_ID, + } + : false; + }, + id: 'cognito', + type: 'builtin', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/feishu.ts b/src/libs/better-auth/sso/providers/feishu.ts new file mode 100644 index 0000000000..c0d62294ad --- /dev/null +++ b/src/libs/better-auth/sso/providers/feishu.ts @@ -0,0 +1,181 @@ +import { authEnv } from '@/envs/auth'; + +import type { GenericProviderDefinition } from '../types'; + +const FEISHU_AUTHORIZATION_URL = 'https://accounts.feishu.cn/open-apis/authen/v1/authorize'; +const FEISHU_TOKEN_URL = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token'; +const FEISHU_USERINFO_URL = 'https://open.feishu.cn/open-apis/authen/v1/user_info'; + +type FeishuUserProfile = { + avatar_big?: string; + avatar_middle?: string; + avatar_thumb?: string; + avatar_url?: string; + email?: string; + en_name?: string; + enterprise_email?: string; + name?: string; + open_id?: string; + tenant_key?: string; + union_id?: string; +}; + +type FeishuUserInfoResponse = { + code?: number; + data?: FeishuUserProfile; + msg?: string; +}; + +type FeishuTokenPayload = { + access_token?: string; + expires_in?: number; + refresh_token?: string; + scope?: string; + tokenType?: string; + token_type?: string; +}; + +type FeishuTokenResponse = { + code?: number; + data?: FeishuTokenPayload; + message?: string; + msg?: string; +} & FeishuTokenPayload; + +const isFeishuProfile = (value: unknown): value is FeishuUserProfile => { + if (!value || typeof value !== 'object') return false; + const candidate = value as Record; + return ( + typeof candidate.union_id === 'string' || + typeof candidate.open_id === 'string' || + typeof candidate.avatar_url === 'string' || + typeof candidate.name === 'string' + ); +}; + +const parseScopes = (scope: string | undefined) => + scope ? scope.split(/[\s,]+/).filter(Boolean) : []; + +const provider: GenericProviderDefinition<{ + AUTH_FEISHU_APP_ID: string; + AUTH_FEISHU_APP_SECRET: string; +}> = { + build: (env) => { + const clientId = env.AUTH_FEISHU_APP_ID; + const clientSecret = env.AUTH_FEISHU_APP_SECRET; + + return { + authorizationUrl: FEISHU_AUTHORIZATION_URL, + authorizationUrlParams: { + app_id: clientId, + response_type: 'code', + scope: '', + }, + clientId, + clientSecret, + /** + * Exchange code directly with Feishu (no proxy needed). + */ + getToken: async ({ code, redirectURI }) => { + const tokenResponse = await fetch(FEISHU_TOKEN_URL, { + body: JSON.stringify({ + app_id: clientId, + app_secret: clientSecret, + code, + grant_type: 'authorization_code', + redirect_uri: redirectURI, + }), + cache: 'no-store', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + method: 'POST', + }); + + const parsed = (await tokenResponse.json()) as FeishuTokenResponse; + const payload = parsed.data ?? parsed; + + const hasErrorCode = typeof parsed.code === 'number' && parsed.code !== 0; + const tokenMissing = !payload.access_token; + + if (!tokenResponse.ok || hasErrorCode || tokenMissing) { + throw new Error(parsed.msg ?? parsed.message ?? 'Failed to fetch Feishu OAuth token'); + } + + return { + accessToken: payload.access_token, + accessTokenExpiresAt: payload.expires_in + ? new Date(Date.now() + payload.expires_in * 1000) + : undefined, + expiresIn: payload.expires_in, + raw: parsed, + refreshToken: payload.refresh_token, + scopes: parseScopes(payload.scope), + tokenType: payload.token_type ?? payload.tokenType ?? 'Bearer', + }; + }, + getUserInfo: async (tokens) => { + if (!tokens.accessToken) return null; + + const response = await fetch(FEISHU_USERINFO_URL, { + cache: 'no-store', + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }); + + if (!response.ok) { + return null; + } + + const payload = (await response.json()) as unknown; + const profileResponse = payload as FeishuUserInfoResponse; + + if (profileResponse.code && profileResponse.code !== 0) { + return null; + } + + const profile: FeishuUserProfile | undefined = + profileResponse.data ?? (isFeishuProfile(payload) ? payload : undefined); + + if (!profile) return null; + + const unionId = profile.union_id ?? profile.open_id; + if (!unionId) return null; + + const syntheticEmail = + profile.email ?? profile.enterprise_email ?? `${unionId}@feishu.lobehub`; + + return { + email: syntheticEmail, + emailVerified: false, + id: unionId, + image: + profile.avatar_url ?? + profile.avatar_thumb ?? + profile.avatar_middle ?? + profile.avatar_big, + name: profile.name ?? profile.en_name ?? unionId, + ...profile, + }; + }, + pkce: false, + providerId: 'feishu', + responseMode: 'query', + scopes: [], + }; + }, + + checkEnvs: () => { + return !!(authEnv.AUTH_FEISHU_APP_ID && authEnv.AUTH_FEISHU_APP_SECRET) + ? { + AUTH_FEISHU_APP_ID: authEnv.AUTH_FEISHU_APP_ID, + AUTH_FEISHU_APP_SECRET: authEnv.AUTH_FEISHU_APP_SECRET, + } + : false; + }, + id: 'feishu', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/generic-oidc.ts b/src/libs/better-auth/sso/providers/generic-oidc.ts new file mode 100644 index 0000000000..3d785efaed --- /dev/null +++ b/src/libs/better-auth/sso/providers/generic-oidc.ts @@ -0,0 +1,44 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_GENERIC_OIDC_ID: string; + AUTH_GENERIC_OIDC_ISSUER: string; + AUTH_GENERIC_OIDC_SECRET: string; +}> = { + build: (env) => + buildOidcConfig({ + clientId: env.AUTH_GENERIC_OIDC_ID, + clientSecret: env.AUTH_GENERIC_OIDC_SECRET, + issuer: env.AUTH_GENERIC_OIDC_ISSUER, + overrides: { + /** + * Mirror NextAuth's fallback that prefers name -> username -> email so Better Auth never + * fails with name_is_missing when upstream profiles only expose username/email fields. + */ + mapProfileToUser: (profile) => ({ + name: profile.name ?? profile.username ?? profile.email ?? profile.id, + }), + }, + providerId: 'generic-oidc', + }), + checkEnvs: () => { + return !!( + authEnv.AUTH_GENERIC_OIDC_ID && + authEnv.AUTH_GENERIC_OIDC_SECRET && + authEnv.AUTH_GENERIC_OIDC_ISSUER + ) + ? { + AUTH_GENERIC_OIDC_ID: authEnv.AUTH_GENERIC_OIDC_ID, + AUTH_GENERIC_OIDC_ISSUER: authEnv.AUTH_GENERIC_OIDC_ISSUER, + AUTH_GENERIC_OIDC_SECRET: authEnv.AUTH_GENERIC_OIDC_SECRET, + } + : false; + }, + id: 'generic-oidc', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/github.ts b/src/libs/better-auth/sso/providers/github.ts new file mode 100644 index 0000000000..5b636f168b --- /dev/null +++ b/src/libs/better-auth/sso/providers/github.ts @@ -0,0 +1,30 @@ +import { authEnv } from '@/envs/auth'; + +import type { BuiltinProviderDefinition } from '../types'; + +const provider: BuiltinProviderDefinition< + { + AUTH_GITHUB_ID: string; + AUTH_GITHUB_SECRET: string; + }, + 'github' +> = { + build: (env) => { + return { + clientId: env.AUTH_GITHUB_ID, + clientSecret: env.AUTH_GITHUB_SECRET, + }; + }, + checkEnvs: () => { + return !!(authEnv.AUTH_GITHUB_ID && authEnv.AUTH_GITHUB_SECRET) + ? { + AUTH_GITHUB_ID: authEnv.AUTH_GITHUB_ID, + AUTH_GITHUB_SECRET: authEnv.AUTH_GITHUB_SECRET, + } + : false; + }, + id: 'github', + type: 'builtin', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/google.ts b/src/libs/better-auth/sso/providers/google.ts new file mode 100644 index 0000000000..c2b52a0215 --- /dev/null +++ b/src/libs/better-auth/sso/providers/google.ts @@ -0,0 +1,30 @@ +import { authEnv } from '@/envs/auth'; + +import type { BuiltinProviderDefinition } from '../types'; + +const provider: BuiltinProviderDefinition< + { + AUTH_GOOGLE_ID: string; + AUTH_GOOGLE_SECRET: string; + }, + 'google' +> = { + build: (env) => { + return { + clientId: env.AUTH_GOOGLE_ID, + clientSecret: env.AUTH_GOOGLE_SECRET, + }; + }, + checkEnvs: () => { + return !!(authEnv.AUTH_GOOGLE_ID && authEnv.AUTH_GOOGLE_SECRET) + ? { + AUTH_GOOGLE_ID: authEnv.AUTH_GOOGLE_ID, + AUTH_GOOGLE_SECRET: authEnv.AUTH_GOOGLE_SECRET, + } + : false; + }, + id: 'google', + type: 'builtin', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/keycloak.ts b/src/libs/better-auth/sso/providers/keycloak.ts new file mode 100644 index 0000000000..94c0cc085f --- /dev/null +++ b/src/libs/better-auth/sso/providers/keycloak.ts @@ -0,0 +1,35 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_KEYCLOAK_ID: string; + AUTH_KEYCLOAK_ISSUER: string; + AUTH_KEYCLOAK_SECRET: string; +}> = { + build: (env) => + buildOidcConfig({ + clientId: env.AUTH_KEYCLOAK_ID, + clientSecret: env.AUTH_KEYCLOAK_SECRET, + issuer: env.AUTH_KEYCLOAK_ISSUER, + providerId: 'keycloak', + }), + checkEnvs: () => { + return !!( + authEnv.AUTH_KEYCLOAK_ID && + authEnv.AUTH_KEYCLOAK_SECRET && + authEnv.AUTH_KEYCLOAK_ISSUER + ) + ? { + AUTH_KEYCLOAK_ID: authEnv.AUTH_KEYCLOAK_ID, + AUTH_KEYCLOAK_ISSUER: authEnv.AUTH_KEYCLOAK_ISSUER, + AUTH_KEYCLOAK_SECRET: authEnv.AUTH_KEYCLOAK_SECRET, + } + : false; + }, + id: 'keycloak', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/logto.ts b/src/libs/better-auth/sso/providers/logto.ts new file mode 100644 index 0000000000..c0c7fc9dec --- /dev/null +++ b/src/libs/better-auth/sso/providers/logto.ts @@ -0,0 +1,38 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_LOGTO_ID: string; + AUTH_LOGTO_ISSUER: string; + AUTH_LOGTO_SECRET: string; +}> = { + build: (env) => + buildOidcConfig({ + clientId: env.AUTH_LOGTO_ID, + clientSecret: env.AUTH_LOGTO_SECRET, + issuer: env.AUTH_LOGTO_ISSUER, + overrides: { + mapProfileToUser: (profile) => ({ + email: profile.email, + name: profile.name ?? profile.username ?? profile.email ?? profile.sub, + }), + }, + providerId: 'logto', + scopes: ['openid', 'profile', 'email', 'offline_access'], + }), + checkEnvs: () => { + return !!(authEnv.AUTH_LOGTO_ID && authEnv.AUTH_LOGTO_SECRET && authEnv.AUTH_LOGTO_ISSUER) + ? { + AUTH_LOGTO_ID: authEnv.AUTH_LOGTO_ID, + AUTH_LOGTO_ISSUER: authEnv.AUTH_LOGTO_ISSUER, + AUTH_LOGTO_SECRET: authEnv.AUTH_LOGTO_SECRET, + } + : false; + }, + id: 'logto', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/microsoft.ts b/src/libs/better-auth/sso/providers/microsoft.ts new file mode 100644 index 0000000000..efbea4da1f --- /dev/null +++ b/src/libs/better-auth/sso/providers/microsoft.ts @@ -0,0 +1,65 @@ +import { authEnv } from '@/envs/auth'; + +import { pickEnv } from '../helpers'; +import type { BuiltinProviderDefinition } from '../types'; + +type MicrosoftEnv = { + AUTH_AZURE_AD_ID?: string; + AUTH_AZURE_AD_SECRET?: string; + AUTH_MICROSOFT_ENTRA_ID_ID?: string; + AUTH_MICROSOFT_ENTRA_ID_SECRET?: string; + AUTH_MICROSOFT_ID?: string; + AUTH_MICROSOFT_SECRET?: string; + AZURE_AD_CLIENT_ID?: string; + AZURE_AD_CLIENT_SECRET?: string; +}; + +const getClientId = (env: MicrosoftEnv) => { + return pickEnv( + env.AUTH_MICROSOFT_ID, + env.AUTH_MICROSOFT_ENTRA_ID_ID, + env.AUTH_AZURE_AD_ID, + env.AZURE_AD_CLIENT_ID, + ); +}; + +const getClientSecret = (env: MicrosoftEnv) => { + return pickEnv( + env.AUTH_MICROSOFT_SECRET, + env.AUTH_MICROSOFT_ENTRA_ID_SECRET, + env.AUTH_AZURE_AD_SECRET, + env.AZURE_AD_CLIENT_SECRET, + ); +}; + +const provider: BuiltinProviderDefinition = { + aliases: ['microsoft-entra-id'], + build: (env) => { + const clientId = getClientId(env)!; + const clientSecret = getClientSecret(env)!; + return { + clientId, + clientSecret, + }; + }, + checkEnvs: () => { + const clientId = getClientId(authEnv); + const clientSecret = getClientSecret(authEnv); + return !!(clientId && clientSecret) + ? { + AUTH_AZURE_AD_ID: authEnv.AUTH_AZURE_AD_ID, + AUTH_AZURE_AD_SECRET: authEnv.AUTH_AZURE_AD_SECRET, + AUTH_MICROSOFT_ENTRA_ID_ID: authEnv.AUTH_MICROSOFT_ENTRA_ID_ID, + AUTH_MICROSOFT_ENTRA_ID_SECRET: authEnv.AUTH_MICROSOFT_ENTRA_ID_SECRET, + AUTH_MICROSOFT_ID: authEnv.AUTH_MICROSOFT_ID, + AUTH_MICROSOFT_SECRET: authEnv.AUTH_MICROSOFT_SECRET, + AZURE_AD_CLIENT_ID: authEnv.AZURE_AD_CLIENT_ID, + AZURE_AD_CLIENT_SECRET: authEnv.AZURE_AD_CLIENT_SECRET, + } + : false; + }, + id: 'microsoft', + type: 'builtin', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/okta.ts b/src/libs/better-auth/sso/providers/okta.ts new file mode 100644 index 0000000000..390d273b41 --- /dev/null +++ b/src/libs/better-auth/sso/providers/okta.ts @@ -0,0 +1,37 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +const provider: GenericProviderDefinition<{ + AUTH_OKTA_ID: string; + AUTH_OKTA_ISSUER: string; + AUTH_OKTA_SECRET: string; +}> = { + build: (env) => + buildOidcConfig({ + clientId: env.AUTH_OKTA_ID, + clientSecret: env.AUTH_OKTA_SECRET, + issuer: env.AUTH_OKTA_ISSUER, + overrides: { + mapProfileToUser: (profile) => ({ + email: profile.email, + name: profile.name ?? profile.preferred_username ?? profile.email ?? profile.sub, + }), + }, + providerId: 'okta', + }), + checkEnvs: () => { + return !!(authEnv.AUTH_OKTA_ID && authEnv.AUTH_OKTA_SECRET && authEnv.AUTH_OKTA_ISSUER) + ? { + AUTH_OKTA_ID: authEnv.AUTH_OKTA_ID, + AUTH_OKTA_ISSUER: authEnv.AUTH_OKTA_ISSUER, + AUTH_OKTA_SECRET: authEnv.AUTH_OKTA_SECRET, + } + : false; + }, + id: 'okta', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/wechat.ts b/src/libs/better-auth/sso/providers/wechat.ts new file mode 100644 index 0000000000..b890a3a843 --- /dev/null +++ b/src/libs/better-auth/sso/providers/wechat.ts @@ -0,0 +1,140 @@ +import { authEnv } from '@/envs/auth'; + +import type { GenericProviderDefinition } from '../types'; + +const WECHAT_AUTHORIZATION_URL = 'https://open.weixin.qq.com/connect/qrconnect'; +const WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token'; +const WECHAT_USERINFO_URL = 'https://api.weixin.qq.com/sns/userinfo'; + +type WeChatTokenResponse = { + access_token?: string; + errcode?: number; + errmsg?: string; + expires_in?: number; + openid?: string; + refresh_token?: string; + scope?: string; + token_type?: string; + unionid?: string; +}; + +const parseWechatScopes = (scope: string | undefined) => + scope ? scope.split(' ').filter(Boolean) : []; + +const provider: GenericProviderDefinition<{ + AUTH_WECHAT_ID: string; + AUTH_WECHAT_SECRET: string; +}> = { + build: (env) => { + const clientId = env.AUTH_WECHAT_ID; + const clientSecret = env.AUTH_WECHAT_SECRET; + + return { + authorizationUrl: WECHAT_AUTHORIZATION_URL, + authorizationUrlParams: { + appid: clientId, + response_type: 'code', + scope: 'snsapi_login', + }, + clientId, + clientSecret, + /** + * WeChat uses a non-standard token endpoint (GET with appid/secret/code) + * and returns openid/unionid alongside tokens, so we exchange the code + * manually instead of proxying through a custom API route. + */ + getToken: async ({ code }) => { + const tokenUrl = new URL(WECHAT_TOKEN_URL); + tokenUrl.searchParams.set('appid', clientId); + tokenUrl.searchParams.set('secret', clientSecret); + tokenUrl.searchParams.set('code', code); + tokenUrl.searchParams.set('grant_type', 'authorization_code'); + + const response = await fetch(tokenUrl, { cache: 'no-store' }); + const data = (await response.json()) as WeChatTokenResponse; + + if (!response.ok || data.errcode) { + throw new Error(data.errmsg ?? 'Failed to fetch WeChat OAuth token'); + } + + if (!data.access_token || !data.openid) { + throw new Error('WeChat token response is missing required fields'); + } + + return { + accessToken: data.access_token, + accessTokenExpiresAt: data.expires_in + ? new Date(Date.now() + data.expires_in * 1000) + : undefined, + expiresIn: data.expires_in, + raw: data, + refreshToken: data.refresh_token, + refreshTokenExpiresAt: undefined, + scopes: parseWechatScopes(data.scope), + tokenType: data.token_type ?? 'Bearer', + }; + }, + /** + * Use openid/unionid returned in the token response; no custom scope encoding needed. + */ + getUserInfo: async (tokens) => { + const accessToken = tokens.accessToken; + const openId = (tokens as { raw?: WeChatTokenResponse }).raw?.openid; + const unionId = (tokens as { raw?: WeChatTokenResponse }).raw?.unionid; + + if (!accessToken || !openId) { + return null; + } + + const url = new URL(WECHAT_USERINFO_URL); + url.searchParams.set('access_token', accessToken); + url.searchParams.set('openid', openId); + url.searchParams.set('lang', 'zh_CN'); + + const response = await fetch(url, { cache: 'no-store' }); + if (!response.ok) { + return null; + } + + const profile = (await response.json()) as { + headimgurl?: string; + nickname?: string; + unionid?: string; + }; + + const finalUnionId = unionId ?? profile.unionid ?? openId; + const syntheticEmail = `${finalUnionId}@wechat.lobehub`; + + return { + email: syntheticEmail, + emailVerified: false, + id: finalUnionId, + image: profile.headimgurl, + name: profile.nickname ?? finalUnionId, + ...profile, + }; + }, + + pkce: false, + + providerId: 'wechat', + + responseMode: 'query', + + scopes: ['snsapi_login'], + }; + }, + + checkEnvs: () => { + return !!(authEnv.AUTH_WECHAT_ID && authEnv.AUTH_WECHAT_SECRET) + ? { + AUTH_WECHAT_ID: authEnv.AUTH_WECHAT_ID, + AUTH_WECHAT_SECRET: authEnv.AUTH_WECHAT_SECRET, + } + : false; + }, + id: 'wechat', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/providers/zitadel.ts b/src/libs/better-auth/sso/providers/zitadel.ts new file mode 100644 index 0000000000..f939a8bd30 --- /dev/null +++ b/src/libs/better-auth/sso/providers/zitadel.ts @@ -0,0 +1,54 @@ +import { authEnv } from '@/envs/auth'; + +import { buildOidcConfig, pickEnv } from '../helpers'; +import type { GenericProviderDefinition } from '../types'; + +type ZitadelEnv = { + AUTH_ZITADEL_ID?: string; + AUTH_ZITADEL_ISSUER?: string; + AUTH_ZITADEL_SECRET?: string; + ZITADEL_CLIENT_ID?: string; + ZITADEL_CLIENT_SECRET?: string; + ZITADEL_ISSUER?: string; +}; + +const getClientId = (env: ZitadelEnv) => { + return pickEnv(env.ZITADEL_CLIENT_ID, env.AUTH_ZITADEL_ID); +}; + +const getClientSecret = (env: ZitadelEnv) => { + return pickEnv(env.ZITADEL_CLIENT_SECRET, env.AUTH_ZITADEL_SECRET); +}; + +const getIssuer = (env: ZitadelEnv) => { + return pickEnv(env.ZITADEL_ISSUER, env.AUTH_ZITADEL_ISSUER); +}; + +const provider: GenericProviderDefinition = { + build: (env) => + buildOidcConfig({ + clientId: getClientId(env)!, + clientSecret: getClientSecret(env)!, + issuer: getIssuer(env)!, + providerId: 'zitadel', + }), + checkEnvs: () => { + const clientId = getClientId(authEnv); + const clientSecret = getClientSecret(authEnv); + const issuer = getIssuer(authEnv); + return !!(clientId && clientSecret && issuer) + ? { + AUTH_ZITADEL_ID: authEnv.AUTH_ZITADEL_ID, + AUTH_ZITADEL_ISSUER: authEnv.AUTH_ZITADEL_ISSUER, + AUTH_ZITADEL_SECRET: authEnv.AUTH_ZITADEL_SECRET, + ZITADEL_CLIENT_ID: authEnv.ZITADEL_CLIENT_ID, + ZITADEL_CLIENT_SECRET: authEnv.ZITADEL_CLIENT_SECRET, + ZITADEL_ISSUER: authEnv.ZITADEL_ISSUER, + } + : false; + }, + id: 'zitadel', + type: 'generic', +}; + +export default provider; diff --git a/src/libs/better-auth/sso/types.ts b/src/libs/better-auth/sso/types.ts new file mode 100644 index 0000000000..8ff2ec0e69 --- /dev/null +++ b/src/libs/better-auth/sso/types.ts @@ -0,0 +1,25 @@ +import type { GenericOAuthConfig } from 'better-auth/plugins'; +import type { SocialProviders } from 'better-auth/social-providers'; + +export type BuiltinProviderDefinition< + E extends Record, + Id extends keyof SocialProviders = keyof SocialProviders, +> = { + aliases?: string[]; + build: (env: E) => SocialProviders[Id]; + checkEnvs: () => E | false; + id: Id; + type: 'builtin'; +}; + +export type GenericProviderDefinition> = { + aliases?: string[]; + build: (env: E) => GenericOAuthConfig; + checkEnvs: () => E | false; + id: string; + type: 'generic'; +}; + +export type BetterAuthProviderDefinition = + | BuiltinProviderDefinition> + | GenericProviderDefinition>; diff --git a/src/libs/better-auth/utils/client.ts b/src/libs/better-auth/utils/client.ts new file mode 100644 index 0000000000..930450bed2 --- /dev/null +++ b/src/libs/better-auth/utils/client.ts @@ -0,0 +1 @@ +export { isBuiltinProvider, normalizeProviderId } from './common'; diff --git a/src/libs/better-auth/utils/common.ts b/src/libs/better-auth/utils/common.ts new file mode 100644 index 0000000000..06718caa99 --- /dev/null +++ b/src/libs/better-auth/utils/common.ts @@ -0,0 +1,20 @@ +import { BUILTIN_BETTER_AUTH_PROVIDERS, PROVIDER_ALIAS_MAP } from '@/libs/better-auth/constants'; + +/** + * Normalize provider id using configured alias map (e.g. microsoft-entra-id -> microsoft). + */ +export const normalizeProviderId = (provider: string) => { + return PROVIDER_ALIAS_MAP[provider] || provider; +}; + +/** + * Check whether a provider is handled by Better-Auth's built-in social providers. + * Uses alias normalization so callers can pass either canonical ids or aliases. + */ +export const isBuiltinProvider = (provider: string) => { + const normalized = normalizeProviderId(provider); + + return BUILTIN_BETTER_AUTH_PROVIDERS.includes( + normalized as (typeof BUILTIN_BETTER_AUTH_PROVIDERS)[number], + ); +}; diff --git a/src/libs/better-auth/utils/server.test.ts b/src/libs/better-auth/utils/server.test.ts new file mode 100644 index 0000000000..bae58468c6 --- /dev/null +++ b/src/libs/better-auth/utils/server.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; + +import { parseSSOProviders } from './server'; + +describe('parseSSOProviders', () => { + it('should return empty array when input is undefined', () => { + expect(parseSSOProviders(undefined)).toEqual([]); + }); + + it('should return empty array when input is empty string', () => { + expect(parseSSOProviders('')).toEqual([]); + }); + + it('should return empty array when input contains only whitespace', () => { + expect(parseSSOProviders(' ')).toEqual([]); + }); + + it('should parse single provider', () => { + expect(parseSSOProviders('google')).toEqual(['google']); + }); + + it('should parse multiple providers separated by English comma', () => { + expect(parseSSOProviders('google,github,microsoft')).toEqual(['google', 'github', 'microsoft']); + }); + + it('should parse multiple providers separated by Chinese comma', () => { + expect(parseSSOProviders('google,github,microsoft')).toEqual([ + 'google', + 'github', + 'microsoft', + ]); + }); + + it('should parse providers with mixed comma separators', () => { + expect(parseSSOProviders('google,github,microsoft')).toEqual([ + 'google', + 'github', + 'microsoft', + ]); + }); + + it('should trim whitespace from providers', () => { + expect(parseSSOProviders(' google , github , microsoft ')).toEqual([ + 'google', + 'github', + 'microsoft', + ]); + }); + + it('should filter out empty entries', () => { + expect(parseSSOProviders('google,,github,,,microsoft')).toEqual([ + 'google', + 'github', + 'microsoft', + ]); + }); + + it('should trim leading and trailing whitespace from input', () => { + expect(parseSSOProviders(' google,github ')).toEqual(['google', 'github']); + }); +}); diff --git a/src/libs/better-auth/utils/server.ts b/src/libs/better-auth/utils/server.ts new file mode 100644 index 0000000000..512008d153 --- /dev/null +++ b/src/libs/better-auth/utils/server.ts @@ -0,0 +1,18 @@ +/** + * Parse Better-Auth SSO providers from environment variable + * Supports comma-separated list (both English and Chinese commas) + * @param providersEnv - Raw environment variable value (e.g., "google,github") + * @returns Array of enabled provider names + */ +export const parseSSOProviders = (providersEnv?: string): string[] => { + const providers = providersEnv?.trim(); + + if (!providers) { + return []; + } + + return providers + .split(/[,,]/) + .map((p) => p.trim()) + .filter(Boolean); +}; diff --git a/src/libs/trpc/lambda/context.test.ts b/src/libs/trpc/lambda/context.test.ts new file mode 100644 index 0000000000..e1b719ff09 --- /dev/null +++ b/src/libs/trpc/lambda/context.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from 'vitest'; + +import { createContextInner } from './context'; + +describe('createContextInner', () => { + it('should create context with default values when no params provided', async () => { + const context = await createContextInner(); + + expect(context).toMatchObject({ + authorizationHeader: undefined, + clerkAuth: undefined, + marketAccessToken: undefined, + nextAuth: undefined, + oidcAuth: undefined, + userAgent: undefined, + userId: undefined, + }); + expect(context.resHeaders).toBeInstanceOf(Headers); + }); + + it('should create context with userId', async () => { + const context = await createContextInner({ userId: 'user-123' }); + + expect(context.userId).toBe('user-123'); + }); + + it('should create context with authorization header', async () => { + const context = await createContextInner({ + authorizationHeader: 'Bearer token-abc', + }); + + expect(context.authorizationHeader).toBe('Bearer token-abc'); + }); + + it('should create context with user agent', async () => { + const context = await createContextInner({ + userAgent: 'Mozilla/5.0', + }); + + expect(context.userAgent).toBe('Mozilla/5.0'); + }); + + it('should create context with market access token', async () => { + const context = await createContextInner({ + marketAccessToken: 'mp-token-xyz', + }); + + expect(context.marketAccessToken).toBe('mp-token-xyz'); + }); + + it('should create context with OIDC auth data', async () => { + const oidcAuth = { + sub: 'oidc-user-123', + payload: { iss: 'https://issuer.com', aud: 'client-id' }, + }; + + const context = await createContextInner({ oidcAuth }); + + expect(context.oidcAuth).toEqual(oidcAuth); + }); + + it('should create context with Clerk auth data', async () => { + const clerkAuth = { + userId: 'clerk-user-id', + sessionId: 'session-id', + getToken: async () => 'clerk-token', + } as any; + + const context = await createContextInner({ clerkAuth }); + + expect(context.clerkAuth).toBe(clerkAuth); + }); + + it('should create context with NextAuth user data', async () => { + const nextAuth = { + id: 'next-auth-user-id', + name: 'Test User', + email: 'test@example.com', + }; + + const context = await createContextInner({ nextAuth }); + + expect(context.nextAuth).toEqual(nextAuth); + }); + + it('should create context with all parameters combined', async () => { + const params = { + authorizationHeader: 'Bearer token', + userId: 'user-123', + userAgent: 'Test Agent', + marketAccessToken: 'mp-token', + oidcAuth: { + sub: 'oidc-sub', + payload: { data: 'test' }, + }, + }; + + const context = await createContextInner(params); + + expect(context).toMatchObject({ + authorizationHeader: 'Bearer token', + userId: 'user-123', + userAgent: 'Test Agent', + marketAccessToken: 'mp-token', + oidcAuth: { sub: 'oidc-sub', payload: { data: 'test' } }, + }); + }); + + it('should always include response headers', async () => { + const context1 = await createContextInner(); + const context2 = await createContextInner({ userId: 'test' }); + + expect(context1.resHeaders).toBeInstanceOf(Headers); + expect(context2.resHeaders).toBeInstanceOf(Headers); + }); +}); diff --git a/src/libs/trpc/lambda/context.ts b/src/libs/trpc/lambda/context.ts index 8d6f3085c9..7fafa5405d 100644 --- a/src/libs/trpc/lambda/context.ts +++ b/src/libs/trpc/lambda/context.ts @@ -7,6 +7,7 @@ import { NextRequest } from 'next/server'; import { LOBE_CHAT_AUTH_HEADER, LOBE_CHAT_OIDC_AUTH_HEADER, + enableBetterAuth, enableClerk, enableNextAuth, } from '@/const/auth'; @@ -163,6 +164,32 @@ export const createLambdaContext = async (request: NextRequest): Promise { if (!ctx.userId) { if (enableClerk) { console.log('clerk auth:', ctx.clerkAuth); - } else { + } else if (enableBetterAuth) { + console.log('better auth: no session found in context'); + } else if (enableNextAuth) { console.log('next auth:', ctx.nextAuth); } throw new TRPCError({ code: 'UNAUTHORIZED' }); diff --git a/src/locales/default/auth.ts b/src/locales/default/auth.ts index 6ef95aeeea..52be2c4714 100644 --- a/src/locales/default/auth.ts +++ b/src/locales/default/auth.ts @@ -52,6 +52,104 @@ export default { required: '内容不得为空', }, }, + betterAuth: { + errors: { + emailInvalid: '请输入有效的邮箱地址', + emailNotRegistered: '该邮箱尚未注册', + emailNotVerified: '邮箱尚未验证,请先验证邮箱', + emailRequired: '请输入邮箱地址', + firstNameRequired: '请输入名字', + lastNameRequired: '请输入姓氏', + loginFailed: '登录失败,请检查邮箱和密码', + passwordFormat: '密码必须同时包含字母和数字', + passwordMaxLength: '密码最多不超过 64 个字符', + passwordMinLength: '密码至少需要 8 个字符', + passwordRequired: '请输入密码', + usernameRequired: '请输入用户名', + }, + resetPassword: { + backToSignIn: '返回登录', + confirmPasswordPlaceholder: '确认新密码', + confirmPasswordRequired: '请确认新密码', + description: '请输入您的新密码', + error: '重置密码失败,请重试', + invalidToken: '无效或已过期的重置链接', + newPasswordPlaceholder: '输入新密码', + passwordMismatch: '两次输入的密码不一致', + submit: '重置密码', + success: '密码重置成功,请使用新密码登录', + title: '重置密码', + }, + signin: { + backToEmail: '返回修改邮箱', + continueWithAuth0: '使用 Auth0 登录', + continueWithAuthelia: '使用 Authelia 登录', + continueWithAuthentik: '使用 Authentik 登录', + continueWithCasdoor: '使用 Casdoor 登录', + continueWithCloudflareZeroTrust: '使用 Cloudflare Zero Trust 登录', + continueWithCognito: '使用 AWS Cognito 登录', + continueWithFeishu: '使用飞书登录', + continueWithGithub: '使用 GitHub 登录', + continueWithGoogle: '使用 Google 登录', + continueWithKeycloak: '使用 Keycloak 登录', + continueWithLogto: '使用 Logto 登录', + continueWithMicrosoft: '使用 Microsoft 登录', + continueWithOIDC: '使用 OIDC 登录', + continueWithOkta: '使用 Okta 登录', + continueWithWechat: '使用微信登录', + continueWithZitadel: '使用 Zitadel 登录', + emailPlaceholder: '请输入邮箱地址', + emailStep: { + subtitle: '请输入您的邮箱地址以继续', + title: '登录', + }, + error: '登录失败,请检查邮箱和密码', + forgotPassword: '忘记密码?', + forgotPasswordError: '发送重置密码链接失败', + forgotPasswordSent: '重置密码链接已发送,请检查邮箱', + magicLinkButton: '发送登录链接', + magicLinkError: '发送登录链接失败,请稍后再试', + magicLinkSent: '登录链接已发送,请检查邮箱', + nextStep: '下一步', + noAccount: '还没有账号?', + orContinueWith: '或', + passwordPlaceholder: '请输入密码', + passwordStep: { + subtitle: '请输入密码以继续', + }, + signupLink: '立即注册', + socialError: '社交登录失败,请重试', + socialOnlyHint: '该邮箱使用社交账号注册,请使用社交账号登录', + submit: '登录', + }, + signup: { + emailPlaceholder: '请输入邮箱地址', + error: '注册失败,请重试', + firstNamePlaceholder: '名字', + hasAccount: '已有账号?', + lastNamePlaceholder: '姓氏', + passwordPlaceholder: '请输入密码', + signinLink: '立即登录', + submit: '注册', + subtitle: '加入 LobeChat 社区', + success: '注册成功!请检查您的邮箱验证邮件', + title: '创建账号', + usernamePlaceholder: '请输入用户名', + }, + verifyEmail: { + backToSignIn: '返回登录', + checkSpam: '如果没有收到邮件,请检查垃圾邮件文件夹', + descriptionPrefix: '我们已向', + descriptionSuffix: '发送了验证邮件', + resend: { + button: '重新发送验证邮件', + error: '发送失败,请稍后重试', + noEmail: '邮箱地址缺失', + success: '验证邮件已重新发送,请检查您的邮箱', + }, + title: '验证您的邮箱', + }, + }, date: { prevMonth: '上个月', recent30Days: '最近30天', @@ -86,17 +184,32 @@ export default { loginOrSignup: '登录 / 注册', profile: { avatar: '头像', + cancel: '取消', + changePassword: '重置密码', email: '电子邮件地址', + fullName: '全名', + fullNameInputHint: '请输入新的全名', + password: '密码', + resetPasswordError: '发送密码重置链接失败', + resetPasswordSent: '密码重置链接已发送,请检查邮箱', + save: '保存', sso: { + link: { + button: '连接帐户', + success: '账户关联成功', + }, loading: '正在加载已绑定的第三方账户', providers: '连接的帐户', unlink: { description: - '解绑后,您将无法使用 {{provider}} 账户“{{providerAccountId}}”登录。如果您需要重新绑定 {{provider}} 账户到当前账户,请确保 {{provider}} 账户的邮件地址为 {{email}} ,我们会在登陆时为你自动绑定到当前登录账户。', + '解绑后,您将无法使用 {{provider}} 账户"{{providerAccountId}}"登录。如果您需要重新绑定 {{provider}} 账户到当前账户,请确保 {{provider}} 账户的邮件地址为 {{email}} ,我们会在登陆时为你自动绑定到当前登录账户。', forbidden: '您至少需要保留一个第三方账户绑定。', title: '是否解绑该第三方账户 {{provider}} ?', }, }, + title: '个人资料详情', + updateAvatar: '更新头像', + updateFullName: '更新全名', username: '用户名', }, signout: '退出登录', diff --git a/src/proxy.ts b/src/proxy.ts index 93f1044311..ef636cb422 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { UAParser } from 'ua-parser-js'; import urlJoin from 'url-join'; +import { auth } from '@/auth'; import { OAUTH_AUTHORIZED } from '@/const/auth'; import { LOBE_LOCALE_COOKIE } from '@/const/locale'; import { LOBE_THEME_APPEARANCE } from '@/const/theme'; @@ -21,6 +22,7 @@ import { RouteVariants } from './utils/server/routeVariants'; const logDefault = debug('middleware:default'); const logNextAuth = debug('middleware:next-auth'); const logClerk = debug('middleware:clerk'); +const logBetterAuth = debug('middleware:better-auth'); // OIDC session pre-sync constant const OIDC_SESSION_HEADER = 'x-oidc-session-sync'; @@ -47,10 +49,12 @@ export const config = { '/login(.*)', '/signup(.*)', + '/signin(.*)', + '/verify-email(.*)', + '/reset-password(.*)', '/next-auth/(.*)', '/oauth(.*)', '/oidc(.*)', - // ↓ cloud ↓ ], }; @@ -129,8 +133,18 @@ const defaultMiddleware = (request: NextRequest) => { // / -> /zh-CN__0__dark // /discover -> /zh-CN__0__dark/discover // All SPA routes that use react-router-dom should be rewritten to just /${route} - const spaRoutes = ['/chat', '/discover', '/knowledge', '/settings', '/image', '/labs', '/changelog', '/profile', '/me']; - const isSpaRoute = spaRoutes.some(route => url.pathname.startsWith(route)); + const spaRoutes = [ + '/chat', + '/discover', + '/knowledge', + '/settings', + '/image', + '/labs', + '/changelog', + '/profile', + '/me', + ]; + const isSpaRoute = spaRoutes.some((route) => url.pathname.startsWith(route)); let nextPathname: string; if (isSpaRoute) { @@ -142,7 +156,6 @@ const defaultMiddleware = (request: NextRequest) => { ? urlJoin(url.origin, nextPathname) : nextPathname; - console.log('nextURL', nextURL); logDefault('URL rewrite: %O', { @@ -194,6 +207,10 @@ const isPublicRoute = createRouteMatcher([ // clerk '/login', '/signup', + // better auth + '/signin', + '/verify-email', + '/reset-password', // oauth // Make only the consent view public (GET page), not other oauth paths '/oauth/consent/(.*)', @@ -304,8 +321,53 @@ const clerkAuthMiddleware = clerkMiddleware( }, ); +const betterAuthMiddleware = async (req: NextRequest) => { + logBetterAuth('BetterAuth middleware processing request: %s %s', req.method, req.url); + + const response = defaultMiddleware(req); + + // when enable auth protection, only public route is not protected, others are all protected + const isProtected = appEnv.ENABLE_AUTH_PROTECTION ? !isPublicRoute(req) : isProtectedRoute(req); + + logBetterAuth('Route protection status: %s, %s', req.url, isProtected ? 'protected' : 'public'); + + // Skip session lookup for public routes to reduce latency + if (!isProtected) return response; + + // Get full session with user data (Next.js 15.2.0+ feature) + const session = await auth.api.getSession({ + headers: req.headers, + }); + + const isLoggedIn = !!session?.user; + + logBetterAuth('BetterAuth session status: %O', { + isLoggedIn, + userId: session?.user?.id, + }); + + if (!isLoggedIn) { + // If request a protected route, redirect to sign-in page + if (isProtected) { + logBetterAuth('Request a protected route, redirecting to sign-in page'); + const signInUrl = new URL('/signin', req.nextUrl.origin); + signInUrl.searchParams.set('callbackUrl', req.nextUrl.href); + const hl = req.nextUrl.searchParams.get('hl'); + if (hl) { + signInUrl.searchParams.set('hl', hl); + logBetterAuth('Preserving locale to sign-in: hl=%s', hl); + } + return Response.redirect(signInUrl); + } + logBetterAuth('Request a free route but not login, allow visit without auth header'); + } + + return response; +}; + logDefault('Middleware configuration: %O', { enableAuthProtection: appEnv.ENABLE_AUTH_PROTECTION, + enableBetterAuth: authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH, enableClerk: authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH, enableNextAuth: authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH, enableOIDC: oidcEnv.ENABLE_OIDC, @@ -313,6 +375,8 @@ logDefault('Middleware configuration: %O', { export default authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH ? clerkAuthMiddleware - : authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH - ? nextAuthMiddleware - : defaultMiddleware; + : authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH + ? betterAuthMiddleware + : authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH + ? nextAuthMiddleware + : defaultMiddleware; diff --git a/src/server/globalConfig/index.ts b/src/server/globalConfig/index.ts index 04de189235..d91db4be53 100644 --- a/src/server/globalConfig/index.ts +++ b/src/server/globalConfig/index.ts @@ -5,6 +5,7 @@ import { fileEnv } from '@/envs/file'; import { imageEnv } from '@/envs/image'; import { knowledgeEnv } from '@/envs/knowledge'; import { langfuseEnv } from '@/envs/langfuse'; +import { parseSSOProviders } from '@/libs/better-auth/utils/server'; import { parseSystemAgent } from '@/server/globalConfig/parseSystemAgent'; import { GlobalServerConfig } from '@/types/serverConfig'; import { cleanObject } from '@/utils/object'; @@ -13,6 +14,14 @@ import { genServerAiProvidersConfig } from './genServerAiProviderConfig'; import { parseAgentConfig } from './parseDefaultAgent'; import { parseFilesConfig } from './parseFilesConfig'; +/** + * Get Better-Auth SSO providers list + * Parses AUTH_SSO_PROVIDERS and returns enabled providers + */ +const getBetterAuthSSOProviders = () => { + return parseSSOProviders(authEnv.AUTH_SSO_PROVIDERS); +}; + export const getServerGlobalConfig = async () => { const { ACCESS_CODES, DEFAULT_AGENT_CONFIG } = getAppConfig(); @@ -63,7 +72,9 @@ export const getServerGlobalConfig = async () => { image: cleanObject({ defaultImageNum: imageEnv.AI_IMAGE_DEFAULT_IMAGE_NUM, }), - oAuthSSOProviders: authEnv.NEXT_AUTH_SSO_PROVIDERS.trim().split(/[,,]/), + oAuthSSOProviders: authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH + ? getBetterAuthSSOProviders() + : authEnv.NEXT_AUTH_SSO_PROVIDERS.trim().split(/[,,]/), systemAgent: parseSystemAgent(appEnv.SYSTEM_AGENT), telemetry: { langfuse: langfuseEnv.ENABLE_LANGFUSE, diff --git a/src/server/routers/lambda/user.ts b/src/server/routers/lambda/user.ts index 9c8cf7f0c4..dbba5570d2 100644 --- a/src/server/routers/lambda/user.ts +++ b/src/server/routers/lambda/user.ts @@ -198,6 +198,10 @@ export const userRouter = router({ return ctx.userModel.updateUser({ avatar: input }); }), + updateFullName: userProcedure.input(z.string()).mutation(async ({ ctx, input }) => { + return ctx.userModel.updateUser({ fullName: input }); + }), + updateGuide: userProcedure.input(UserGuideSchema).mutation(async ({ ctx, input }) => { return ctx.userModel.updateGuide(input); }), diff --git a/src/server/services/email/README.md b/src/server/services/email/README.md new file mode 100644 index 0000000000..350da30175 --- /dev/null +++ b/src/server/services/email/README.md @@ -0,0 +1,241 @@ +# Email Service + +A flexible email service implementation supporting multiple email providers. + +## Architecture + +Based on the search service pattern, this service provides a unified interface for sending emails across different providers. + +```plaintext +EmailService + └── EmailServiceImpl (interface) + └── NodemailerImpl (SMTP provider) +``` + +## Usage + +### Basic Example + +```typescript +import { EmailService } from '@/server/services/email'; + +const emailService = new EmailService(); + +// Send a simple text email +await emailService.sendMail({ + from: 'noreply@example.com', + to: 'user@example.com', + subject: 'Welcome to LobeChat', + text: 'Thanks for signing up!', + html: '

Thanks for signing up!

', +}); +``` + +### With Multiple Recipients + +```typescript +await emailService.sendMail({ + from: 'team@example.com', + to: ['user1@example.com', 'user2@example.com'], + subject: 'Team Update', + text: 'Check out our latest updates', +}); +``` + +### With Attachments + +```typescript +await emailService.sendMail({ + from: 'support@example.com', + to: 'user@example.com', + subject: 'Your Invoice', + text: 'Please find your invoice attached.', + attachments: [ + { + filename: 'invoice.pdf', + path: '/path/to/invoice.pdf', + }, + ], +}); +``` + +### With Reply-To Address + +```typescript +await emailService.sendMail({ + from: 'noreply@example.com', + replyTo: 'support@example.com', + to: 'user@example.com', + subject: 'Contact Us', + text: 'Reply to this email for support.', +}); +``` + +## Configuration + +### Environment Variables + +Configure SMTP settings using environment variables: + +```bash +# SMTP Server Configuration +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_SECURE=false # true for port 465, false for other ports +SMTP_USER=your-username +SMTP_PASS=your-password +``` + +### Using Well-Known Services + +You can also use well-known email services (Gmail, SendGrid, etc.): + +```typescript +import { EmailImplType, EmailService } from '@/server/services/email'; +import { NodemailerImpl } from '@/server/services/email/impls/nodemailer'; + +const emailService = new EmailService(EmailImplType.Nodemailer); +// Configure in constructor with service name +``` + +### Testing with Ethereal + +For development and testing, use [Ethereal Email](https://ethereal.email/): + +```typescript +// The preview URL will be logged automatically in development +const result = await emailService.sendMail({...}); +console.log('Preview URL:', result.previewUrl); +``` + +## Verify Connection + +Before sending emails, verify your SMTP configuration: + +```typescript +import { EmailService } from '@/server/services/email'; + +const emailService = new EmailService(); + +try { + await emailService.verify(); + console.log('SMTP connection verified ✓'); +} catch (error) { + console.error('SMTP verification failed:', error); +} +``` + +## Integration with Better-Auth + +Example integration for email verification: + +```typescript +import { betterAuth } from 'better-auth'; + +import { EmailService } from '@/server/services/email'; + +export const auth = betterAuth({ + emailAndPassword: { + enabled: true, + sendResetPasswordEmail: async ({ user, url }) => { + const emailService = new EmailService(); + + await emailService.sendMail({ + from: 'noreply@lobechat.com', + to: user.email, + subject: 'Reset Your Password', + text: `Click here to reset your password: ${url}`, + html: ` +

Reset Your Password

+

Click the link below to reset your password:

+ Reset Password + `, + }); + }, + }, + emailVerification: { + enabled: true, + sendVerificationEmail: async ({ user, url }) => { + const emailService = new EmailService(); + + await emailService.sendMail({ + from: 'noreply@lobechat.com', + to: user.email, + subject: 'Verify Your Email', + text: `Click here to verify your email: ${url}`, + html: ` +

Verify Your Email

+

Click the link below to verify your email address:

+ Verify Email + `, + }); + }, + }, +}); +``` + +## Adding New Providers + +To add a new email provider (e.g., Resend, SendGrid): + +1. Create provider implementation in `impls/[provider-name]/index.ts`: + +```typescript +import { EmailPayload, EmailResponse, EmailServiceImpl } from '../type'; + +export class ResendImpl implements EmailServiceImpl { + async sendMail(payload: EmailPayload): Promise { + // Implement using Resend API + } +} +``` + +2. Add to the enum in `impls/index.ts`: + +```typescript +export enum EmailImplType { + Nodemailer = 'nodemailer', + Resend = 'resend', // Add new provider +} +``` + +3. Update factory function in `impls/index.ts`: + +```typescript +export const createEmailServiceImpl = (type: EmailImplType) => { + switch (type) { + case EmailImplType.Nodemailer: + return new NodemailerImpl(); + case EmailImplType.Resend: + return new ResendImpl(); + default: + return new NodemailerImpl(); + } +}; +``` + +## Error Handling + +The service throws `TRPCError` for various failure scenarios: + +```typescript +try { + await emailService.sendMail({...}); +} catch (error) { + if (error.code === 'SERVICE_UNAVAILABLE') { + // Handle SMTP connection issues + } else if (error.code === 'PRECONDITION_FAILED') { + // Handle configuration errors + } +} +``` + +## Debugging + +Enable debug logging: + +```bash +DEBUG=lobe-email:* node your-app.js +``` + +This will log detailed information about email sending operations. diff --git a/src/server/services/email/impls/index.test.ts b/src/server/services/email/impls/index.test.ts new file mode 100644 index 0000000000..b3555423a3 --- /dev/null +++ b/src/server/services/email/impls/index.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { EmailImplType, createEmailServiceImpl } from './index'; + +vi.mock('./nodemailer', () => ({ + NodemailerImpl: vi.fn().mockImplementation(() => ({ + sendMail: vi.fn().mockResolvedValue({ messageId: 'test-id' }), + verify: vi.fn().mockResolvedValue(true), + })), +})); + +describe('createEmailServiceImpl', () => { + it('should create NodemailerImpl by default', () => { + const impl = createEmailServiceImpl(); + + expect(impl).toBeDefined(); + expect(impl.sendMail).toBeDefined(); + }); + + it('should create NodemailerImpl when explicitly specified', () => { + const impl = createEmailServiceImpl(EmailImplType.Nodemailer); + + expect(impl).toBeDefined(); + expect(impl.sendMail).toBeDefined(); + }); + + it('should fall back to NodemailerImpl for unknown type', () => { + const impl = createEmailServiceImpl('unknown' as EmailImplType); + + expect(impl).toBeDefined(); + expect(impl.sendMail).toBeDefined(); + }); +}); + +describe('EmailImplType enum', () => { + it('should have Nodemailer as a valid type', () => { + expect(EmailImplType.Nodemailer).toBe('nodemailer'); + }); +}); diff --git a/src/server/services/email/impls/index.ts b/src/server/services/email/impls/index.ts new file mode 100644 index 0000000000..8f2002beb6 --- /dev/null +++ b/src/server/services/email/impls/index.ts @@ -0,0 +1,32 @@ +import { NodemailerImpl } from './nodemailer'; +import { EmailServiceImpl } from './type'; + +/** + * Available email service implementations + */ +export enum EmailImplType { + Nodemailer = 'nodemailer', + // Future providers can be added here: + // Resend = 'resend', + // SendGrid = 'sendgrid', +} + +/** + * Create an email service implementation instance + */ +export const createEmailServiceImpl = ( + type: EmailImplType = EmailImplType.Nodemailer, +): EmailServiceImpl => { + switch (type) { + case EmailImplType.Nodemailer: { + return new NodemailerImpl(); + } + + default: { + return new NodemailerImpl(); + } + } +}; + +export type { EmailServiceImpl } from './type'; +export type { EmailPayload, EmailResponse } from './type'; diff --git a/src/server/services/email/impls/nodemailer/index.ts b/src/server/services/email/impls/nodemailer/index.ts new file mode 100644 index 0000000000..309ce5750c --- /dev/null +++ b/src/server/services/email/impls/nodemailer/index.ts @@ -0,0 +1,108 @@ +import { TRPCError } from '@trpc/server'; +import debug from 'debug'; +import nodemailer from 'nodemailer'; +import type { Transporter } from 'nodemailer'; + +import { emailEnv } from '@/envs/email'; + +import { EmailPayload, EmailResponse, EmailServiceImpl } from '../type'; +import { NodemailerConfig } from './type'; + +const log = debug('lobe-email:Nodemailer'); + +/** + * Nodemailer implementation of the email service + */ +export class NodemailerImpl implements EmailServiceImpl { + private transporter: Transporter; + + constructor() { + log('Initializing Nodemailer from environment variables'); + + if (!emailEnv.SMTP_USER || !emailEnv.SMTP_PASS) { + throw new Error( + 'SMTP_USER and SMTP_PASS environment variables are required to use email service. Please configure SMTP settings in your .env file.', + ); + } + + const transportConfig: NodemailerConfig = { + auth: { + pass: emailEnv.SMTP_PASS, + user: emailEnv.SMTP_USER, + }, + host: emailEnv.SMTP_HOST ?? 'localhost', + port: emailEnv.SMTP_PORT ?? 587, + secure: emailEnv.SMTP_SECURE ?? false, + }; + + try { + this.transporter = nodemailer.createTransport(transportConfig); + log('Nodemailer transporter created successfully'); + } catch (error) { + log.extend('error')('Failed to create Nodemailer transporter: %o', error); + throw new TRPCError({ + cause: error, + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to initialize Nodemailer transport', + }); + } + } + + async sendMail(payload: EmailPayload): Promise { + // Use SMTP_USER as default sender if not provided + const from = payload.from ?? emailEnv.SMTP_USER!; + + log('Sending email with payload: %o', { + from, + subject: payload.subject, + to: payload.to, + }); + + try { + const info = await this.transporter.sendMail({ + attachments: payload.attachments, + from, + html: payload.html, + replyTo: payload.replyTo, + subject: payload.subject, + text: payload.text, + to: payload.to, + }); + + log('Email sent successfully with message ID: %s', info.messageId); + + const previewUrl = nodemailer.getTestMessageUrl(info); + + return { + messageId: info.messageId, + previewUrl: previewUrl || undefined, + }; + } catch (error) { + log.extend('error')('Failed to send email: %o', error); + throw new TRPCError({ + cause: error, + code: 'SERVICE_UNAVAILABLE', + message: `Failed to send email: ${(error as Error).message}`, + }); + } + } + + /** + * Verify the SMTP connection configuration + */ + async verify(): Promise { + try { + log('Verifying SMTP connection...'); + await this.transporter.verify(); + log('SMTP connection verified successfully'); + return true; + } catch (error) { + log.extend('error')('SMTP verification failed: %o', error); + throw new TRPCError({ + cause: error, + code: 'SERVICE_UNAVAILABLE', + message: 'Failed to verify SMTP connection', + }); + } + } +} diff --git a/src/server/services/email/impls/nodemailer/type.ts b/src/server/services/email/impls/nodemailer/type.ts new file mode 100644 index 0000000000..f27989963a --- /dev/null +++ b/src/server/services/email/impls/nodemailer/type.ts @@ -0,0 +1,31 @@ +/** + * Nodemailer SMTP transport configuration + */ +export interface NodemailerConfig { + /** + * Authentication credentials + */ + auth?: { + pass: string; + user: string; + }; + /** + * SMTP server hostname + */ + host?: string; + /** + * SMTP server port + * @default 587 + */ + port?: number; + /** + * Use TLS connection + * @default false + */ + secure?: boolean; + /** + * Well-known service name (e.g., 'Gmail', 'SendGrid') + * When set, overrides host, port, and secure + */ + service?: string; +} diff --git a/src/server/services/email/impls/type.ts b/src/server/services/email/impls/type.ts new file mode 100644 index 0000000000..0356f3a717 --- /dev/null +++ b/src/server/services/email/impls/type.ts @@ -0,0 +1,61 @@ +/** + * Email message payload + */ +export interface EmailPayload { + /** + * Email attachments + */ + attachments?: Array<{ + content?: Buffer | string; + filename?: string; + path?: string; + }>; + /** + * Sender address (defaults to SMTP_USER if not provided) + */ + from?: string; + /** + * HTML body of the email + */ + html?: string; + /** + * Reply-To address + */ + replyTo?: string; + /** + * Subject line + */ + subject: string; + /** + * Plain text body of the email + */ + text?: string; + /** + * Recipient address(es) + */ + to: string | string[]; +} + +/** + * Email send response + */ +export interface EmailResponse { + /** + * Message ID assigned by the email service + */ + messageId: string; + /** + * Preview URL for test emails (e.g., Ethereal) + */ + previewUrl?: string; +} + +/** + * Email service implementation interface + */ +export interface EmailServiceImpl { + /** + * Send an email + */ + sendMail(payload: EmailPayload): Promise; +} diff --git a/src/server/services/email/index.test.ts b/src/server/services/email/index.test.ts new file mode 100644 index 0000000000..e84ff07364 --- /dev/null +++ b/src/server/services/email/index.test.ts @@ -0,0 +1,144 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { EmailImplType, createEmailServiceImpl } from './impls'; +import { EmailService } from './index'; + +// Mock dependencies +vi.mock('./impls'); + +describe('EmailService', () => { + let emailService: EmailService; + let mockEmailImpl: ReturnType; + + function createMockEmailImpl() { + return { + sendMail: vi.fn(), + verify: vi.fn(), + }; + } + + beforeEach(() => { + vi.clearAllMocks(); + mockEmailImpl = createMockEmailImpl(); + vi.mocked(createEmailServiceImpl).mockReturnValue(mockEmailImpl as any); + emailService = new EmailService(); + }); + + describe('constructor', () => { + it('should create instance with default email implementation', () => { + expect(createEmailServiceImpl).toHaveBeenCalledWith(undefined); + }); + + it('should create instance with specified implementation type', () => { + emailService = new EmailService(EmailImplType.Nodemailer); + expect(createEmailServiceImpl).toHaveBeenCalledWith(EmailImplType.Nodemailer); + }); + }); + + describe('sendMail', () => { + it('should call emailImpl.sendMail with correct payload', async () => { + const mockResponse = { + messageId: 'test-message-id', + previewUrl: 'https://ethereal.email/message/xxx', + }; + mockEmailImpl.sendMail.mockResolvedValue(mockResponse); + + const payload = { + from: 'sender@example.com', + html: '

Hello world

', + subject: 'Test Email', + text: 'Hello world', + to: 'recipient@example.com', + }; + + const result = await emailService.sendMail(payload); + + expect(mockEmailImpl.sendMail).toHaveBeenCalledWith(payload); + expect(result).toBe(mockResponse); + }); + + it('should support multiple recipients', async () => { + const mockResponse = { + messageId: 'test-message-id', + }; + mockEmailImpl.sendMail.mockResolvedValue(mockResponse); + + const payload = { + from: 'sender@example.com', + subject: 'Test Email', + text: 'Hello world', + to: ['recipient1@example.com', 'recipient2@example.com'], + }; + + await emailService.sendMail(payload); + + expect(mockEmailImpl.sendMail).toHaveBeenCalledWith(payload); + }); + + it('should support attachments', async () => { + const mockResponse = { + messageId: 'test-message-id', + }; + mockEmailImpl.sendMail.mockResolvedValue(mockResponse); + + const payload = { + attachments: [ + { + content: Buffer.from('test content'), + filename: 'test.txt', + }, + ], + from: 'sender@example.com', + subject: 'Test Email', + text: 'Hello world', + to: 'recipient@example.com', + }; + + await emailService.sendMail(payload); + + expect(mockEmailImpl.sendMail).toHaveBeenCalledWith(payload); + }); + + it('should support reply-to address', async () => { + const mockResponse = { + messageId: 'test-message-id', + }; + mockEmailImpl.sendMail.mockResolvedValue(mockResponse); + + const payload = { + from: 'noreply@example.com', + replyTo: 'support@example.com', + subject: 'Test Email', + text: 'Hello world', + to: 'recipient@example.com', + }; + + await emailService.sendMail(payload); + + expect(mockEmailImpl.sendMail).toHaveBeenCalledWith(payload); + }); + }); + + describe('verify', () => { + it('should call emailImpl.verify if available', async () => { + mockEmailImpl.verify.mockResolvedValue(true); + + const result = await emailService.verify(); + + expect(mockEmailImpl.verify).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('should return true if verify method is not available', async () => { + const mockImplWithoutVerify = { + sendMail: vi.fn(), + }; + vi.mocked(createEmailServiceImpl).mockReturnValue(mockImplWithoutVerify as any); + emailService = new EmailService(); + + const result = await emailService.verify(); + + expect(result).toBe(true); + }); + }); +}); diff --git a/src/server/services/email/index.ts b/src/server/services/email/index.ts new file mode 100644 index 0000000000..7e5889936d --- /dev/null +++ b/src/server/services/email/index.ts @@ -0,0 +1,40 @@ + +import { EmailImplType, EmailPayload, EmailResponse, createEmailServiceImpl } from './impls'; +import type { EmailServiceImpl } from './impls'; + +/** + * Email service class + * Provides email sending functionality with multiple provider support + */ +export class EmailService { + private emailImpl: EmailServiceImpl; + + constructor(implType?: EmailImplType) { + this.emailImpl = createEmailServiceImpl(implType); + } + + /** + * Send an email + */ + async sendMail(payload: EmailPayload): Promise { + return this.emailImpl.sendMail(payload); + } + + /** + * Verify the email service configuration + * Note: Only available for Nodemailer implementation + */ + async verify(): Promise { + // Check if the implementation has a verify method + if ('verify' in this.emailImpl && typeof this.emailImpl.verify === 'function') { + return this.emailImpl.verify(); + } + + // For implementations without verify, assume it's valid + return true; + } +} + +// Export types +export type { EmailPayload, EmailResponse } from './impls'; +export { EmailImplType } from './impls'; diff --git a/src/services/user/index.test.ts b/src/services/user/index.test.ts index 253239256e..43dceafaaa 100644 --- a/src/services/user/index.test.ts +++ b/src/services/user/index.test.ts @@ -1,8 +1,168 @@ -import { describe } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { testService } from '~test-utils'; -import { UserService } from './index'; +import { UserService, userService } from './index'; + +const mockLambdaClient = vi.hoisted(() => ({ + user: { + getUserRegistrationDuration: { query: vi.fn() }, + getUserState: { query: vi.fn() }, + getUserSSOProviders: { query: vi.fn() }, + unlinkSSOProvider: { mutate: vi.fn() }, + makeUserOnboarded: { mutate: vi.fn() }, + updateAvatar: { mutate: vi.fn() }, + updateFullName: { mutate: vi.fn() }, + updatePreference: { mutate: vi.fn() }, + updateGuide: { mutate: vi.fn() }, + updateSettings: { mutate: vi.fn() }, + resetSettings: { mutate: vi.fn() }, + }, +})); + +vi.mock('@/libs/trpc/client', () => ({ + lambdaClient: mockLambdaClient, +})); describe('UserService', () => { testService(UserService); + + describe('getUserRegistrationDuration', () => { + it('should call lambdaClient.user.getUserRegistrationDuration.query', async () => { + const mockResult = { createdAt: '2024-01-01', duration: 100, updatedAt: '2024-01-02' }; + mockLambdaClient.user.getUserRegistrationDuration.query.mockResolvedValueOnce(mockResult); + + const result = await userService.getUserRegistrationDuration(); + + expect(mockLambdaClient.user.getUserRegistrationDuration.query).toHaveBeenCalled(); + expect(result).toEqual(mockResult); + }); + }); + + describe('getUserState', () => { + it('should call lambdaClient.user.getUserState.query', async () => { + const mockState = { isOnboarded: true, preference: {}, settings: {} }; + mockLambdaClient.user.getUserState.query.mockResolvedValueOnce(mockState); + + const result = await userService.getUserState(); + + expect(mockLambdaClient.user.getUserState.query).toHaveBeenCalled(); + expect(result).toEqual(mockState); + }); + }); + + describe('getUserSSOProviders', () => { + it('should call lambdaClient.user.getUserSSOProviders.query', async () => { + const mockProviders = [ + { provider: 'github', email: 'test@example.com', providerAccountId: '123' }, + ]; + mockLambdaClient.user.getUserSSOProviders.query.mockResolvedValueOnce(mockProviders); + + const result = await userService.getUserSSOProviders(); + + expect(mockLambdaClient.user.getUserSSOProviders.query).toHaveBeenCalled(); + expect(result).toEqual(mockProviders); + }); + }); + + describe('unlinkSSOProvider', () => { + it('should call lambdaClient.user.unlinkSSOProvider.mutate with correct params', async () => { + mockLambdaClient.user.unlinkSSOProvider.mutate.mockResolvedValueOnce({ success: true }); + + await userService.unlinkSSOProvider('github', 'account-123'); + + expect(mockLambdaClient.user.unlinkSSOProvider.mutate).toHaveBeenCalledWith({ + provider: 'github', + providerAccountId: 'account-123', + }); + }); + }); + + describe('makeUserOnboarded', () => { + it('should call lambdaClient.user.makeUserOnboarded.mutate', async () => { + mockLambdaClient.user.makeUserOnboarded.mutate.mockResolvedValueOnce({ success: true }); + + await userService.makeUserOnboarded(); + + expect(mockLambdaClient.user.makeUserOnboarded.mutate).toHaveBeenCalled(); + }); + }); + + describe('updateAvatar', () => { + it('should call lambdaClient.user.updateAvatar.mutate with avatar string', async () => { + mockLambdaClient.user.updateAvatar.mutate.mockResolvedValueOnce({ success: true }); + + await userService.updateAvatar('https://example.com/avatar.png'); + + expect(mockLambdaClient.user.updateAvatar.mutate).toHaveBeenCalledWith( + 'https://example.com/avatar.png', + ); + }); + }); + + describe('updateFullName', () => { + it('should call lambdaClient.user.updateFullName.mutate with fullName string', async () => { + mockLambdaClient.user.updateFullName.mutate.mockResolvedValueOnce({ success: true }); + + await userService.updateFullName('John Doe'); + + expect(mockLambdaClient.user.updateFullName.mutate).toHaveBeenCalledWith('John Doe'); + }); + }); + + describe('updatePreference', () => { + it('should call lambdaClient.user.updatePreference.mutate with preference object', async () => { + const preference = { hideSyncAlert: true }; + mockLambdaClient.user.updatePreference.mutate.mockResolvedValueOnce({ success: true }); + + await userService.updatePreference(preference); + + expect(mockLambdaClient.user.updatePreference.mutate).toHaveBeenCalledWith(preference); + }); + }); + + describe('updateGuide', () => { + it('should call lambdaClient.user.updateGuide.mutate with guide object', async () => { + const guide = { moveSettingsToAvatar: true }; + mockLambdaClient.user.updateGuide.mutate.mockResolvedValueOnce({ success: true }); + + await userService.updateGuide(guide); + + expect(mockLambdaClient.user.updateGuide.mutate).toHaveBeenCalledWith(guide); + }); + }); + + describe('updateUserSettings', () => { + it('should call lambdaClient.user.updateSettings.mutate with settings', async () => { + const settings = { general: { fontSize: 14 } }; + mockLambdaClient.user.updateSettings.mutate.mockResolvedValueOnce({ success: true }); + + await userService.updateUserSettings(settings); + + expect(mockLambdaClient.user.updateSettings.mutate).toHaveBeenCalledWith(settings, { + signal: undefined, + }); + }); + + it('should pass abort signal when provided', async () => { + const settings = { general: { fontSize: 16 } }; + const abortController = new AbortController(); + mockLambdaClient.user.updateSettings.mutate.mockResolvedValueOnce({ success: true }); + + await userService.updateUserSettings(settings, abortController.signal); + + expect(mockLambdaClient.user.updateSettings.mutate).toHaveBeenCalledWith(settings, { + signal: abortController.signal, + }); + }); + }); + + describe('resetUserSettings', () => { + it('should call lambdaClient.user.resetSettings.mutate', async () => { + mockLambdaClient.user.resetSettings.mutate.mockResolvedValueOnce({ success: true }); + + await userService.resetUserSettings(); + + expect(mockLambdaClient.user.resetSettings.mutate).toHaveBeenCalled(); + }); + }); }); diff --git a/src/services/user/index.ts b/src/services/user/index.ts index 6599905aa1..3af0a22fda 100644 --- a/src/services/user/index.ts +++ b/src/services/user/index.ts @@ -1,8 +1,7 @@ -import type { AdapterAccount } from 'next-auth/adapters'; import type { PartialDeep } from 'type-fest'; import { lambdaClient } from '@/libs/trpc/client'; -import { UserGuide, UserInitializationState, UserPreference } from '@/types/user'; +import { SSOProvider, UserGuide, UserInitializationState, UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; export class UserService { @@ -18,7 +17,7 @@ export class UserService { return lambdaClient.user.getUserState.query(); }; - getUserSSOProviders = async (): Promise => { + getUserSSOProviders = async (): Promise => { return lambdaClient.user.getUserSSOProviders.query(); }; @@ -34,6 +33,10 @@ export class UserService { return lambdaClient.user.updateAvatar.mutate(avatar); }; + updateFullName = async (fullName: string) => { + return lambdaClient.user.updateFullName.mutate(fullName); + }; + updatePreference = async (preference: Partial) => { return lambdaClient.user.updatePreference.mutate(preference); }; diff --git a/src/store/user/slices/auth/action.test.ts b/src/store/user/slices/auth/action.test.ts index f9e72e5dce..fc9b5bee42 100644 --- a/src/store/user/slices/auth/action.test.ts +++ b/src/store/user/slices/auth/action.test.ts @@ -14,26 +14,60 @@ vi.mock('swr', async (importOriginal) => { }; }); -// 定义一个变量来存储 enableAuth 的值 -let enableClerk = false; +// Use vi.hoisted to ensure variables exist before vi.mock factory executes +const { enableClerk, enableNextAuth, enableBetterAuth, enableAuth } = vi.hoisted(() => ({ + enableClerk: { value: false }, + enableNextAuth: { value: false }, + enableBetterAuth: { value: false }, + enableAuth: { value: true }, +})); -let enableNextAuth = false; - -// 模拟 @/const/auth 模块 vi.mock('@/const/auth', () => ({ get enableClerk() { - return enableClerk; + return enableClerk.value; }, get enableNextAuth() { - return enableNextAuth; + return enableNextAuth.value; + }, + get enableBetterAuth() { + return enableBetterAuth.value; + }, + get enableAuth() { + return enableAuth.value; }, })); +const mockUserService = vi.hoisted(() => ({ + getUserSSOProviders: vi.fn().mockResolvedValue([]), +})); + +vi.mock('@/services/user', () => ({ + userService: mockUserService, +})); + +const mockBetterAuthClient = vi.hoisted(() => ({ + listAccounts: vi.fn().mockResolvedValue({ data: [] }), + accountInfo: vi.fn().mockResolvedValue({ data: { user: {} } }), + signOut: vi.fn().mockResolvedValue({}), +})); + +vi.mock('@/libs/better-auth/auth-client', () => mockBetterAuthClient); + afterEach(() => { vi.restoreAllMocks(); + vi.clearAllMocks(); - enableNextAuth = false; - enableClerk = false; + enableNextAuth.value = false; + enableClerk.value = false; + enableBetterAuth.value = false; + enableAuth.value = true; + + // Reset store state + useUserStore.setState({ + isLoadedAuthProviders: false, + authProviders: [], + isEmailPasswordAuth: false, + }); }); /** @@ -61,7 +95,7 @@ describe('createAuthSlice', () => { describe('logout', () => { it('should call clerkSignOut when Clerk is enabled', async () => { - enableClerk = true; + enableClerk.value = true; const clerkSignOutMock = vi.fn(); useUserStore.setState({ clerkSignOut: clerkSignOutMock }); @@ -89,7 +123,7 @@ describe('createAuthSlice', () => { }); it('should call next-auth signOut when NextAuth is enabled', async () => { - enableNextAuth = true; + enableNextAuth.value = true; const { result } = renderHook(() => useUserStore()); @@ -100,7 +134,7 @@ describe('createAuthSlice', () => { const { signOut } = await import('next-auth/react'); expect(signOut).toHaveBeenCalled(); - enableNextAuth = false; + enableNextAuth.value = false; }); it('should not call next-auth signOut when NextAuth is disabled', async () => { @@ -118,7 +152,7 @@ describe('createAuthSlice', () => { describe('openLogin', () => { it('should call clerkSignIn when Clerk is enabled', async () => { - enableClerk = true; + enableClerk.value = true; const clerkSignInMock = vi.fn(); useUserStore.setState({ clerkSignIn: clerkSignInMock }); @@ -144,7 +178,7 @@ describe('createAuthSlice', () => { }); it('should call next-auth signIn when NextAuth is enabled', async () => { - enableNextAuth = true; + enableNextAuth.value = true; const { result } = renderHook(() => useUserStore()); @@ -155,7 +189,7 @@ describe('createAuthSlice', () => { const { signIn } = await import('next-auth/react'); expect(signIn).toHaveBeenCalled(); - enableNextAuth = false; + enableNextAuth.value = false; }); it('should not call next-auth signIn when NextAuth is disabled', async () => { const { result } = renderHook(() => useUserStore()); @@ -168,5 +202,168 @@ describe('createAuthSlice', () => { expect(signIn).not.toHaveBeenCalled(); }); + + it('should redirect to signin page when BetterAuth is enabled', async () => { + enableBetterAuth.value = true; + + const originalLocation = window.location; + Object.defineProperty(window, 'location', { + configurable: true, + value: { ...originalLocation, href: '', toString: () => 'http://localhost/chat' }, + writable: true, + }); + + const { result } = renderHook(() => useUserStore()); + + await act(async () => { + await result.current.openLogin(); + }); + + expect(window.location.href).toContain('/signin'); + expect(window.location.href).toContain('callbackUrl'); + + Object.defineProperty(window, 'location', { + configurable: true, + value: originalLocation, + writable: true, + }); + }); + + it('should call signIn with single provider when only one OAuth provider available', async () => { + enableNextAuth.value = true; + useUserStore.setState({ oAuthSSOProviders: ['github'] }); + + const { result } = renderHook(() => useUserStore()); + + await act(async () => { + await result.current.openLogin(); + }); + + const { signIn } = await import('next-auth/react'); + + expect(signIn).toHaveBeenCalledWith('github'); + }); + }); + + describe('enableAuth', () => { + it('should return true when auth is enabled', () => { + enableAuth.value = true; + const { result } = renderHook(() => useUserStore()); + + expect(result.current.enableAuth()).toBe(true); + }); + + it('should return false when auth is disabled', () => { + enableAuth.value = false; + const { result } = renderHook(() => useUserStore()); + + expect(result.current.enableAuth()).toBe(false); + }); + }); + + describe('fetchAuthProviders', () => { + it('should skip fetching if already loaded', async () => { + useUserStore.setState({ isLoadedAuthProviders: true }); + + const { result } = renderHook(() => useUserStore()); + + await act(async () => { + await result.current.fetchAuthProviders(); + }); + + expect(mockUserService.getUserSSOProviders).not.toHaveBeenCalled(); + }); + + it('should fetch providers from NextAuth when BetterAuth is disabled', async () => { + enableBetterAuth.value = false; + const mockProviders = [ + { provider: 'github', email: 'test@example.com', providerAccountId: '123' }, + ]; + mockUserService.getUserSSOProviders.mockResolvedValueOnce(mockProviders); + + const { result } = renderHook(() => useUserStore()); + + await act(async () => { + await result.current.fetchAuthProviders(); + }); + + expect(mockUserService.getUserSSOProviders).toHaveBeenCalled(); + expect(result.current.isLoadedAuthProviders).toBe(true); + expect(result.current.authProviders).toEqual(mockProviders); + }); + + it('should fetch providers from BetterAuth when enabled', async () => { + enableBetterAuth.value = true; + mockBetterAuthClient.listAccounts.mockResolvedValueOnce({ + data: [ + { providerId: 'github', accountId: 'gh-123' }, + { providerId: 'credential', accountId: 'cred-1' }, + ], + }); + mockBetterAuthClient.accountInfo.mockResolvedValueOnce({ + data: { user: { email: 'test@github.com' } }, + }); + + const { result } = renderHook(() => useUserStore()); + + await act(async () => { + await result.current.fetchAuthProviders(); + }); + + expect(mockBetterAuthClient.listAccounts).toHaveBeenCalled(); + expect(result.current.isLoadedAuthProviders).toBe(true); + expect(result.current.isEmailPasswordAuth).toBe(true); + }); + + it('should handle fetch error gracefully', async () => { + enableBetterAuth.value = false; + mockUserService.getUserSSOProviders.mockRejectedValueOnce(new Error('Network error')); + + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { result } = renderHook(() => useUserStore()); + + await act(async () => { + await result.current.fetchAuthProviders(); + }); + + expect(result.current.isLoadedAuthProviders).toBe(true); + consoleSpy.mockRestore(); + }); + }); + + describe('refreshAuthProviders', () => { + it('should refresh providers from NextAuth', async () => { + enableBetterAuth.value = false; + const mockProviders = [ + { provider: 'google', email: 'user@gmail.com', providerAccountId: 'g-1' }, + ]; + mockUserService.getUserSSOProviders.mockResolvedValueOnce(mockProviders); + + const { result } = renderHook(() => useUserStore()); + + await act(async () => { + await result.current.refreshAuthProviders(); + }); + + expect(mockUserService.getUserSSOProviders).toHaveBeenCalled(); + expect(result.current.authProviders).toEqual(mockProviders); + }); + + it('should handle refresh error gracefully', async () => { + enableBetterAuth.value = false; + mockUserService.getUserSSOProviders.mockRejectedValueOnce(new Error('Refresh failed')); + + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { result } = renderHook(() => useUserStore()); + + await act(async () => { + await result.current.refreshAuthProviders(); + }); + + // Should not throw + consoleSpy.mockRestore(); + }); }); }); diff --git a/src/store/user/slices/auth/action.ts b/src/store/user/slices/auth/action.ts index 7f939adce3..c50c561d49 100644 --- a/src/store/user/slices/auth/action.ts +++ b/src/store/user/slices/auth/action.ts @@ -1,11 +1,22 @@ +import { SSOProvider } from '@lobechat/types'; import { StateCreator } from 'zustand/vanilla'; -import { enableAuth, enableClerk, enableNextAuth } from '@/const/auth'; +import { enableAuth, enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth'; +import { userService } from '@/services/user'; import type { UserStore } from '../../store'; +interface AuthProvidersData { + isEmailPasswordAuth: boolean; + providers: SSOProvider[]; +} + export interface UserAuthAction { enableAuth: () => boolean; + /** + * Fetch auth providers (SSO accounts) for the current user + */ + fetchAuthProviders: () => Promise; /** * universal logout method */ @@ -14,8 +25,40 @@ export interface UserAuthAction { * universal login method */ openLogin: () => Promise; + /** + * Refresh auth providers after link/unlink + */ + refreshAuthProviders: () => Promise; } +const fetchAuthProvidersData = async (): Promise => { + if (enableBetterAuth) { + const { accountInfo, listAccounts } = await import('@/libs/better-auth/auth-client'); + const result = await listAccounts(); + const accounts = result.data || []; + const isEmailPasswordAuth = accounts.some((account) => account.providerId === 'credential'); + const providers = await Promise.all( + accounts + .filter((account) => account.providerId !== 'credential') + .map(async (account) => { + const info = await accountInfo({ + query: { accountId: account.accountId }, + }); + return { + email: info.data?.user?.email ?? undefined, + provider: account.providerId, + providerAccountId: account.accountId, + }; + }), + ); + return { isEmailPasswordAuth, providers }; + } + + // Fallback for NextAuth + const providers = await userService.getUserSSOProviders(); + return { isEmailPasswordAuth: false, providers }; +}; + export const createAuthSlice: StateCreator< UserStore, [['zustand/devtools', never]], @@ -25,6 +68,18 @@ export const createAuthSlice: StateCreator< enableAuth: () => { return enableAuth; }, + fetchAuthProviders: async () => { + // Skip if already loaded + if (get().isLoadedAuthProviders) return; + + try { + const { isEmailPasswordAuth, providers } = await fetchAuthProvidersData(); + set({ authProviders: providers, isEmailPasswordAuth, isLoadedAuthProviders: true }); + } catch (error) { + console.error('Failed to fetch auth providers:', error); + set({ isLoadedAuthProviders: true }); + } + }, logout: async () => { if (enableClerk) { get().clerkSignOut?.({ redirectUrl: location.toString() }); @@ -32,6 +87,21 @@ export const createAuthSlice: StateCreator< return; } + if (enableBetterAuth) { + const { signOut } = await import('@/libs/better-auth/auth-client'); + await signOut({ + fetchOptions: { + onSuccess: () => { + // Use window.location.href to trigger a full page reload + // This ensures all client-side state (React, Zustand, cache) is cleared + window.location.href = '/signin'; + }, + }, + }); + + return; + } + if (enableNextAuth) { const { signOut } = await import('next-auth/react'); signOut(); @@ -49,6 +119,13 @@ export const createAuthSlice: StateCreator< return; } + if (enableBetterAuth) { + const currentUrl = location.toString(); + window.location.href = `/signin?callbackUrl=${encodeURIComponent(currentUrl)}`; + + return; + } + if (enableNextAuth) { const { signIn } = await import('next-auth/react'); // Check if only one provider is available @@ -60,4 +137,12 @@ export const createAuthSlice: StateCreator< signIn(); } }, + refreshAuthProviders: async () => { + try { + const { isEmailPasswordAuth, providers } = await fetchAuthProvidersData(); + set({ authProviders: providers, isEmailPasswordAuth }); + } catch (error) { + console.error('Failed to refresh auth providers:', error); + } + }, }); diff --git a/src/store/user/slices/auth/initialState.ts b/src/store/user/slices/auth/initialState.ts index b34462e51d..1e0df69c9d 100644 --- a/src/store/user/slices/auth/initialState.ts +++ b/src/store/user/slices/auth/initialState.ts @@ -6,17 +6,25 @@ import { UserProfileProps, UserResource, } from '@clerk/types'; +import { SSOProvider } from '@lobechat/types'; +import { enableClerk } from '@/const/auth'; import { LobeUser } from '@/types/user'; export interface UserAuthState { + authProviders?: SSOProvider[]; clerkOpenUserProfile?: (props?: UserProfileProps) => void; - clerkSession?: SignedInSessionResource; + clerkSignIn?: (props?: SignInProps) => void; clerkSignOut?: SignOut; clerkUser?: UserResource; + /** + * Whether user registered with email/password (credential login) + */ + isEmailPasswordAuth?: boolean; isLoaded?: boolean; + isLoadedAuthProviders?: boolean; isSignedIn?: boolean; nextSession?: Session; @@ -25,4 +33,7 @@ export interface UserAuthState { user?: LobeUser; } -export const initialAuthState: UserAuthState = {}; +export const initialAuthState: UserAuthState = { + // Clerk doesn't need to fetch auth providers + isLoadedAuthProviders: enableClerk ? true : undefined, +}; diff --git a/src/store/user/slices/auth/selectors.ts b/src/store/user/slices/auth/selectors.ts index 2227208d64..d24907751e 100644 --- a/src/store/user/slices/auth/selectors.ts +++ b/src/store/user/slices/auth/selectors.ts @@ -1,8 +1,8 @@ import { BRANDING_NAME, isDesktop } from '@lobechat/const'; -import { LobeUser } from '@lobechat/types'; +import { LobeUser, SSOProvider } from '@lobechat/types'; import { t } from 'i18next'; -import { enableAuth, enableClerk, enableNextAuth } from '@/const/auth'; +import { enableAuth, enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth'; import type { UserStore } from '@/store/user'; const DEFAULT_USERNAME = BRANDING_NAME; @@ -56,9 +56,13 @@ const isLogin = (s: UserStore) => { }; export const authSelectors = { + authProviders: (s: UserStore): SSOProvider[] => s.authProviders || [], + isEmailPasswordAuth: (s: UserStore) => s.isEmailPasswordAuth ?? false, isLoaded: (s: UserStore) => s.isLoaded, + isLoadedAuthProviders: (s: UserStore) => s.isLoadedAuthProviders ?? false, isLogin, isLoginWithAuth: (s: UserStore) => s.isSignedIn, + isLoginWithBetterAuth: (s: UserStore): boolean => (s.isSignedIn && enableBetterAuth) || false, isLoginWithClerk: (s: UserStore): boolean => (s.isSignedIn && enableClerk) || false, isLoginWithNextAuth: (s: UserStore): boolean => (s.isSignedIn && !!enableNextAuth) || false, }; diff --git a/src/store/user/slices/common/action.ts b/src/store/user/slices/common/action.ts index bc0148e526..4ff103b417 100644 --- a/src/store/user/slices/common/action.ts +++ b/src/store/user/slices/common/action.ts @@ -24,6 +24,7 @@ const GET_USER_STATE_KEY = 'initUserState'; export interface CommonAction { refreshUserState: () => Promise; updateAvatar: (avatar: string) => Promise; + updateFullName: (fullName: string) => Promise; updateKeyVaultConfig: (provider: string, config: any) => Promise; useCheckTrace: (shouldFetch: boolean) => SWRResponse; useInitUserState: ( @@ -46,9 +47,12 @@ export const createCommonSlice: StateCreator< await mutate(GET_USER_STATE_KEY); }, updateAvatar: async (avatar) => { - // 1. 更新服务端/数据库中的头像 await userService.updateAvatar(avatar); + await get().refreshUserState(); + }, + updateFullName: async (fullName) => { + await userService.updateFullName(fullName); await get().refreshUserState(); },