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
This commit is contained in:
@@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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<ApiResponse> {
|
||||
// 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<ApiResponse> {
|
||||
const res = await api.delete(`/api/user/oauth/bindings/${providerId}`)
|
||||
return res.data
|
||||
|
||||
@@ -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({
|
||||
</p>
|
||||
<div className='grid grid-cols-1 gap-2.5 sm:grid-cols-2 sm:gap-3'>
|
||||
{customProviders.map((provider) => {
|
||||
const binding = customBindings.find(
|
||||
(b) => b.provider_id === String(provider.id)
|
||||
)
|
||||
const binding = customBindingsByProviderId.get(provider.id)
|
||||
const isBound = !!binding
|
||||
return (
|
||||
<div
|
||||
@@ -500,7 +500,7 @@ export function AccountBindingsTab({
|
||||
</div>
|
||||
<p className='text-muted-foreground truncate text-xs'>
|
||||
{isBound
|
||||
? binding?.external_id || t('Bound')
|
||||
? binding?.provider_user_id || t('Bound')
|
||||
: t('Not bound')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+40
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
+76
-2
@@ -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 }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Access Policy (JSON)')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Evaluate fields from the provider user info response. Conditions and nested groups use and/or logic.'
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<JsonCodeEditor
|
||||
value={field.value || ''}
|
||||
@@ -618,9 +627,39 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{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.'
|
||||
)}
|
||||
</FormDescription>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='xs'
|
||||
onClick={() =>
|
||||
form.setValue(
|
||||
'access_policy',
|
||||
ACCESS_POLICY_TEMPLATES.levelAndActive,
|
||||
{ shouldDirty: true, shouldValidate: true }
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('Fill template: level and active')}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='xs'
|
||||
onClick={() =>
|
||||
form.setValue(
|
||||
'access_policy',
|
||||
ACCESS_POLICY_TEMPLATES.orgOrRole,
|
||||
{ shouldDirty: true, shouldValidate: true }
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('Fill template: organization or role')}
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -635,11 +674,46 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t(
|
||||
'Custom message shown when access is denied'
|
||||
'e.g. Requires level {{required}}; your current level is {{current}}'
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Available variables: {{provider}}, {{field}}, {{op}}, {{required}}, {{current}}, and paths such as {{current.roles}}.'
|
||||
)}
|
||||
</FormDescription>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='xs'
|
||||
onClick={() =>
|
||||
form.setValue(
|
||||
'access_denied_message',
|
||||
ACCESS_DENIED_MESSAGE_TEMPLATES.level,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('Fill template: level message')}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='xs'
|
||||
onClick={() =>
|
||||
form.setValue(
|
||||
'access_denied_message',
|
||||
ACCESS_DENIED_MESSAGE_TEMPLATES.org,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('Fill template: organization message')}
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -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<PermissionCatalog> {
|
||||
// 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<ApiResponse<OAuthBinding[]>> {
|
||||
): Promise<ApiResponse<CustomOAuthBinding[]>> {
|
||||
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<ApiResponse> {
|
||||
const res = await api.delete(
|
||||
`/api/user/${userId}/oauth/bindings/${providerId}`
|
||||
|
||||
@@ -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<User | null>(null)
|
||||
const [oauthBindings, setOauthBindings] = useState<OAuthBinding[]>([])
|
||||
const [oauthBindings, setOauthBindings] = useState<CustomOAuthBinding[]>([])
|
||||
const [statusInfo, setStatusInfo] = useState<StatusInfo>({})
|
||||
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<string>()
|
||||
const seenProviderIds = new Set<number>()
|
||||
|
||||
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: <CustomProviderIcon iconUrl={provider.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: <Link2 className='h-4 w-4' />,
|
||||
value: binding.external_id || '-',
|
||||
value: binding.provider_user_id || '-',
|
||||
type: 'custom',
|
||||
providerId: String(binding.provider_id),
|
||||
providerId: binding.provider_id,
|
||||
isBound: true,
|
||||
isEnabled: false,
|
||||
})
|
||||
|
||||
@@ -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-",
|
||||
|
||||
@@ -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-",
|
||||
|
||||
@@ -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-」をサポートします",
|
||||
|
||||
@@ -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-",
|
||||
|
||||
@@ -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-",
|
||||
|
||||
@@ -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-`",
|
||||
|
||||
@@ -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-`",
|
||||
|
||||
@@ -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<number, CustomOAuthBinding> {
|
||||
return new Map(bindings.map((binding) => [binding.provider_id, binding]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Build GitHub OAuth URL
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user