多用户与角色: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:
Sakurasan committed 2026-10-01 22:35:37 +08:00
1 parent e5dee4daf1
commit 4bb2ff4145
23 files changed
+917 -222

No files matched your search

+48 -2
View File
@@ -18,13 +18,39 @@ const uploading = ref(false)
const regBusy = ref(false)
const fileInput = ref(null)
// 修改登录密码(owner / admin 都在这里改;密码存在自己的账号行上)
const pw = ref({ old: '', next: '', confirm: '' })
const pwBusy = ref(false)
async function changePassword() {
if (pw.value.next !== pw.value.confirm) {
toastErr('两次输入的新密码不一致')
return
}
if (pw.value.next.length < 6) {
toastErr('新密码至少 6 位')
return
}
pwBusy.value = true
try {
await adminApi.changePassword({ old_password: pw.value.old, new_password: pw.value.next })
toastOk('密码已更新,下次登录用新密码')
pw.value = { old: '', next: '', confirm: '' }
} catch (e) {
toastErr(e.message || '修改失败')
} finally {
pwBusy.value = false
}
}
const route = useRoute()
const router = useRouter()
async function load() {
console.warn('ACCT: load() start')
loading.value = true
try {
acct.value = await adminApi.account()
console.warn('ACCT: loaded', !!acct.value)
} catch (e) {
toastErr(e.message || '读取账户信息失败')
} finally {
@@ -185,8 +211,8 @@ onMounted(async () => {
<label>用户名(handle)</label>
<input :value="acct.handle" class="input" disabled />
<p class="field-hint">
登录用户名在「设置 → 登录与存储」里改;这里是站主在评论区的
身份标识(handle),改它要连登录名一起换。
后台账号的用户名即登录名:站主的在「设置 → 登录与存储」里改,
管理员账号由站主在「用户」页创建。
</p>
</div>
@@ -195,6 +221,26 @@ onMounted(async () => {
</button>
</div>
<!-- ---------- 登录密码 ---------- -->
<div class="panel" style="margin-top: 18px;">
<div class="panel-title"><h2>登录密码</h2></div>
<div class="field">
<label>旧密码</label>
<input v-model="pw.old" class="input" type="password" autocomplete="current-password" placeholder="从未设过密码可不填" />
</div>
<div class="field">
<label>新密码(6-72 位)</label>
<input v-model="pw.next" class="input" type="password" autocomplete="new-password" />
</div>
<div class="field">
<label>确认新密码</label>
<input v-model="pw.confirm" class="input" type="password" autocomplete="new-password" />
</div>
<button class="btn btn-primary" :disabled="pwBusy" @click="changePassword">
{{ pwBusy ? '提交中…' : '更新密码' }}
</button>
</div>
<!-- ---------- 登录方式 ---------- -->
<div class="panel" style="margin-top: 18px;">
<div class="panel-title"><h2>登录方式</h2></div>