From 116255f076a3e9d92b0c9a85303daae73997b55e Mon Sep 17 00:00:00 2001 From: Seefs <40468931+seefs001@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:56:06 +0800 Subject: [PATCH] fix(oauth): align custom binding response fields in frontend (#6818) * fix(oauth): align custom binding response fields in frontend * fix(oauth): restore custom access policy guidance --- web/src/features/profile/api.ts | 9 +-- .../components/tabs/account-bindings-tab.tsx | 18 ++--- .../components/access-policy-templates.ts | 40 ++++++++++ .../components/provider-form-dialog.tsx | 78 ++++++++++++++++++- web/src/features/users/api.ts | 12 +-- .../dialogs/user-binding-dialog.tsx | 34 ++++---- web/src/i18n/locales/en.json | 8 ++ web/src/i18n/locales/fr.json | 8 ++ web/src/i18n/locales/ja.json | 8 ++ web/src/i18n/locales/ru.json | 8 ++ web/src/i18n/locales/vi.json | 8 ++ web/src/i18n/locales/zh-TW.json | 8 ++ web/src/i18n/locales/zh.json | 8 ++ web/src/lib/oauth.ts | 14 ++++ 14 files changed, 216 insertions(+), 45 deletions(-) create mode 100644 web/src/features/system-settings/auth/custom-oauth/components/access-policy-templates.ts diff --git a/web/src/features/profile/api.ts b/web/src/features/profile/api.ts index 9a603092..1d112ab4 100644 --- a/web/src/features/profile/api.ts +++ b/web/src/features/profile/api.ts @@ -17,6 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { api } from '@/lib/api' +import type { CustomOAuthBinding } from '@/lib/oauth' import type { LoginSession } from '@/stores/auth-store' import type { @@ -172,12 +173,6 @@ export async function revokeOtherLoginSessions(): Promise { // Custom OAuth Binding APIs // ============================================================================ -export interface CustomOAuthBinding { - provider_id: string - provider_name: string - external_id?: string -} - /** * Get current user's custom OAuth bindings */ @@ -192,7 +187,7 @@ export async function getSelfOAuthBindings(): Promise< * Unbind a custom OAuth provider for current user */ export async function unbindCustomOAuth( - providerId: string + providerId: number ): Promise { const res = await api.delete(`/api/user/oauth/bindings/${providerId}`) return res.data diff --git a/web/src/features/profile/components/tabs/account-bindings-tab.tsx b/web/src/features/profile/components/tabs/account-bindings-tab.tsx index 188f0cde..a8e3cf53 100644 --- a/web/src/features/profile/components/tabs/account-bindings-tab.tsx +++ b/web/src/features/profile/components/tabs/account-bindings-tab.tsx @@ -44,15 +44,13 @@ import { api } from '@/lib/api' import { buildDiscordOAuthUrl, buildGitHubOAuthUrl, + indexCustomOAuthBindings, buildLinuxDOOAuthUrl, buildOIDCOAuthUrl, + type CustomOAuthBinding, } from '@/lib/oauth' -import { - getSelfOAuthBindings, - unbindCustomOAuth, - type CustomOAuthBinding, -} from '../../api' +import { getSelfOAuthBindings, unbindCustomOAuth } from '../../api' import type { UserProfile, BindingItem } from '../../types' import { EmailBindDialog } from '../dialogs/email-bind-dialog' import { TelegramBindDialog } from '../dialogs/telegram-bind-dialog' @@ -112,6 +110,10 @@ export function AccountBindingsTab({ const customProviders = status?.custom_oauth_providers as | CustomOAuthProviderInfo[] | undefined + const customBindingsByProviderId = useMemo( + () => indexCustomOAuthBindings(customBindings), + [customBindings] + ) const fetchCustomBindings = useCallback(async () => { if (!customProviders || customProviders.length === 0) return @@ -474,9 +476,7 @@ export function AccountBindingsTab({

{customProviders.map((provider) => { - const binding = customBindings.find( - (b) => b.provider_id === String(provider.id) - ) + const binding = customBindingsByProviderId.get(provider.id) const isBound = !!binding return (

{isBound - ? binding?.external_id || t('Bound') + ? binding?.provider_user_id || t('Bound') : t('Not bound')}

diff --git a/web/src/features/system-settings/auth/custom-oauth/components/access-policy-templates.ts b/web/src/features/system-settings/auth/custom-oauth/components/access-policy-templates.ts new file mode 100644 index 00000000..0f19f63b --- /dev/null +++ b/web/src/features/system-settings/auth/custom-oauth/components/access-policy-templates.ts @@ -0,0 +1,40 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +export const ACCESS_POLICY_TEMPLATES = { + levelAndActive: `{ + "logic": "and", + "conditions": [ + { "field": "trust_level", "op": "gte", "value": 2 }, + { "field": "active", "op": "eq", "value": true } + ] +}`, + orgOrRole: `{ + "logic": "or", + "conditions": [ + { "field": "org", "op": "eq", "value": "core" }, + { "field": "roles", "op": "contains", "value": "admin" } + ] +}`, +} as const + +export const ACCESS_DENIED_MESSAGE_TEMPLATES = { + level: + 'Requires level {{required}}; your current level is {{current}} (field: {{field}}).', + org: 'Access is limited to approved organizations or roles. Organization: {{current.org}}; roles: {{current.roles}}.', +} as const diff --git a/web/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx b/web/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx index c83266f4..acafc1b8 100644 --- a/web/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx +++ b/web/src/features/system-settings/auth/custom-oauth/components/provider-form-dialog.tsx @@ -63,6 +63,10 @@ import { type CustomOAuthProvider, type CustomOAuthFormValues, } from '../types' +import { + ACCESS_DENIED_MESSAGE_TEMPLATES, + ACCESS_POLICY_TEMPLATES, +} from './access-policy-templates' import { DiscoveryButton } from './discovery-button' import { PresetSelector } from './preset-selector' @@ -603,6 +607,11 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { render={({ field }) => ( {t('Access Policy (JSON)')} + + {t( + 'Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.' + )} + {t( - 'JSON-based access control rules. Leave empty to allow all users.' + 'Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.' )} +
+ + +
)} @@ -635,11 +674,46 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) { + + {t( + 'Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.' + )} + +
+ + +
)} diff --git a/web/src/features/users/api.ts b/web/src/features/users/api.ts index f3f2ba91..1d1d16ad 100644 --- a/web/src/features/users/api.ts +++ b/web/src/features/users/api.ts @@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import type { PermissionCatalog } from '@/lib/admin-permissions' import { api } from '@/lib/api' +import type { CustomOAuthBinding } from '@/lib/oauth' import type { User, @@ -178,19 +179,12 @@ export async function getPermissionCatalog(): Promise { // Admin Binding Management APIs // ============================================================================ -export interface OAuthBinding { - provider_id: string - provider_name: string - user_id?: number - external_id?: string -} - /** * Get user's custom OAuth bindings (admin) */ export async function getUserOAuthBindings( userId: number -): Promise> { +): Promise> { const res = await api.get(`/api/user/${userId}/oauth/bindings`) return res.data } @@ -211,7 +205,7 @@ export async function adminClearUserBinding( */ export async function adminUnbindCustomOAuth( userId: number, - providerId: string + providerId: number ): Promise { const res = await api.delete( `/api/user/${userId}/oauth/bindings/${providerId}` diff --git a/web/src/features/users/components/dialogs/user-binding-dialog.tsx b/web/src/features/users/components/dialogs/user-binding-dialog.tsx index 6db44645..c4a56c22 100644 --- a/web/src/features/users/components/dialogs/user-binding-dialog.tsx +++ b/web/src/features/users/components/dialogs/user-binding-dialog.tsx @@ -45,13 +45,13 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { api } from '@/lib/api' +import { indexCustomOAuthBindings, type CustomOAuthBinding } from '@/lib/oauth' import { getUser, getUserOAuthBindings, adminClearUserBinding, adminUnbindCustomOAuth, - type OAuthBinding, } from '../../api' import type { User } from '../../types' @@ -68,7 +68,7 @@ interface BindingItem { icon: React.ReactNode value: string type: 'builtin' | 'custom' - providerId?: string + providerId?: number isBound: boolean isEnabled: boolean } @@ -81,7 +81,7 @@ interface StatusInfo { telegram_oauth?: boolean linuxdo_oauth?: boolean custom_oauth_providers?: Array<{ - id: string + id: number name: string icon?: string }> @@ -162,7 +162,7 @@ function CustomProviderIcon(props: { iconUrl?: string }) { export function UserBindingDialog(props: Props) { const { t } = useTranslation() const [user, setUser] = useState(null) - const [oauthBindings, setOauthBindings] = useState([]) + const [oauthBindings, setOauthBindings] = useState([]) const [statusInfo, setStatusInfo] = useState({}) const [loading, setLoading] = useState(false) const [showBoundOnly, setShowBoundOnly] = useState(true) @@ -191,7 +191,7 @@ export function UserBindingDialog(props: Props) { setUser(userRes.data) } if (oauthRes.success && oauthRes.data) { - setOauthBindings(oauthRes.data as OAuthBinding[]) + setOauthBindings(oauthRes.data) } if (statusRes.success && statusRes.data) { setStatusInfo(statusRes.data as StatusInfo) @@ -236,37 +236,35 @@ export function UserBindingDialog(props: Props) { }) } - const oauthBindingMap = new Map( - oauthBindings.map((b) => [String(b.provider_id), b]) - ) + const oauthBindingMap = indexCustomOAuthBindings(oauthBindings) const customProviders = statusInfo.custom_oauth_providers || [] - const seenProviderIds = new Set() + const seenProviderIds = new Set() for (const provider of customProviders) { - seenProviderIds.add(String(provider.id)) - const binding = oauthBindingMap.get(String(provider.id)) + seenProviderIds.add(provider.id) + const binding = oauthBindingMap.get(provider.id) items.push({ key: `oauth_${provider.id}`, - label: provider.name || provider.id, + label: provider.name || String(provider.id), icon: , - value: binding?.external_id || '', + value: binding?.provider_user_id || '', type: 'custom', - providerId: String(provider.id), + providerId: provider.id, isBound: !!binding, isEnabled: true, }) } for (const binding of oauthBindings) { - if (!seenProviderIds.has(String(binding.provider_id))) { + if (!seenProviderIds.has(binding.provider_id)) { items.push({ key: `oauth_${binding.provider_id}`, - label: binding.provider_name || binding.provider_id, + label: binding.provider_name || String(binding.provider_id), icon: , - value: binding.external_id || '-', + value: binding.provider_user_id || '-', type: 'custom', - providerId: String(binding.provider_id), + providerId: binding.provider_id, isBound: true, isEnabled: false, }) diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index cbac6b61..b3fec138 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -536,6 +536,7 @@ "Available Models": "Available Models", "Available reset credits": "Available reset credits", "Available Rewards": "Available Rewards", + "Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.": "Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.", "Average latency": "Average latency", "Average latency, TTFT, and success rate by group": "Average latency, TTFT, and success rate by group", "Average latency, TTFT, TPS, and success rate": "Average latency, TTFT, TPS, and success rate", @@ -1487,6 +1488,7 @@ "e.g. my-gitlab": "e.g. my-gitlab", "e.g. New API Console": "e.g. New API Console", "e.g. openid profile email": "e.g. openid profile email", + "e.g. Requires level {{required}}; your current level is {{current}}": "e.g. Requires level {{required}}; your current level is {{current}}", "e.g. Suitable for light usage": "e.g. Suitable for light usage", "e.g. This request does not meet access policy": "e.g. This request does not meet access policy", "e.g., 0.95": "e.g., 0.95", @@ -1736,6 +1738,7 @@ "Error Type (optional)": "Error Type (optional)", "Estimated cost": "Estimated cost", "Estimated quota cost": "Estimated quota cost", + "Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.": "Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.", "Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.": "Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.", "Every other device will lose access immediately. This device will remain signed in.": "Every other device will lose access immediately. This device will remain signed in.", "Everything configured for this group, in one place.": "Everything configured for this group, in one place.", @@ -1975,6 +1978,10 @@ "Fill in the following info to create a new subscription plan": "Fill in the following info to create a new subscription plan", "Fill Related Models": "Fill Related Models", "Fill Template": "Fill Template", + "Fill template: level and active": "Fill template: level and active", + "Fill template: level message": "Fill template: level message", + "Fill template: organization message": "Fill template: organization message", + "Fill template: organization or role": "Fill template: organization or role", "Fill Templates": "Fill Templates", "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.", "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format", @@ -4396,6 +4403,7 @@ "Supported Applications": "Supported Applications", "Supported Imagine Models": "Supported Imagine Models", "Supported modalities": "Supported modalities", + "Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.": "Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.", "Supported parameters": "Supported parameters", "Supported variables": "Supported variables", "Supports `-thinking`, `-thinking-": "Supports `-thinking`, `-thinking-", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index fefe91d5..e5528b77 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -536,6 +536,7 @@ "Available Models": "Modèles disponibles", "Available reset credits": "Crédits de réinitialisation disponibles", "Available Rewards": "Récompenses disponibles", + "Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.": "Variables disponibles : {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, ainsi que des chemins comme {{current.roles}}.", "Average latency": "Latence moyenne", "Average latency, TTFT, and success rate by group": "Latence moyenne, TTFT et taux de réussite par groupe", "Average latency, TTFT, TPS, and success rate": "Latence moyenne, TTFT, TPS et taux de réussite", @@ -1487,6 +1488,7 @@ "e.g. my-gitlab": "par ex. mon-gitlab", "e.g. New API Console": "par ex. console New API", "e.g. openid profile email": "par ex. openid profile email", + "e.g. Requires level {{required}}; your current level is {{current}}": "ex. Niveau {{required}} requis ; votre niveau actuel est {{current}}", "e.g. Suitable for light usage": "ex. Adapté à une utilisation légère", "e.g. This request does not meet access policy": "ex. Cette requête ne satisfait pas la politique d'accès", "e.g., 0.95": "par ex., 0.95", @@ -1736,6 +1738,7 @@ "Error Type (optional)": "Type d'erreur (optionnel)", "Estimated cost": "Coût estimé", "Estimated quota cost": "Coût de quota estimé", + "Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.": "Évalue les champs de la réponse d'informations utilisateur du fournisseur. Les conditions et groupes imbriqués utilisent la logique and/or.", "Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.": "Chaque nom de groupe du tableau tarifaire peut être utilisé à deux endroits : sur un utilisateur (groupe d’utilisateurs, attribué par les admins) et sur un jeton (groupe de jetons, choisi à la création du jeton). Même ensemble de noms, deux rôles différents.", "Every other device will lose access immediately. This device will remain signed in.": "Tous les autres appareils perdront immédiatement l’accès. Cet appareil restera connecté.", "Everything configured for this group, in one place.": "Toute la configuration de ce groupe, au même endroit.", @@ -1975,6 +1978,10 @@ "Fill in the following info to create a new subscription plan": "Remplissez les informations suivantes pour créer un nouveau plan d'abonnement", "Fill Related Models": "Remplir les modèles associés", "Fill Template": "Remplir le modèle", + "Fill template: level and active": "Insérer le modèle : niveau et état actif", + "Fill template: level message": "Insérer le modèle : message de niveau", + "Fill template: organization message": "Insérer le modèle : message d'organisation", + "Fill template: organization or role": "Insérer le modèle : organisation ou rôle", "Fill Templates": "Remplir les modèles", "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "Saisissez la valeur model complète du corps de requête client, par exemple gpt-4o ou gemini-2.5-flash. Séparez plusieurs modèles par des virgules.", "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "Remplit thoughtSignature uniquement pour les canaux Gemini/Vertex utilisant le format OpenAI", @@ -4396,6 +4403,7 @@ "Supported Applications": "Applications prises en charge", "Supported Imagine Models": "Modèles Imagine pris en charge", "Supported modalities": "Modalités prises en charge", + "Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.": "Opérateurs pris en charge : eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Laissez vide pour autoriser tous les utilisateurs.", "Supported parameters": "Paramètres pris en charge", "Supported variables": "Variables supportées", "Supports `-thinking`, `-thinking-": "Prend en charge `-thinking`, `-thinking-", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index d1b56e3e..dc72689c 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -536,6 +536,7 @@ "Available Models": "利用可能なモデル", "Available reset credits": "利用可能なリセット回数", "Available Rewards": "利用可能な報酬", + "Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.": "使用可能な変数:{{provider}}、{{field}}、{{op}}、{{required}}、{{current}}、および {{current.roles}} のようなパス。", "Average latency": "平均レイテンシ", "Average latency, TTFT, and success rate by group": "グループ別の平均レイテンシ、TTFT、成功率", "Average latency, TTFT, TPS, and success rate": "平均レイテンシ、TTFT、TPS、成功率", @@ -1487,6 +1488,7 @@ "e.g. my-gitlab": "例: my-gitlab", "e.g. New API Console": "例: New API コンソール", "e.g. openid profile email": "例: openid profile email", + "e.g. Requires level {{required}}; your current level is {{current}}": "例:レベル {{required}} が必要です。現在のレベルは {{current}} です", "e.g. Suitable for light usage": "例:ライトユーザー向け", "e.g. This request does not meet access policy": "例:このリクエストはアクセスポリシーを満たしていません", "e.g., 0.95": "例: 0.95", @@ -1736,6 +1738,7 @@ "Error Type (optional)": "エラータイプ(任意)", "Estimated cost": "推定コスト", "Estimated quota cost": "想定クォートコスト", + "Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.": "プロバイダーのユーザー情報レスポンスのフィールドを評価します。条件とネストしたグループでは and/or ロジックを使用します。", "Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.": "料金表の各グループ名は2つの場所で使えます。ユーザー側(ユーザーグループ、管理者が割り当て)とトークン側(トークングループ、トークン作成時に選択)です。同じ名前プールで、役割は2つです。", "Every other device will lose access immediately. This device will remain signed in.": "他のすべてのデバイスは直ちにアクセスできなくなります。このデバイスはログイン状態を維持します。", "Everything configured for this group, in one place.": "このグループのすべての設定を一か所で確認できます。", @@ -1975,6 +1978,10 @@ "Fill in the following info to create a new subscription plan": "以下の情報を入力して新しいサブスクリプションプランを作成", "Fill Related Models": "関連モデルを入力", "Fill Template": "テンプレートを入力", + "Fill template: level and active": "テンプレートを入力:レベルと有効状態", + "Fill template: level message": "テンプレートを入力:レベルメッセージ", + "Fill template: organization message": "テンプレートを入力:組織メッセージ", + "Fill template: organization or role": "テンプレートを入力:組織またはロール", "Fill Templates": "テンプレートを入力", "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "クライアントリクエスト本文の完全な model 値を入力します。例: gpt-4o または gemini-2.5-flash。複数のモデルはカンマで区切ります。", "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "OpenAI形式を利用するGemini/VertexチャネルにのみthoughtSignatureを付与します", @@ -4396,6 +4403,7 @@ "Supported Applications": "サポートされているアプリケーション", "Supported Imagine Models": "対応Imagineモデル", "Supported modalities": "サポートされるモダリティ", + "Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.": "対応演算子:eq、ne、gt、gte、lt、lte、in、not_in、contains、not_contains、exists、not_exists。すべてのユーザーを許可する場合は空のままにしてください。", "Supported parameters": "対応パラメータ", "Supported variables": "サポートされる変数", "Supports `-thinking`, `-thinking-": "「-thinking」、「-thinking-」をサポートします", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 895627bb..4ea86070 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -536,6 +536,7 @@ "Available Models": "Доступные модели", "Available reset credits": "Доступные сбросы лимита", "Available Rewards": "Доступные награды", + "Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.": "Доступные переменные: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, а также пути вида {{current.roles}}.", "Average latency": "Средняя задержка", "Average latency, TTFT, and success rate by group": "Средняя задержка, TTFT и доля успешных запросов по группам", "Average latency, TTFT, TPS, and success rate": "Средняя задержка, TTFT, TPS и доля успешных запросов", @@ -1487,6 +1488,7 @@ "e.g. my-gitlab": "например, my-gitlab", "e.g. New API Console": "напр. консоль New API", "e.g. openid profile email": "например, openid profile email", + "e.g. Requires level {{required}}; your current level is {{current}}": "напр. Требуется уровень {{required}}; ваш текущий уровень — {{current}}", "e.g. Suitable for light usage": "напр. Подходит для лёгкого использования", "e.g. This request does not meet access policy": "напр. Этот запрос не соответствует политике доступа", "e.g., 0.95": "напр., 0.95", @@ -1736,6 +1738,7 @@ "Error Type (optional)": "Тип ошибки (необязательно)", "Estimated cost": "Примерная стоимость", "Estimated quota cost": "Ориентир стоимости квоты", + "Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.": "Проверяет поля ответа с данными пользователя от провайдера. Условия и вложенные группы используют логику and/or.", "Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.": "Каждое имя группы из таблицы тарифов используется в двух местах: у пользователя (группа пользователя, назначается администратором) и у токена (группа токена, выбирается при создании). Один набор имён — две разные роли.", "Every other device will lose access immediately. This device will remain signed in.": "Все остальные устройства немедленно потеряют доступ. Это устройство останется в системе.", "Everything configured for this group, in one place.": "Все настройки этой группы в одном месте.", @@ -1975,6 +1978,10 @@ "Fill in the following info to create a new subscription plan": "Заполните следующую информацию для создания нового плана подписки", "Fill Related Models": "Заполнить связанные модели", "Fill Template": "Заполнить шаблон", + "Fill template: level and active": "Заполнить шаблон: уровень и активность", + "Fill template: level message": "Заполнить шаблон: сообщение об уровне", + "Fill template: organization message": "Заполнить шаблон: сообщение об организации", + "Fill template: organization or role": "Заполнить шаблон: организация или роль", "Fill Templates": "Заполнить шаблоны", "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "Укажите полное значение model из тела запроса клиента, например gpt-4o или gemini-2.5-flash. Несколько моделей разделяйте запятыми.", "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "Заполнять thoughtSignature только для каналов Gemini/Vertex, использующих формат OpenAI", @@ -4396,6 +4403,7 @@ "Supported Applications": "Поддерживаемые приложения", "Supported Imagine Models": "Поддерживаемые модели Imagine", "Supported modalities": "Поддерживаемые модальности", + "Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.": "Поддерживаемые операторы: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Оставьте поле пустым, чтобы разрешить доступ всем пользователям.", "Supported parameters": "Поддерживаемые параметры", "Supported variables": "Поддерживаемые переменные", "Supports `-thinking`, `-thinking-": "Поддерживает `-thinking`, `-thinking-", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 73b1b6c7..4f42938a 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -536,6 +536,7 @@ "Available Models": "Mô hình khả dụng", "Available reset credits": "Lượt đặt lại khả dụng", "Available Rewards": "Phần thưởng hiện có", + "Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.": "Các biến khả dụng: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}} và các đường dẫn như {{current.roles}}.", "Average latency": "Độ trễ trung bình", "Average latency, TTFT, and success rate by group": "Độ trễ trung bình, TTFT và tỷ lệ thành công theo nhóm", "Average latency, TTFT, TPS, and success rate": "Độ trễ trung bình, TTFT, TPS và tỷ lệ thành công", @@ -1487,6 +1488,7 @@ "e.g. my-gitlab": "ví dụ: my-gitlab", "e.g. New API Console": "Ví dụ: Bảng điều khiển API mới", "e.g. openid profile email": "ví dụ: openid profile email", + "e.g. Requires level {{required}}; your current level is {{current}}": "ví dụ: Yêu cầu cấp độ {{required}}; cấp độ hiện tại của bạn là {{current}}", "e.g. Suitable for light usage": "ví dụ: Phù hợp cho sử dụng nhẹ", "e.g. This request does not meet access policy": "ví dụ: Yêu cầu này không đáp ứng chính sách truy cập", "e.g., 0.95": "e.g., 0.95", @@ -1736,6 +1738,7 @@ "Error Type (optional)": "Loại lỗi (tùy chọn)", "Estimated cost": "Chi phí ước tính", "Estimated quota cost": "Ước tính chi phí hạn mức", + "Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.": "Đánh giá các trường trong phản hồi thông tin người dùng của nhà cung cấp. Điều kiện và nhóm lồng nhau sử dụng logic and/or.", "Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.": "Mỗi tên nhóm trong bảng định giá có thể dùng ở hai nơi: trên người dùng (nhóm người dùng, do quản trị viên gán) và trên token (nhóm token, chọn khi tạo token). Cùng một bộ tên, hai vai trò khác nhau.", "Every other device will lose access immediately. This device will remain signed in.": "Mọi thiết bị khác sẽ mất quyền truy cập ngay lập tức. Thiết bị này vẫn duy trì đăng nhập.", "Everything configured for this group, in one place.": "Toàn bộ cấu hình của nhóm này, tại một nơi.", @@ -1975,6 +1978,10 @@ "Fill in the following info to create a new subscription plan": "Điền thông tin sau để tạo gói đăng ký mới", "Fill Related Models": "Điền Mô hình Liên quan", "Fill Template": "Điền Mẫu", + "Fill template: level and active": "Điền mẫu: cấp độ và trạng thái hoạt động", + "Fill template: level message": "Điền mẫu: thông báo cấp độ", + "Fill template: organization message": "Điền mẫu: thông báo tổ chức", + "Fill template: organization or role": "Điền mẫu: tổ chức hoặc vai trò", "Fill Templates": "Điền mẫu", "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "Nhập đầy đủ giá trị model trong body yêu cầu của client, ví dụ gpt-4o hoặc gemini-2.5-flash. Ngăn cách nhiều model bằng dấu phẩy.", "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "Điền thoughtSignature chỉ dành cho các kênh Gemini/Vertex sử dụng định dạng OpenAI", @@ -4396,6 +4403,7 @@ "Supported Applications": "Ứng dụng được hỗ trợ", "Supported Imagine Models": "Mô hình Imagine được hỗ trợ", "Supported modalities": "Phương thức hỗ trợ", + "Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.": "Toán tử được hỗ trợ: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Để trống để cho phép tất cả người dùng.", "Supported parameters": "Tham số hỗ trợ", "Supported variables": "Biến được hỗ trợ", "Supports `-thinking`, `-thinking-": "Hỗ trợ `-thinking`, `-thinking-", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index a0fb9d1f..ab6acc0d 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -536,6 +536,7 @@ "Available Models": "可用模型", "Available reset credits": "可用重置次數", "Available Rewards": "可用獎勵", + "Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.": "可用變數:{{provider}}、{{field}}、{{op}}、{{required}}、{{current}},以及 {{current.roles}} 等路徑變數。", "Average latency": "平均延遲", "Average latency, TTFT, and success rate by group": "各分組的平均延遲、首 Token 延遲和成功率", "Average latency, TTFT, TPS, and success rate": "平均延遲、TTFT、TPS 和成功率", @@ -1487,6 +1488,7 @@ "e.g. my-gitlab": "例如:my-gitlab", "e.g. New API Console": "例如,New API 控制台", "e.g. openid profile email": "例如:openid profile email", + "e.g. Requires level {{required}}; your current level is {{current}}": "例如:需要等級 {{required}};你目前的等級是 {{current}}", "e.g. Suitable for light usage": "例如:適合輕度使用", "e.g. This request does not meet access policy": "例如:該請求不滿足准入策略", "e.g., 0.95": "例如,0.95", @@ -1736,6 +1738,7 @@ "Error Type (optional)": "錯誤類型(可選)", "Estimated cost": "預計成本", "Estimated quota cost": "估算配額費用", + "Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.": "根據提供商回傳的用戶資訊欄位執行政策判斷。條件和巢狀分組支援 and/or 邏輯。", "Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.": "定價表中的每個分組名可用在兩個地方:用戶身上(用戶分組,由管理員分配)和令牌身上(令牌分組,建立令牌時選擇)。同一批名字,兩種不同職責。", "Every other device will lose access immediately. This device will remain signed in.": "其他所有裝置將立即失去存取權限,目前裝置將保持登入。", "Everything configured for this group, in one place.": "該分組的全部設定,一處看全。", @@ -1975,6 +1978,10 @@ "Fill in the following info to create a new subscription plan": "填寫以下資訊建立新的訂閱套餐", "Fill Related Models": "填入相關模型", "Fill Template": "填入模板", + "Fill template: level and active": "填入模板:等級和啟用狀態", + "Fill template: level message": "填入模板:等級提示", + "Fill template: organization message": "填入模板:組織提示", + "Fill template: organization or role": "填入模板:組織或角色", "Fill Templates": "填充模板", "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "填寫客戶端請求體裡的完整 model 值,例如 gpt-4o 或 gemini-2.5-flash。多個模型用英文逗號分隔。", "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "僅為使用 OpenAI 格式的 Gemini/Vertex 渠道填充 thoughtSignature", @@ -4396,6 +4403,7 @@ "Supported Applications": "常用套用支援", "Supported Imagine Models": "支援的 Imagine 模型", "Supported modalities": "支援的模態", + "Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.": "支援的操作符:eq、ne、gt、gte、lt、lte、in、not_in、contains、not_contains、exists、not_exists。留空則允許所有用戶。", "Supported parameters": "支援的參數", "Supported variables": "支援變數", "Supports `-thinking`, `-thinking-": "支援 `-thinking`、`-thinking-`", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 9366feb6..848b5b9d 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -536,6 +536,7 @@ "Available Models": "可用模型", "Available reset credits": "可用重置次数", "Available Rewards": "可用奖励", + "Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.": "可用变量:{{provider}}、{{field}}、{{op}}、{{required}}、{{current}},以及 {{current.roles}} 等路径变量。", "Average latency": "平均延迟", "Average latency, TTFT, and success rate by group": "各分组的平均延迟、首 Token 延迟和成功率", "Average latency, TTFT, TPS, and success rate": "平均延迟、TTFT、TPS 和成功率", @@ -1487,6 +1488,7 @@ "e.g. my-gitlab": "例如:my-gitlab", "e.g. New API Console": "例如,New API 控制台", "e.g. openid profile email": "例如:openid profile email", + "e.g. Requires level {{required}}; your current level is {{current}}": "例如:需要等级 {{required}};你当前的等级是 {{current}}", "e.g. Suitable for light usage": "例如:适合轻度使用", "e.g. This request does not meet access policy": "例如:该请求不满足准入策略", "e.g., 0.95": "例如,0.95", @@ -1736,6 +1738,7 @@ "Error Type (optional)": "错误类型(可选)", "Estimated cost": "预计成本", "Estimated quota cost": "估算配额费用", + "Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.": "根据提供商返回的用户信息字段执行策略判断。条件和嵌套分组支持 and/or 逻辑。", "Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.": "定价表中的每个分组名可用在两个地方:用户身上(用户分组,由管理员分配)和令牌身上(令牌分组,创建令牌时选择)。同一批名字,两种不同职责。", "Every other device will lose access immediately. This device will remain signed in.": "其他所有设备将立即失去访问权限,当前设备将保持登录。", "Everything configured for this group, in one place.": "该分组的全部配置,一处看全。", @@ -1975,6 +1978,10 @@ "Fill in the following info to create a new subscription plan": "填写以下信息创建新的订阅套餐", "Fill Related Models": "填入相关模型", "Fill Template": "填入模板", + "Fill template: level and active": "填充模板:等级和激活状态", + "Fill template: level message": "填充模板:等级提示", + "Fill template: organization message": "填充模板:组织提示", + "Fill template: organization or role": "填充模板:组织或角色", "Fill Templates": "填充模板", "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "填写客户端请求体里的完整 model 值,例如 gpt-4o 或 gemini-2.5-flash。多个模型用英文逗号分隔。", "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "仅为使用 OpenAI 格式的 Gemini/Vertex 渠道填充 thoughtSignature", @@ -4396,6 +4403,7 @@ "Supported Applications": "常用应用支持", "Supported Imagine Models": "支持的 Imagine 模型", "Supported modalities": "支持的模态", + "Supported operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, exists, not_exists. Leave empty to allow all users.": "支持的操作符:eq、ne、gt、gte、lt、lte、in、not_in、contains、not_contains、exists、not_exists。留空则允许所有用户。", "Supported parameters": "支持的参数", "Supported variables": "支持变量", "Supports `-thinking`, `-thinking-": "支持 `-thinking`、`-thinking-`", diff --git a/web/src/lib/oauth.ts b/web/src/lib/oauth.ts index 3432d13a..d5f03942 100644 --- a/web/src/lib/oauth.ts +++ b/web/src/lib/oauth.ts @@ -20,6 +20,20 @@ For commercial licensing, please contact support@quantumnous.com // OAuth URL Builders // ============================================================================ +export interface CustomOAuthBinding { + provider_id: number + provider_name: string + provider_slug: string + provider_icon: string + provider_user_id: string +} + +export function indexCustomOAuthBindings( + bindings: CustomOAuthBinding[] +): Map { + return new Map(bindings.map((binding) => [binding.provider_id, binding])) +} + /** * Build GitHub OAuth URL */