feat:1.2.0初始版本未自测
This commit is contained in:
@@ -5,7 +5,7 @@ using Hua.Todo.Application.CloudSync;
|
||||
using Hua.Todo.Application.DynamicApi;
|
||||
using Hua.Todo.Application.DynamicApi.Swagger;
|
||||
using Hua.Todo.Application.Interfaces;
|
||||
using Hua.Todo.Application.Models;
|
||||
using Hua.Todo.Application.CloudSync.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -30,11 +30,24 @@ builder.Services.AddCors(options =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Apply database migrations
|
||||
// Apply database migrations and seed default admin
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<Hua.Todo.Application.Data.TodoDbContext>();
|
||||
var services = scope.ServiceProvider;
|
||||
var dbContext = services.GetRequiredService<Hua.Todo.Application.Data.TodoDbContext>();
|
||||
dbContext.Database.Migrate();
|
||||
|
||||
// Seed default admin from configuration
|
||||
var config = services.GetRequiredService<IConfiguration>();
|
||||
var adminConfig = config.GetSection("DefaultAdmin");
|
||||
var adminUserName = adminConfig["UserName"];
|
||||
var adminPassword = adminConfig["Password"];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(adminUserName) && !string.IsNullOrWhiteSpace(adminPassword))
|
||||
{
|
||||
var authService = services.GetRequiredService<CloudAuthService>();
|
||||
await authService.BootstrapAdminAsync(adminUserName, adminPassword, "system", "seeding", CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
@@ -44,9 +57,14 @@ if (app.Environment.IsDevelopment())
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseCors("AllowAll");
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// 简单的管理后台入口跳转
|
||||
app.MapGet("/admin", () => Results.Redirect("/admin/index.html"));
|
||||
|
||||
app.MapCloudSyncEndpoints();
|
||||
app.UseDynamicApi();
|
||||
|
||||
|
||||
@@ -5,5 +5,9 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"DefaultAdmin": {
|
||||
"UserName": "admin",
|
||||
"Password": "ChangeMe@123"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hua.Todo 管理后台</title>
|
||||
<!-- Element Plus & Vue 3 via CDN -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://unpkg.com/element-plus"></script>
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body { margin: 0; font-family: -apple-system, sans-serif; background: #f5f7fa; }
|
||||
#app { height: 100vh; display: flex; flex-direction: column; }
|
||||
.header { background: #409EFF; color: white; padding: 0 20px; height: 60px; line-height: 60px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.main-container { flex: 1; overflow: hidden; display: flex; }
|
||||
.sidebar { width: 200px; background: #fff; border-right: 1px solid #e6e6e6; }
|
||||
.content { flex: 1; padding: 20px; overflow-y: auto; }
|
||||
.login-container { height: 100vh; display: flex; justify-content: center; align-items: center; background: #f5f7fa; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.tag-admin { background-color: #f56c6c !important; border-color: #f56c6c !important; color: white !important; }
|
||||
.audit-failed { color: #f56c6c; }
|
||||
.audit-success { color: #67c23a; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 登录界面 -->
|
||||
<div v-if="!isLoggedIn" class="login-container">
|
||||
<el-card style="width: 400px;">
|
||||
<template #header>
|
||||
<div style="text-align: center; font-weight: bold;">管理员登录</div>
|
||||
</template>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="loginForm.userName" prefix-icon="User"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="loginForm.password" type="password" prefix-icon="Lock" show-password @keyup.enter="handleLogin"></el-input>
|
||||
</el-form-item>
|
||||
<el-button type="primary" style="width: 100%" @click="handleLogin" :loading="loggingIn">登录</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 主界面 -->
|
||||
<template v-else>
|
||||
<div class="header">
|
||||
<div style="font-size: 1.2rem; font-weight: bold;">Hua.Todo 管理后台 <small style="font-size: 0.8rem; opacity: 0.8;">v1.2.0</small></div>
|
||||
<div>
|
||||
<span style="margin-right: 15px;">欢迎, {{ currentUser.userName }}</span>
|
||||
<el-button type="info" size="small" @click="refreshAll">刷新</el-button>
|
||||
<el-button type="danger" size="small" @click="handleLogout">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<div class="sidebar">
|
||||
<el-menu :default-active="activeTab" @select="handleTabSelect" style="border-right: none;">
|
||||
<el-menu-item index="users">用户管理</el-menu-item>
|
||||
<el-menu-item index="sessions">会话管理</el-menu-item>
|
||||
<el-menu-item index="logs">审计日志</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- 用户管理 -->
|
||||
<div v-if="activeTab === 'users'">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户列表</span>
|
||||
<el-button type="primary" @click="showCreateUserDialog = true">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="users" v-loading="loadingUsers" style="width: 100%">
|
||||
<el-table-column prop="userName" label="用户名" width="180"></el-table-column>
|
||||
<el-table-column prop="role" label="角色" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :class="scope.row.role === 'admin' ? 'tag-admin' : ''">{{ scope.row.role }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAtUtc" label="创建时间" width="200">
|
||||
<template #default="scope">{{ formatDate(scope.row.createdAtUtc) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleResetPassword(scope.row)">重置密码</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDeleteUser(scope.row)" :disabled="scope.row.role === 'admin'">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 会话管理 -->
|
||||
<div v-if="activeTab === 'sessions'">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>当前在线会话</span>
|
||||
<el-button size="small" @click="fetchSessions">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="sessions" v-loading="loadingSessions" style="width: 100%">
|
||||
<el-table-column prop="userName" label="用户名" width="150"></el-table-column>
|
||||
<el-table-column prop="isSteppedUp" label="已二次认证" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.isSteppedUp ? 'success' : 'info'">{{ scope.row.isSteppedUp ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expiresAtUtc" label="过期时间" width="200">
|
||||
<template #default="scope">{{ formatDate(scope.row.expiresAtUtc) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="warning" @click="handleRevokeSession(scope.row)">强制下线</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 审计日志 -->
|
||||
<div v-if="activeTab === 'logs'">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>最近 100 条审计日志</span>
|
||||
<el-button size="small" @click="fetchLogs">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="logs" v-loading="loadingLogs" stripe style="width: 100%; font-size: 0.85rem;">
|
||||
<el-table-column prop="timestampUtc" label="时间" width="160">
|
||||
<template #default="scope">{{ formatDate(scope.row.timestampUtc) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="eventType" label="事件" width="140"></el-table-column>
|
||||
<el-table-column prop="userName" label="用户" width="120"></el-table-column>
|
||||
<el-table-column prop="description" label="描述"></el-table-column>
|
||||
<el-table-column prop="isSuccess" label="结果" width="80">
|
||||
<template #default="scope">
|
||||
<span :class="scope.row.isSuccess ? 'audit-success' : 'audit-failed'">{{ scope.row.isSuccess ? '成功' : '失败' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="clientIp" label="IP" width="120"></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 新增用户对话框 -->
|
||||
<el-dialog v-model="showCreateUserDialog" title="新增用户" width="400px">
|
||||
<el-form :model="newUser" label-width="80px">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="newUser.userName"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="newUser.password" type="password" show-password></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="newUser.role" style="width: 100%">
|
||||
<el-option label="普通用户" value="user"></el-option>
|
||||
<el-option label="管理员" value="admin"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreateUserDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleCreateUser" :loading="creatingUser">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref, reactive, onMounted } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const activeTab = ref('users');
|
||||
const isLoggedIn = ref(false);
|
||||
const loggingIn = ref(false);
|
||||
const loginForm = reactive({ userName: '', password: '' });
|
||||
const currentUser = ref({ userName: '', role: '' });
|
||||
|
||||
const users = ref([]);
|
||||
const sessions = ref([]);
|
||||
const logs = ref([]);
|
||||
const loadingUsers = ref(false);
|
||||
const loadingSessions = ref(false);
|
||||
const loadingLogs = ref(false);
|
||||
const showCreateUserDialog = ref(false);
|
||||
const creatingUser = ref(false);
|
||||
const newUser = reactive({ userName: '', password: '', role: 'user' });
|
||||
|
||||
// Axios 拦截器:注入 Token
|
||||
axios.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Axios 拦截器:处理鉴权失败
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
|
||||
handleLogout();
|
||||
ElementPlus.ElMessage.error('会话已失效或权限不足,请重新登录');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
const user = localStorage.getItem('admin_user');
|
||||
if (token && user) {
|
||||
isLoggedIn.value = true;
|
||||
currentUser.value = JSON.parse(user);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginForm.userName || !loginForm.password) {
|
||||
return ElementPlus.ElMessage.warning('请输入用户名和密码');
|
||||
}
|
||||
loggingIn.value = true;
|
||||
try {
|
||||
const res = await axios.post('/auth/login', loginForm);
|
||||
const data = res.data;
|
||||
|
||||
if (data.role !== 'admin') {
|
||||
throw new Error('只有管理员可以访问此后台');
|
||||
}
|
||||
|
||||
localStorage.setItem('admin_token', data.accessToken);
|
||||
localStorage.setItem('admin_user', JSON.stringify({ userName: loginForm.userName, role: data.role }));
|
||||
|
||||
isLoggedIn.value = true;
|
||||
currentUser.value = { userName: loginForm.userName, role: data.role };
|
||||
loginForm.password = '';
|
||||
|
||||
ElementPlus.ElMessage.success('登录成功');
|
||||
refreshAll();
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error(e.message || '登录失败,请检查凭据');
|
||||
} finally {
|
||||
loggingIn.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_user');
|
||||
isLoggedIn.value = false;
|
||||
currentUser.value = { userName: '', role: '' };
|
||||
};
|
||||
|
||||
const fetchUsers = async () => {
|
||||
if (!isLoggedIn.value) return;
|
||||
loadingUsers.value = true;
|
||||
try {
|
||||
const res = await axios.get('/admin/users');
|
||||
users.value = res.data;
|
||||
} catch (e) {} finally {
|
||||
loadingUsers.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSessions = async () => {
|
||||
if (!isLoggedIn.value) return;
|
||||
loadingSessions.value = true;
|
||||
try {
|
||||
const res = await axios.get('/admin/sessions');
|
||||
sessions.value = res.data;
|
||||
} catch (e) {} finally {
|
||||
loadingSessions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLogs = async () => {
|
||||
if (!isLoggedIn.value) return;
|
||||
loadingLogs.value = true;
|
||||
try {
|
||||
const res = await axios.get('/admin/audit-logs?count=100');
|
||||
logs.value = res.data;
|
||||
} catch (e) {} finally {
|
||||
loadingLogs.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabSelect = (index) => {
|
||||
activeTab.value = index;
|
||||
if (index === 'users') fetchUsers();
|
||||
if (index === 'sessions') fetchSessions();
|
||||
if (index === 'logs') fetchLogs();
|
||||
};
|
||||
|
||||
const handleCreateUser = async () => {
|
||||
if (!newUser.userName || !newUser.password) {
|
||||
return ElementPlus.ElMessage.warning('请填写完整信息');
|
||||
}
|
||||
creatingUser.value = true;
|
||||
try {
|
||||
await axios.post('/admin/users', newUser);
|
||||
ElementPlus.ElMessage.success('创建用户成功');
|
||||
showCreateUserDialog.value = false;
|
||||
newUser.userName = '';
|
||||
newUser.password = '';
|
||||
fetchUsers();
|
||||
} catch (e) {} finally {
|
||||
creatingUser.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPassword = async (user) => {
|
||||
try {
|
||||
const { value: newPass } = await ElementPlus.ElMessageBox.prompt(
|
||||
`请输入用户 [${user.userName}] 的新密码`, '重置密码', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'password'
|
||||
});
|
||||
if (newPass) {
|
||||
await axios.post(`/admin/users/${user.id}/reset-password`, { newPassword: newPass });
|
||||
ElementPlus.ElMessage.success('重置成功');
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (user) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm(
|
||||
`确定删除用户 [${user.userName}] 吗?此操作不可逆!`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
await axios.delete(`/admin/users/${user.id}`);
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
fetchUsers();
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const handleRevokeSession = async (session) => {
|
||||
try {
|
||||
await axios.delete(`/admin/sessions/${session.id}`);
|
||||
ElementPlus.ElMessage.success('已强制下线');
|
||||
fetchSessions();
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const refreshAll = () => {
|
||||
fetchUsers();
|
||||
fetchSessions();
|
||||
fetchLogs();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (checkAuth()) {
|
||||
refreshAll();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
activeTab, users, sessions, logs,
|
||||
loadingUsers, loadingSessions, loadingLogs,
|
||||
showCreateUserDialog, creatingUser, newUser,
|
||||
isLoggedIn, loggingIn, loginForm, currentUser,
|
||||
handleTabSelect, handleCreateUser, handleResetPassword, handleDeleteUser, handleRevokeSession,
|
||||
handleLogin, handleLogout,
|
||||
formatDate, fetchSessions, fetchLogs, refreshAll
|
||||
};
|
||||
}
|
||||
}).use(ElementPlus).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user