多用户与角色:owner / admin / reader 三级,用户管理页 + 密码上库
- users 表加 password_hash 列;后台账号(owner+admin)密码 bcrypt 存行内, 首次登录把 env / settings 引导凭据自迁移成行哈希 - 会话 token 从用户名改为携带用户 ID,角色与停用状态每请求查库, 改角色 / 停用账号即时生效(存量会话立即 401) - 登录:先查 users 表,再走 settings 哈希 / env 引导链; admin/admin 开发模式在任何账号设过密码后失效 - 权限:系统设置、用户管理仅 owner;内容管理 admin+owner; admin 后台新增 用户 页(创建 / 重置密码 / 停用 / 删除), 设置页「登录与存储」tab 对管理员隐藏 - 账户页加修改密码表单(旧密码校验,OAuth/Passkey 首设免旧密码); 评论区管理员身份跟随各自账号,不再统一挂站主名下 - 修复:providers 为 nil 时账户页白屏(Go nil slice 序列化成 null)
This commit is contained in:
1 parent
e5dee4daf1
commit
4bb2ff4145
23 files changed
+917
-222
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { toastOk, toastErr } from './toast'
|
||||
import { adminApi } from '../api'
|
||||
import { adminApi, session } from '../api'
|
||||
import { SKIN_CONSTANTS, UI_CONSTANTS } from '../site'
|
||||
import { SECTIONS } from '../ui/sections'
|
||||
|
||||
@@ -47,9 +47,6 @@ function secretPlaceholder(key) {
|
||||
return '未配置'
|
||||
}
|
||||
|
||||
// 管理员账号:新密码 + 确认,只在前端做一次一致性与长度预检
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
// ---------- 社交链接(settings.social_links <-> 多行文本) ----------
|
||||
// 文本框里每行一条「名称 | 链接」;数组存后端,文本框给人编辑。
|
||||
@@ -179,8 +176,6 @@ async function load() {
|
||||
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'
|
||||
@@ -211,24 +206,12 @@ 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)
|
||||
// 秘密项走 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
|
||||
@@ -247,8 +230,6 @@ async function save() {
|
||||
}
|
||||
}
|
||||
secrets.value = {}
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
savedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
toastOk('设置已保存')
|
||||
} catch (e) {
|
||||
@@ -272,7 +253,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>
|
||||
<button v-if="session.isOwner" :class="{ on: tab === 'system' }" @click="tab = 'system'">登录与存储</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -493,8 +474,8 @@ async function save() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- system:登录方式 / 对象存储 / 管理员账号(原环境变量配置项) -->
|
||||
<div v-if="tab === 'system'" class="panel">
|
||||
<!-- system:登录方式 / 对象存储 / 管理员账号(原环境变量配置项)。站主专属 -->
|
||||
<div v-if="tab === 'system' && session.isOwner" class="panel">
|
||||
<div class="panel-title"><h2>登录方式</h2></div>
|
||||
<p class="field-hint" style="margin: 0 0 14px;">
|
||||
评论区读者的第三方登录。凭据齐全的登录方式自动开放,清空即关闭。
|
||||
@@ -569,25 +550,15 @@ async function save() {
|
||||
<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 只作解锁后路保留(仍可用);不填则维持现状。
|
||||
站主登录后台的用户名(改掉后环境变量 ONE_ADMIN_USER 不再生效)。
|
||||
密码在「账户」页修改;给其他人开账号去「用户」页。
|
||||
</p>
|
||||
<div class="field" style="padding: 0; border: 0; margin-bottom: 8px;">
|
||||
<div class="field" style="padding: 0; border: 0;">
|
||||
<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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user