diff --git a/backend/app/__init__.py b/backend/app/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/backend/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/app/config.py b/backend/app/config.py index ba16b7a..e44a716 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -6,4 +6,7 @@ load_dotenv() DATABASE_URL = os.getenv( "DATABASE_URL", "postgresql+asyncpg://postgres:password@localhost:5432/hadoop_fault_db", -) \ No newline at end of file +) + +JWT_SECRET = os.getenv("JWT_SECRET", "dev-secret") +JWT_EXPIRE_MINUTES = int(os.getenv("JWT_EXPIRE_MINUTES", "60")) diff --git a/backend/app/db.py b/backend/app/db.py index 7b09034..5c1fcd9 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -1,5 +1,5 @@ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession -from app.config import DATABASE_URL +from .config import DATABASE_URL engine = create_async_engine(DATABASE_URL, echo=False, pool_pre_ping=True) SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) @@ -7,4 +7,4 @@ SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSe async def get_db() -> AsyncSession: """获取一个异步数据库会话,用于依赖注入。""" async with SessionLocal() as session: - yield session \ No newline at end of file + yield session diff --git a/backend/app/main.py b/backend/app/main.py index 1e60dd6..9630e79 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.routers import health, clusters, faults, logs +from .routers import auth, health app = FastAPI(title="Hadoop Fault Detecting API", version="v1") @@ -13,6 +13,4 @@ app.add_middleware( ) app.include_router(health.router, prefix="/api/v1") -app.include_router(clusters.router, prefix="/api/v1") -app.include_router(faults.router, prefix="/api/v1") -app.include_router(logs.router, prefix="/api/v1") \ No newline at end of file +app.include_router(auth.router, prefix="/api/v1") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 07797bd..88d1930 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column - -class Base(DeclarativeBase): - pass \ No newline at end of file +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass diff --git a/backend/app/models/clusters.py b/backend/app/models/clusters.py deleted file mode 100644 index 6e1733e..0000000 --- a/backend/app/models/clusters.py +++ /dev/null @@ -1,34 +0,0 @@ -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy import String, Integer -from sqlalchemy.dialects.postgresql import UUID, JSONB -from sqlalchemy import TIMESTAMP -from app.models import Base - -class Cluster(Base): - __tablename__ = "clusters" - - id: Mapped[int] = mapped_column(primary_key=True) - uuid: Mapped[str] = mapped_column(UUID(as_uuid=False), unique=True) - name: Mapped[str] = mapped_column(String(100), unique=True) - type: Mapped[str] = mapped_column(String(50)) - node_count: Mapped[int] = mapped_column(Integer, default=0) - health_status: Mapped[str] = mapped_column(String(20), default="unknown") - description: Mapped[str | None] = mapped_column(String, nullable=True) - config_info: Mapped[dict | None] = mapped_column(JSONB, nullable=True) - created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) - updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) - - def to_dict(self) -> dict: - """将集群对象转换为可序列化字典。""" - return { - "id": self.id, - "uuid": self.uuid, - "name": self.name, - "type": self.type, - "node_count": self.node_count, - "health_status": self.health_status, - "description": self.description, - "config_info": self.config_info, - "created_at": self.created_at.isoformat() if self.created_at else None, - "updated_at": self.updated_at.isoformat() if self.updated_at else None, - } \ No newline at end of file diff --git a/backend/app/models/exec_logs.py b/backend/app/models/exec_logs.py deleted file mode 100644 index 0da859f..0000000 --- a/backend/app/models/exec_logs.py +++ /dev/null @@ -1,39 +0,0 @@ -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy import String, Integer -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy import TIMESTAMP, Text -from app.models import Base - -class ExecLog(Base): - __tablename__ = "exec_logs" - - id: Mapped[int] = mapped_column(primary_key=True) - exec_id: Mapped[str] = mapped_column(String(32), unique=True) - fault_id: Mapped[str] = mapped_column(String(32)) - command_type: Mapped[str] = mapped_column(String(50)) - script_path: Mapped[str | None] = mapped_column(String(255), nullable=True) - command_content: Mapped[str] = mapped_column(Text) - target_nodes: Mapped[dict | None] = mapped_column(JSONB, nullable=True) - risk_level: Mapped[str] = mapped_column(String(20), default="medium") - execution_status: Mapped[str] = mapped_column(String(20), default="pending") - start_time: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True) - end_time: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True) - duration: Mapped[int | None] = mapped_column(Integer, nullable=True) - stdout_log: Mapped[str | None] = mapped_column(Text, nullable=True) - stderr_log: Mapped[str | None] = mapped_column(Text, nullable=True) - exit_code: Mapped[int | None] = mapped_column(Integer, nullable=True) - operator: Mapped[str] = mapped_column(String(50), default="system") - created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) - updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) - - def to_dict(self) -> dict: - """将执行日志转换为可序列化字典。""" - return { - "exec_id": self.exec_id, - "fault_id": self.fault_id, - "command_type": self.command_type, - "execution_status": self.execution_status, - "start_time": self.start_time.isoformat() if self.start_time else None, - "end_time": self.end_time.isoformat() if self.end_time else None, - "exit_code": self.exit_code, - } \ No newline at end of file diff --git a/backend/app/models/fault_records.py b/backend/app/models/fault_records.py deleted file mode 100644 index b933d04..0000000 --- a/backend/app/models/fault_records.py +++ /dev/null @@ -1,38 +0,0 @@ -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy import String -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy import TIMESTAMP -from app.models import Base - -class FaultRecord(Base): - __tablename__ = "fault_records" - - id: Mapped[int] = mapped_column(primary_key=True) - fault_id: Mapped[str] = mapped_column(String(32), unique=True) - cluster_id: Mapped[int | None] = mapped_column(nullable=True) - fault_type: Mapped[str] = mapped_column(String(50)) - fault_level: Mapped[str] = mapped_column(String(20), default="medium") - title: Mapped[str] = mapped_column(String(200)) - description: Mapped[str | None] = mapped_column(String, nullable=True) - affected_nodes: Mapped[dict | None] = mapped_column(JSONB, nullable=True) - affected_clusters: Mapped[dict | None] = mapped_column(JSONB, nullable=True) - root_cause: Mapped[str | None] = mapped_column(String, nullable=True) - repair_suggestion: Mapped[str | None] = mapped_column(String, nullable=True) - status: Mapped[str] = mapped_column(String(20), default="detected") - assignee: Mapped[str | None] = mapped_column(String(50), nullable=True) - reporter: Mapped[str] = mapped_column(String(50), default="system") - created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) - updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) - resolved_at: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True) - - def to_dict(self) -> dict: - """将故障记录转换为可序列化字典。""" - return { - "fault_id": self.fault_id, - "cluster_id": self.cluster_id, - "fault_type": self.fault_type, - "fault_level": self.fault_level, - "title": self.title, - "status": self.status, - "created_at": self.created_at.isoformat() if self.created_at else None, - } \ No newline at end of file diff --git a/backend/app/models/system_logs.py b/backend/app/models/system_logs.py deleted file mode 100644 index 21a9ce4..0000000 --- a/backend/app/models/system_logs.py +++ /dev/null @@ -1,33 +0,0 @@ -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy import String, Boolean -from sqlalchemy import TIMESTAMP, Text -from app.models import Base - -class SystemLog(Base): - __tablename__ = "system_logs" - - id: Mapped[int] = mapped_column(primary_key=True) - log_id: Mapped[str] = mapped_column(String(32), unique=True) - fault_id: Mapped[str | None] = mapped_column(String(32), nullable=True) - cluster_id: Mapped[int | None] = mapped_column(nullable=True) - timestamp: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) - host: Mapped[str] = mapped_column(String(100)) - service: Mapped[str] = mapped_column(String(50)) - log_level: Mapped[str] = mapped_column(String(10)) - message: Mapped[str] = mapped_column(Text) - exception: Mapped[str | None] = mapped_column(Text, nullable=True) - raw_log: Mapped[str | None] = mapped_column(Text, nullable=True) - processed: Mapped[bool] = mapped_column(Boolean, default=False) - created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) - - def to_dict(self) -> dict: - """将系统日志转换为可序列化字典。""" - return { - "log_id": self.log_id, - "cluster_id": self.cluster_id, - "timestamp": self.timestamp.isoformat() if self.timestamp else None, - "service": self.service, - "log_level": self.log_level, - "message": self.message, - "processed": self.processed, - } \ No newline at end of file diff --git a/backend/app/models/users.py b/backend/app/models/users.py new file mode 100644 index 0000000..b6092eb --- /dev/null +++ b/backend/app/models/users.py @@ -0,0 +1,17 @@ +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Boolean +from sqlalchemy import TIMESTAMP +from . import Base + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(primary_key=True) + username: Mapped[str] = mapped_column(String(50), unique=True) + email: Mapped[str] = mapped_column(String(100), unique=True) + password_hash: Mapped[str] = mapped_column(String(255)) + full_name: Mapped[str] = mapped_column(String(100)) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + last_login: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True) + created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) + updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..eafa83b --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, update, func +from ..db import get_db +from ..models.users import User +from passlib.hash import bcrypt + +router = APIRouter() + +class LoginRequest(BaseModel): + username: str + password: str + +@router.post("/user/login") +async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)): + """处理登录请求,验证用户名与密码。""" + result = await db.execute(select(User).where(User.username == req.username).limit(1)) + user = result.scalars().first() + if not user: + raise HTTPException(status_code=401, detail="invalid_credentials") + if not user.is_active: + raise HTTPException(status_code=403, detail="inactive_user") + if not bcrypt.verify(req.password, user.password_hash): + raise HTTPException(status_code=401, detail="invalid_credentials") + await db.execute( + update(User).where(User.id == user.id).values(last_login=func.now(), updated_at=func.now()) + ) + await db.commit() + return {"ok": True, "username": user.username, "fullName": user.full_name} diff --git a/backend/app/routers/clusters.py b/backend/app/routers/clusters.py deleted file mode 100644 index 9dd894f..0000000 --- a/backend/app/routers/clusters.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from app.db import get_db -from app.models.clusters import Cluster - -router = APIRouter() - -@router.get("/clusters") -async def list_clusters(db: AsyncSession = Depends(get_db)): - """查询集群列表。""" - result = await db.execute(select(Cluster).limit(100)) - rows = result.scalars().all() - return {"total": len(rows), "list": [c.to_dict() for c in rows]} \ No newline at end of file diff --git a/backend/app/routers/faults.py b/backend/app/routers/faults.py deleted file mode 100644 index 0d044a8..0000000 --- a/backend/app/routers/faults.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from app.db import get_db -from app.models.fault_records import FaultRecord - -router = APIRouter() - -@router.get("/faults") -async def list_faults(db: AsyncSession = Depends(get_db)): - """查询故障记录。""" - result = await db.execute(select(FaultRecord).limit(100)) - rows = result.scalars().all() - return {"total": len(rows), "list": [f.to_dict() for f in rows]} \ No newline at end of file diff --git a/backend/app/routers/health.py b/backend/app/routers/health.py index e6051bc..dcf4653 100644 --- a/backend/app/routers/health.py +++ b/backend/app/routers/health.py @@ -1,11 +1,8 @@ -from fastapi import APIRouter, Depends -from sqlalchemy.ext.asyncio import AsyncSession -from app.db import get_db +from fastapi import APIRouter router = APIRouter() @router.get("/health") -async def health_check(db: AsyncSession = Depends(get_db)): - """健康检查:测试数据库连通性。""" - await db.execute("SELECT 1") - return {"status": "ok"} \ No newline at end of file +async def health_check(): + """简单健康检查,用于开发阶段验证服务可用。""" + return {"status": "ok"} diff --git a/backend/app/routers/logs.py b/backend/app/routers/logs.py deleted file mode 100644 index c8566d4..0000000 --- a/backend/app/routers/logs.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import APIRouter, Depends, Query -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from app.db import get_db -from app.models.system_logs import SystemLog - -router = APIRouter() - -@router.get("/logs") -async def list_logs( - db: AsyncSession = Depends(get_db), - level: str | None = Query(None), - page: int = Query(1, ge=1), - pageSize: int = Query(10, ge=1, le=100), -): - """查询系统日志,支持按级别筛选与分页。""" - stmt = select(SystemLog) - if level: - stmt = stmt.where(SystemLog.log_level == level) - stmt = stmt.offset((page - 1) * pageSize).limit(pageSize) - result = await db.execute(stmt) - rows = result.scalars().all() - return {"total": len(rows), "list": [l.to_dict() for l in rows]} \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..534317a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +SQLAlchemy +asyncpg +python-dotenv +passlib[bcrypt] diff --git a/doc/project/数据库建表脚本_postgres.sql b/doc/project/数据库建表脚本_postgres.sql index 98c2361..6d0139a 100644 --- a/doc/project/数据库建表脚本_postgres.sql +++ b/doc/project/数据库建表脚本_postgres.sql @@ -476,7 +476,7 @@ INSERT INTO user_cluster_mapping (user_id, cluster_id, role_id) SELECT u.id, c.id, r.id FROM users u, clusters c, roles r WHERE u.username = 'admin' AND c.name = 'Hadoop主集群' AND r.role_key = 'cluster_admin'; INSERT INTO user_cluster_mapping (user_id, cluster_id, role_id) SELECT u.id, c.id, r.id FROM users u, clusters c, roles r WHERE u.username = 'admin' AND c.name = 'Hadoop测试集群' AND r.role_key = 'cluster_admin'; - + -- ===================================================== -- 脚本执行完成 -- ===================================================== diff --git a/frontend-vue/package-lock.json b/frontend-vue/package-lock.json index 6e94841..619b835 100644 --- a/frontend-vue/package-lock.json +++ b/frontend-vue/package-lock.json @@ -1411,6 +1411,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1425,6 +1426,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -1484,6 +1486,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", diff --git a/src/backend/app/db.py b/src/backend/app/db.py index 9297ae3..5c1fcd9 100644 --- a/src/backend/app/db.py +++ b/src/backend/app/db.py @@ -7,4 +7,4 @@ SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSe async def get_db() -> AsyncSession: """获取一个异步数据库会话,用于依赖注入。""" async with SessionLocal() as session: - yield session \ No newline at end of file + yield session diff --git a/src/backend/app/main.py b/src/backend/app/main.py index 45c5af2..a283e66 100644 --- a/src/backend/app/main.py +++ b/src/backend/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from .routers import health, clusters, faults, logs +from .routers import user app = FastAPI(title="Hadoop Fault Detecting API", version="v1") @@ -12,7 +12,4 @@ app.add_middleware( allow_headers=["*"], ) -app.include_router(health.router, prefix="/api/v1") -app.include_router(clusters.router, prefix="/api/v1") -app.include_router(faults.router, prefix="/api/v1") -app.include_router(logs.router, prefix="/api/v1") \ No newline at end of file +app.include_router(user.router, prefix="/api/v1") diff --git a/src/backend/app/models/__init__.py b/src/backend/app/models/__init__.py index ac5d784..f75ec44 100644 --- a/src/backend/app/models/__init__.py +++ b/src/backend/app/models/__init__.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass diff --git a/src/backend/app/models/users.py b/src/backend/app/models/users.py new file mode 100644 index 0000000..b6092eb --- /dev/null +++ b/src/backend/app/models/users.py @@ -0,0 +1,17 @@ +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Boolean +from sqlalchemy import TIMESTAMP +from . import Base + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(primary_key=True) + username: Mapped[str] = mapped_column(String(50), unique=True) + email: Mapped[str] = mapped_column(String(100), unique=True) + password_hash: Mapped[str] = mapped_column(String(255)) + full_name: Mapped[str] = mapped_column(String(100)) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + last_login: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True) + created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) + updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True)) diff --git a/src/backend/app/routers/user.py b/src/backend/app/routers/user.py new file mode 100644 index 0000000..eafa83b --- /dev/null +++ b/src/backend/app/routers/user.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, update, func +from ..db import get_db +from ..models.users import User +from passlib.hash import bcrypt + +router = APIRouter() + +class LoginRequest(BaseModel): + username: str + password: str + +@router.post("/user/login") +async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)): + """处理登录请求,验证用户名与密码。""" + result = await db.execute(select(User).where(User.username == req.username).limit(1)) + user = result.scalars().first() + if not user: + raise HTTPException(status_code=401, detail="invalid_credentials") + if not user.is_active: + raise HTTPException(status_code=403, detail="inactive_user") + if not bcrypt.verify(req.password, user.password_hash): + raise HTTPException(status_code=401, detail="invalid_credentials") + await db.execute( + update(User).where(User.id == user.id).values(last_login=func.now(), updated_at=func.now()) + ) + await db.commit() + return {"ok": True, "username": user.username, "fullName": user.full_name} diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index bbcbbb8..534317a 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -3,4 +3,4 @@ uvicorn[standard] SQLAlchemy asyncpg python-dotenv -psycopg2-binary +passlib[bcrypt]