From 69588de82c5402c36ddadaf5bc9253c9df8cc6cf Mon Sep 17 00:00:00 2001 From: NPS Agent Date: Mon, 11 May 2026 16:23:05 +0930 Subject: [PATCH] Fixed password settings so that changed passwords actually work and I can actually change the passwords --- PROGRESS.md | 13 ++++++++++ api.js | 17 +++++++++++++ app.jsx | 19 +++++++------- backend/main.py | 11 ++++++++ backend/schemas.py | 4 +++ dashy.db | Bin 69632 -> 69632 bytes screens.jsx | 62 ++++++++++++++++++++++++++++++++++++++++----- 7 files changed, 110 insertions(+), 16 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index b853887..61906f4 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -54,6 +54,19 @@ 10. **Task Editing:** Implemented inline editing for task descriptions using an active text box state with "Save/Cancel" actions. 11. **UI Cleanup:** Removed hardcoded, prototype placeholder notes from the `TaskDetail` modal to prepare for future dynamic notes integration. 12. **Permanent Deletion:** Added a "Delete task permanently" button to the `TaskDetail` sidebar with a confirmation dialog, backed by a new `DELETE /tasks/{id}` API endpoint. +13. **Permanent Deletion Wiring Fix:** Resolved a three-layer bug where the delete button was non-functional: + - Added the missing `onDeleteTask` prop to the `TaskDetail` component signature in `screens.jsx`. + - Added the missing `deleteTask` handler in `app.jsx` (calls `api.deleteTask`, writes an audit entry, and closes the modal). + - Restarted the FastAPI backend so the previously-added `DELETE /tasks/{id}` route was loaded into the running process (was returning 405 prior to restart). +14. **Password Management:** Made the "Change password" flow real (previously a placeholder UI). + - **Backend:** Added `PasswordChange` schema and a new `POST /users/{user_id}/password` endpoint that verifies the current password (401 on mismatch) before re-hashing and saving the new one. + - **API client:** Added `api.changePassword(id, oldPwd, newPwd)` that surfaces the backend's `detail` message inline rather than just the HTTP status text. + - **Frontend:** Wired the previously-inert "Update password" button in `SettingsScreen` — submits via `onChangePassword`, shows inline error / success states, disables while in flight, clears the fields on success, and writes a `password_changed` audit entry. + - **Defaults confirmed:** Seeded users (`seed.py`) and admin-created users (`app.jsx`) both default to `password123`. +15. **Real Login Authentication:** Fixed a security bug where the login screen accepted any password. + - The password input on `LoginScreen` was a decorative `defaultValue` field — the button submitted with no password, and `onLogin` had a fallback default of `"password123"` which matched every seeded account. + - Bound the input to component state, send the actual typed password to `api.login`, and let backend `401`s propagate so the screen can render an inline "Incorrect password" message instead of silently letting anyone in. + - Enter key now submits, and the button disables while the request is in flight. ### Phase 3: Advanced Features - **Real-time Notifications:** Explore WebSockets for task assignments. diff --git a/api.js b/api.js index 355e77c..ea7b3f8 100644 --- a/api.js +++ b/api.js @@ -92,6 +92,23 @@ class ApiService { return data; } + async changePassword(id, oldPassword, newPassword) { + const response = await fetch(`${this.baseUrl}/users/${id}/password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), + }, + body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }), + }); + if (!response.ok) { + let detail = response.statusText; + try { const j = await response.json(); if (j.detail) detail = j.detail; } catch {} + throw new Error(detail); + } + return response.json(); + } + async getTasks() { return this.request('/tasks'); } diff --git a/app.jsx b/app.jsx index 26bff3a..f5be8dc 100644 --- a/app.jsx +++ b/app.jsx @@ -72,16 +72,11 @@ function App() { ]); if (!authed) { - return { - try { - await api.login(id, pwd); - setMeId(id); - setAuthed(true); - // Fire & forget audit log - api.addAudit({ actor: id, action: 'login', summary: 'Signed in' }).catch(console.error); - } catch (e) { - alert("Login failed: " + e.message); - } + return { + await api.login(id, pwd); + setMeId(id); + setAuthed(true); + api.addAudit({ actor: id, action: 'login', summary: 'Signed in' }).catch(console.error); }} />; } @@ -303,6 +298,10 @@ function App() { alert("Failed to update user"); } }} + onChangePassword={async (oldPwd, newPwd) => { + await api.changePassword(meId, oldPwd, newPwd); + await api.addAudit({ actor: meId, action: 'password_changed', summary: 'Updated password', target: meId }); + }} /> )} diff --git a/backend/main.py b/backend/main.py index f176752..171958d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -68,6 +68,17 @@ def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Dep db.refresh(db_user) return db_user +@app.post("/users/{user_id}/password") +def change_password(user_id: str, payload: schemas.PasswordChange, db: Session = Depends(get_db)): + db_user = db.query(models.User).filter(models.User.id == user_id).first() + if not db_user: + raise HTTPException(status_code=404, detail="User not found") + if not auth.verify_password(payload.old_password, db_user.password_hash): + raise HTTPException(status_code=401, detail="Current password is incorrect") + db_user.password_hash = auth.get_password_hash(payload.new_password) + db.commit() + return {"message": "Password updated"} + @app.delete("/users/{user_id}") def delete_user(user_id: str, db: Session = Depends(get_db)): db_user = db.query(models.User).filter(models.User.id == user_id).first() diff --git a/backend/schemas.py b/backend/schemas.py index f476bd5..09708a3 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -26,6 +26,10 @@ class UserUpdate(BaseModel): account_type: Optional[str] = None photo: Optional[str] = None +class PasswordChange(BaseModel): + old_password: str + new_password: str + class User(UserBase): created_at: datetime class Config: diff --git a/dashy.db b/dashy.db index 3ddbe1a8e4065558ef03370becdd1697ecefe6f1..cebf391f30d134071dc2e3b63e5b7a12585e5c8d 100644 GIT binary patch delta 863 zcma)(%WD%+6vi_cUw9u%Edebg@v#wN=g!=@cP4G1wqUU$T3fZ1Kyl$lP`l8TNWgBKIFpnxE?j>1p5Hm=`_9T*a%C-fcfU}( zH#8yCo|k6Q%928lo6Nf)O;?wP?Z(OKiL;IBQCjvdyW^Eh)n&(+_Z$9P&77PVo;!1D z_KH5QJFWHf&{(MeRulL){)xZiFSv%DqP^f9eviJw68H$G(N=U2zlE>i9rOT?!9(B~ z`UTc8LAUWmxDDMvui!X323FAr(8i~shYvs*Jc5JpA-)d3;Usv|nk^jxO|&H|=tXYG zWzp8OLMT<0aZ#t*Xeb%6%al@cB$NgIg5Tzr@tOSYBiz6T}BXs1^yozRP2o3UP0 z2?`gyisQ~W71wTB8OLQK0XO)U>*f*TWNkrrolUUADf!;KZg4S9()#9guqmx)nKYw> zV;rIX=~?aVK%(=+dWlkO2hIa5l}a~*tXOo%evYbnh3nVqp4)GlvycqH?!{xEO#T7Z al^wyz!?%Pz!N{_za+_+>%Qg~r3V#6ibn>1L3d?ZL{@QXPfKw~ zHD`BtFi>!BE@N3pW;sfAc~dV%WHDB7LOD5Cv*;fjKu|Re1^^HG5AzS~59JSS4yq0m z57rLm4PXsX4h0S)562C<4UG<)4?qnh4)qT)4vY_84F(Q<4!I3N4nYpr4^Itm4YzDIoZTON30}kQ<52X)Q4&t*RAk+@G;tl~G0aX7z Ang9R* diff --git a/screens.jsx b/screens.jsx index c90b559..7b1d26f 100644 --- a/screens.jsx +++ b/screens.jsx @@ -2,6 +2,24 @@ function LoginScreen({ onLogin }) { const [pickedId, setPickedId] = React.useState('rod'); + const [password, setPassword] = React.useState(''); + const [error, setError] = React.useState(''); + const [busy, setBusy] = React.useState(false); + + React.useEffect(() => { setPassword(''); setError(''); }, [pickedId]); + + const submit = async () => { + if (!password) { setError('Enter your password'); return; } + setError(''); setBusy(true); + try { + await onLogin(pickedId, password); + } catch (e) { + setError('Incorrect password'); + } finally { + setBusy(false); + } + }; + return (
@@ -31,11 +49,21 @@ function LoginScreen({ onLogin }) { - @@ -654,7 +682,7 @@ function FilterChip({ on, onClick, children }) { ); } -function SettingsScreen({ user, dbUsers, isAdmin, onClose, onSave, onLogout, onSwitchUser, onCreateUser, onDeleteUser, onUpdateUserRole }) { +function SettingsScreen({ user, dbUsers, isAdmin, onClose, onSave, onLogout, onSwitchUser, onCreateUser, onDeleteUser, onUpdateUserRole, onChangePassword }) { const [name, setName] = React.useState(user.name); const [role, setRole] = React.useState(user.role); const [photo, setPhoto] = React.useState(user.photo || null); @@ -662,8 +690,27 @@ function SettingsScreen({ user, dbUsers, isAdmin, onClose, onSave, onLogout, onS const [pwOld, setPwOld] = React.useState(''); const [pwNew, setPwNew] = React.useState(''); const [pwConfirm, setPwConfirm] = React.useState(''); + const [pwSaved, setPwSaved] = React.useState(false); + const [pwError, setPwError] = React.useState(''); + const [pwBusy, setPwBusy] = React.useState(false); const [saved, setSaved] = React.useState(false); + const submitPasswordChange = async () => { + setPwError(''); + if (pwNew !== pwConfirm) { setPwError('New passwords do not match'); return; } + setPwBusy(true); + try { + await onChangePassword(pwOld, pwNew); + setPwOld(''); setPwNew(''); setPwConfirm(''); + setPwSaved(true); + setTimeout(() => setPwSaved(false), 2000); + } catch (e) { + setPwError(e.message || 'Failed to update password'); + } finally { + setPwBusy(false); + } + }; + React.useEffect(() => { setName(user.name); setRole(user.role); setPhoto(user.photo || null); }, [user.id]); @@ -783,9 +830,12 @@ function SettingsScreen({ user, dbUsers, isAdmin, onClose, onSave, onLogout, onS
+ {pwError &&
{pwError}
} +
- - + {pwSaved && Password updated} + +