diff --git a/backend/app/api/__pycache__/auth.cpython-310.pyc b/backend/app/api/__pycache__/auth.cpython-310.pyc
index 2540428..b4dd7ca 100644
Binary files a/backend/app/api/__pycache__/auth.cpython-310.pyc and b/backend/app/api/__pycache__/auth.cpython-310.pyc differ
diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py
index 56cf5b5..06a389b 100644
--- a/backend/app/api/auth.py
+++ b/backend/app/api/auth.py
@@ -17,25 +17,13 @@ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depend
"""用户登录"""
user = db.query(User).filter(User.username == form_data.username).first()
- # 检查是否为测试环境
- is_test = os.environ.get("TESTING", "False").lower() == "true"
-
- if is_test:
- # 测试环境:检查用户名和密码
- if not user or form_data.password != "test_password":
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Incorrect username or password",
- headers={"WWW-Authenticate": "Bearer"},
- )
- else:
- # 生产环境:正常验证密码
- if not user or not verify_password(form_data.password, user.password):
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Incorrect username or password",
- headers={"WWW-Authenticate": "Bearer"},
- )
+ # 密码验证:比较用户存储的密码
+ if not user or form_data.password != user.password:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect username or password",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(
diff --git a/backend/test.db b/backend/test.db
index 248dbfd..99c317d 100644
Binary files a/backend/test.db and b/backend/test.db differ
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
index 4fa125d..09594ec 100644
--- a/frontend/eslint.config.js
+++ b/frontend/eslint.config.js
@@ -2,10 +2,12 @@ import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
-import { defineConfig, globalIgnores } from 'eslint/config'
+import { defineConfig } from 'eslint/config'
export default defineConfig([
- globalIgnores(['dist']),
+ {
+ ignores: ['dist', '**/__tests__/**', '**/*.test.{js,jsx}'],
+ },
{
files: ['**/*.{js,jsx}'],
extends: [
@@ -26,4 +28,27 @@ export default defineConfig([
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
+ {
+ files: ['**/__tests__/**', '**/*.test.{js,jsx}', 'src/setupTests.js'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: {
+ ...globals.node,
+ ...globals.jest,
+ describe: 'readonly',
+ test: 'readonly',
+ expect: 'readonly',
+ beforeEach: 'readonly',
+ afterEach: 'readonly',
+ beforeAll: 'readonly',
+ afterAll: 'readonly',
+ jest: 'readonly',
+ },
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ },
])
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f01c281..b73c022 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -29,6 +29,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
+ "identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"vite": "^7.2.4"
@@ -5921,6 +5922,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/harmony-reflect": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
+ "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==",
+ "dev": true,
+ "license": "(Apache-2.0 OR MPL-1.1)"
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -6058,6 +6066,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/identity-obj-proxy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
+ "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "harmony-reflect": "^1.4.6"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 9627657..4952475 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,6 +17,9 @@
"testEnvironment": "jsdom",
"transform": {
"^.+\\.(js|jsx)$": "babel-jest"
+ },
+ "moduleNameMapper": {
+ "\\.(css|less|scss|sass)$": "identity-obj-proxy"
}
},
"dependencies": {
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 2cb23da..8094129 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -20,261 +20,3 @@ body {
display: flex;
flex-direction: column;
}
-
-/* 导航栏 */
-.navbar {
- background-color: #333;
- color: white;
- padding: 1rem 2rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.navbar-brand a {
- color: white;
- text-decoration: none;
- font-size: 1.5rem;
- font-weight: bold;
-}
-
-.navbar-links {
- display: flex;
- gap: 1.5rem;
- align-items: center;
-}
-
-.navbar-links a {
- color: white;
- text-decoration: none;
- font-size: 1rem;
-}
-
-.navbar-links a:hover {
- text-decoration: underline;
-}
-
-.logout-button {
- background-color: #f44336;
- color: white;
- border: none;
- padding: 0.5rem 1rem;
- border-radius: 4px;
- cursor: pointer;
- font-size: 1rem;
-}
-
-.logout-button:hover {
- background-color: #d32f2f;
-}
-
-/* 主内容区域 */
-.main-content {
- flex: 1;
- padding: 2rem;
- max-width: 1200px;
- margin: 0 auto;
- width: 100%;
-}
-
-/* 登录页面 */
-.login-container {
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 80vh;
-}
-
-.login-form {
- background-color: white;
- padding: 2rem;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- width: 100%;
- max-width: 400px;
-}
-
-.login-form h2 {
- margin-bottom: 1.5rem;
- text-align: center;
- color: #333;
-}
-
-.form-group {
- margin-bottom: 1rem;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- color: #555;
-}
-
-.form-group input {
- width: 100%;
- padding: 0.75rem;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 1rem;
-}
-
-.login-button {
- width: 100%;
- padding: 0.75rem;
- background-color: #4caf50;
- color: white;
- border: none;
- border-radius: 4px;
- font-size: 1rem;
- cursor: pointer;
- margin-top: 1rem;
-}
-
-.login-button:hover {
- background-color: #45a049;
-}
-
-.login-button:disabled {
- background-color: #cccccc;
- cursor: not-allowed;
-}
-
-.error-message {
- background-color: #ffebee;
- color: #c62828;
- padding: 0.75rem;
- border-radius: 4px;
- margin-bottom: 1rem;
-}
-
-/* 项目列表 */
-.project-list {
- margin-top: 2rem;
-}
-
-.project-list h2 {
- margin-bottom: 1.5rem;
- color: #333;
-}
-
-.projects-container {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 1.5rem;
-}
-
-.project-card {
- background-color: white;
- padding: 1.5rem;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
-}
-
-.project-card h3 {
- margin-bottom: 0.75rem;
- color: #333;
-}
-
-.project-card p {
- margin-bottom: 1rem;
- color: #666;
- line-height: 1.4;
-}
-
-.project-details {
- margin-bottom: 1rem;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.project-details span {
- font-size: 0.9rem;
- color: #555;
-}
-
-.project-actions {
- display: flex;
- gap: 0.75rem;
- margin-top: 1rem;
-}
-
-.edit-button {
- padding: 0.5rem 1rem;
- background-color: #2196f3;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.9rem;
-}
-
-.edit-button:hover {
- background-color: #0b7dda;
-}
-
-.delete-button {
- padding: 0.5rem 1rem;
- background-color: #f44336;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.9rem;
-}
-
-.delete-button:hover {
- background-color: #d32f2f;
-}
-
-.empty-message {
- grid-column: 1 / -1;
- text-align: center;
- padding: 3rem;
- color: #666;
- background-color: white;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
-}
-
-/* 加载和错误状态 */
-.loading {
- text-align: center;
- padding: 3rem;
- color: #666;
-}
-
-.error {
- text-align: center;
- padding: 3rem;
- color: #c62828;
- background-color: #ffebee;
- border-radius: 8px;
- margin: 2rem 0;
-}
-
-/* 响应式设计 */
-@media (max-width: 768px) {
- .navbar {
- flex-direction: column;
- gap: 1rem;
- padding: 1rem;
- }
-
- .navbar-links {
- flex-direction: column;
- gap: 1rem;
- }
-
- .main-content {
- padding: 1rem;
- }
-
- .projects-container {
- grid-template-columns: 1fr;
- }
-
- .login-form {
- padding: 1.5rem;
- }
-}
\ No newline at end of file
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 683da7b..51ff0a4 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,41 +1,77 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
-import Navbar from './components/Navbar';
+import Layout from './components/Layout';
import Login from './components/Login';
-import ProjectList from './components/ProjectList';
+import ProjectManagement from './components/ProjectManagement';
+import ProjectDetail from './components/ProjectDetail';
+import ProjectForm from './components/ProjectForm';
+import UserManagement from './components/UserManagement';
+import Statistics from './components/Statistics';
import './App.css';
-function App() {
+function PrivateRoute({ children }) {
const isAuthenticated = !!localStorage.getItem('token');
+ return isAuthenticated ? children :