feat(oidc): 支持自定义 OIDC 登录显示名称 (#6012)
* docs: add design spec for OIDC custom display name Mirrors the existing Custom OAuth Provider name pattern so admins can show a meaningful label instead of the hardcoded "OIDC" on the login page and in related copy. * feat(oidc): add configurable display name with OIDC fallback * feat(oidc): use configured display name in provider name and status API * feat(oidc): add display name field to default-theme OIDC settings Claude-Session: https://claude.ai/code/session_01FDkWJqigJi9yE3HG5pjZP5 * feat(oidc): show configured display name on default-theme login button * feat(oidc): add display name field to classic-theme OIDC settings * feat(oidc): show configured display name on classic-theme login button * fix(oidc): trim whitespace before applying display name fallback * chore: remove internal design doc from PR Design/planning docs are working artifacts for this session and shouldn't be submitted to the upstream project. * fix(oidc): lead with example in classic-theme display name placeholder Reorders the combined placeholder to show the example first, then the fallback note, matching the Custom OAuth Provider Name field's placeholder convention (example-only) that this feature mirrors. * fix(i18n): improve Russian grammar in OIDC display-name placeholder translation Leads each clause with its condition/subject and adds the missing verb, per PR review feedback. * test(web): remove redundant OIDC harness tests
This commit is contained in:
@@ -111,6 +111,7 @@ func GetStatus(c *gin.Context) {
|
||||
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
|
||||
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
|
||||
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
|
||||
"oidc_display_name": system_setting.GetOIDCSettings().GetEffectiveDisplayName(),
|
||||
"passkey_login": passkeySetting.Enabled,
|
||||
"passkey_display_name": passkeySetting.RPDisplayName,
|
||||
"passkey_rp_id": passkeySetting.RPID,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetStatusReturnsEffectiveOIDCDisplayName(t *testing.T) {
|
||||
settings := system_setting.GetOIDCSettings()
|
||||
originalDisplayName := settings.DisplayName
|
||||
originalOptionMap := common.OptionMap
|
||||
t.Cleanup(func() {
|
||||
settings.DisplayName = originalDisplayName
|
||||
common.OptionMap = originalOptionMap
|
||||
})
|
||||
common.OptionMap = map[string]string{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
displayName string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "custom name is trimmed",
|
||||
displayName: " Acme SSO ",
|
||||
want: "Acme SSO",
|
||||
},
|
||||
{
|
||||
name: "whitespace-only name falls back",
|
||||
displayName: " ",
|
||||
want: "OIDC",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
settings.DisplayName = tt.displayName
|
||||
response := httptest.NewRecorder()
|
||||
context, _ := gin.CreateTestContext(response)
|
||||
context.Request = httptest.NewRequest(http.MethodGet, "/api/status", nil)
|
||||
|
||||
GetStatus(context)
|
||||
|
||||
var payload struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
require.NoError(t, common.Unmarshal(response.Body.Bytes(), &payload))
|
||||
require.True(t, payload.Success)
|
||||
assert.Equal(t, tt.want, payload.Data["oidc_display_name"])
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -41,7 +41,7 @@ type oidcUser struct {
|
||||
}
|
||||
|
||||
func (p *OIDCProvider) GetName() string {
|
||||
return "OIDC"
|
||||
return system_setting.GetOIDCSettings().GetEffectiveDisplayName()
|
||||
}
|
||||
|
||||
func (p *OIDCProvider) IsEnabled() bool {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOIDCProvider_GetName(t *testing.T) {
|
||||
settings := system_setting.GetOIDCSettings()
|
||||
originalDisplayName := settings.DisplayName
|
||||
defer func() { settings.DisplayName = originalDisplayName }()
|
||||
|
||||
p := &OIDCProvider{}
|
||||
|
||||
settings.DisplayName = ""
|
||||
assert.Equal(t, "OIDC", p.GetName())
|
||||
|
||||
settings.DisplayName = " Acme SSO "
|
||||
assert.Equal(t, "Acme SSO", p.GetName())
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package system_setting
|
||||
|
||||
import "github.com/QuantumNous/new-api/setting/config"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/config"
|
||||
)
|
||||
|
||||
type OIDCSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
WellKnown string `json:"well_known"`
|
||||
@@ -23,3 +28,14 @@ func init() {
|
||||
func GetOIDCSettings() *OIDCSettings {
|
||||
return &defaultOIDCSettings
|
||||
}
|
||||
|
||||
// GetEffectiveDisplayName returns the admin-configured display name, or the
|
||||
// literal "OIDC" when none has been set. Centralizing this fallback keeps the
|
||||
// default in one place for both the OAuth provider name and the public
|
||||
// status payload.
|
||||
func (s *OIDCSettings) GetEffectiveDisplayName() string {
|
||||
if trimmed := strings.TrimSpace(s.DisplayName); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
return "OIDC"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package system_setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOIDCSettings_GetEffectiveDisplayName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
displayName string
|
||||
want string
|
||||
}{
|
||||
{name: "blank falls back to OIDC", displayName: "", want: "OIDC"},
|
||||
{name: "custom name is returned verbatim", displayName: "Acme SSO", want: "Acme SSO"},
|
||||
{name: "whitespace-only falls back to OIDC", displayName: " ", want: "OIDC"},
|
||||
{name: "surrounding whitespace is trimmed", displayName: " Acme SSO ", want: "Acme SSO"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &OIDCSettings{DisplayName: tt.displayName}
|
||||
assert.Equal(t, tt.want, s.GetEffectiveDisplayName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCSettings_DisplayNamePersistenceRoundTrip(t *testing.T) {
|
||||
settings := &OIDCSettings{DisplayName: " Acme SSO "}
|
||||
manager := config.NewConfigManager()
|
||||
manager.Register("oidc", settings)
|
||||
|
||||
saved := make(map[string]string)
|
||||
require.NoError(t, manager.SaveToDB(func(key, value string) error {
|
||||
saved[key] = value
|
||||
return nil
|
||||
}))
|
||||
require.Equal(t, " Acme SSO ", saved["oidc.display_name"])
|
||||
|
||||
settings.DisplayName = ""
|
||||
require.NoError(t, manager.LoadFromDB(saved))
|
||||
assert.Equal(t, " Acme SSO ", settings.DisplayName)
|
||||
assert.Equal(t, "Acme SSO", settings.GetEffectiveDisplayName())
|
||||
}
|
||||
@@ -107,9 +107,12 @@ export function OAuthProviders({
|
||||
}
|
||||
|
||||
if (status?.oidc_enabled) {
|
||||
const oidcDisplayName = status.oidc_display_name?.trim() || 'OIDC'
|
||||
providerButtons.push({
|
||||
key: 'oidc',
|
||||
label: t('Continue with OIDC'),
|
||||
label: t('Continue with {{name}}', {
|
||||
name: oidcDisplayName,
|
||||
}),
|
||||
onClick: handleOIDCLogin,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ export interface SystemStatus {
|
||||
oidc_enabled?: boolean
|
||||
oidc_authorization_endpoint?: string
|
||||
oidc_client_id?: string
|
||||
oidc_display_name?: string
|
||||
linuxdo_oauth?: boolean
|
||||
linuxdo_client_id?: string
|
||||
telegram_oauth?: boolean
|
||||
@@ -147,6 +148,7 @@ export interface SystemStatus {
|
||||
oidc_enabled?: boolean
|
||||
oidc_authorization_endpoint?: string
|
||||
oidc_client_id?: string
|
||||
oidc_display_name?: string
|
||||
linuxdo_oauth?: boolean
|
||||
linuxdo_client_id?: string
|
||||
telegram_oauth?: boolean
|
||||
|
||||
@@ -40,6 +40,7 @@ const defaultAuthSettings: AuthSettings = {
|
||||
'discord.client_id': '',
|
||||
'discord.client_secret': '',
|
||||
'oidc.enabled': false,
|
||||
'oidc.display_name': '',
|
||||
'oidc.client_id': '',
|
||||
'oidc.client_secret': '',
|
||||
'oidc.well_known': '',
|
||||
|
||||
@@ -72,6 +72,7 @@ const oauthSchema = z.object({
|
||||
}),
|
||||
oidc: z.object({
|
||||
enabled: z.boolean(),
|
||||
display_name: z.string(),
|
||||
client_id: z.string(),
|
||||
client_secret: z.string(),
|
||||
well_known: z.string(),
|
||||
@@ -102,6 +103,7 @@ type FlatOAuthDefaults = {
|
||||
'discord.client_id': string
|
||||
'discord.client_secret': string
|
||||
'oidc.enabled': boolean
|
||||
'oidc.display_name': string
|
||||
'oidc.client_id': string
|
||||
'oidc.client_secret': string
|
||||
'oidc.well_known': string
|
||||
@@ -184,6 +186,7 @@ const buildFormDefaults = (defaults: FlatOAuthDefaults): OAuthFormValues => ({
|
||||
},
|
||||
oidc: {
|
||||
enabled: defaults['oidc.enabled'],
|
||||
display_name: defaults['oidc.display_name'] ?? '',
|
||||
client_id: defaults['oidc.client_id'] ?? '',
|
||||
client_secret: defaults['oidc.client_secret'] ?? '',
|
||||
well_known: defaults['oidc.well_known'] ?? '',
|
||||
@@ -212,6 +215,7 @@ const normalizeFormValues = (values: OAuthFormValues): FlatOAuthDefaults => ({
|
||||
'discord.client_id': values.discord.client_id,
|
||||
'discord.client_secret': values.discord.client_secret,
|
||||
'oidc.enabled': values.oidc.enabled,
|
||||
'oidc.display_name': values.oidc.display_name,
|
||||
'oidc.client_id': values.oidc.client_id,
|
||||
'oidc.client_secret': values.oidc.client_secret,
|
||||
'oidc.well_known': values.oidc.well_known,
|
||||
@@ -617,6 +621,33 @@ export function OAuthSection(props: OAuthSectionProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='oidc.display_name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('OIDC Display Name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('e.g. Company SSO')}
|
||||
autoComplete='off'
|
||||
value={field.value ?? ''}
|
||||
onChange={(event) =>
|
||||
field.onChange(event.target.value)
|
||||
}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('Defaults to "OIDC" if left blank')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='oidc.client_id'
|
||||
|
||||
@@ -56,6 +56,7 @@ const AUTH_SECTIONS = [
|
||||
'discord.client_id': settings['discord.client_id'],
|
||||
'discord.client_secret': settings['discord.client_secret'],
|
||||
'oidc.enabled': settings['oidc.enabled'],
|
||||
'oidc.display_name': settings['oidc.display_name'],
|
||||
'oidc.client_id': settings['oidc.client_id'],
|
||||
'oidc.client_secret': settings['oidc.client_secret'],
|
||||
'oidc.well_known': settings['oidc.well_known'],
|
||||
|
||||
@@ -24,7 +24,7 @@ import { updateSystemOption } from '../api'
|
||||
import type { UpdateOptionRequest } from '../types'
|
||||
|
||||
// Configuration keys that require status refresh
|
||||
const STATUS_RELATED_KEYS = [
|
||||
const STATUS_RELATED_KEYS = new Set([
|
||||
'HeaderNavModules',
|
||||
'SidebarModulesAdmin',
|
||||
'Notice',
|
||||
@@ -36,7 +36,8 @@ const STATUS_RELATED_KEYS = [
|
||||
'general_setting.quota_display_type',
|
||||
'general_setting.custom_currency_symbol',
|
||||
'general_setting.custom_currency_exchange_rate',
|
||||
]
|
||||
'oidc.display_name',
|
||||
])
|
||||
|
||||
export function useUpdateOption() {
|
||||
const queryClient = useQueryClient()
|
||||
@@ -49,7 +50,7 @@ export function useUpdateOption() {
|
||||
queryClient.invalidateQueries({ queryKey: ['system-options'] })
|
||||
|
||||
// If updating frontend-display-related config, also refresh status
|
||||
if (STATUS_RELATED_KEYS.includes(variables.key)) {
|
||||
if (STATUS_RELATED_KEYS.has(variables.key)) {
|
||||
queryClient.invalidateQueries({ queryKey: ['status'] })
|
||||
try {
|
||||
window.localStorage.removeItem('status')
|
||||
|
||||
@@ -136,6 +136,7 @@ export type AuthSettings = {
|
||||
'discord.client_id': string
|
||||
'discord.client_secret': string
|
||||
'oidc.enabled': boolean
|
||||
'oidc.display_name': string
|
||||
'oidc.client_id': string
|
||||
'oidc.client_secret': string
|
||||
'oidc.well_known': string
|
||||
|
||||
@@ -1580,6 +1580,9 @@
|
||||
"Enable LinuxDO OAuth": "Enable LinuxDO OAuth",
|
||||
"Enable model performance metrics": "Enable model performance metrics",
|
||||
"Enable OIDC": "Enable OIDC",
|
||||
"OIDC Display Name": "OIDC Display Name",
|
||||
"e.g. Company SSO": "e.g. Company SSO",
|
||||
"Defaults to \"OIDC\" if left blank": "Defaults to \"OIDC\" if left blank",
|
||||
"Enable or disable this channel": "Enable or disable this channel",
|
||||
"Enable or disable this model": "Enable or disable this model",
|
||||
"Enable Passkey": "Enable Passkey",
|
||||
|
||||
@@ -1580,6 +1580,9 @@
|
||||
"Enable LinuxDO OAuth": "Activer LinuxDO OAuth",
|
||||
"Enable model performance metrics": "Activer les indicateurs de performance des modèles",
|
||||
"Enable OIDC": "Activer OIDC",
|
||||
"OIDC Display Name": "Nom d'affichage OIDC",
|
||||
"e.g. Company SSO": "ex. SSO de l'entreprise",
|
||||
"Defaults to \"OIDC\" if left blank": "Par défaut « OIDC » si laissé vide",
|
||||
"Enable or disable this channel": "Activer ou désactiver ce canal",
|
||||
"Enable or disable this model": "Activer ou désactiver ce modèle",
|
||||
"Enable Passkey": "Activer Passkey",
|
||||
|
||||
@@ -1580,6 +1580,9 @@
|
||||
"Enable LinuxDO OAuth": "LinuxDO OAuthを有効にする",
|
||||
"Enable model performance metrics": "モデル性能メトリクスを有効化",
|
||||
"Enable OIDC": "OIDCを有効にする",
|
||||
"OIDC Display Name": "OIDC 表示名",
|
||||
"e.g. Company SSO": "例:会社の SSO",
|
||||
"Defaults to \"OIDC\" if left blank": "空欄の場合は「OIDC」がデフォルトで表示されます",
|
||||
"Enable or disable this channel": "このチャネルを有効または無効にする",
|
||||
"Enable or disable this model": "このモデルを有効または無効にする",
|
||||
"Enable Passkey": "Passkeyを有効にする",
|
||||
|
||||
@@ -1580,6 +1580,9 @@
|
||||
"Enable LinuxDO OAuth": "Включить LinuxDO OAuth",
|
||||
"Enable model performance metrics": "Включить метрики производительности моделей",
|
||||
"Enable OIDC": "Включить OIDC",
|
||||
"OIDC Display Name": "Отображаемое имя OIDC",
|
||||
"e.g. Company SSO": "например, корпоративный SSO",
|
||||
"Defaults to \"OIDC\" if left blank": "Если оставить пустым, будет отображаться «OIDC».",
|
||||
"Enable or disable this channel": "Включить или отключить этот канал",
|
||||
"Enable or disable this model": "Включить или отключить эту модель",
|
||||
"Enable Passkey": "Включить Passkey",
|
||||
|
||||
@@ -1580,6 +1580,9 @@
|
||||
"Enable LinuxDO OAuth": "Bật LinuxDO OAuth",
|
||||
"Enable model performance metrics": "Bật chỉ số hiệu năng mô hình",
|
||||
"Enable OIDC": "Bật OIDC",
|
||||
"OIDC Display Name": "Tên hiển thị OIDC",
|
||||
"e.g. Company SSO": "ví dụ: SSO công ty",
|
||||
"Defaults to \"OIDC\" if left blank": "Mặc định là \"OIDC\" nếu để trống",
|
||||
"Enable or disable this channel": "Bật hoặc tắt kênh này",
|
||||
"Enable or disable this model": "Bật hoặc tắt mô hình này",
|
||||
"Enable Passkey": "Bật khóa truy cập",
|
||||
|
||||
@@ -1580,6 +1580,9 @@
|
||||
"Enable LinuxDO OAuth": "啟用 LinuxDO OAuth",
|
||||
"Enable model performance metrics": "啟用模型效能指標",
|
||||
"Enable OIDC": "啟用 OIDC",
|
||||
"OIDC Display Name": "OIDC 顯示名稱",
|
||||
"e.g. Company SSO": "例如:公司 SSO",
|
||||
"Defaults to \"OIDC\" if left blank": "留空則預設顯示為 \"OIDC\"",
|
||||
"Enable or disable this channel": "啟用或停用此渠道",
|
||||
"Enable or disable this model": "啟用或停用此模型",
|
||||
"Enable Passkey": "啟用 Passkey",
|
||||
|
||||
@@ -1580,6 +1580,9 @@
|
||||
"Enable LinuxDO OAuth": "启用 LinuxDO OAuth",
|
||||
"Enable model performance metrics": "启用模型性能指标",
|
||||
"Enable OIDC": "启用 OIDC",
|
||||
"OIDC Display Name": "OIDC 显示名称",
|
||||
"e.g. Company SSO": "例如:公司 SSO",
|
||||
"Defaults to \"OIDC\" if left blank": "留空则默认显示为 \"OIDC\"",
|
||||
"Enable or disable this channel": "启用或禁用此渠道",
|
||||
"Enable or disable this model": "启用或禁用此模型",
|
||||
"Enable Passkey": "启用 Passkey",
|
||||
|
||||
Reference in New Issue
Block a user