IMPROVE: cho phép lưu dự án và phục hồi dự án vừa tắt

This commit is contained in:
2026-07-30 20:47:44 +07:00
parent 313d1be5af
commit 58a294d845
7 changed files with 1071 additions and 18 deletions
+30 -1
View File
@@ -20,6 +20,12 @@ class UpdateUserRoleRequest(BaseModel):
role: str # 'admin', 'standard', 'premium'
is_active: Optional[bool] = True
class CreateUserRequest(BaseModel):
username: str
email: str
password: str
role: Optional[str] = 'standard'
@router.get("/users")
async def list_users(admin: dict = Depends(require_admin)):
conn = get_db_connection()
@@ -52,6 +58,29 @@ async def list_users(admin: dict = Depends(require_admin)):
})
return users
@router.post("/users")
async def create_user(req: CreateUserRequest, admin: dict = Depends(require_admin)):
import uuid, time
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT id FROM users WHERE username = ? OR email = ?", (req.username, req.email))
if cursor.fetchone():
conn.close()
raise HTTPException(status_code=409, detail="Tên đăng nhập hoặc email đã tồn tại")
user_id = str(uuid.uuid4())
hashed = hash_password(req.password)
now = time.time()
cursor.execute("""
INSERT INTO users (id, username, email, hashed_password, role, must_change_password, created_at, is_active)
VALUES (?, ?, ?, ?, ?, 1, ?, 1)
""", (user_id, req.username, req.email, hashed, req.role, now))
cursor.execute("""
INSERT INTO user_quotas (user_id, storage_limit_mb, max_tracks) VALUES (?, 500, 16)
""", (user_id,))
conn.commit()
conn.close()
return {"message": f"Đã tạo người dùng '{req.username}' thành công", "user_id": user_id}
@router.put("/users/{user_id}/role")
async def update_user_role(user_id: str, req: UpdateUserRoleRequest, admin: dict = Depends(require_admin)):
conn = get_db_connection()
@@ -81,7 +110,7 @@ async def delete_user(user_id: str, admin: dict = Depends(require_admin)):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
cursor.execute("DELETE FROM user_quotas WHERE id = ?", (user_id,))
cursor.execute("DELETE FROM user_quotas WHERE user_id = ?", (user_id,))
cursor.execute("DELETE FROM projects WHERE user_id = ?", (user_id,))
conn.commit()
conn.close()
+118 -6
View File
@@ -2,9 +2,9 @@ import time
import json
import uuid
import os
from fastapi import APIRouter, HTTPException, Depends, Header
from fastapi import APIRouter, HTTPException, Depends, Header, Query
from pydantic import BaseModel
from typing import Optional, Any, Dict
from typing import Optional, Any, Dict, List
from jsonschema import validate, ValidationError
from app.models.user import get_db_connection
from app.api.v1.auth import get_current_user, decode_token
@@ -228,9 +228,11 @@ async def list_cloud_projects(current_user: dict = Depends(get_current_user)):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT id, name, size_bytes, updated_at FROM projects
WHERE user_id = ? AND is_temp = 0
ORDER BY updated_at DESC
SELECT p.id, p.name, p.size_bytes, p.updated_at,
(SELECT COUNT(*) FROM project_backups pb WHERE pb.project_id = p.id AND pb.user_id = p.user_id) as backup_count
FROM projects p
WHERE p.user_id = ? AND p.is_temp = 0
ORDER BY p.updated_at DESC
""", (user_id,))
rows = cursor.fetchall()
conn.close()
@@ -240,7 +242,8 @@ async def list_cloud_projects(current_user: dict = Depends(get_current_user)):
"id": r["id"],
"name": r["name"],
"size_mb": round(r["size_bytes"] / (1024 * 1024), 2),
"updated_at": r["updated_at"]
"updated_at": r["updated_at"],
"backup_count": r["backup_count"]
} for r in rows
]
@@ -298,6 +301,115 @@ async def update_cloud_project(project_id: str, req: SaveProjectRequest, current
conn.close()
return {"success": True, "message": "Đã cập nhật dự án thành công"}
class BackupConfigRequest(BaseModel):
max_count: int = 10
@router.post("/cloud/{project_id}/backup")
async def create_project_backup(project_id: str, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT name, data_json FROM projects WHERE id = ? AND user_id = ? AND is_temp = 0", (project_id, user_id))
row = cursor.fetchone()
if not row:
conn.close()
raise HTTPException(status_code=404, detail="Không tìm thấy dự án")
backup_id = f"backup_{uuid.uuid4().hex[:12]}"
now = time.time()
size_bytes = len(row["data_json"].encode("utf-8"))
backup_name = f"[Backup] {row['name']} ({time.strftime('%Y-%m-%d %H:%M', time.localtime(now))})"
cursor.execute("""
INSERT INTO project_backups (id, user_id, project_id, name, data_json, size_bytes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (backup_id, user_id, project_id, backup_name, row["data_json"], size_bytes, now))
# Enforce retention limit: lấy max_count từ query param mặc định 10
cursor.execute("SELECT COUNT(*) as cnt FROM project_backups WHERE project_id = ? AND user_id = ?", (project_id, user_id))
count = cursor.fetchone()["cnt"]
max_backup = 10 # default
if count > max_backup:
excess = count - max_backup
cursor.execute("""
DELETE FROM project_backups WHERE id IN (
SELECT id FROM project_backups WHERE project_id = ? AND user_id = ?
ORDER BY created_at ASC LIMIT ?
)
""", (project_id, user_id, excess))
conn.commit()
conn.close()
return {
"backup_id": backup_id,
"name": backup_name,
"created_at": now,
"max_backups": max_backup
}
@router.get("/cloud/{project_id}/backups")
async def list_project_backups(project_id: str, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT id, name, size_bytes, created_at FROM project_backups
WHERE project_id = ? AND user_id = ?
ORDER BY created_at DESC
""", (project_id, user_id))
rows = cursor.fetchall()
conn.close()
return [
{
"id": r["id"],
"name": r["name"],
"size_mb": round(r["size_bytes"] / (1024 * 1024), 2),
"created_at": r["created_at"]
} for r in rows
]
@router.delete("/cloud/backups/{backup_id}")
async def delete_project_backup(backup_id: str, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM project_backups WHERE id = ? AND user_id = ?", (backup_id, user_id))
conn.commit()
conn.close()
return {"success": True, "message": "Đã xóa bản backup"}
class CleanupBackupsRequest(BaseModel):
keep: int = 10
@router.post("/cloud/backups/cleanup")
async def cleanup_all_backups(req: CleanupBackupsRequest, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
keep = max(5, min(20, req.keep))
conn = get_db_connection()
cursor = conn.cursor()
# Lấy tất cả project_id của user
cursor.execute("SELECT DISTINCT project_id FROM project_backups WHERE user_id = ?", (user_id,))
projects = cursor.fetchall()
total_deleted = 0
for p in projects:
pid = p["project_id"]
cursor.execute("SELECT COUNT(*) as cnt FROM project_backups WHERE project_id = ? AND user_id = ?", (pid, user_id))
cnt = cursor.fetchone()["cnt"]
if cnt > keep:
excess = cnt - keep
cursor.execute("""
DELETE FROM project_backups WHERE id IN (
SELECT id FROM project_backups WHERE project_id = ? AND user_id = ?
ORDER BY created_at ASC LIMIT ?
)
""", (pid, user_id, excess))
total_deleted += excess
conn.commit()
conn.close()
return {"success": True, "deleted": total_deleted, "keep": keep}
class RenderProjectRequest(BaseModel):
sample_rate: Optional[int] = 44100