0.9.9
parent
4a22d48b19
commit
a1a7580d63
|
|
@ -11,7 +11,7 @@ from pydantic import BaseModel
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.db import User, Role, Menu
|
from app.models.db import User, Role, Menu
|
||||||
from app.services.auth import verify_password, create_access_token
|
from app.services.auth import verify_password, create_access_token, hash_password
|
||||||
from app.services.auth_deps import get_current_user
|
from app.services.auth_deps import get_current_user
|
||||||
from app.services.token_service import token_service
|
from app.services.token_service import token_service
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
@ -29,6 +29,13 @@ class LoginRequest(BaseModel):
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterRequest(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
email: str | None = None
|
||||||
|
full_name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class LoginResponse(BaseModel):
|
class LoginResponse(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
token_type: str = "bearer"
|
token_type: str = "bearer"
|
||||||
|
|
@ -53,6 +60,91 @@ class MenuNode(BaseModel):
|
||||||
children: list['MenuNode'] | None = None
|
children: list['MenuNode'] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register", response_model=LoginResponse)
|
||||||
|
async def register(
|
||||||
|
register_data: RegisterRequest,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Register a new user
|
||||||
|
"""
|
||||||
|
# Check if username already exists
|
||||||
|
result = await db.execute(
|
||||||
|
select(User).where(User.username == register_data.username)
|
||||||
|
)
|
||||||
|
if result.scalar_one_or_none():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Username already registered"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if email already exists (if provided)
|
||||||
|
if register_data.email:
|
||||||
|
result = await db.execute(
|
||||||
|
select(User).where(User.email == register_data.email)
|
||||||
|
)
|
||||||
|
if result.scalar_one_or_none():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Email already registered"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get 'user' role
|
||||||
|
result = await db.execute(
|
||||||
|
select(Role).where(Role.name == "user")
|
||||||
|
)
|
||||||
|
user_role = result.scalar_one_or_none()
|
||||||
|
if not user_role:
|
||||||
|
# Should not happen if seeded correctly, but fallback handling
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Default role 'user' not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create new user
|
||||||
|
new_user = User(
|
||||||
|
username=register_data.username,
|
||||||
|
password_hash=hash_password(register_data.password),
|
||||||
|
email=register_data.email,
|
||||||
|
full_name=register_data.full_name,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
db.add(new_user)
|
||||||
|
await db.flush() # Flush to get ID
|
||||||
|
|
||||||
|
# Assign role
|
||||||
|
from app.models.db.user import user_roles
|
||||||
|
await db.execute(
|
||||||
|
user_roles.insert().values(
|
||||||
|
user_id=new_user.id,
|
||||||
|
role_id=user_role.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(new_user)
|
||||||
|
|
||||||
|
# Create access token
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": str(new_user.id), "username": new_user.username}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save token to Redis
|
||||||
|
await token_service.save_token(access_token, new_user.id, new_user.username)
|
||||||
|
|
||||||
|
# Return token and user info (simulate fetch with roles loaded)
|
||||||
|
return LoginResponse(
|
||||||
|
access_token=access_token,
|
||||||
|
user={
|
||||||
|
"id": new_user.id,
|
||||||
|
"username": new_user.username,
|
||||||
|
"email": new_user.email,
|
||||||
|
"full_name": new_user.full_name,
|
||||||
|
"roles": [user_role.name]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=LoginResponse)
|
@router.post("/login", response_model=LoginResponse)
|
||||||
async def login(
|
async def login(
|
||||||
login_data: LoginRequest,
|
login_data: LoginRequest,
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
API routes for celestial data
|
API routes for celestial data
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, HTTPException, Query, Depends, UploadFile, File
|
from fastapi import APIRouter, HTTPException, Query, Depends, UploadFile, File, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from typing import Optional
|
from typing import Optional, Dict, Any
|
||||||
import logging
|
import logging
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.models.celestial import (
|
from app.models.celestial import (
|
||||||
CelestialDataResponse,
|
CelestialDataResponse,
|
||||||
|
|
@ -31,6 +32,77 @@ logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/celestial", tags=["celestial"])
|
router = APIRouter(prefix="/celestial", tags=["celestial"])
|
||||||
|
|
||||||
|
|
||||||
|
# Pydantic models for CRUD
|
||||||
|
class CelestialBodyCreate(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
name_zh: Optional[str] = None
|
||||||
|
type: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
is_active: bool = True
|
||||||
|
extra_data: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
class CelestialBodyUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
name_zh: Optional[str] = None
|
||||||
|
type: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
extra_data: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_celestial_body(
|
||||||
|
body_data: CelestialBodyCreate,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Create a new celestial body"""
|
||||||
|
# Check if exists
|
||||||
|
existing = await celestial_body_service.get_body_by_id(body_data.id, db)
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Body with ID {body_data.id} already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
new_body = await celestial_body_service.create_body(body_data.dict(), db)
|
||||||
|
return new_body
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{body_id}")
|
||||||
|
async def update_celestial_body(
|
||||||
|
body_id: str,
|
||||||
|
body_data: CelestialBodyUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Update a celestial body"""
|
||||||
|
# Filter out None values
|
||||||
|
update_data = {k: v for k, v in body_data.dict().items() if v is not None}
|
||||||
|
|
||||||
|
updated = await celestial_body_service.update_body(body_id, update_data, db)
|
||||||
|
if not updated:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Body {body_id} not found"
|
||||||
|
)
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{body_id}")
|
||||||
|
async def delete_celestial_body(
|
||||||
|
body_id: str,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Delete a celestial body"""
|
||||||
|
deleted = await celestial_body_service.delete_body(body_id, db)
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Body {body_id} not found"
|
||||||
|
)
|
||||||
|
return {"message": "Body deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/positions", response_model=CelestialDataResponse)
|
@router.get("/positions", response_model=CelestialDataResponse)
|
||||||
async def get_celestial_positions(
|
async def get_celestial_positions(
|
||||||
start_time: Optional[str] = Query(
|
start_time: Optional[str] = Query(
|
||||||
|
|
@ -476,6 +548,7 @@ async def list_bodies(
|
||||||
"name_zh": body.name_zh,
|
"name_zh": body.name_zh,
|
||||||
"type": body.type,
|
"type": body.type,
|
||||||
"description": body.description,
|
"description": body.description,
|
||||||
|
"is_active": body.is_active,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return {"bodies": bodies_list}
|
return {"bodies": bodies_list}
|
||||||
|
|
@ -519,8 +592,79 @@ async def preheat_cache(
|
||||||
raise HTTPException(status_code=500, detail=f"Preheat failed: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Preheat failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# Static Data CRUD Models
|
||||||
|
class StaticDataCreate(BaseModel):
|
||||||
|
category: str
|
||||||
|
name: str
|
||||||
|
name_zh: Optional[str] = None
|
||||||
|
data: Dict[str, Any]
|
||||||
|
|
||||||
|
class StaticDataUpdate(BaseModel):
|
||||||
|
category: Optional[str] = None
|
||||||
|
name: Optional[str] = None
|
||||||
|
name_zh: Optional[str] = None
|
||||||
|
data: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
# === Static Data Endpoints ===
|
# === Static Data Endpoints ===
|
||||||
|
|
||||||
|
@router.get("/static/list")
|
||||||
|
async def list_static_data(db: AsyncSession = Depends(get_db)):
|
||||||
|
"""Get all static data items"""
|
||||||
|
items = await static_data_service.get_all_items(db)
|
||||||
|
result = []
|
||||||
|
for item in items:
|
||||||
|
result.append({
|
||||||
|
"id": item.id,
|
||||||
|
"category": item.category,
|
||||||
|
"name": item.name,
|
||||||
|
"name_zh": item.name_zh,
|
||||||
|
"data": item.data
|
||||||
|
})
|
||||||
|
return {"items": result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/static", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_static_data(
|
||||||
|
item_data: StaticDataCreate,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Create new static data"""
|
||||||
|
new_item = await static_data_service.create_static(item_data.dict(), db)
|
||||||
|
return new_item
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/static/{item_id}")
|
||||||
|
async def update_static_data(
|
||||||
|
item_id: int,
|
||||||
|
item_data: StaticDataUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Update static data"""
|
||||||
|
update_data = {k: v for k, v in item_data.dict().items() if v is not None}
|
||||||
|
updated = await static_data_service.update_static(item_id, update_data, db)
|
||||||
|
if not updated:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Static data {item_id} not found"
|
||||||
|
)
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/static/{item_id}")
|
||||||
|
async def delete_static_data(
|
||||||
|
item_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Delete static data"""
|
||||||
|
deleted = await static_data_service.delete_static(item_id, db)
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Static data {item_id} not found"
|
||||||
|
)
|
||||||
|
return {"message": "Deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/static/categories")
|
@router.get("/static/categories")
|
||||||
async def get_static_categories(db: AsyncSession = Depends(get_db)):
|
async def get_static_categories(db: AsyncSession = Depends(get_db)):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from sqlalchemy import select
|
||||||
|
from typing import List
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models.db import User
|
||||||
|
from app.services.auth import hash_password
|
||||||
|
from app.services.auth_deps import get_current_user # To protect endpoints
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/users", tags=["users"])
|
||||||
|
|
||||||
|
# Pydantic models
|
||||||
|
class UserListItem(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
email: str | None
|
||||||
|
full_name: str | None
|
||||||
|
is_active: bool
|
||||||
|
roles: list[str]
|
||||||
|
last_login_at: str | None
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class UserStatusUpdate(BaseModel):
|
||||||
|
is_active: bool
|
||||||
|
|
||||||
|
@router.get("/list")
|
||||||
|
async def get_user_list(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user) # Protect this route
|
||||||
|
):
|
||||||
|
"""Get a list of all users"""
|
||||||
|
# Ensure only admins can see all users
|
||||||
|
if "admin" not in [role.name for role in current_user.roles]:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(User).options(selectinload(User.roles)).order_by(User.id)
|
||||||
|
)
|
||||||
|
users = result.scalars().all()
|
||||||
|
|
||||||
|
users_list = []
|
||||||
|
for user in users:
|
||||||
|
users_list.append({
|
||||||
|
"id": user.id,
|
||||||
|
"username": user.username,
|
||||||
|
"email": user.email,
|
||||||
|
"full_name": user.full_name,
|
||||||
|
"is_active": user.is_active,
|
||||||
|
"roles": [role.name for role in user.roles],
|
||||||
|
"last_login_at": user.last_login_at.isoformat() if user.last_login_at else None,
|
||||||
|
"created_at": user.created_at.isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"users": users_list}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{user_id}/status")
|
||||||
|
async def update_user_status(
|
||||||
|
user_id: int,
|
||||||
|
status_update: UserStatusUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Update a user's active status"""
|
||||||
|
if "admin" not in [role.name for role in current_user.roles]:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
|
||||||
|
|
||||||
|
result = await db.execute(select(User).where(User.id == user_id))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
|
|
||||||
|
user.is_active = status_update.is_active
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return {"message": "User status updated successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{user_id}/reset-password")
|
||||||
|
async def reset_user_password(
|
||||||
|
user_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Reset a user's password to the default"""
|
||||||
|
if "admin" not in [role.name for role in current_user.roles]:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
|
||||||
|
|
||||||
|
result = await db.execute(select(User).where(User.id == user_id))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
|
|
||||||
|
# Hardcoded default password for now.
|
||||||
|
# TODO: Move to a configurable system parameter.
|
||||||
|
default_password = "password123"
|
||||||
|
user.password_hash = hash_password(default_password)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return {"message": f"Password for user {user.username} has been reset."}
|
||||||
|
|
@ -19,6 +19,7 @@ from fastapi.staticfiles import StaticFiles
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.api.routes import router as celestial_router
|
from app.api.routes import router as celestial_router
|
||||||
from app.api.auth import router as auth_router
|
from app.api.auth import router as auth_router
|
||||||
|
from app.api.user import router as user_router
|
||||||
from app.services.redis_cache import redis_cache
|
from app.services.redis_cache import redis_cache
|
||||||
from app.services.cache_preheat import preheat_all_caches
|
from app.services.cache_preheat import preheat_all_caches
|
||||||
from app.database import close_db
|
from app.database import close_db
|
||||||
|
|
@ -85,6 +86,7 @@ app.add_middleware(
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(celestial_router, prefix=settings.api_prefix)
|
app.include_router(celestial_router, prefix=settings.api_prefix)
|
||||||
app.include_router(auth_router, prefix=settings.api_prefix)
|
app.include_router(auth_router, prefix=settings.api_prefix)
|
||||||
|
app.include_router(user_router, prefix=settings.api_prefix)
|
||||||
|
|
||||||
# Mount static files for uploaded resources
|
# Mount static files for uploaded resources
|
||||||
upload_dir = Path(__file__).parent.parent / "upload"
|
upload_dir = Path(__file__).parent.parent / "upload"
|
||||||
|
|
@ -92,6 +94,12 @@ upload_dir.mkdir(exist_ok=True)
|
||||||
app.mount("/upload", StaticFiles(directory=str(upload_dir)), name="upload")
|
app.mount("/upload", StaticFiles(directory=str(upload_dir)), name="upload")
|
||||||
logger.info(f"Static files mounted at /upload -> {upload_dir}")
|
logger.info(f"Static files mounted at /upload -> {upload_dir}")
|
||||||
|
|
||||||
|
# Mount public assets directory
|
||||||
|
public_assets_dir = Path(__file__).parent.parent / "public" / "assets"
|
||||||
|
public_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
app.mount("/public/assets", StaticFiles(directory=str(public_assets_dir)), name="public_assets")
|
||||||
|
logger.info(f"Public assets mounted at /public/assets -> {public_assets_dir}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,63 @@ class CelestialBodyService:
|
||||||
async with AsyncSessionLocal() as s:
|
async with AsyncSessionLocal() as s:
|
||||||
return await _create(s)
|
return await _create(s)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_body(
|
||||||
|
body_id: str,
|
||||||
|
update_data: Dict[str, Any],
|
||||||
|
session: Optional[AsyncSession] = None
|
||||||
|
) -> Optional[CelestialBody]:
|
||||||
|
"""Update a celestial body"""
|
||||||
|
async def _update(s: AsyncSession):
|
||||||
|
# Query the body
|
||||||
|
result = await s.execute(
|
||||||
|
select(CelestialBody).where(CelestialBody.id == body_id)
|
||||||
|
)
|
||||||
|
body = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not body:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Update fields
|
||||||
|
for key, value in update_data.items():
|
||||||
|
if hasattr(body, key):
|
||||||
|
setattr(body, key, value)
|
||||||
|
|
||||||
|
await s.commit()
|
||||||
|
await s.refresh(body)
|
||||||
|
return body
|
||||||
|
|
||||||
|
if session:
|
||||||
|
return await _update(session)
|
||||||
|
else:
|
||||||
|
async with AsyncSessionLocal() as s:
|
||||||
|
return await _update(s)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_body(
|
||||||
|
body_id: str,
|
||||||
|
session: Optional[AsyncSession] = None
|
||||||
|
) -> bool:
|
||||||
|
"""Delete a celestial body"""
|
||||||
|
async def _delete(s: AsyncSession):
|
||||||
|
result = await s.execute(
|
||||||
|
select(CelestialBody).where(CelestialBody.id == body_id)
|
||||||
|
)
|
||||||
|
body = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not body:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await s.delete(body)
|
||||||
|
await s.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
if session:
|
||||||
|
return await _delete(session)
|
||||||
|
else:
|
||||||
|
async with AsyncSessionLocal() as s:
|
||||||
|
return await _delete(s)
|
||||||
|
|
||||||
|
|
||||||
class PositionService:
|
class PositionService:
|
||||||
"""Service for position data operations"""
|
"""Service for position data operations"""
|
||||||
|
|
@ -354,6 +411,95 @@ class NasaCacheService:
|
||||||
class StaticDataService:
|
class StaticDataService:
|
||||||
"""Service for static data operations"""
|
"""Service for static data operations"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_all_items(
|
||||||
|
session: Optional[AsyncSession] = None
|
||||||
|
) -> List[StaticData]:
|
||||||
|
"""Get all static data items"""
|
||||||
|
async def _query(s: AsyncSession):
|
||||||
|
result = await s.execute(
|
||||||
|
select(StaticData).order_by(StaticData.category, StaticData.name)
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
if session:
|
||||||
|
return await _query(session)
|
||||||
|
else:
|
||||||
|
async with AsyncSessionLocal() as s:
|
||||||
|
return await _query(s)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create_static(
|
||||||
|
data: Dict[str, Any],
|
||||||
|
session: Optional[AsyncSession] = None
|
||||||
|
) -> StaticData:
|
||||||
|
"""Create new static data"""
|
||||||
|
async def _create(s: AsyncSession):
|
||||||
|
item = StaticData(**data)
|
||||||
|
s.add(item)
|
||||||
|
await s.commit()
|
||||||
|
await s.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
if session:
|
||||||
|
return await _create(session)
|
||||||
|
else:
|
||||||
|
async with AsyncSessionLocal() as s:
|
||||||
|
return await _create(s)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_static(
|
||||||
|
item_id: int,
|
||||||
|
update_data: Dict[str, Any],
|
||||||
|
session: Optional[AsyncSession] = None
|
||||||
|
) -> Optional[StaticData]:
|
||||||
|
"""Update static data"""
|
||||||
|
async def _update(s: AsyncSession):
|
||||||
|
result = await s.execute(
|
||||||
|
select(StaticData).where(StaticData.id == item_id)
|
||||||
|
)
|
||||||
|
item = result.scalar_one_or_none()
|
||||||
|
if not item:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for key, value in update_data.items():
|
||||||
|
if hasattr(item, key):
|
||||||
|
setattr(item, key, value)
|
||||||
|
|
||||||
|
await s.commit()
|
||||||
|
await s.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
if session:
|
||||||
|
return await _update(session)
|
||||||
|
else:
|
||||||
|
async with AsyncSessionLocal() as s:
|
||||||
|
return await _update(s)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_static(
|
||||||
|
item_id: int,
|
||||||
|
session: Optional[AsyncSession] = None
|
||||||
|
) -> bool:
|
||||||
|
"""Delete static data"""
|
||||||
|
async def _delete(s: AsyncSession):
|
||||||
|
result = await s.execute(
|
||||||
|
select(StaticData).where(StaticData.id == item_id)
|
||||||
|
)
|
||||||
|
item = result.scalar_one_or_none()
|
||||||
|
if not item:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await s.delete(item)
|
||||||
|
await s.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
if session:
|
||||||
|
return await _delete(session)
|
||||||
|
else:
|
||||||
|
async with AsyncSessionLocal() as s:
|
||||||
|
return await _delete(s)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_by_category(
|
async def get_by_category(
|
||||||
category: str,
|
category: str,
|
||||||
|
|
|
||||||
|
|
@ -36,15 +36,15 @@ class TokenService:
|
||||||
await redis_cache.set(
|
await redis_cache.set(
|
||||||
f"{self.prefix}{token}",
|
f"{self.prefix}{token}",
|
||||||
json.dumps(token_data),
|
json.dumps(token_data),
|
||||||
expire=ttl_seconds
|
ttl_seconds=ttl_seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
# Track user's active tokens (for multi-device support)
|
# Track user's active tokens (for multi-device support)
|
||||||
user_tokens_key = f"{self.user_tokens_prefix}{user_id}"
|
user_tokens_key = f"{self.user_tokens_prefix}{user_id}"
|
||||||
# Add token to user's token set
|
# Add token to user's token set
|
||||||
if redis_cache.redis:
|
if redis_cache.client:
|
||||||
await redis_cache.redis.sadd(user_tokens_key, token)
|
await redis_cache.client.sadd(user_tokens_key, token)
|
||||||
await redis_cache.redis.expire(user_tokens_key, ttl_seconds)
|
await redis_cache.client.expire(user_tokens_key, ttl_seconds)
|
||||||
|
|
||||||
async def get_token_data(self, token: str) -> Optional[dict]:
|
async def get_token_data(self, token: str) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -89,10 +89,10 @@ class TokenService:
|
||||||
await redis_cache.delete(f"{self.prefix}{token}")
|
await redis_cache.delete(f"{self.prefix}{token}")
|
||||||
|
|
||||||
# Remove from user's token set
|
# Remove from user's token set
|
||||||
if token_data and redis_cache.redis:
|
if token_data and redis_cache.client:
|
||||||
user_id = token_data.get("user_id")
|
user_id = token_data.get("user_id")
|
||||||
if user_id:
|
if user_id:
|
||||||
await redis_cache.redis.srem(
|
await redis_cache.client.srem(
|
||||||
f"{self.user_tokens_prefix}{user_id}",
|
f"{self.user_tokens_prefix}{user_id}",
|
||||||
token
|
token
|
||||||
)
|
)
|
||||||
|
|
@ -104,12 +104,12 @@ class TokenService:
|
||||||
Args:
|
Args:
|
||||||
user_id: User ID
|
user_id: User ID
|
||||||
"""
|
"""
|
||||||
if not redis_cache.redis:
|
if not redis_cache.client:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get all user's tokens
|
# Get all user's tokens
|
||||||
user_tokens_key = f"{self.user_tokens_prefix}{user_id}"
|
user_tokens_key = f"{self.user_tokens_prefix}{user_id}"
|
||||||
tokens = await redis_cache.redis.smembers(user_tokens_key)
|
tokens = await redis_cache.client.smembers(user_tokens_key)
|
||||||
|
|
||||||
# Revoke each token
|
# Revoke each token
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
-- This script adds a new top-level menu "Platform Management"
|
||||||
|
-- with two sub-menus "User Management" and "Platform Parameters Management".
|
||||||
|
-- These menus will be assigned to the 'admin' role.
|
||||||
|
|
||||||
|
-- Start Transaction for atomicity
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1. Find the ID of the 'admin' role
|
||||||
|
-- Assuming 'admin' role name exists and is unique.
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
admin_role_id INTEGER;
|
||||||
|
platform_management_menu_id INTEGER;
|
||||||
|
user_management_menu_id INTEGER;
|
||||||
|
platform_parameters_menu_id INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO admin_role_id FROM roles WHERE name = 'admin';
|
||||||
|
|
||||||
|
IF admin_role_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Admin role not found. Please ensure the admin role exists.';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 2. Insert the top-level menu: "Platform Management"
|
||||||
|
-- Check if it already exists to prevent duplicates on re-run
|
||||||
|
SELECT id INTO platform_management_menu_id FROM menus WHERE name = 'platform_management' AND parent_id IS NULL;
|
||||||
|
|
||||||
|
IF platform_management_menu_id IS NULL THEN
|
||||||
|
INSERT INTO menus (name, title, icon, path, component, sort_order, is_active, description, created_at, updated_at)
|
||||||
|
VALUES (
|
||||||
|
'platform_management',
|
||||||
|
'平台管理',
|
||||||
|
'settings', -- Using a generic settings icon for platform management
|
||||||
|
NULL, -- It's a parent menu, no direct path
|
||||||
|
NULL,
|
||||||
|
3, -- Assuming sort_order 1 & 2 are for Dashboard & Data Management
|
||||||
|
TRUE,
|
||||||
|
'管理用户和系统参数',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
) RETURNING id INTO platform_management_menu_id;
|
||||||
|
RAISE NOTICE 'Inserted Platform Management menu with ID: %', platform_management_menu_id;
|
||||||
|
|
||||||
|
-- Assign to admin role
|
||||||
|
INSERT INTO role_menus (role_id, menu_id, created_at)
|
||||||
|
VALUES (admin_role_id, platform_management_menu_id, NOW());
|
||||||
|
RAISE NOTICE 'Assigned Platform Management to admin role.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Platform Management menu already exists with ID: %', platform_management_menu_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
|
||||||
|
-- 3. Insert sub-menu: "User Management"
|
||||||
|
-- Check if it already exists
|
||||||
|
SELECT id INTO user_management_menu_id FROM menus WHERE name = 'user_management' AND parent_id = platform_management_menu_id;
|
||||||
|
|
||||||
|
IF user_management_menu_id IS NULL THEN
|
||||||
|
INSERT INTO menus (parent_id, name, title, icon, path, component, sort_order, is_active, description, created_at, updated_at)
|
||||||
|
VALUES (
|
||||||
|
platform_management_menu_id,
|
||||||
|
'user_management',
|
||||||
|
'用户管理',
|
||||||
|
'users', -- Icon for user management
|
||||||
|
'/admin/users', -- Admin users page path
|
||||||
|
'admin/Users', -- React component path
|
||||||
|
1,
|
||||||
|
TRUE,
|
||||||
|
'管理系统用户账号',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
) RETURNING id INTO user_management_menu_id;
|
||||||
|
RAISE NOTICE 'Inserted User Management menu with ID: %', user_management_menu_id;
|
||||||
|
|
||||||
|
-- Assign to admin role
|
||||||
|
INSERT INTO role_menus (role_id, menu_id, created_at)
|
||||||
|
VALUES (admin_role_id, user_management_menu_id, NOW());
|
||||||
|
RAISE NOTICE 'Assigned User Management to admin role.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'User Management menu already exists with ID: %', user_management_menu_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
|
||||||
|
-- 4. Insert sub-menu: "Platform Parameters Management"
|
||||||
|
-- Check if it already exists
|
||||||
|
SELECT id INTO platform_parameters_menu_id FROM menus WHERE name = 'platform_parameters_management' AND parent_id = platform_management_menu_id;
|
||||||
|
|
||||||
|
IF platform_parameters_menu_id IS NULL THEN
|
||||||
|
INSERT INTO menus (parent_id, name, title, icon, path, component, sort_order, is_active, description, created_at, updated_at)
|
||||||
|
VALUES (
|
||||||
|
platform_management_menu_id,
|
||||||
|
'platform_parameters_management',
|
||||||
|
'平台参数管理',
|
||||||
|
'sliders', -- Icon for parameters/settings
|
||||||
|
'/admin/settings', -- Admin settings page path
|
||||||
|
'admin/Settings', -- React component path
|
||||||
|
2,
|
||||||
|
TRUE,
|
||||||
|
'管理系统通用配置参数',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
) RETURNING id INTO platform_parameters_menu_id;
|
||||||
|
RAISE NOTICE 'Inserted Platform Parameters Management menu with ID: %', platform_parameters_menu_id;
|
||||||
|
|
||||||
|
-- Assign to admin role
|
||||||
|
INSERT INTO role_menus (role_id, menu_id, created_at)
|
||||||
|
VALUES (admin_role_id, platform_parameters_menu_id, NOW());
|
||||||
|
RAISE NOTICE 'Assigned Platform Parameters Management to admin role.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Platform Parameters Management menu already exists with ID: %', platform_parameters_menu_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Commit the transaction
|
||||||
|
COMMIT;
|
||||||
Binary file not shown.
Loading…
Reference in New Issue