Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
50de5d05
Commit
50de5d05
authored
Jan 12, 2026
by
shaw
Browse files
fix: 修复PR合并后的功能回退和安全问题
parent
7844dc4f
Changes
7
Hide whitespace changes
Inline
Side-by-side
backend/internal/config/config.go
View file @
50de5d05
...
@@ -617,7 +617,7 @@ func setDefaults() {
...
@@ -617,7 +617,7 @@ func setDefaults() {
// Turnstile
// Turnstile
viper
.
SetDefault
(
"turnstile.required"
,
false
)
viper
.
SetDefault
(
"turnstile.required"
,
false
)
// LinuxDo Connect OAuth 登录
(终端用户 SSO)
// LinuxDo Connect OAuth 登录
viper
.
SetDefault
(
"linuxdo_connect.enabled"
,
false
)
viper
.
SetDefault
(
"linuxdo_connect.enabled"
,
false
)
viper
.
SetDefault
(
"linuxdo_connect.client_id"
,
""
)
viper
.
SetDefault
(
"linuxdo_connect.client_id"
,
""
)
viper
.
SetDefault
(
"linuxdo_connect.client_secret"
,
""
)
viper
.
SetDefault
(
"linuxdo_connect.client_secret"
,
""
)
...
...
backend/internal/handler/admin/setting_handler.go
View file @
50de5d05
...
@@ -104,7 +104,7 @@ type UpdateSettingsRequest struct {
...
@@ -104,7 +104,7 @@ type UpdateSettingsRequest struct {
TurnstileSiteKey
string
`json:"turnstile_site_key"`
TurnstileSiteKey
string
`json:"turnstile_site_key"`
TurnstileSecretKey
string
`json:"turnstile_secret_key"`
TurnstileSecretKey
string
`json:"turnstile_secret_key"`
// LinuxDo Connect OAuth 登录
(终端用户 SSO)
// LinuxDo Connect OAuth 登录
LinuxDoConnectEnabled
bool
`json:"linuxdo_connect_enabled"`
LinuxDoConnectEnabled
bool
`json:"linuxdo_connect_enabled"`
LinuxDoConnectClientID
string
`json:"linuxdo_connect_client_id"`
LinuxDoConnectClientID
string
`json:"linuxdo_connect_client_id"`
LinuxDoConnectClientSecret
string
`json:"linuxdo_connect_client_secret"`
LinuxDoConnectClientSecret
string
`json:"linuxdo_connect_client_secret"`
...
...
backend/internal/service/auth_service.go
View file @
50de5d05
...
@@ -357,7 +357,7 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string
...
@@ -357,7 +357,7 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string
// - 如果邮箱已存在:直接登录(不需要本地密码)
// - 如果邮箱已存在:直接登录(不需要本地密码)
// - 如果邮箱不存在:创建新用户并登录
// - 如果邮箱不存在:创建新用户并登录
//
//
// 注意:该函数用于
“终端用户登录 Sub2API 本身”的
场景(不同于上游账号的 OAuth,例如 OpenAI/Gemini)。
// 注意:该函数用于
LinuxDo OAuth 登录
场景(不同于上游账号的 OAuth,例如
Claude/
OpenAI/Gemini)。
// 为了满足现有数据库约束(需要密码哈希),新用户会生成随机密码并进行哈希保存。
// 为了满足现有数据库约束(需要密码哈希),新用户会生成随机密码并进行哈希保存。
func
(
s
*
AuthService
)
LoginOrRegisterOAuth
(
ctx
context
.
Context
,
email
,
username
string
)
(
string
,
*
User
,
error
)
{
func
(
s
*
AuthService
)
LoginOrRegisterOAuth
(
ctx
context
.
Context
,
email
,
username
string
)
(
string
,
*
User
,
error
)
{
email
=
strings
.
TrimSpace
(
email
)
email
=
strings
.
TrimSpace
(
email
)
...
@@ -376,8 +376,8 @@ func (s *AuthService) LoginOrRegisterOAuth(ctx context.Context, email, username
...
@@ -376,8 +376,8 @@ func (s *AuthService) LoginOrRegisterOAuth(ctx context.Context, email, username
user
,
err
:=
s
.
userRepo
.
GetByEmail
(
ctx
,
email
)
user
,
err
:=
s
.
userRepo
.
GetByEmail
(
ctx
,
email
)
if
err
!=
nil
{
if
err
!=
nil
{
if
errors
.
Is
(
err
,
ErrUserNotFound
)
{
if
errors
.
Is
(
err
,
ErrUserNotFound
)
{
// OAuth 首次登录视为注册
。
// OAuth 首次登录视为注册
(fail-close:settingService 未配置时不允许注册)
if
s
.
settingService
!
=
nil
&&
!
s
.
settingService
.
IsRegistrationEnabled
(
ctx
)
{
if
s
.
settingService
=
=
nil
||
!
s
.
settingService
.
IsRegistrationEnabled
(
ctx
)
{
return
""
,
nil
,
ErrRegDisabled
return
""
,
nil
,
ErrRegDisabled
}
}
...
...
backend/internal/service/setting_service.go
View file @
50de5d05
...
@@ -176,7 +176,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
...
@@ -176,7 +176,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates
[
SettingKeyTurnstileSecretKey
]
=
settings
.
TurnstileSecretKey
updates
[
SettingKeyTurnstileSecretKey
]
=
settings
.
TurnstileSecretKey
}
}
// LinuxDo Connect OAuth 登录
(终端用户 SSO)
// LinuxDo Connect OAuth 登录
updates
[
SettingKeyLinuxDoConnectEnabled
]
=
strconv
.
FormatBool
(
settings
.
LinuxDoConnectEnabled
)
updates
[
SettingKeyLinuxDoConnectEnabled
]
=
strconv
.
FormatBool
(
settings
.
LinuxDoConnectEnabled
)
updates
[
SettingKeyLinuxDoConnectClientID
]
=
settings
.
LinuxDoConnectClientID
updates
[
SettingKeyLinuxDoConnectClientID
]
=
settings
.
LinuxDoConnectClientID
updates
[
SettingKeyLinuxDoConnectRedirectURL
]
=
settings
.
LinuxDoConnectRedirectURL
updates
[
SettingKeyLinuxDoConnectRedirectURL
]
=
settings
.
LinuxDoConnectRedirectURL
...
@@ -227,8 +227,8 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
...
@@ -227,8 +227,8 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
func
(
s
*
SettingService
)
IsRegistrationEnabled
(
ctx
context
.
Context
)
bool
{
func
(
s
*
SettingService
)
IsRegistrationEnabled
(
ctx
context
.
Context
)
bool
{
value
,
err
:=
s
.
settingRepo
.
GetValue
(
ctx
,
SettingKeyRegistrationEnabled
)
value
,
err
:=
s
.
settingRepo
.
GetValue
(
ctx
,
SettingKeyRegistrationEnabled
)
if
err
!=
nil
{
if
err
!=
nil
{
//
默认开放
注册
//
安全默认:如果设置不存在或查询出错,默认关闭
注册
return
tru
e
return
fals
e
}
}
return
value
==
"true"
return
value
==
"true"
}
}
...
...
backend/internal/service/settings_view.go
View file @
50de5d05
...
@@ -18,7 +18,7 @@ type SystemSettings struct {
...
@@ -18,7 +18,7 @@ type SystemSettings struct {
TurnstileSecretKey
string
TurnstileSecretKey
string
TurnstileSecretKeyConfigured
bool
TurnstileSecretKeyConfigured
bool
// LinuxDo Connect OAuth 登录
(终端用户 SSO)
// LinuxDo Connect OAuth 登录
LinuxDoConnectEnabled
bool
LinuxDoConnectEnabled
bool
LinuxDoConnectClientID
string
LinuxDoConnectClientID
string
LinuxDoConnectClientSecret
string
LinuxDoConnectClientSecret
string
...
...
frontend/src/api/admin/settings.ts
View file @
50de5d05
...
@@ -36,6 +36,12 @@ export interface SystemSettings {
...
@@ -36,6 +36,12 @@ export interface SystemSettings {
turnstile_site_key
:
string
turnstile_site_key
:
string
turnstile_secret_key_configured
:
boolean
turnstile_secret_key_configured
:
boolean
// LinuxDo Connect OAuth settings
linuxdo_connect_enabled
:
boolean
linuxdo_connect_client_id
:
string
linuxdo_connect_client_secret_configured
:
boolean
linuxdo_connect_redirect_url
:
string
// Model fallback configuration
// Model fallback configuration
enable_model_fallback
:
boolean
enable_model_fallback
:
boolean
fallback_model_anthropic
:
string
fallback_model_anthropic
:
string
...
@@ -76,6 +82,10 @@ export interface UpdateSettingsRequest {
...
@@ -76,6 +82,10 @@ export interface UpdateSettingsRequest {
turnstile_enabled
?:
boolean
turnstile_enabled
?:
boolean
turnstile_site_key
?:
string
turnstile_site_key
?:
string
turnstile_secret_key
?:
string
turnstile_secret_key
?:
string
linuxdo_connect_enabled
?:
boolean
linuxdo_connect_client_id
?:
string
linuxdo_connect_client_secret
?:
string
linuxdo_connect_redirect_url
?:
string
enable_model_fallback
?:
boolean
enable_model_fallback
?:
boolean
fallback_model_anthropic
?:
string
fallback_model_anthropic
?:
string
fallback_model_openai
?:
string
fallback_model_openai
?:
string
...
...
frontend/src/views/admin/SettingsView.vue
View file @
50de5d05
...
@@ -261,6 +261,106 @@
...
@@ -261,6 +261,106 @@
</div>
</div>
</div>
</div>
<!-- LinuxDo Connect OAuth 登录 -->
<div
class=
"card"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
<h2
class=
"text-lg font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
admin.settings.linuxdo.title
'
)
}}
</h2>
<p
class=
"mt-1 text-sm text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.linuxdo.description
'
)
}}
</p>
</div>
<div
class=
"space-y-5 p-6"
>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"font-medium text-gray-900 dark:text-white"
>
{{
t
(
'
admin.settings.linuxdo.enable
'
)
}}
</label>
<p
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.linuxdo.enableHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"form.linuxdo_connect_enabled"
/>
</div>
<div
v-if=
"form.linuxdo_connect_enabled"
class=
"border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div
class=
"grid grid-cols-1 gap-6"
>
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.settings.linuxdo.clientId
'
)
}}
</label>
<input
v-model=
"form.linuxdo_connect_client_id"
type=
"text"
class=
"input font-mono text-sm"
:placeholder=
"t('admin.settings.linuxdo.clientIdPlaceholder')"
/>
<p
class=
"mt-1.5 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.linuxdo.clientIdHint
'
)
}}
</p>
</div>
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.settings.linuxdo.clientSecret
'
)
}}
</label>
<input
v-model=
"form.linuxdo_connect_client_secret"
type=
"password"
class=
"input font-mono text-sm"
:placeholder=
"
form.linuxdo_connect_client_secret_configured
? t('admin.settings.linuxdo.clientSecretConfiguredPlaceholder')
: t('admin.settings.linuxdo.clientSecretPlaceholder')
"
/>
<p
class=
"mt-1.5 text-xs text-gray-500 dark:text-gray-400"
>
{{
form
.
linuxdo_connect_client_secret_configured
?
t
(
'
admin.settings.linuxdo.clientSecretConfiguredHint
'
)
:
t
(
'
admin.settings.linuxdo.clientSecretHint
'
)
}}
</p>
</div>
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.settings.linuxdo.redirectUrl
'
)
}}
</label>
<input
v-model=
"form.linuxdo_connect_redirect_url"
type=
"url"
class=
"input font-mono text-sm"
:placeholder=
"t('admin.settings.linuxdo.redirectUrlPlaceholder')"
/>
<div
class=
"mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm w-fit"
@
click=
"setAndCopyLinuxdoRedirectUrl"
>
{{
t
(
'
admin.settings.linuxdo.quickSetCopy
'
)
}}
</button>
<code
v-if=
"linuxdoRedirectUrlSuggestion"
class=
"select-all break-all rounded bg-gray-50 px-2 py-1 font-mono text-xs text-gray-600 dark:bg-dark-800 dark:text-gray-300"
>
{{
linuxdoRedirectUrlSuggestion
}}
</code>
</div>
<p
class=
"mt-1.5 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.linuxdo.redirectUrlHint
'
)
}}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Default Settings -->
<!-- Default Settings -->
<div
class=
"card"
>
<div
class=
"card"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
...
@@ -712,19 +812,19 @@
...
@@ -712,19 +812,19 @@
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
reactive
,
onMounted
,
compu
ted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMoun
ted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
adminAPI
}
from
'
@/api
'
import
{
adminAPI
}
from
'
@/api
'
import
type
{
SystemSettings
,
UpdateSettingsRequest
}
from
'
@/api/admin/settings
'
import
type
{
SystemSettings
,
UpdateSettingsRequest
}
from
'
@/api/admin/settings
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Toggle
from
'
@/components/common/Toggle.vue
'
import
Toggle
from
'
@/components/common/Toggle.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
import
{
useAdminSettingsStore
,
useAppStore
}
from
'
@/stores
'
import
{
useAppStore
}
from
'
@/stores
'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
appStore
=
useAppStore
()
const
adminSettingsStore
=
useAdminSettingsStore
()
const
{
copyToClipboard
}
=
useClipboard
()
const
loading
=
ref
(
true
)
const
loading
=
ref
(
true
)
const
saving
=
ref
(
false
)
const
saving
=
ref
(
false
)
...
@@ -743,6 +843,7 @@ const newAdminApiKey = ref('')
...
@@ -743,6 +843,7 @@ const newAdminApiKey = ref('')
type
SettingsForm
=
SystemSettings
&
{
type
SettingsForm
=
SystemSettings
&
{
smtp_password
:
string
smtp_password
:
string
turnstile_secret_key
:
string
turnstile_secret_key
:
string
linuxdo_connect_client_secret
:
string
}
}
const
form
=
reactive
<
SettingsForm
>
({
const
form
=
reactive
<
SettingsForm
>
({
...
@@ -770,6 +871,12 @@ const form = reactive<SettingsForm>({
...
@@ -770,6 +871,12 @@ const form = reactive<SettingsForm>({
turnstile_site_key
:
''
,
turnstile_site_key
:
''
,
turnstile_secret_key
:
''
,
turnstile_secret_key
:
''
,
turnstile_secret_key_configured
:
false
,
turnstile_secret_key_configured
:
false
,
// LinuxDo Connect OAuth 登录
linuxdo_connect_enabled
:
false
,
linuxdo_connect_client_id
:
''
,
linuxdo_connect_client_secret
:
''
,
linuxdo_connect_client_secret_configured
:
false
,
linuxdo_connect_redirect_url
:
''
,
// Model fallback
// Model fallback
enable_model_fallback
:
false
,
enable_model_fallback
:
false
,
fallback_model_anthropic
:
'
claude-3-5-sonnet-20241022
'
,
fallback_model_anthropic
:
'
claude-3-5-sonnet-20241022
'
,
...
@@ -778,9 +885,30 @@ const form = reactive<SettingsForm>({
...
@@ -778,9 +885,30 @@ const form = reactive<SettingsForm>({
fallback_model_antigravity
:
'
gemini-2.5-pro
'
,
fallback_model_antigravity
:
'
gemini-2.5-pro
'
,
// Identity patch (Claude -> Gemini)
// Identity patch (Claude -> Gemini)
enable_identity_patch
:
true
,
enable_identity_patch
:
true
,
identity_patch_prompt
:
''
identity_patch_prompt
:
''
,
// Ops monitoring (vNext)
ops_monitoring_enabled
:
true
,
ops_realtime_monitoring_enabled
:
true
,
ops_query_mode_default
:
'
auto
'
,
ops_metrics_interval_seconds
:
60
})
// LinuxDo OAuth redirect URL suggestion
const
linuxdoRedirectUrlSuggestion
=
computed
(()
=>
{
if
(
typeof
window
===
'
undefined
'
)
return
''
const
origin
=
window
.
location
.
origin
||
`
${
window
.
location
.
protocol
}
//
${
window
.
location
.
host
}
`
return
`
${
origin
}
/api/v1/auth/oauth/linuxdo/callback`
})
})
async
function
setAndCopyLinuxdoRedirectUrl
()
{
const
url
=
linuxdoRedirectUrlSuggestion
.
value
if
(
!
url
)
return
form
.
linuxdo_connect_redirect_url
=
url
await
copyToClipboard
(
url
,
t
(
'
admin.settings.linuxdo.redirectUrlSetAndCopied
'
))
}
function
handleLogoUpload
(
event
:
Event
)
{
function
handleLogoUpload
(
event
:
Event
)
{
const
input
=
event
.
target
as
HTMLInputElement
const
input
=
event
.
target
as
HTMLInputElement
const
file
=
input
.
files
?.[
0
]
const
file
=
input
.
files
?.[
0
]
...
@@ -826,6 +954,7 @@ async function loadSettings() {
...
@@ -826,6 +954,7 @@ async function loadSettings() {
Object
.
assign
(
form
,
settings
)
Object
.
assign
(
form
,
settings
)
form
.
smtp_password
=
''
form
.
smtp_password
=
''
form
.
turnstile_secret_key
=
''
form
.
turnstile_secret_key
=
''
form
.
linuxdo_connect_client_secret
=
''
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
appStore
.
showError
(
appStore
.
showError
(
t
(
'
admin.settings.failedToLoad
'
)
+
'
:
'
+
(
error
.
message
||
t
(
'
common.unknownError
'
))
t
(
'
admin.settings.failedToLoad
'
)
+
'
:
'
+
(
error
.
message
||
t
(
'
common.unknownError
'
))
...
@@ -860,6 +989,10 @@ async function saveSettings() {
...
@@ -860,6 +989,10 @@ async function saveSettings() {
turnstile_enabled
:
form
.
turnstile_enabled
,
turnstile_enabled
:
form
.
turnstile_enabled
,
turnstile_site_key
:
form
.
turnstile_site_key
,
turnstile_site_key
:
form
.
turnstile_site_key
,
turnstile_secret_key
:
form
.
turnstile_secret_key
||
undefined
,
turnstile_secret_key
:
form
.
turnstile_secret_key
||
undefined
,
linuxdo_connect_enabled
:
form
.
linuxdo_connect_enabled
,
linuxdo_connect_client_id
:
form
.
linuxdo_connect_client_id
,
linuxdo_connect_client_secret
:
form
.
linuxdo_connect_client_secret
||
undefined
,
linuxdo_connect_redirect_url
:
form
.
linuxdo_connect_redirect_url
,
enable_model_fallback
:
form
.
enable_model_fallback
,
enable_model_fallback
:
form
.
enable_model_fallback
,
fallback_model_anthropic
:
form
.
fallback_model_anthropic
,
fallback_model_anthropic
:
form
.
fallback_model_anthropic
,
fallback_model_openai
:
form
.
fallback_model_openai
,
fallback_model_openai
:
form
.
fallback_model_openai
,
...
@@ -872,6 +1005,7 @@ async function saveSettings() {
...
@@ -872,6 +1005,7 @@ async function saveSettings() {
Object
.
assign
(
form
,
updated
)
Object
.
assign
(
form
,
updated
)
form
.
smtp_password
=
''
form
.
smtp_password
=
''
form
.
turnstile_secret_key
=
''
form
.
turnstile_secret_key
=
''
form
.
linuxdo_connect_client_secret
=
''
// Refresh cached public settings so sidebar/header update immediately
// Refresh cached public settings so sidebar/header update immediately
await
appStore
.
fetchPublicSettings
(
true
)
await
appStore
.
fetchPublicSettings
(
true
)
appStore
.
showSuccess
(
t
(
'
admin.settings.settingsSaved
'
))
appStore
.
showSuccess
(
t
(
'
admin.settings.settingsSaved
'
))
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment