- 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)
147 lines
5.4 KiB
Vue
147 lines
5.4 KiB
Vue
<script setup>
|
||
import { onMounted, ref } from 'vue'
|
||
import { toastOk, toastErr } from './toast'
|
||
import { adminApi, session } from '../api'
|
||
import { formatDateShort } from '../utils'
|
||
|
||
// 用户管理(站主专属):创建内容管理员(admin 角色)、重置密码、停用 / 删除。
|
||
// owner 行(站主本人)只读展示——它是「谁登录算站主」的锚点,密码走账户页改。
|
||
|
||
const users = ref([])
|
||
const loading = ref(true)
|
||
const error = ref('')
|
||
const creating = ref(false)
|
||
const form = ref({ username: '', password: '' })
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
const data = await adminApi.users()
|
||
users.value = data.users || []
|
||
} catch (e) {
|
||
error.value = e.message || '加载失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
onMounted(load)
|
||
|
||
async function create() {
|
||
if (creating.value) return
|
||
creating.value = true
|
||
try {
|
||
await adminApi.createUser({ username: form.value.username.trim(), password: form.value.password })
|
||
toastOk(`已创建 ${form.value.username.trim()}`)
|
||
form.value = { username: '', password: '' }
|
||
await load()
|
||
} catch (e) {
|
||
toastErr(e.message || '创建失败')
|
||
} finally {
|
||
creating.value = false
|
||
}
|
||
}
|
||
|
||
async function resetPassword(u) {
|
||
const pw = window.prompt(`给「${u.handle}」设置新密码(6-72 位):`)
|
||
if (!pw) return
|
||
try {
|
||
await adminApi.patchUser(u.id, { password: pw })
|
||
toastOk('密码已重置')
|
||
} catch (e) {
|
||
toastErr(e.message || '重置失败')
|
||
}
|
||
}
|
||
|
||
async function toggleBan(u) {
|
||
if (u.banned) {
|
||
try {
|
||
await adminApi.patchUser(u.id, { banned: false })
|
||
toastOk(`已恢复「${u.handle}」`)
|
||
await load()
|
||
} catch (e) {
|
||
toastErr(e.message || '操作失败')
|
||
}
|
||
return
|
||
}
|
||
if (!window.confirm(`停用「${u.handle}」?停用后其立即无法登录,存量会话同步失效。`)) return
|
||
try {
|
||
await adminApi.patchUser(u.id, { banned: true })
|
||
toastOk('已停用')
|
||
await load()
|
||
} catch (e) {
|
||
toastErr(e.message || '操作失败')
|
||
}
|
||
}
|
||
|
||
async function remove(u) {
|
||
if (!window.confirm(`删除「${u.handle}」?其发布的评论保留,但无法再登录。`)) return
|
||
try {
|
||
await adminApi.deleteUser(u.id)
|
||
toastOk('已删除')
|
||
await load()
|
||
} catch (e) {
|
||
toastErr(e.message || '删除失败')
|
||
}
|
||
}
|
||
|
||
const roleLabel = (r) => (r === 'owner' ? '站主' : '管理员')
|
||
</script>
|
||
|
||
<template>
|
||
<section v-if="loading" class="loading">载入中…</section>
|
||
<section v-else-if="error" class="empty">{{ error }}</section>
|
||
<section v-else>
|
||
<h1 style="font-family: var(--serif); font-size: 22px; margin-bottom: 14px;">用户</h1>
|
||
|
||
<div class="panel">
|
||
<div class="panel-title"><h2>添加内容管理员</h2></div>
|
||
<form style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;" @submit.prevent="create">
|
||
<div class="field" style="margin: 0; flex: 1; min-width: 160px;">
|
||
<label>用户名(2-32 位,字母 / 数字 / _ - . @)</label>
|
||
<input v-model="form.username" class="input" spellcheck="false" autocomplete="off" placeholder="editor" />
|
||
</div>
|
||
<div class="field" style="margin: 0; flex: 1; min-width: 160px;">
|
||
<label>初始密码(6-72 位)</label>
|
||
<input v-model="form.password" class="input" type="password" autocomplete="new-password" placeholder="至少 6 位" />
|
||
</div>
|
||
<button class="btn btn-primary" type="submit" :disabled="creating">添加</button>
|
||
</form>
|
||
<p class="field-hint" style="margin-top: 10px;">
|
||
管理员可以写文章、管理评论与文件;系统设置、用户管理与第三方凭据只有你能动。
|
||
他们自己的密码在「账户」页修改。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="panel" style="margin-top: 18px;">
|
||
<div class="panel-title"><h2>后台账号</h2></div>
|
||
<div class="recent-list">
|
||
<div v-for="u in users" :key="u.id" class="recent-item" style="gap: 14px;">
|
||
<span class="title" style="font-weight: 700;">{{ u.handle }}</span>
|
||
<span class="meta" style="font-size: 12px;">{{ roleLabel(u.role) }}</span>
|
||
<span v-if="u.banned" class="meta" style="color: #b4553f;">已停用</span>
|
||
<span class="meta" style="margin-left: auto; white-space: normal; text-align: right;">
|
||
{{ formatDateShort(u.created_at) }}
|
||
</span>
|
||
<span v-if="u.role !== 'owner'" style="display: flex; gap: 4px; flex: none;">
|
||
<button class="btn" style="font-size: 12px; padding: 4px 10px;" type="button" @click="resetPassword(u)">重置密码</button>
|
||
<button class="btn" style="font-size: 12px; padding: 4px 10px;" type="button" @click="toggleBan(u)">
|
||
{{ u.banned ? '恢复' : '停用' }}
|
||
</button>
|
||
<button v-if="u.id !== 0" class="btn danger" style="font-size: 12px; padding: 4px 10px;" type="button" @click="remove(u)">删除</button>
|
||
</span>
|
||
<span v-else class="meta" style="flex: none;">站主本人 · 密码在账户页修改</span>
|
||
</div>
|
||
</div>
|
||
<p class="field-hint" style="margin-top: 12px;">
|
||
当前登录:{{ session.user }}({{ roleLabel(session.role) }})。
|
||
停用或删除后,对方的存量登录会话立即失效。
|
||
</p>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.btn.danger {color: #b4553f;}
|
||
</style>
|