Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions service/passkey/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ func BuildWebAuthn(r *http.Request) (*webauthn.WebAuthn, error) {
}

func resolveOrigins(r *http.Request, settings *system_setting.PasskeySettings) ([]string, error) {
if len(settings.Origins) > 0 {
origins := make([]string, 0, len(settings.Origins))
for _, origin := range settings.Origins {
originsStr := strings.TrimSpace(settings.Origins)
if originsStr != "" {
originList := strings.Split(originsStr, ",")
origins := make([]string, 0, len(originList))
for _, origin := range originList {
trimmed := strings.TrimSpace(origin)
if trimmed == "" {
continue
Expand Down
28 changes: 20 additions & 8 deletions setting/system_setting/passkey.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
package system_setting

import (
"net/url"
"one-api/common"
"one-api/setting/config"
"strings"
)

type PasskeySettings struct {
Enabled bool `json:"enabled"`
RPDisplayName string `json:"rp_display_name"`
RPID string `json:"rp_id"`
Origins []string `json:"origins"`
AllowInsecureOrigin bool `json:"allow_insecure_origin"`
UserVerification string `json:"user_verification"`
AttachmentPreference string `json:"attachment_preference"`
Enabled bool `json:"enabled"`
RPDisplayName string `json:"rp_display_name"`
RPID string `json:"rp_id"`
Origins string `json:"origins"`
AllowInsecureOrigin bool `json:"allow_insecure_origin"`
UserVerification string `json:"user_verification"`
AttachmentPreference string `json:"attachment_preference"`
}

var defaultPasskeySettings = PasskeySettings{
Enabled: false,
RPDisplayName: common.SystemName,
RPID: "",
Origins: []string{},
Origins: "",
AllowInsecureOrigin: false,
UserVerification: "preferred",
AttachmentPreference: "",
Expand All @@ -30,5 +32,15 @@ func init() {
}

func GetPasskeySettings() *PasskeySettings {
if defaultPasskeySettings.RPID == "" && ServerAddress != "" {
// 从ServerAddress提取域名作为RPID
// ServerAddress可能是 "https://newapi.pro" 这种格式
serverAddr := strings.TrimSpace(ServerAddress)
if parsed, err := url.Parse(serverAddr); err == nil && parsed.Host != "" {
defaultPasskeySettings.RPID = parsed.Host
} else {
defaultPasskeySettings.RPID = serverAddr
}
}
return &defaultPasskeySettings
}
99 changes: 38 additions & 61 deletions web/src/components/settings/SystemSetting.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ const SystemSetting = () => {
const [domainList, setDomainList] = useState([]);
const [ipList, setIpList] = useState([]);
const [allowedPorts, setAllowedPorts] = useState([]);
const [passkeyOrigins, setPasskeyOrigins] = useState([]);

const getOptions = async () => {
setLoading(true);
Expand Down Expand Up @@ -188,22 +187,19 @@ const SystemSetting = () => {
item.value = toBoolean(item.value);
break;
case 'passkey.origins':
try {
const origins = item.value ? JSON.parse(item.value) : [];
setPasskeyOrigins(Array.isArray(origins) ? origins : []);
item.value = Array.isArray(origins) ? origins : [];
} catch (e) {
setPasskeyOrigins([]);
item.value = [];
}
// origins是逗号分隔的字符串,直接使用
item.value = item.value || '';
break;
case 'passkey.rp_display_name':
case 'passkey.rp_id':
case 'passkey.user_verification':
case 'passkey.attachment_preference':
// 确保字符串字段不为null/undefined
item.value = item.value || '';
break;
case 'passkey.user_verification':
// 确保有默认值
item.value = item.value || 'preferred';
break;
case 'Price':
case 'MinTopUp':
item.value = parseFloat(item.value);
Expand Down Expand Up @@ -611,42 +607,33 @@ const SystemSetting = () => {
};

const submitPasskeySettings = async () => {
// 使用formApi直接获取当前表单值
const formValues = formApiRef.current?.getValues() || {};

const options = [];

// 只在值有变化时才提交,并确保空值转换为空字符串
if (originInputs['passkey.rp_display_name'] !== inputs['passkey.rp_display_name']) {
options.push({
key: 'passkey.rp_display_name',
value: inputs['passkey.rp_display_name'] || '',
});
}
if (originInputs['passkey.rp_id'] !== inputs['passkey.rp_id']) {
options.push({
key: 'passkey.rp_id',
value: inputs['passkey.rp_id'] || '',
});
}
if (originInputs['passkey.user_verification'] !== inputs['passkey.user_verification']) {
options.push({
key: 'passkey.user_verification',
value: inputs['passkey.user_verification'] || 'preferred',
});
}
if (originInputs['passkey.attachment_preference'] !== inputs['passkey.attachment_preference']) {
options.push({
key: 'passkey.attachment_preference',
value: inputs['passkey.attachment_preference'] || '',
});
}
// Origins总是提交,因为它们可能会被用户清空
options.push({
key: 'passkey.rp_display_name',
value: formValues['passkey.rp_display_name'] || inputs['passkey.rp_display_name'] || '',
});
options.push({
key: 'passkey.rp_id',
value: formValues['passkey.rp_id'] || inputs['passkey.rp_id'] || '',
});
options.push({
key: 'passkey.user_verification',
value: formValues['passkey.user_verification'] || inputs['passkey.user_verification'] || 'preferred',
});
options.push({
key: 'passkey.attachment_preference',
value: formValues['passkey.attachment_preference'] || inputs['passkey.attachment_preference'] || '',
});
options.push({
key: 'passkey.origins',
value: JSON.stringify(Array.isArray(passkeyOrigins) ? passkeyOrigins : []),
value: formValues['passkey.origins'] || inputs['passkey.origins'] || '',
});

if (options.length > 0) {
await updateOptions(options);
}
await updateOptions(options);
};

const handleCheckboxChange = async (optionKey, event) => {
Expand Down Expand Up @@ -1037,7 +1024,7 @@ const SystemSetting = () => {
>
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
<Form.Checkbox
field='passkey.enabled'
field="['passkey.enabled']"
noLabel
onChange={(e) =>
handleCheckboxChange('passkey.enabled', e)
Expand All @@ -1052,15 +1039,15 @@ const SystemSetting = () => {
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='passkey.rp_display_name'
field="['passkey.rp_display_name']"
label={t('服务显示名称')}
placeholder={t('默认使用系统名称')}
extraText={t('用户注册时看到的网站名称,比如\'我的网站\'')}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='passkey.rp_id'
field="['passkey.rp_id']"
label={t('网站域名标识')}
placeholder={t('例如:example.com')}
extraText={t('留空自动使用当前域名')}
Expand All @@ -1073,7 +1060,7 @@ const SystemSetting = () => {
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Select
field='passkey.user_verification'
field="['passkey.user_verification']"
label={t('安全验证级别')}
placeholder={t('是否要求指纹/面容等生物识别')}
optionList={[
Expand All @@ -1086,7 +1073,7 @@ const SystemSetting = () => {
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Select
field='passkey.attachment_preference'
field="['passkey.attachment_preference']"
label={t('设备类型偏好')}
placeholder={t('选择支持的认证设备类型')}
optionList={[
Expand All @@ -1104,7 +1091,7 @@ const SystemSetting = () => {
>
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
<Form.Checkbox
field='passkey.allow_insecure_origin'
field="['passkey.allow_insecure_origin']"
noLabel
extraText={t('仅用于开发环境,生产环境应使用 HTTPS')}
onChange={(e) =>
Expand All @@ -1120,21 +1107,11 @@ const SystemSetting = () => {
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
<Text strong>{t('允许的 Origins')}</Text>
<Text type="secondary" style={{ display: 'block', marginBottom: 8 }}>
{t('留空将自动使用服务器地址,多个 Origin 用于支持多域名部署')}
</Text>
<TagInput
value={passkeyOrigins}
onChange={(value) => {
setPasskeyOrigins(value);
setInputs(prev => ({
...prev,
'passkey.origins': value
}));
}}
placeholder={t('输入 Origin 后回车,如:https://example.com')}
style={{ width: '100%' }}
<Form.Input
field="['passkey.origins']"
label={t('允许的 Origins')}
placeholder={t('填写带https的域名,逗号分隔')}
extraText={t('空的话则不限制 Origin,多个 Origin 用逗号分隔')}
/>
</Col>
</Row>
Expand Down