diff --git a/backend/routers/auth_router.py b/backend/routers/auth_router.py index e43faf3..eae64da 100644 --- a/backend/routers/auth_router.py +++ b/backend/routers/auth_router.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session from auth import create_access_token, get_current_user, hash_password, verify_password from database import get_db from models import User, UserSettings -from schemas import TokenResponse, UserLogin, UserOut, UserRegister +from schemas import TokenResponse, UserLogin, UserOut, UserRegister, UserUpdate router = APIRouter(prefix="/api/auth", tags=["auth"]) @@ -53,3 +53,38 @@ def login(data: UserLogin, db: Session = Depends(get_db)): @router.get("/me", response_model=UserOut) def me(current_user: User = Depends(get_current_user)): return current_user + + +@router.patch("/me", response_model=UserOut) +def update_me( + data: UserUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + if data.username is None and data.new_password is None: + raise HTTPException(status_code=400, detail="请填写要修改的内容") + + if data.username is not None: + username = data.username.strip() + if not username: + raise HTTPException(status_code=400, detail="用户名不能为空") + exists = ( + db.query(User) + .filter(User.username == username, User.id != current_user.id) + .first() + ) + if exists: + raise HTTPException(status_code=400, detail="用户名已存在") + current_user.username = username + + if data.new_password is not None: + if not data.current_password: + raise HTTPException(status_code=400, detail="修改密码需要先输入当前密码") + if not verify_password(data.current_password, current_user.password_hash): + raise HTTPException(status_code=400, detail="当前密码不正确") + current_user.password_hash = hash_password(data.new_password) + + db.add(current_user) + db.commit() + db.refresh(current_user) + return current_user diff --git a/backend/schemas.py b/backend/schemas.py index 6f2cd1d..b8df197 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -28,6 +28,12 @@ class UserOut(BaseModel): from_attributes = True +class UserUpdate(BaseModel): + username: Optional[str] = Field(None, min_length=2, max_length=50) + current_password: Optional[str] = Field(None, min_length=6, max_length=100) + new_password: Optional[str] = Field(None, min_length=6, max_length=100) + + # Translate class TranslateRequest(BaseModel): text: str = Field(min_length=1) diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts index 0a3d61c..297fa0c 100644 --- a/frontend/src/api/request.ts +++ b/frontend/src/api/request.ts @@ -172,6 +172,18 @@ export interface Settings { weak_wrong_threshold: number } +export interface UserProfile { + id: number + username: string + created_at: string +} + +export interface UserProfileUpdate { + username?: string + current_password?: string + new_password?: string +} + export interface MemoryToken { role: string key: string @@ -242,7 +254,8 @@ export const api = { request.post('/auth/register', { username, password }), login: (username: string, password: string, remember = true) => request.post<{ access_token: string }>('/auth/login', { username, password, remember }), - me: () => request.get('/auth/me'), + me: () => request.get('/auth/me'), + updateMe: (data: UserProfileUpdate) => request.patch('/auth/me', data), translate: (text: string) => request.post('/translate', { text }), createWord: (data: Partial) => request.post('/words', data), listWords: (status?: string) => diff --git a/frontend/src/pages/PasswordSettings.vue b/frontend/src/pages/PasswordSettings.vue new file mode 100644 index 0000000..a5063b3 --- /dev/null +++ b/frontend/src/pages/PasswordSettings.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue index 47be031..64c4861 100644 --- a/frontend/src/pages/Settings.vue +++ b/frontend/src/pages/Settings.vue @@ -1,30 +1,54 @@ diff --git a/frontend/src/pages/UsernameSettings.vue b/frontend/src/pages/UsernameSettings.vue new file mode 100644 index 0000000..0f11bea --- /dev/null +++ b/frontend/src/pages/UsernameSettings.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 26836f0..2fd3d57 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -27,6 +27,16 @@ const router = createRouter({ component: () => import('../pages/MemoryCoach.vue'), }, { path: 'settings', name: 'Settings', component: () => import('../pages/Settings.vue') }, + { + path: 'settings/username', + name: 'SettingsUsername', + component: () => import('../pages/UsernameSettings.vue'), + }, + { + path: 'settings/password', + name: 'SettingsPassword', + component: () => import('../pages/PasswordSettings.vue'), + }, ], }, ], diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts index 76e8a50..532b998 100644 --- a/frontend/src/utils/auth.ts +++ b/frontend/src/utils/auth.ts @@ -17,6 +17,10 @@ export function getSavedUsername(): string { return localStorage.getItem(USERNAME_KEY) || '' } +export function setSavedUsername(username: string) { + localStorage.setItem(USERNAME_KEY, username) +} + function clearTokenStores() { localStorage.removeItem(TOKEN_KEY) sessionStorage.removeItem(TOKEN_KEY) @@ -38,7 +42,7 @@ export function setAuth(token: string, remember: boolean, username?: string) { sessionStorage.setItem(TOKEN_KEY, token) } if (username) { - localStorage.setItem(USERNAME_KEY, username) + setSavedUsername(username) } } diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 33dadd5..b9276e7 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/request.ts","./src/composables/usegraphfilter.ts","./src/composables/usequizsession.ts","./src/composables/usequiztimer.ts","./src/router/index.ts","./src/utils/auth.ts","./src/utils/buildpracticequestion.ts","./src/utils/practicefocus.ts","./src/utils/practicegraphdisplay.ts","./src/utils/practicegraphsession.ts","./src/utils/spellquestion.ts","./src/app.vue","./src/components/memorycurvechart.vue","./src/components/quizcard.vue","./src/components/spellcard.vue","./src/components/trainfab.vue","./src/components/wordcard.vue","./src/components/wordgraphcanvas.vue","./src/layouts/mainlayout.vue","./src/pages/dailyquiz.vue","./src/pages/dashboard.vue","./src/pages/graphpractice.vue","./src/pages/login.vue","./src/pages/memorycoach.vue","./src/pages/register.vue","./src/pages/settings.vue","./src/pages/spellquiz.vue","./src/pages/translate.vue","./src/pages/wordlibrary.vue"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/request.ts","./src/composables/usegraphfilter.ts","./src/composables/usequizsession.ts","./src/composables/usequiztimer.ts","./src/router/index.ts","./src/utils/auth.ts","./src/utils/buildpracticequestion.ts","./src/utils/practicefocus.ts","./src/utils/practicegraphdisplay.ts","./src/utils/practicegraphsession.ts","./src/utils/spellquestion.ts","./src/app.vue","./src/components/memorycurvechart.vue","./src/components/quizcard.vue","./src/components/spellcard.vue","./src/components/trainfab.vue","./src/components/wordcard.vue","./src/components/wordgraphcanvas.vue","./src/layouts/mainlayout.vue","./src/pages/dailyquiz.vue","./src/pages/dashboard.vue","./src/pages/graphpractice.vue","./src/pages/login.vue","./src/pages/memorycoach.vue","./src/pages/passwordsettings.vue","./src/pages/register.vue","./src/pages/settings.vue","./src/pages/spellquiz.vue","./src/pages/translate.vue","./src/pages/usernamesettings.vue","./src/pages/wordlibrary.vue"],"version":"5.6.3"} \ No newline at end of file