Files
web_security/app.js
starsac bb9e208381 feat: 添加文档管理系统前后端基础功能
- 新增后端FastAPI应用,包含用户管理、文档管理和操作日志功能
- 实现JWT认证机制,支持用户注册、登录、登出操作
- 添加数据库模型定义,包括用户表、文档表和操作日志表
- 实现文档的增删改查功能及权限控制
- 添加管理员功能,支持用户管理和全局操作日志查看
- 新增前端界面,实现完整的用户交互体验
- 配置环境变量示例和Git忽略规则
- 编写详细的README文档,包含安装和使用说明
2026-01-11 15:56:55 +08:00

617 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// API基础URL
const API_BASE_URL = 'http://localhost:8001';
// 全局状态
let currentUser = null;
let accessToken = localStorage.getItem('accessToken');
let lastApiCalls = {}; // 记录上次API调用时间防止重复请求
// DOM元素
const navMenu = document.getElementById('navMenu');
const userInfo = document.getElementById('userInfo');
const loginBtn = document.getElementById('loginBtn');
const registerBtn = document.getElementById('registerBtn');
const logoutBtn = document.getElementById('logoutBtn');
const loginSection = document.getElementById('loginSection');
const registerSection = document.getElementById('registerSection');
const mainSection = document.getElementById('mainSection');
const loginForm = document.getElementById('loginForm');
const registerForm = document.getElementById('registerForm');
const editProfileForm = document.getElementById('editProfileForm');
const createDocForm = document.getElementById('createDocForm');
const editDocForm = document.getElementById('editDocForm');
const currentUserName = document.getElementById('currentUserName');
const editProfileBtn = document.getElementById('editProfileBtn');
const viewLogsBtn = document.getElementById('viewLogsBtn');
const createDocBtn = document.getElementById('createDocBtn');
const documentsList = document.getElementById('documentsList');
const logsList = document.getElementById('logsList');
const message = document.getElementById('message');
// 模态框相关元素
const editProfileModal = document.getElementById('editProfileModal');
const createDocModal = document.getElementById('createDocModal');
const editDocModal = document.getElementById('editDocModal');
const logsModal = document.getElementById('logsModal');
// 用户管理相关元素
const userManagementSection = document.getElementById('userManagementSection');
const usersList = document.getElementById('usersList');
const refreshUsersBtn = document.getElementById('refreshUsersBtn');
// API请求函数
async function apiRequest(url, options = {}) {
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`;
}
try {
const response = await fetch(`${API_BASE_URL}${url}`, config);
if (response.status === 401) {
// Token过期清除本地存储并重新登录
accessToken = null;
localStorage.removeItem('accessToken');
currentUser = null;
showLoginSection();
showMessage('登录已过期,请重新登录', 'error');
return null;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('API请求错误:', error);
showMessage(`请求失败: ${error.message}`, 'error');
return null;
}
}
// 显示消息提示
function showMessage(text, type = 'info') {
message.textContent = text;
message.className = `message ${type}`;
message.style.display = 'block';
setTimeout(() => {
message.style.display = 'none';
}, 3000);
}
// 用户认证相关函数
async function login(username, password) {
const data = await apiRequest('/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
if (data && data.access_token) {
accessToken = data.access_token;
localStorage.setItem('accessToken', accessToken);
await loadCurrentUser();
showMainSection();
showMessage('登录成功', 'success');
return true;
}
return false;
}
async function register(userData) {
// 将isAdmin添加到userData中
const registrationData = {
...userData,
is_admin: document.getElementById('registerIsAdmin') ? document.getElementById('registerIsAdmin').checked : false
};
const data = await apiRequest('/auth/register', {
method: 'POST',
body: JSON.stringify(registrationData)
});
if (data) {
showMessage('注册成功,请登录', 'success');
// 重置勾选框
if(document.getElementById('registerIsAdmin')) {
document.getElementById('registerIsAdmin').checked = false;
}
showLoginSection();
return true;
}
return false;
}
async function logout() {
await apiRequest('/auth/logout', { method: 'POST' });
accessToken = null;
localStorage.removeItem('accessToken');
currentUser = null;
showLoginSection();
showMessage('已退出登录', 'info');
}
async function loadCurrentUser() {
const data = await apiRequest('/users/me');
if (data) {
currentUser = data;
userInfo.textContent = `欢迎, ${data.username}`;
currentUserName.textContent = data.username;
return true;
}
return false;
}
async function updateUserProfile(updateData) {
const data = await apiRequest('/users/me', {
method: 'PUT',
body: JSON.stringify(updateData)
});
if (data) {
await loadCurrentUser();
showMessage('个人信息更新成功', 'success');
return true;
}
return false;
}
// 文档管理相关函数
async function loadDocuments() {
const data = await apiRequest('/documents');
if (data) {
displayDocuments(data);
return true;
}
return false;
}
async function createDocument(docData) {
const data = await apiRequest('/documents', {
method: 'POST',
body: JSON.stringify(docData)
});
if (data) {
await loadDocuments();
showMessage('文档创建成功', 'success');
return true;
}
return false;
}
async function updateDocument(docId, docData) {
const data = await apiRequest(`/documents/${docId}`, {
method: 'PUT',
body: JSON.stringify(docData)
});
if (data) {
await loadDocuments();
showMessage('文档更新成功', 'success');
return true;
}
return false;
}
async function deleteDocument(docId) {
const data = await apiRequest(`/documents/${docId}`, {
method: 'DELETE'
});
if (data) {
await loadDocuments();
showMessage('文档删除成功', 'success');
return true;
}
return false;
}
// 操作日志相关函数
async function loadLogs() {
const data = await apiRequest('/logs/my');
if (data) {
displayLogs(data);
return true;
}
return false;
}
// 用户管理相关函数
async function loadUsers() {
const data = await apiRequest('/users');
if (data) {
displayUsers(data);
return true;
}
return false;
}
async function deleteUser(userId) {
if (confirm('确定要删除这个用户吗?此操作不可恢复。')) {
const data = await apiRequest(`/users/${userId}`, {
method: 'DELETE'
});
if (data) {
await loadUsers();
showMessage('用户删除成功', 'success');
return true;
}
}
return false;
}
// 界面显示控制函数
function showLoginSection() {
loginSection.style.display = 'block';
registerSection.style.display = 'none';
mainSection.style.display = 'none';
loginBtn.style.display = 'inline-block';
registerBtn.style.display = 'inline-block';
logoutBtn.style.display = 'none';
userInfo.style.display = 'none';
}
function showRegisterSection() {
loginSection.style.display = 'none';
registerSection.style.display = 'block';
mainSection.style.display = 'none';
}
function showMainSection() {
loginSection.style.display = 'none';
registerSection.style.display = 'none';
mainSection.style.display = 'block';
loginBtn.style.display = 'none';
registerBtn.style.display = 'none';
logoutBtn.style.display = 'inline-block';
userInfo.style.display = 'inline-block';
// 检查是否为管理员,如果是则显示用户管理界面
if (currentUser && currentUser.is_admin) {
userManagementSection.style.display = 'block';
loadUsers();
} else {
userManagementSection.style.display = 'none';
}
loadDocuments();
}
// 显示/隐藏模态框函数
function showModal(modal) {
modal.style.display = 'flex';
}
function hideModal(modal) {
modal.style.display = 'none';
}
// 文档显示函数
function displayDocuments(documents) {
if (documents.length === 0) {
documentsList.innerHTML = `
<div class="empty-state">
<h3>暂无文档</h3>
<p>点击"创建新文档"按钮开始创建您的第一个文档</p>
</div>
`;
return;
}
documentsList.innerHTML = documents.map(doc => `
<div class="document-item" onclick="openEditDocumentModal(${doc.id})">
<div class="document-header">
<div>
<div class="document-title">${escapeHtml(doc.title)}</div>
<div class="document-meta">
<span>类型: ${doc.file_type}</span>
<span>大小: ${formatFileSize(doc.file_size)}</span>
<span>${doc.is_public ? '公开' : '私有'}</span>
</div>
</div>
<div class="document-actions">
<button class="btn-secondary" onclick="event.stopPropagation(); openEditDocumentModal(${doc.id})">编辑</button>
</div>
</div>
${doc.description ? `<div class="document-description">${escapeHtml(doc.description)}</div>` : ''}
<div class="document-content-preview">${escapeHtml(doc.content.substring(0, 100))}${doc.content.length > 100 ? '...' : ''}</div>
<div class="document-meta">创建时间: ${formatDate(doc.created_at)}</div>
</div>
`).join('');
}
// 用户显示函数
function displayUsers(users) {
if (users.length === 0) {
usersList.innerHTML = '<div class="empty-state"><h3>暂无用户</h3></div>';
return;
}
usersList.innerHTML = users.map(user => `
<div class="user-item">
<div class="user-header">
<div class="user-info">
<div class="user-username">${escapeHtml(user.username)}</div>
<div class="user-email">${escapeHtml(user.email)}</div>
${user.full_name ? `<div class="user-fullname">${escapeHtml(user.full_name)}</div>` : ''}
<div class="user-meta">
<span>ID: ${user.id}</span>
<span>注册时间: ${formatDate(user.created_at)}</span>
${user.is_admin ? '<span class="user-admin-badge">管理员</span>' : ''}
</div>
</div>
<div class="user-actions">
${user.id !== currentUser.id ? `<button class="btn-danger" onclick="deleteUser(${user.id})">删除</button>` : ''}
</div>
</div>
</div>
`).join('');
}
// 日志显示函数
function displayLogs(logs) {
if (logs.length === 0) {
logsList.innerHTML = '<div class="empty-state"><h3>暂无操作日志</h3></div>';
return;
}
logsList.innerHTML = logs.map(log => `
<div class="log-item">
<div class="log-header">
<span class="log-action">${escapeHtml(log.operation)}</span>
<span class="log-time">${formatDate(log.created_at)}</span>
</div>
<div class="log-details">
详情: ${escapeHtml(log.details || '无')}
</div>
</div>
`).join('');
}
// 工具函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
return new Date(dateString).toLocaleString('zh-CN');
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 模态框操作函数
function openEditProfileModal() {
if (!currentUser) return;
document.getElementById('editEmail').value = currentUser.email || '';
document.getElementById('editFullName').value = currentUser.full_name || '';
document.getElementById('editPassword').value = '';
showModal(editProfileModal);
}
function openCreateDocumentModal() {
document.getElementById('docTitle').value = '';
document.getElementById('docDescription').value = '';
document.getElementById('docContent').value = '';
document.getElementById('docType').value = 'txt';
document.getElementById('docPublic').checked = false;
showModal(createDocModal);
}
async function openEditDocumentModal(docId) {
const data = await apiRequest(`/documents/${docId}`);
if (!data) return;
document.getElementById('editDocId').value = docId;
document.getElementById('editDocTitle').value = data.title;
document.getElementById('editDocDescription').value = data.description || '';
document.getElementById('editDocContent').value = data.content;
document.getElementById('editDocType').value = data.file_type;
document.getElementById('editDocPublic').checked = data.is_public;
showModal(editDocModal);
}
async function openLogsModal() {
await loadLogs();
showModal(logsModal);
}
// 导航按钮事件
loginBtn.addEventListener('click', showLoginSection);
registerBtn.addEventListener('click', showRegisterSection);
logoutBtn.addEventListener('click', logout);
// 表单切换链接
document.getElementById('showRegister').addEventListener('click', (e) => {
e.preventDefault();
showRegisterSection();
});
document.getElementById('showLogin').addEventListener('click', (e) => {
e.preventDefault();
showLoginSection();
});
// 删除文档按钮
document.getElementById('deleteDocBtn').addEventListener('click', async (e) => {
e.preventDefault();
const docId = document.getElementById('editDocId').value;
if (confirm('确定要删除这个文档吗?此操作不可恢复。')) {
const success = await deleteDocument(docId);
if (success) {
hideModal(editDocModal);
}
}
});
// 主界面按钮事件
editProfileBtn.addEventListener('click', openEditProfileModal);
viewLogsBtn.addEventListener('click', openLogsModal);
createDocBtn.addEventListener('click', openCreateDocumentModal);
// 用户管理按钮事件
refreshUsersBtn.addEventListener('click', loadUsers);
// 模态框关闭事件
document.getElementById('closeEditModal').addEventListener('click', () => hideModal(editProfileModal));
document.getElementById('closeCreateDocModal').addEventListener('click', () => hideModal(createDocModal));
document.getElementById('closeEditDocModal').addEventListener('click', () => hideModal(editDocModal));
document.getElementById('closeLogsModal').addEventListener('click', () => hideModal(logsModal));
// 点击模态框外部关闭
[editProfileModal, createDocModal, editDocModal, logsModal].forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
hideModal(modal);
}
});
});
// 键盘事件ESC键关闭模态框
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
[editProfileModal, createDocModal, editDocModal, logsModal].forEach(modal => {
if (modal.style.display === 'flex') {
hideModal(modal);
}
});
}
});
// 表单提交事件
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
await login(username, password);
});
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
const userData = {
username: document.getElementById('registerUsername').value,
email: document.getElementById('registerEmail').value,
full_name: document.getElementById('registerFullName').value,
password: document.getElementById('registerPassword').value,
is_admin: document.getElementById('registerIsAdmin').checked
};
await register(userData);
});
editProfileForm.addEventListener('submit', async (e) => {
e.preventDefault();
const updateData = {
email: document.getElementById('editEmail').value,
full_name: document.getElementById('editFullName').value
};
const newPassword = document.getElementById('editPassword').value;
if (newPassword) {
updateData.password = newPassword;
}
const success = await updateUserProfile(updateData);
if (success) {
hideModal(editProfileModal);
}
});
createDocForm.addEventListener('submit', async (e) => {
e.preventDefault();
const docData = {
title: document.getElementById('docTitle').value,
description: document.getElementById('docDescription').value,
content: document.getElementById('docContent').value,
file_type: document.getElementById('docType').value,
is_public: document.getElementById('docPublic').checked
};
const success = await createDocument(docData);
if (success) {
hideModal(createDocModal);
}
});
editDocForm.addEventListener('submit', async (e) => {
e.preventDefault();
const docId = document.getElementById('editDocId').value;
const docData = {
title: document.getElementById('editDocTitle').value,
description: document.getElementById('editDocDescription').value,
content: document.getElementById('editDocContent').value,
file_type: document.getElementById('editDocType').value,
is_public: document.getElementById('editDocPublic').checked
};
const success = await updateDocument(docId, docData);
if (success) {
hideModal(editDocModal);
}
});
// 事件监听器
let isInitialized = false; // 防止重复初始化
document.addEventListener('DOMContentLoaded', async function() {
// 防止重复初始化
if (isInitialized) {
console.log('页面已初始化,跳过重复初始化');
return;
}
isInitialized = true;
// 检查是否已登录
if (accessToken) {
try {
const success = await loadCurrentUser();
if (success) {
showMainSection();
} else {
// 如果loadCurrentUser返回false说明token无效清除token并显示登录界面
accessToken = null;
localStorage.removeItem('accessToken');
showLoginSection();
}
} catch (error) {
// 捕获可能的错误清除token并显示登录界面
console.error('初始化错误:', error);
accessToken = null;
localStorage.removeItem('accessToken');
showLoginSection();
}
} else {
showLoginSection();
}
});