Skip to content

Commit 26b95fa

Browse files
authored
Merge pull request #121 from djedi/feat/change-password
feat: add password change in settings
2 parents de06576 + 044827d commit 26b95fa

2 files changed

Lines changed: 219 additions & 0 deletions

File tree

app/routes.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,57 @@ async def get_profile():
792792
)
793793

794794

795+
@app.route("/api/settings/password", methods=["PUT"])
796+
@jwt_required()
797+
async def change_password():
798+
"""
799+
Change user's password.
800+
801+
Request body:
802+
- current_password: Current password for verification
803+
- new_password: New password to set
804+
805+
Returns:
806+
- 200: Password changed successfully
807+
- 400: Missing fields or password too short
808+
- 401: Current password is incorrect
809+
"""
810+
req = await request.get_json()
811+
current_password = req.get("current_password", "")
812+
new_password = req.get("new_password", "")
813+
814+
username = get_jwt_identity()
815+
816+
if not username:
817+
abort(401)
818+
819+
user = User.query.filter_by(username=username.lower()).first()
820+
821+
if not user:
822+
abort(400)
823+
824+
# Validate inputs
825+
if not current_password:
826+
return jsonify(error="Current password is required"), 400
827+
828+
if not new_password:
829+
return jsonify(error="New password is required"), 400
830+
831+
if len(new_password) < 4:
832+
return jsonify(error="Password must be at least 4 characters"), 400
833+
834+
# Verify current password
835+
if not argon2.check_password_hash(user.password_hash, current_password):
836+
return jsonify(error="Current password is incorrect"), 401
837+
838+
# Update password
839+
user.password_hash = argon2.generate_password_hash(new_password)
840+
db.session.add(user)
841+
db.session.commit()
842+
843+
return jsonify(message="Password changed successfully"), 200
844+
845+
795846
@app.route("/api/settings/email", methods=["PUT"])
796847
@jwt_required()
797848
async def update_email():

client/src/components/Settings.vue

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,61 @@
9191
</div>
9292
</div>
9393
</div>
94+
95+
<div class="password-section">
96+
<div class="password-header">
97+
<span class="password-label">Password</span>
98+
<b-button
99+
v-if="!showChangePassword"
100+
size="is-small"
101+
@click="showChangePassword = true"
102+
>
103+
Change Password
104+
</b-button>
105+
</div>
106+
<div v-if="showChangePassword" class="password-change-form">
107+
<b-field label="Current password">
108+
<b-input
109+
v-model="currentPassword"
110+
type="password"
111+
placeholder="Enter current password"
112+
size="is-small"
113+
password-reveal
114+
/>
115+
</b-field>
116+
<b-field label="New password">
117+
<b-input
118+
v-model="newPassword"
119+
type="password"
120+
placeholder="Enter new password"
121+
size="is-small"
122+
password-reveal
123+
/>
124+
</b-field>
125+
<b-field label="Confirm new password">
126+
<b-input
127+
v-model="confirmPassword"
128+
type="password"
129+
placeholder="Confirm new password"
130+
size="is-small"
131+
password-reveal
132+
/>
133+
</b-field>
134+
<div class="password-change-actions">
135+
<b-button
136+
size="is-small"
137+
type="is-primary"
138+
@click="changePassword"
139+
:loading="passwordSaving"
140+
>
141+
Update Password
142+
</b-button>
143+
<b-button size="is-small" @click="cancelChangePassword">
144+
Cancel
145+
</b-button>
146+
</div>
147+
</div>
148+
</div>
94149
</div>
95150

96151
<div class="settings-card">
@@ -311,6 +366,13 @@ const emailLoading = ref(false);
311366
const emailSaving = ref(false);
312367
const showChangeEmail = ref(false);
313368
369+
// Password change
370+
const showChangePassword = ref(false);
371+
const currentPassword = ref('');
372+
const newPassword = ref('');
373+
const confirmPassword = ref('');
374+
const passwordSaving = ref(false);
375+
314376
const themeOptions = [
315377
{ value: 'light', label: 'Light', icon: 'fas fa-sun' },
316378
{ value: 'dark', label: 'Dark', icon: 'fas fa-moon' },
@@ -449,6 +511,79 @@ const removeEmail = async () => {
449511
}
450512
};
451513
514+
const changePassword = async () => {
515+
// Validate inputs
516+
if (!currentPassword.value) {
517+
buefy?.toast.open({
518+
message: 'Please enter your current password',
519+
type: 'is-danger',
520+
duration: 3000,
521+
});
522+
return;
523+
}
524+
525+
if (!newPassword.value) {
526+
buefy?.toast.open({
527+
message: 'Please enter a new password',
528+
type: 'is-danger',
529+
duration: 3000,
530+
});
531+
return;
532+
}
533+
534+
if (newPassword.value.length < 4) {
535+
buefy?.toast.open({
536+
message: 'Password must be at least 4 characters',
537+
type: 'is-danger',
538+
duration: 3000,
539+
});
540+
return;
541+
}
542+
543+
if (newPassword.value !== confirmPassword.value) {
544+
buefy?.toast.open({
545+
message: 'New passwords do not match',
546+
type: 'is-danger',
547+
duration: 3000,
548+
});
549+
return;
550+
}
551+
552+
passwordSaving.value = true;
553+
try {
554+
await Requests.put('/settings/password', {
555+
current_password: currentPassword.value,
556+
new_password: newPassword.value,
557+
});
558+
// Clear form and hide it
559+
currentPassword.value = '';
560+
newPassword.value = '';
561+
confirmPassword.value = '';
562+
showChangePassword.value = false;
563+
buefy?.toast.open({
564+
message: 'Password changed successfully',
565+
type: 'is-success',
566+
duration: 2000,
567+
});
568+
} catch (e: unknown) {
569+
const error = e as { response?: { data?: { error?: string } } };
570+
buefy?.toast.open({
571+
message: error?.response?.data?.error || 'Unable to change password',
572+
type: 'is-danger',
573+
duration: 3000,
574+
});
575+
} finally {
576+
passwordSaving.value = false;
577+
}
578+
};
579+
580+
const cancelChangePassword = () => {
581+
currentPassword.value = '';
582+
newPassword.value = '';
583+
confirmPassword.value = '';
584+
showChangePassword.value = false;
585+
};
586+
452587
const onAutoSaveChange = (value: boolean) => {
453588
// Update the setting immediately
454589
sidebar.toggleAutoSave(value);
@@ -1144,4 +1279,37 @@ const close = () => {
11441279
display: flex;
11451280
gap: 8px;
11461281
}
1282+
1283+
/* Password management */
1284+
.password-section {
1285+
margin-top: 16px;
1286+
padding-top: 16px;
1287+
border-top: 1px solid var(--border-color);
1288+
}
1289+
1290+
.password-header {
1291+
display: flex;
1292+
justify-content: space-between;
1293+
align-items: center;
1294+
}
1295+
1296+
.password-label {
1297+
color: var(--text-secondary);
1298+
font-weight: 500;
1299+
}
1300+
1301+
.password-change-form {
1302+
display: flex;
1303+
flex-direction: column;
1304+
gap: 12px;
1305+
margin-top: 12px;
1306+
padding-top: 12px;
1307+
border-top: 1px solid var(--border-color);
1308+
}
1309+
1310+
.password-change-actions {
1311+
display: flex;
1312+
gap: 8px;
1313+
margin-top: 4px;
1314+
}
11471315
</style>

0 commit comments

Comments
 (0)