系统设置迁入后台:站点地址 / OAuth 凭据 / R2 存储 / 管理员账号,保存即生效无需重启
- 生效规则统一为「后台填了用后台的,没填回落环境变量」,老部署不改 env 照常跑 - config.Resolver:短缓存解析有效配置,存储后端按配置签名热重建;后台保存主动失效 - 秘密项(client secret / bot token / R2 密钥 / 管理员密码)接口永不回显明文, 只报「是否已配置、来自哪里」;留空保存 = 保持现值 - 管理员密码 bcrypt 入库,DB 哈希优先、显式设置的 env 密码作解锁后路; 后台改过密码后 admin/admin 开发模式立即失效 - 设置页新增「登录与存储」标签,基础信息加站点地址;秘密项带来源提示 - 监听地址 / 数据库 / 目录 / ONE_SECRET / Passkey 仍留环境变量(启动期依赖)
This commit is contained in:
1 parent
bf3ce934dd
commit
e68400b389
23 files changed
+957
-126
No files matched your search
@@ -12,6 +12,45 @@ const error = ref('')
|
||||
const saving = ref(false)
|
||||
const savedAt = ref('')
|
||||
|
||||
// ---------- 系统设置(从环境变量迁进后台的部分) ----------
|
||||
// 秘密项永不明文回显:输入框留空 = 保持现值。credential_meta 告诉每一项
|
||||
// 现在是否生效、来自后台(db)还是环境变量(env)兜底。
|
||||
const credMeta = ref({})
|
||||
const secrets = ref({})
|
||||
|
||||
const secretFields = [
|
||||
{ key: 'github_client_secret', label: 'GitHub Client Secret' },
|
||||
{ key: 'google_client_secret', label: 'Google Client Secret' },
|
||||
{ key: 'telegram_bot_token', label: 'Telegram Bot Token' },
|
||||
{ key: 'r2_access_key', label: 'R2 Access Key' },
|
||||
{ key: 'r2_secret_key', label: 'R2 Secret Key' }
|
||||
]
|
||||
|
||||
function srcOf(key) {
|
||||
const m = credMeta.value[key]
|
||||
return m ? m.source : 'unset'
|
||||
}
|
||||
|
||||
// 非秘密项的提示:值存哪、改了是否立即生效
|
||||
function srcHint(key) {
|
||||
const s = srcOf(key)
|
||||
if (s === 'db') return '已在此配置'
|
||||
if (s === 'env') return '当前来自环境变量,在此填写后将覆盖'
|
||||
return '未配置'
|
||||
}
|
||||
|
||||
// 秘密项输入框的 placeholder:不回显值,只报状态
|
||||
function secretPlaceholder(key) {
|
||||
const s = srcOf(key)
|
||||
if (s === 'db') return '已配置(后台)· 留空保持不变'
|
||||
if (s === 'env') return '已通过环境变量配置 · 填写后覆盖'
|
||||
return '未配置'
|
||||
}
|
||||
|
||||
// 管理员账号:新密码 + 确认,只在前端做一次一致性与长度预检
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
// ---------- 社交链接(settings.social_links <-> 多行文本) ----------
|
||||
// 文本框里每行一条「名称 | 链接」;数组存后端,文本框给人编辑。
|
||||
const socialText = ref('')
|
||||
@@ -137,7 +176,12 @@ const currentSection = computed(() => SECTIONS.find((s) => s.id === cssSection.v
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
settings.value = await adminApi.settings()
|
||||
const res = await adminApi.settings()
|
||||
settings.value = res.settings
|
||||
credMeta.value = res.credential_meta || {}
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
secrets.value = {}
|
||||
if (!SKIN_CONSTANTS.LIGHT_SKINS.includes(settings.value.light_skin_id)) {
|
||||
settings.value.light_skin_id = 'paper'
|
||||
}
|
||||
@@ -167,13 +211,32 @@ onMounted(load)
|
||||
|
||||
async function save() {
|
||||
if (!settings.value) return
|
||||
// 管理员改密码:两次输入一致才提交
|
||||
if (newPassword.value || confirmPassword.value) {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
toastErr('两次输入的新密码不一致')
|
||||
return
|
||||
}
|
||||
if (newPassword.value.length < 6) {
|
||||
toastErr('新密码至少 6 位')
|
||||
return
|
||||
}
|
||||
}
|
||||
saving.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
settings.value.social_links = textToSocial(socialText.value)
|
||||
const s = await adminApi.saveSettings(settings.value)
|
||||
settings.value = s
|
||||
socialText.value = socialToText(s.social_links)
|
||||
// 秘密项走 secrets 单独通道(留空的键不会提交,服务端按「保持现值」处理)
|
||||
const payload = { ...settings.value, secrets: {} }
|
||||
if (newPassword.value) payload.secrets.admin_password = newPassword.value
|
||||
for (const f of secretFields) {
|
||||
const v = (secrets.value[f.key] || '').trim()
|
||||
if (v) payload.secrets[f.key] = v
|
||||
}
|
||||
const res = await adminApi.saveSettings(payload)
|
||||
settings.value = res.settings
|
||||
credMeta.value = res.credential_meta || {}
|
||||
socialText.value = socialToText(settings.value.social_links)
|
||||
// 保存后服务端会丢掉空的/超长的分区,重新补齐本地键,免得输入框失焦
|
||||
if (!settings.value.custom_css || typeof settings.value.custom_css !== 'object') {
|
||||
settings.value.custom_css = {}
|
||||
@@ -183,6 +246,9 @@ async function save() {
|
||||
settings.value.custom_css[sec.id] = ''
|
||||
}
|
||||
}
|
||||
secrets.value = {}
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
savedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
toastOk('设置已保存')
|
||||
} catch (e) {
|
||||
@@ -206,6 +272,7 @@ async function save() {
|
||||
<button :class="{ on: tab === 'theme' }" @click="tab = 'theme'">主题外观</button>
|
||||
<button :class="{ on: tab === 'ui' }" @click="tab = 'ui'">界面与自定义</button>
|
||||
<button :class="{ on: tab === 'advanced' }" @click="tab = 'advanced'">高级</button>
|
||||
<button :class="{ on: tab === 'system' }" @click="tab = 'system'">登录与存储</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -220,6 +287,15 @@ async function save() {
|
||||
<label>站点副标题</label>
|
||||
<input v-model="settings.site_desc" class="input" spellcheck="false" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>站点地址</label>
|
||||
<input v-model="settings.site_url" class="input" spellcheck="false"
|
||||
:placeholder="srcOf('site_url') === 'env' ? '当前来自环境变量 ONE_SITE_URL' : 'https://your.domain'" />
|
||||
<p class="field-hint">
|
||||
RSS 订阅链接、GitHub / Google 登录的回调地址都以它为基座。
|
||||
{{ srcHint('site_url') }},保存后立即生效,无需重启。
|
||||
</p>
|
||||
</div>
|
||||
<!-- 作者昵称/简介不在这里改:它们和头像、登录方式同属「账户」,
|
||||
两处编辑同一个字段迟早漂移,所以收归账户页独占。 -->
|
||||
<div class="field">
|
||||
@@ -417,6 +493,104 @@ async function save() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- system:登录方式 / 对象存储 / 管理员账号(原环境变量配置项) -->
|
||||
<div v-if="tab === 'system'" class="panel">
|
||||
<div class="panel-title"><h2>登录方式</h2></div>
|
||||
<p class="field-hint" style="margin: 0 0 14px;">
|
||||
评论区读者的第三方登录。凭据齐全的登录方式自动开放,清空即关闭。
|
||||
GitHub / Google 的回调地址以「站点地址 + /api/auth/callback/平台名」为准,改完站点地址无需再去平台改回调。
|
||||
</p>
|
||||
|
||||
<div class="field">
|
||||
<label>GitHub OAuth App</label>
|
||||
<input v-model="settings.github_client_id" class="input" spellcheck="false"
|
||||
placeholder="Client ID" style="margin-bottom: 8px;" />
|
||||
<input v-model="secrets.github_client_secret" class="input" type="password"
|
||||
autocomplete="new-password" :placeholder="secretPlaceholder('github_client_secret')" />
|
||||
<p class="field-hint">Client ID:{{ srcHint('github_client_id') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Google OAuth 客户端</label>
|
||||
<input v-model="settings.google_client_id" class="input" spellcheck="false"
|
||||
placeholder="Client ID" style="margin-bottom: 8px;" />
|
||||
<input v-model="secrets.google_client_secret" class="input" type="password"
|
||||
autocomplete="new-password" :placeholder="secretPlaceholder('google_client_secret')" />
|
||||
<p class="field-hint">Client ID:{{ srcHint('google_client_id') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Telegram 登录 Widget</label>
|
||||
<input v-model="settings.telegram_bot" class="input" spellcheck="false"
|
||||
placeholder="Bot 用户名(不含 @)" style="margin-bottom: 8px;" />
|
||||
<input v-model="secrets.telegram_bot_token" class="input" type="password"
|
||||
autocomplete="new-password" :placeholder="secretPlaceholder('telegram_bot_token')" />
|
||||
<p class="field-hint">Bot 用户名:{{ srcHint('telegram_bot') }};Token 在 @BotFather 里创建 Bot 后获得。</p>
|
||||
</div>
|
||||
|
||||
<div class="field" style="border-top: 1px solid var(--admin-line); padding-top: 14px;">
|
||||
<div class="panel-title"><h2>对象存储</h2></div>
|
||||
<p class="field-hint" style="margin: 0 0 14px;">
|
||||
文件上传的存储端。四项凭据齐全走 R2,缺任一项回落本地磁盘
|
||||
(data/uploads),切换不影响已上传文件的链接。保存后立即生效,无需重启。
|
||||
</p>
|
||||
<div class="field" style="padding: 0; border: 0; margin-bottom: 8px;">
|
||||
<label>S3 API 端点</label>
|
||||
<input v-model="settings.s3_endpoint" class="input" spellcheck="false"
|
||||
placeholder="https://<账户ID>.r2.cloudflarestorage.com" />
|
||||
<p class="field-hint">{{ srcHint('s3_endpoint') }}</p>
|
||||
</div>
|
||||
<div class="field" style="padding: 0; border: 0; margin-bottom: 8px;">
|
||||
<label>桶名(Bucket)</label>
|
||||
<input v-model="settings.r2_bucket" class="input" spellcheck="false" />
|
||||
<p class="field-hint">{{ srcHint('r2_bucket') }}</p>
|
||||
</div>
|
||||
<div class="field" style="padding: 0; border: 0; margin-bottom: 8px;">
|
||||
<label>访问密钥</label>
|
||||
<input v-model="secrets.r2_access_key" class="input" type="password"
|
||||
autocomplete="new-password" :placeholder="secretPlaceholder('r2_access_key')" />
|
||||
</div>
|
||||
<div class="field" style="padding: 0; border: 0; margin-bottom: 8px;">
|
||||
<label>私密密钥</label>
|
||||
<input v-model="secrets.r2_secret_key" class="input" type="password"
|
||||
autocomplete="new-password" :placeholder="secretPlaceholder('r2_secret_key')" />
|
||||
</div>
|
||||
<div class="field" style="padding: 0; border: 0;">
|
||||
<label>公开访问域名</label>
|
||||
<input v-model="settings.uploads_public_base" class="input" spellcheck="false"
|
||||
placeholder="https://cdn.example.com(r2.dev 或绑定的自定义域名)" />
|
||||
<p class="field-hint">
|
||||
{{ srcHint('uploads_public_base') }}。配置后 R2 文件直链走 CDN;
|
||||
不配则由本站 /uploads/ 路由代为输出。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" style="border-top: 1px solid var(--admin-line); padding-top: 14px;">
|
||||
<div class="panel-title"><h2>管理员账号</h2></div>
|
||||
<p class="field-hint" style="margin: 0 0 14px;">
|
||||
登录后台用的用户名与密码。在这里设置密码后,环境变量
|
||||
ONE_ADMIN_PASSWORD 只作解锁后路保留(仍可用);不填则维持现状。
|
||||
</p>
|
||||
<div class="field" style="padding: 0; border: 0; margin-bottom: 8px;">
|
||||
<label>用户名</label>
|
||||
<input v-model="settings.admin_username" class="input" spellcheck="false"
|
||||
:placeholder="srcOf('admin_username') === 'env' ? '当前来自环境变量 ONE_ADMIN_USER' : ''" />
|
||||
<p class="field-hint">{{ srcHint('admin_username') }}</p>
|
||||
</div>
|
||||
<div class="field" style="padding: 0; border: 0; margin-bottom: 8px;">
|
||||
<label>新密码</label>
|
||||
<input v-model="newPassword" class="input" type="password"
|
||||
autocomplete="new-password" placeholder="留空保持不变(6-72 位)" />
|
||||
</div>
|
||||
<div class="field" style="padding: 0; border: 0;">
|
||||
<label>确认新密码</label>
|
||||
<input v-model="confirmPassword" class="input" type="password"
|
||||
autocomplete="new-password" placeholder="再输入一遍" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-top: 14px;">
|
||||
<button class="btn btn-primary" :disabled="saving" @click="save">
|
||||
{{ saving ? '保存中…' : '保存设置' }}
|
||||
|
||||
Reference in new issue
Block a user