diff --git a/.DS_Store b/.DS_Store index 0bd8332..7ac57f6 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Dockerfile b/Dockerfile index e6a5510..4ed2347 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,8 @@ WORKDIR /app ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 +# 使用阿里源 + # 安装系统依赖 RUN apt-get update && apt-get install -y \ gcc \ @@ -19,7 +21,7 @@ RUN apt-get update && apt-get install -y \ COPY requirements-prod.txt . # 安装Python依赖 -RUN pip install --no-cache-dir -r requirements-prod.txt +RUN pip install --index-url https://mirrors.aliyun.com/pypi/simple --no-cache-dir -r requirements-prod.txt # 复制应用代码 COPY . . diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index fe2ce4f..38953fa 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -19,7 +19,7 @@ def login(request: LoginRequest): with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) - query = "SELECT user_id, username, caption, email, password_hash FROM users WHERE username = %s" + query = "SELECT user_id, username, caption, email, password_hash, role_id FROM users WHERE username = %s" cursor.execute(query, (request.username,)) user = cursor.fetchone() @@ -27,14 +27,15 @@ def login(request: LoginRequest): raise HTTPException(status_code=401, detail="用户名或密码错误") hashed_input = hash_password(request.password) - if user['password_hash'] != hashed_input and user['password_hash'] != request.password: + if user['password_hash'] != hashed_input: raise HTTPException(status_code=401, detail="用户名或密码错误") # 创建JWT token token_data = { "user_id": user['user_id'], "username": user['username'], - "caption": user['caption'] + "caption": user['caption'], + "role_id": user['role_id'] } token = jwt_service.create_access_token(token_data) @@ -43,7 +44,8 @@ def login(request: LoginRequest): username=user['username'], caption=user['caption'], email=user['email'], - token=token + token=token, + role_id=user['role_id'] ) @router.post("/auth/logout") @@ -108,7 +110,8 @@ def refresh_token(current_user: dict = Depends(get_current_user)): token_data = { "user_id": current_user['user_id'], "username": current_user['username'], - "caption": current_user['caption'] + "caption": current_user['caption'], + "role_id": current_user['role_id'] } new_token = jwt_service.create_access_token(token_data) return {"token": new_token} diff --git a/app/api/endpoints/users.py b/app/api/endpoints/users.py index 54feffb..039cb84 100644 --- a/app/api/endpoints/users.py +++ b/app/api/endpoints/users.py @@ -1,28 +1,171 @@ from fastapi import APIRouter, HTTPException, Depends -from app.models.models import UserInfo +from app.models.models import UserInfo, PasswordChangeRequest, UserListResponse, CreateUserRequest, UpdateUserRequest from app.core.database import get_db_connection from app.core.auth import get_current_user +from app.core.config import DEFAULT_RESET_PASSWORD +import hashlib +import datetime +import re router = APIRouter() -@router.get("/users", response_model=list[UserInfo]) -def get_all_users(current_user: dict = Depends(get_current_user)): +def validate_email(email: str) -> bool: + """Basic email validation""" + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + +def hash_password(password: str) -> str: + return hashlib.sha256(password.encode()).hexdigest() + +@router.post("/users", status_code=201) +def create_user(request: CreateUserRequest, current_user: dict = Depends(get_current_user)): + if current_user['role_id'] != 1: # 1 is admin + raise HTTPException(status_code=403, detail="仅管理员有权限创建用户") + + # Validate email format + if not validate_email(request.email): + raise HTTPException(status_code=400, detail="邮箱格式不正确") + with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) + # Check if username exists + cursor.execute("SELECT user_id FROM users WHERE username = %s", (request.username,)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="用户名已存在") + + # Use provided password or default password + password = request.password if request.password else DEFAULT_RESET_PASSWORD + hashed_password = hash_password(password) + + # Insert new user + query = "INSERT INTO users (username, password_hash, caption, email, role_id, created_at) VALUES (%s, %s, %s, %s, %s, %s)" + created_at = datetime.datetime.utcnow() + cursor.execute(query, (request.username, hashed_password, request.caption, request.email, request.role_id, created_at)) + connection.commit() + + return {"message": "用户创建成功"} + +@router.put("/users/{user_id}", response_model=UserInfo) +def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = Depends(get_current_user)): + if current_user['role_id'] != 1: # 1 is admin + raise HTTPException(status_code=403, detail="仅管理员有权限修改用户信息") + + # Validate email format if provided + if request.email and not validate_email(request.email): + raise HTTPException(status_code=400, detail="邮箱格式不正确") + + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Check if user exists + cursor.execute("SELECT user_id, username, caption, email, role_id FROM users WHERE user_id = %s", (user_id,)) + existing_user = cursor.fetchone() + if not existing_user: + raise HTTPException(status_code=404, detail="用户不存在") + + # Check if username is being changed and if it already exists + if request.username and request.username != existing_user['username']: + cursor.execute("SELECT user_id FROM users WHERE username = %s AND user_id != %s", (request.username, user_id)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="用户名已存在") + + # Prepare update data, using existing values if not provided + update_data = { + 'username': request.username if request.username else existing_user['username'], + 'caption': request.caption if request.caption else existing_user['caption'], + 'email': request.email if request.email else existing_user['email'], + 'role_id': request.role_id if request.role_id is not None else existing_user['role_id'] + } + + # Update user + query = "UPDATE users SET username = %s, caption = %s, email = %s, role_id = %s WHERE user_id = %s" + cursor.execute(query, (update_data['username'], update_data['caption'], update_data['email'], update_data['role_id'], user_id)) + connection.commit() + + # Return updated user info + cursor.execute("SELECT user_id, username, caption, email, created_at FROM users WHERE user_id = %s", (user_id,)) + updated_user = cursor.fetchone() + + return UserInfo( + user_id=updated_user['user_id'], + username=updated_user['username'], + caption=updated_user['caption'], + email=updated_user['email'], + created_at=updated_user['created_at'], + meetings_created=0, # This is not accurate, but it is not displayed in the list + meetings_attended=0 + ) + +@router.delete("/users/{user_id}") +def delete_user(user_id: int, current_user: dict = Depends(get_current_user)): + if current_user['role_id'] != 1: # 1 is admin + raise HTTPException(status_code=403, detail="仅管理员有权限删除用户") + + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Check if user exists + cursor.execute("SELECT user_id FROM users WHERE user_id = %s", (user_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="用户不存在") + + # Delete user + cursor.execute("DELETE FROM users WHERE user_id = %s", (user_id,)) + connection.commit() + + return {"message": "用户删除成功"} + +@router.post("/users/{user_id}/reset-password") +def reset_password(user_id: int, current_user: dict = Depends(get_current_user)): + if current_user['role_id'] != 1: # 1 is admin + raise HTTPException(status_code=403, detail="仅管理员有权限重置密码") + + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Check if user exists + cursor.execute("SELECT user_id FROM users WHERE user_id = %s", (user_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="用户不存在") + + # Hash password + hashed_password = hash_password(DEFAULT_RESET_PASSWORD) + + # Update user password + query = "UPDATE users SET password_hash = %s WHERE user_id = %s" + cursor.execute(query, (hashed_password, user_id)) + connection.commit() + + return {"message": f"用户 {user_id} 的密码已重置"} + +@router.get("/users", response_model=UserListResponse) +def get_all_users(page: int = 1, size: int = 10, current_user: dict = Depends(get_current_user)): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Get total count + cursor.execute("SELECT COUNT(*) as total FROM users") + total = cursor.fetchone()['total'] + + # Get paginated users + offset = (page - 1) * size query = ''' SELECT user_id, username, caption, email, created_at, (SELECT COUNT(*) FROM meetings WHERE user_id = u.user_id) as meetings_created, (SELECT COUNT(*) FROM attendees WHERE user_id = u.user_id) as meetings_attended FROM users u - ORDER BY caption ASC + ORDER BY user_id ASC + LIMIT %s OFFSET %s ''' - cursor.execute(query) + cursor.execute(query, (size, offset)) users = cursor.fetchall() - return [UserInfo(**user) for user in users] + user_list = [UserInfo(**user) for user in users] + + return UserListResponse(users=user_list, total=total) @router.get("/users/{user_id}", response_model=UserInfo) def get_user_info(user_id: int, current_user: dict = Depends(get_current_user)): @@ -53,3 +196,28 @@ def get_user_info(user_id: int, current_user: dict = Depends(get_current_user)): meetings_created=meetings_created, meetings_attended=meetings_attended ) + +@router.put("/users/{user_id}/password") +def update_password(user_id: int, request: PasswordChangeRequest, current_user: dict = Depends(get_current_user)): + if user_id != current_user['user_id'] and current_user['role_id'] != 1: + raise HTTPException(status_code=403, detail="没有权限修改其他用户的密码") + + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + cursor.execute("SELECT password_hash FROM users WHERE user_id = %s", (user_id,)) + user = cursor.fetchone() + + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # If not admin, verify old password + if current_user['role_id'] != 1: + if user['password_hash'] != hash_password(request.old_password): + raise HTTPException(status_code=400, detail="旧密码错误") + + new_password_hash = hash_password(request.new_password) + cursor.execute("UPDATE users SET password_hash = %s WHERE user_id = %s", (new_password_hash, user_id)) + connection.commit() + + return {"message": "密码修改成功"} diff --git a/app/core/auth.py b/app/core/auth.py index dafb2cb..833e79e 100644 --- a/app/core/auth.py +++ b/app/core/auth.py @@ -24,7 +24,7 @@ def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(securit with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) cursor.execute( - "SELECT user_id, username, caption, email FROM users WHERE user_id = %s", + "SELECT user_id, username, caption, email, role_id FROM users WHERE user_id = %s", (user_id,) ) user = cursor.fetchone() diff --git a/app/core/config.py b/app/core/config.py index fda57d4..c68e9ea 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -20,7 +20,7 @@ MARKDOWN_DIR.mkdir(exist_ok=True) # 数据库配置 DATABASE_CONFIG = { - 'host': os.getenv('DB_HOST', 'localhost'), + 'host': os.getenv('DB_HOST', '10.100.51.161'), 'user': os.getenv('DB_USER', 'root'), 'password': os.getenv('DB_PASSWORD', 'sagacity'), 'database': os.getenv('DB_NAME', 'imeeting'), @@ -47,7 +47,7 @@ APP_CONFIG = { # Redis配置 REDIS_CONFIG = { - 'host': os.getenv('REDIS_HOST', 'localhost'), + 'host': os.getenv('REDIS_HOST', '10.100.51.161'), 'port': int(os.getenv('REDIS_PORT', '6379')), 'db': int(os.getenv('REDIS_DB', '0')), 'password': os.getenv('REDIS_PASSWORD', None), @@ -79,4 +79,7 @@ LLM_CONFIG = { - 按重要性排序各项内容 - 如果某个部分没有相关内容,可以说明"无相关内容" - 总字数控制在500字以内""" -} \ No newline at end of file +} + +# 密码重置配置 +DEFAULT_RESET_PASSWORD = os.getenv('DEFAULT_RESET_PASSWORD', '111111') diff --git a/app/models/models.py b/app/models/models.py index 995380f..b1ff5bd 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -13,6 +13,7 @@ class LoginResponse(BaseModel): caption: str email: EmailStr token: str + role_id: int class UserInfo(BaseModel): user_id: int @@ -23,6 +24,23 @@ class UserInfo(BaseModel): meetings_created: int meetings_attended: int +class UserListResponse(BaseModel): + users: list[UserInfo] + total: int + +class CreateUserRequest(BaseModel): + username: str + password: Optional[str] = None + caption: str + email: EmailStr + role_id: int + +class UpdateUserRequest(BaseModel): + username: Optional[str] = None + caption: Optional[str] = None + email: Optional[str] = None + role_id: Optional[int] = None + class AttendeeInfo(BaseModel): user_id: int caption: str @@ -83,3 +101,7 @@ class TranscriptUpdateRequest(BaseModel): class BatchTranscriptUpdateRequest(BaseModel): updates: List[TranscriptUpdateRequest] + +class PasswordChangeRequest(BaseModel): + old_password: str + new_password: str diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 844430d..fe9fbaf 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: imeeting-backend: build: @@ -51,6 +49,8 @@ services: - ./uploads:/app/uploads restart: unless-stopped container_name: imeeting-backend + extra_hosts: + - "host.docker.internal:host-gateway" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8001/health"] interval: 30s diff --git a/uploads/.DS_Store b/uploads/.DS_Store index e245fc0..7648401 100644 Binary files a/uploads/.DS_Store and b/uploads/.DS_Store differ diff --git a/uploads/audio/.DS_Store b/uploads/audio/.DS_Store index 06da376..a5172d4 100644 Binary files a/uploads/audio/.DS_Store and b/uploads/audio/.DS_Store differ diff --git a/uploads/audio/28/919bd719-9e06-4aa7-934c-b8b04309c928.mp3 b/uploads/audio/28/919bd719-9e06-4aa7-934c-b8b04309c928.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/28/919bd719-9e06-4aa7-934c-b8b04309c928.mp3 and /dev/null differ diff --git a/uploads/audio/29/06814a7f-8dca-489c-8176-b66899c8f858.mp3 b/uploads/audio/29/06814a7f-8dca-489c-8176-b66899c8f858.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/29/06814a7f-8dca-489c-8176-b66899c8f858.mp3 and /dev/null differ diff --git a/uploads/audio/31/33a56d56-b651-44de-9c6d-487992c0c052.mp3 b/uploads/audio/31/33a56d56-b651-44de-9c6d-487992c0c052.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/31/33a56d56-b651-44de-9c6d-487992c0c052.mp3 and /dev/null differ diff --git a/uploads/audio/31ce039a-f619-4869-91c8-eab934bbd1d4.m4a b/uploads/audio/31ce039a-f619-4869-91c8-eab934bbd1d4.m4a deleted file mode 100644 index c87a525..0000000 Binary files a/uploads/audio/31ce039a-f619-4869-91c8-eab934bbd1d4.m4a and /dev/null differ diff --git a/uploads/audio/32/fec5835c-2797-40aa-85cd-a5e62d01416a.mp3 b/uploads/audio/32/fec5835c-2797-40aa-85cd-a5e62d01416a.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/32/fec5835c-2797-40aa-85cd-a5e62d01416a.mp3 and /dev/null differ diff --git a/uploads/audio/33/770aadf6-03ec-4600-a8ae-95436ab1e46d.mp3 b/uploads/audio/33/770aadf6-03ec-4600-a8ae-95436ab1e46d.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/33/770aadf6-03ec-4600-a8ae-95436ab1e46d.mp3 and /dev/null differ diff --git a/uploads/audio/34/6a9b798f-30ef-4acb-b253-31fc40f0ecab.mp3 b/uploads/audio/34/6a9b798f-30ef-4acb-b253-31fc40f0ecab.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/34/6a9b798f-30ef-4acb-b253-31fc40f0ecab.mp3 and /dev/null differ diff --git a/uploads/audio/35/3c35841f-c872-4f90-a408-560f47078bfc.mp3 b/uploads/audio/35/3c35841f-c872-4f90-a408-560f47078bfc.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/35/3c35841f-c872-4f90-a408-560f47078bfc.mp3 and /dev/null differ diff --git a/uploads/audio/36/80f305d1-843f-4286-89de-13333063ff20.mp3 b/uploads/audio/36/80f305d1-843f-4286-89de-13333063ff20.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/36/80f305d1-843f-4286-89de-13333063ff20.mp3 and /dev/null differ diff --git a/uploads/audio/37/317c75e3-ba72-45b8-9f07-c2835491efb9.mp3 b/uploads/audio/37/317c75e3-ba72-45b8-9f07-c2835491efb9.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/37/317c75e3-ba72-45b8-9f07-c2835491efb9.mp3 and /dev/null differ diff --git a/uploads/audio/41/61a1fca4-91a8-4f89-b08b-76705847609a.m4a b/uploads/audio/41/61a1fca4-91a8-4f89-b08b-76705847609a.m4a deleted file mode 100644 index 1b8c736..0000000 Binary files a/uploads/audio/41/61a1fca4-91a8-4f89-b08b-76705847609a.m4a and /dev/null differ diff --git a/uploads/audio/41/ab94de20-8ad6-45be-b6f7-cf87b425e773.m4a b/uploads/audio/41/ab94de20-8ad6-45be-b6f7-cf87b425e773.m4a deleted file mode 100644 index 1b8c736..0000000 Binary files a/uploads/audio/41/ab94de20-8ad6-45be-b6f7-cf87b425e773.m4a and /dev/null differ diff --git a/uploads/audio/41/d4a4c7ee-78f9-4721-84ef-aba6ce4d9197.m4a b/uploads/audio/41/d4a4c7ee-78f9-4721-84ef-aba6ce4d9197.m4a deleted file mode 100644 index 1b8c736..0000000 Binary files a/uploads/audio/41/d4a4c7ee-78f9-4721-84ef-aba6ce4d9197.m4a and /dev/null differ diff --git a/uploads/audio/42/5e4da95d-4019-435d-ab52-86564329a195.m4a b/uploads/audio/42/5e4da95d-4019-435d-ab52-86564329a195.m4a deleted file mode 100644 index 1b8c736..0000000 Binary files a/uploads/audio/42/5e4da95d-4019-435d-ab52-86564329a195.m4a and /dev/null differ diff --git a/uploads/audio/43/f956e9b0-e7e2-4a09-8ee6-44601abd2533.m4a b/uploads/audio/43/f956e9b0-e7e2-4a09-8ee6-44601abd2533.m4a deleted file mode 100644 index 1b8c736..0000000 Binary files a/uploads/audio/43/f956e9b0-e7e2-4a09-8ee6-44601abd2533.m4a and /dev/null differ diff --git a/uploads/audio/6/03637f67-96ae-4167-820a-fb87b3df2646.mp3 b/uploads/audio/6/03637f67-96ae-4167-820a-fb87b3df2646.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/audio/6/03637f67-96ae-4167-820a-fb87b3df2646.mp3 and /dev/null differ diff --git a/uploads/audio/809605cc-504c-4e08-aa49-4ed3d95fa8f9.m4a b/uploads/audio/809605cc-504c-4e08-aa49-4ed3d95fa8f9.m4a deleted file mode 100644 index a8d86b3..0000000 Binary files a/uploads/audio/809605cc-504c-4e08-aa49-4ed3d95fa8f9.m4a and /dev/null differ diff --git a/uploads/markdown/.DS_Store b/uploads/markdown/.DS_Store index df22303..981f715 100644 Binary files a/uploads/markdown/.DS_Store and b/uploads/markdown/.DS_Store differ diff --git a/uploads/meeting_records_2.mp3 b/uploads/meeting_records_2.mp3 deleted file mode 100644 index 0fd96c9..0000000 Binary files a/uploads/meeting_records_2.mp3 and /dev/null differ diff --git a/uploads/r.txt b/uploads/r.txt deleted file mode 100644 index a552ffd..0000000 --- a/uploads/r.txt +++ /dev/null @@ -1,7 +0,0 @@ -fastapi -mysql-connector-python -uvicorn[standard] -python-multipart -pydantic[email] -passlib[bcrypt] -qiniu \ No newline at end of file diff --git a/uploads/test_1.xlsx b/uploads/test_1.xlsx deleted file mode 100644 index b332b1c..0000000 Binary files a/uploads/test_1.xlsx and /dev/null differ diff --git a/venv.zip b/venv.zip new file mode 100644 index 0000000..4114a76 Binary files /dev/null and b/venv.zip differ