From 86247aa5a2ce3ee4b24e62b22203e8b39a8ba3dd Mon Sep 17 00:00:00 2001 From: bdim404 Date: Mon, 26 Jan 2026 16:41:22 +0800 Subject: [PATCH] Implement password change functionality and initialize superuser --- qwen3-tts-backend/api/auth.py | 34 +++- qwen3-tts-backend/core/init_admin.py | 38 +++++ qwen3-tts-backend/db/crud.py | 18 ++ qwen3-tts-backend/main.py | 7 + qwen3-tts-backend/schemas/user.py | 24 ++- qwen3-tts-frontend/src/components/Navbar.tsx | 104 +++++++---- .../components/users/ChangePasswordDialog.tsx | 161 ++++++++++++++++++ qwen3-tts-frontend/src/lib/api.ts | 13 +- qwen3-tts-frontend/src/lib/constants.ts | 1 + qwen3-tts-frontend/src/types/auth.ts | 6 + 10 files changed, 368 insertions(+), 38 deletions(-) create mode 100644 qwen3-tts-backend/core/init_admin.py create mode 100644 qwen3-tts-frontend/src/components/users/ChangePasswordDialog.tsx diff --git a/qwen3-tts-backend/api/auth.py b/qwen3-tts-backend/api/auth.py index e239ec2..ec8a599 100644 --- a/qwen3-tts-backend/api/auth.py +++ b/qwen3-tts-backend/api/auth.py @@ -14,8 +14,8 @@ from core.security import ( decode_access_token ) from db.database import get_db -from db.crud import get_user_by_username, get_user_by_email, create_user -from schemas.user import User, UserCreate, Token +from db.crud import get_user_by_username, get_user_by_email, create_user, change_user_password +from schemas.user import User, UserCreate, Token, PasswordChange router = APIRouter(prefix="/auth", tags=["authentication"]) @@ -105,3 +105,33 @@ async def get_current_user_info( current_user: Annotated[User, Depends(get_current_user)] ): return current_user + +@router.post("/change-password", response_model=User) +@limiter.limit("5/minute") +async def change_password( + request: Request, + password_data: PasswordChange, + current_user: Annotated[User, Depends(get_current_user)], + db: Session = Depends(get_db) +): + if not verify_password(password_data.current_password, current_user.hashed_password): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Current password is incorrect" + ) + + new_hashed_password = get_password_hash(password_data.new_password) + + user = change_user_password( + db, + user_id=current_user.id, + new_hashed_password=new_hashed_password + ) + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return user diff --git a/qwen3-tts-backend/core/init_admin.py b/qwen3-tts-backend/core/init_admin.py new file mode 100644 index 0000000..20cf809 --- /dev/null +++ b/qwen3-tts-backend/core/init_admin.py @@ -0,0 +1,38 @@ +import logging +from core.database import SessionLocal +from core.security import get_password_hash +from db.crud import count_users, create_user_by_admin + +logger = logging.getLogger(__name__) + +def init_superuser(): + db = SessionLocal() + try: + user_count = count_users(db) + + if user_count > 0: + logger.info(f"Database already has {user_count} user(s), skipping admin initialization") + return + + logger.info("No users found in database, initializing default superuser") + + hashed_password = get_password_hash("admin123456") + admin_user = create_user_by_admin( + db, + username="admin", + email="admin@example.com", + hashed_password=hashed_password, + is_superuser=True + ) + + logger.info(f"Default superuser created successfully: {admin_user.username}") + logger.warning("SECURITY WARNING: Default admin credentials are in use!") + logger.warning(" Username: admin") + logger.warning(" Password: admin123456") + logger.warning(" Please change the password immediately after first login!") + + except Exception as e: + logger.error(f"Failed to initialize superuser: {e}") + raise + finally: + db.close() diff --git a/qwen3-tts-backend/db/crud.py b/qwen3-tts-backend/db/crud.py index c4bbc9d..0a24e95 100644 --- a/qwen3-tts-backend/db/crud.py +++ b/qwen3-tts-backend/db/crud.py @@ -11,6 +11,9 @@ def get_user_by_username(db: Session, username: str) -> Optional[User]: def get_user_by_email(db: Session, email: str) -> Optional[User]: return db.query(User).filter(User.email == email).first() +def count_users(db: Session) -> int: + return db.query(User).count() + def create_user(db: Session, username: str, email: str, hashed_password: str) -> User: user = User( username=username, @@ -85,6 +88,21 @@ def delete_user(db: Session, user_id: int) -> bool: db.commit() return True +def change_user_password( + db: Session, + user_id: int, + new_hashed_password: str +) -> Optional[User]: + user = get_user_by_id(db, user_id) + if not user: + return None + + user.hashed_password = new_hashed_password + user.updated_at = datetime.utcnow() + db.commit() + db.refresh(user) + return user + def create_job(db: Session, user_id: int, job_type: str, input_data: Dict[str, Any]) -> Job: job = Job( user_id=user_id, diff --git a/qwen3-tts-backend/main.py b/qwen3-tts-backend/main.py index c142ac6..6fe8c7b 100644 --- a/qwen3-tts-backend/main.py +++ b/qwen3-tts-backend/main.py @@ -70,6 +70,13 @@ async def lifespan(app: FastAPI): logger.error(f"Database initialization failed: {e}") raise + try: + from core.init_admin import init_superuser + init_superuser() + except Exception as e: + logger.error(f"Superuser initialization failed: {e}") + raise + try: model_manager = await ModelManager.get_instance() await model_manager.load_model("custom-voice") diff --git a/qwen3-tts-backend/schemas/user.py b/qwen3-tts-backend/schemas/user.py index 055566d..9eb2189 100644 --- a/qwen3-tts-backend/schemas/user.py +++ b/qwen3-tts-backend/schemas/user.py @@ -1,6 +1,6 @@ from datetime import datetime from typing import Optional -from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict +from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator, ConfigDict import re class UserBase(BaseModel): @@ -89,3 +89,25 @@ class Token(BaseModel): class TokenData(BaseModel): username: Optional[str] = None + +class PasswordChange(BaseModel): + current_password: str = Field(..., min_length=1) + new_password: str = Field(..., min_length=8, max_length=128) + confirm_password: str = Field(..., min_length=8, max_length=128) + + @field_validator('new_password') + @classmethod + def validate_password_strength(cls, v: str) -> str: + if not re.search(r'[A-Z]', v): + raise ValueError('Password must contain at least one uppercase letter') + if not re.search(r'[a-z]', v): + raise ValueError('Password must contain at least one lowercase letter') + if not re.search(r'\d', v): + raise ValueError('Password must contain at least one digit') + return v + + @model_validator(mode='after') + def passwords_match(self) -> 'PasswordChange': + if self.new_password != self.confirm_password: + raise ValueError('Passwords do not match') + return self diff --git a/qwen3-tts-frontend/src/components/Navbar.tsx b/qwen3-tts-frontend/src/components/Navbar.tsx index 3583b33..e1abe8b 100644 --- a/qwen3-tts-frontend/src/components/Navbar.tsx +++ b/qwen3-tts-frontend/src/components/Navbar.tsx @@ -1,8 +1,13 @@ -import { Menu, LogOut, Users } from 'lucide-react' +import { Menu, LogOut, Users, KeyRound } from 'lucide-react' import { Link } from 'react-router-dom' +import { useState } from 'react' +import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { ThemeToggle } from '@/components/ThemeToggle' import { useAuth } from '@/contexts/AuthContext' +import { authApi } from '@/lib/api' +import { ChangePasswordDialog } from '@/components/users/ChangePasswordDialog' +import type { PasswordChangeRequest } from '@/types/auth' interface NavbarProps { onToggleSidebar?: () => void @@ -10,41 +15,72 @@ interface NavbarProps { export function Navbar({ onToggleSidebar }: NavbarProps) { const { logout, user } = useAuth() + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false) + const [isChangingPassword, setIsChangingPassword] = useState(false) + + const handlePasswordChange = async (data: PasswordChangeRequest) => { + try { + setIsChangingPassword(true) + await authApi.changePassword(data) + toast.success('密码修改成功') + setPasswordDialogOpen(false) + } catch (error: any) { + toast.error(error.message || '密码修改失败') + } finally { + setIsChangingPassword(false) + } + } return ( - + +
+ +

+ Qwen3-TTS-WebUI +

+ +
+ +
+ {user?.is_superuser && ( + + + + )} + + + +
+ + + + ) } diff --git a/qwen3-tts-frontend/src/components/users/ChangePasswordDialog.tsx b/qwen3-tts-frontend/src/components/users/ChangePasswordDialog.tsx new file mode 100644 index 0000000..fd034dc --- /dev/null +++ b/qwen3-tts-frontend/src/components/users/ChangePasswordDialog.tsx @@ -0,0 +1,161 @@ +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import * as z from 'zod' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import type { PasswordChangeRequest } from '@/types/auth' + +const passwordChangeSchema = z.object({ + current_password: z.string().min(1, '请输入当前密码'), + new_password: z + .string() + .min(8, '密码至少8个字符') + .regex(/[A-Z]/, '密码必须包含至少一个大写字母') + .regex(/[a-z]/, '密码必须包含至少一个小写字母') + .regex(/\d/, '密码必须包含至少一个数字'), + confirm_password: z.string().min(1, '请确认新密码'), +}).refine((data) => data.new_password === data.confirm_password, { + message: '两次输入的密码不一致', + path: ['confirm_password'], +}) + +type PasswordChangeFormValues = z.infer + +interface ChangePasswordDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSubmit: (data: PasswordChangeRequest) => Promise + isLoading: boolean +} + +export function ChangePasswordDialog({ + open, + onOpenChange, + onSubmit, + isLoading, +}: ChangePasswordDialogProps) { + const form = useForm({ + resolver: zodResolver(passwordChangeSchema), + defaultValues: { + current_password: '', + new_password: '', + confirm_password: '', + }, + }) + + const handleSubmit = async (data: PasswordChangeFormValues) => { + await onSubmit(data) + form.reset() + } + + const handleOpenChange = (open: boolean) => { + if (!open && !isLoading) { + form.reset() + } + onOpenChange(open) + } + + return ( + + + + 修改密码 + + 请输入当前密码和新密码。新密码至少8个字符,包含大小写字母和数字。 + + + +
+ + ( + + 当前密码 + + + + + + )} + /> + + ( + + 新密码 + + + + + + )} + /> + + ( + + 确认新密码 + + + + + + )} + /> + + + + + + + +
+
+ ) +} diff --git a/qwen3-tts-frontend/src/lib/api.ts b/qwen3-tts-frontend/src/lib/api.ts index 97ba969..0e37beb 100644 --- a/qwen3-tts-frontend/src/lib/api.ts +++ b/qwen3-tts-frontend/src/lib/api.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import type { LoginRequest, LoginResponse, User } from '@/types/auth' +import type { LoginRequest, LoginResponse, User, PasswordChangeRequest } from '@/types/auth' import type { Job, JobCreateResponse, JobListResponse, JobType } from '@/types/job' import type { Language, Speaker, CustomVoiceForm, VoiceDesignForm, VoiceCloneForm } from '@/types/tts' import type { UserCreateRequest, UserUpdateRequest, UserListResponse } from '@/types/user' @@ -32,6 +32,9 @@ const FIELD_NAMES: Record = { username: '用户名', email: '邮箱', password: '密码', + current_password: '当前密码', + new_password: '新密码', + confirm_password: '确认密码', is_active: '激活状态', is_superuser: '超级管理员', } @@ -174,6 +177,14 @@ export const authApi = { const response = await apiClient.get(API_ENDPOINTS.AUTH.ME) return response.data }, + + changePassword: async (data: PasswordChangeRequest): Promise => { + const response = await apiClient.post( + API_ENDPOINTS.AUTH.CHANGE_PASSWORD, + data + ) + return response.data + }, } export const ttsApi = { diff --git a/qwen3-tts-frontend/src/lib/constants.ts b/qwen3-tts-frontend/src/lib/constants.ts index b9f3ddc..c7a8d05 100644 --- a/qwen3-tts-frontend/src/lib/constants.ts +++ b/qwen3-tts-frontend/src/lib/constants.ts @@ -2,6 +2,7 @@ export const API_ENDPOINTS = { AUTH: { LOGIN: '/auth/token', ME: '/auth/me', + CHANGE_PASSWORD: '/auth/change-password', }, TTS: { LANGUAGES: '/tts/languages', diff --git a/qwen3-tts-frontend/src/types/auth.ts b/qwen3-tts-frontend/src/types/auth.ts index 1be746a..ec76c89 100644 --- a/qwen3-tts-frontend/src/types/auth.ts +++ b/qwen3-tts-frontend/src/types/auth.ts @@ -23,3 +23,9 @@ export interface AuthState { isLoading: boolean isAuthenticated: boolean } + +export interface PasswordChangeRequest { + current_password: string + new_password: string + confirm_password: string +}