Fixed password settings so that changed passwords actually work and I can actually change the passwords
This commit is contained in:
+13
@@ -54,6 +54,19 @@
|
|||||||
10. **Task Editing:** Implemented inline editing for task descriptions using an active text box state with "Save/Cancel" actions.
|
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.
|
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.
|
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
|
### Phase 3: Advanced Features
|
||||||
- **Real-time Notifications:** Explore WebSockets for task assignments.
|
- **Real-time Notifications:** Explore WebSockets for task assignments.
|
||||||
|
|||||||
@@ -92,6 +92,23 @@ class ApiService {
|
|||||||
return data;
|
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() {
|
async getTasks() {
|
||||||
return this.request('/tasks');
|
return this.request('/tasks');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,16 +72,11 @@ function App() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!authed) {
|
if (!authed) {
|
||||||
return <LoginScreen onLogin={async (id, pwd = "password123") => {
|
return <LoginScreen onLogin={async (id, pwd) => {
|
||||||
try {
|
await api.login(id, pwd);
|
||||||
await api.login(id, pwd);
|
setMeId(id);
|
||||||
setMeId(id);
|
setAuthed(true);
|
||||||
setAuthed(true);
|
api.addAudit({ actor: id, action: 'login', summary: 'Signed in' }).catch(console.error);
|
||||||
// Fire & forget audit log
|
|
||||||
api.addAudit({ actor: id, action: 'login', summary: 'Signed in' }).catch(console.error);
|
|
||||||
} catch (e) {
|
|
||||||
alert("Login failed: " + e.message);
|
|
||||||
}
|
|
||||||
}} />;
|
}} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +298,10 @@ function App() {
|
|||||||
alert("Failed to update user");
|
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 });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Dep
|
|||||||
db.refresh(db_user)
|
db.refresh(db_user)
|
||||||
return 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}")
|
@app.delete("/users/{user_id}")
|
||||||
def delete_user(user_id: str, db: Session = Depends(get_db)):
|
def delete_user(user_id: str, db: Session = Depends(get_db)):
|
||||||
db_user = db.query(models.User).filter(models.User.id == user_id).first()
|
db_user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class UserUpdate(BaseModel):
|
|||||||
account_type: Optional[str] = None
|
account_type: Optional[str] = None
|
||||||
photo: Optional[str] = None
|
photo: Optional[str] = None
|
||||||
|
|
||||||
|
class PasswordChange(BaseModel):
|
||||||
|
old_password: str
|
||||||
|
new_password: str
|
||||||
|
|
||||||
class User(UserBase):
|
class User(UserBase):
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
+56
-6
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
function LoginScreen({ onLogin }) {
|
function LoginScreen({ onLogin }) {
|
||||||
const [pickedId, setPickedId] = React.useState('rod');
|
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 (
|
return (
|
||||||
<div className="login">
|
<div className="login">
|
||||||
<div className="login__card">
|
<div className="login__card">
|
||||||
@@ -31,11 +49,21 @@ function LoginScreen({ onLogin }) {
|
|||||||
|
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span className="field__label">Password</span>
|
<span className="field__label">Password</span>
|
||||||
<input className="field__input" type="password" defaultValue="••••••••••" />
|
<input
|
||||||
|
className="field__input"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
onKeyDown={e => { if (e.key === 'Enter') submit(); }}
|
||||||
|
placeholder="Enter password"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button className="btn btn--primary btn--full" onClick={() => onLogin(pickedId)}>
|
{error && <div className="mono" style={{ color: 'var(--prio-high-dot)', marginTop: '0.25rem' }}>{error}</div>}
|
||||||
Sign in as {findUser(pickedId).name}
|
|
||||||
|
<button className="btn btn--primary btn--full" onClick={submit} disabled={busy}>
|
||||||
|
{busy ? 'Signing in…' : <>Sign in as {findUser(pickedId).name}</>}
|
||||||
<Icon.Arrow />
|
<Icon.Arrow />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -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 [name, setName] = React.useState(user.name);
|
||||||
const [role, setRole] = React.useState(user.role);
|
const [role, setRole] = React.useState(user.role);
|
||||||
const [photo, setPhoto] = React.useState(user.photo || null);
|
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 [pwOld, setPwOld] = React.useState('');
|
||||||
const [pwNew, setPwNew] = React.useState('');
|
const [pwNew, setPwNew] = React.useState('');
|
||||||
const [pwConfirm, setPwConfirm] = 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 [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(() => {
|
React.useEffect(() => {
|
||||||
setName(user.name); setRole(user.role); setPhoto(user.photo || null);
|
setName(user.name); setRole(user.role); setPhoto(user.photo || null);
|
||||||
}, [user.id]);
|
}, [user.id]);
|
||||||
@@ -783,9 +830,12 @@ function SettingsScreen({ user, dbUsers, isAdmin, onClose, onSave, onLogout, onS
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{pwError && <div className="settings__pw-error mono" style={{ color: 'var(--prio-high-dot)', marginTop: '0.5rem' }}>{pwError}</div>}
|
||||||
|
|
||||||
<div className="settings__save-row">
|
<div className="settings__save-row">
|
||||||
<button className="btn btn--ghost" onClick={() => { setPwOld(''); setPwNew(''); setPwConfirm(''); }}>Cancel</button>
|
{pwSaved && <span className="settings__saved mono"><Icon.Check /> Password updated</span>}
|
||||||
<button className="btn btn--primary" disabled={!pwOld || !pwNew || pwNew !== pwConfirm}>Update password</button>
|
<button className="btn btn--ghost" onClick={() => { setPwOld(''); setPwNew(''); setPwConfirm(''); setPwError(''); }}>Cancel</button>
|
||||||
|
<button className="btn btn--primary" disabled={!pwOld || !pwNew || pwNew !== pwConfirm || pwBusy} onClick={submitPasswordChange}>{pwBusy ? 'Updating…' : 'Update password'}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings__divider" />
|
<div className="settings__divider" />
|
||||||
|
|||||||
Reference in New Issue
Block a user