From 97db4d714ab93142570184dbd7147d1d525cfa53 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 22 Jun 2023 20:54:52 +0100 Subject: [PATCH] userpage: implement change password functionality --- api-userpage.go | 77 ++++++++++++++++++++++++++++++++++++++--- api-users.go | 1 + html/form.html | 2 +- lang/form/en-us.json | 2 ++ models.go | 5 +++ router.go | 1 + ts/form.ts | 13 +++---- ts/modules/validator.ts | 5 +++ ts/user.ts | 30 +++++++++++++--- 9 files changed, 118 insertions(+), 18 deletions(-) diff --git a/api-userpage.go b/api-userpage.go index f129a08..413f8cf 100644 --- a/api-userpage.go +++ b/api-userpage.go @@ -275,6 +275,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) { // @Failure 500 {object} boolResponse // @Param invCode path string true "invite Code" // @Router /my/discord/invite [get] +// @Security Bearer // @tags User Page func (app *appContext) MyDiscordServerInvite(gc *gin.Context) { if app.discord.inviteChannelName == "" { @@ -295,6 +296,7 @@ func (app *appContext) MyDiscordServerInvite(gc *gin.Context) { // @Failure 400 {object} stringResponse // Param service path string true "discord/telegram" // @Router /my/pin/{service} [get] +// @Security Bearer // @tags User Page func (app *appContext) GetMyPIN(gc *gin.Context) { service := gc.Param("service") @@ -319,6 +321,7 @@ func (app *appContext) GetMyPIN(gc *gin.Context) { // @Failure 401 {object} boolResponse // @Param pin path string true "PIN code to check" // @Router /my/discord/verified/{pin} [get] +// @Security Bearer // @tags User Page func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) { pin := gc.Param("pin") @@ -347,6 +350,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) { // @Failure 401 {object} boolResponse // @Param pin path string true "PIN code to check" // @Router /my/telegram/verified/{pin} [get] +// @Security Bearer // @tags User Page func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) { pin := gc.Param("pin") @@ -386,6 +390,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) { // @Failure 500 {object} boolResponse // @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID." // @Router /my/matrix/user [post] +// @Security Bearer // @tags User Page func (app *appContext) MatrixSendMyPIN(gc *gin.Context) { var req MatrixSendPINDTO @@ -419,6 +424,7 @@ func (app *appContext) MatrixSendMyPIN(gc *gin.Context) { // @Param invCode path string true "invite Code" // @Param userID path string true "Matrix User ID" // @Router /my/matrix/verified/{userID}/{pin} [get] +// @Security Bearer // @tags User Page func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) { userID := gc.Param("userID") @@ -452,7 +458,8 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) { // @Produce json // @Success 200 {object} boolResponse // @Router /my/discord [delete] -// @Tags Users +// @Security Bearer +// @Tags User Page func (app *appContext) UnlinkMyDiscord(gc *gin.Context) { app.storage.DeleteDiscordKey(gc.GetString("jfId")) respondBool(200, true, gc) @@ -462,7 +469,8 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) { // @Produce json // @Success 200 {object} boolResponse // @Router /my/telegram [delete] -// @Tags Users +// @Security Bearer +// @Tags User Page func (app *appContext) UnlinkMyTelegram(gc *gin.Context) { app.storage.DeleteTelegramKey(gc.GetString("jfId")) respondBool(200, true, gc) @@ -472,7 +480,8 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) { // @Produce json // @Success 200 {object} boolResponse // @Router /my/matrix [delete] -// @Tags Users +// @Security Bearer +// @Tags User Page func (app *appContext) UnlinkMyMatrix(gc *gin.Context) { app.storage.DeleteMatrixKey(gc.GetString("jfId")) respondBool(200, true, gc) @@ -485,7 +494,7 @@ func (app *appContext) UnlinkMyMatrix(gc *gin.Context) { // @Failure 400 {object} boolResponse // @Failure 500 {object} boolResponse // @Router /my/password/reset/{address} [post] -// @tags Users +// @Tags User Page func (app *appContext) ResetMyPassword(gc *gin.Context) { // All requests should take 1 second, to make it harder to tell if a success occured or not. timerWait := make(chan bool) @@ -551,3 +560,63 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) { return } } + +// @Summary Change your password, given the old one and the new one. +// @Produce json +// @Param ChangeMyPasswordDTO body ChangeMyPasswordDTO true "User's old & new passwords." +// @Success 204 {object} boolResponse +// @Failure 400 {object} PasswordValidation +// @Failure 401 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /my/password [post] +// @Security Bearer +// @Tags User Page +func (app *appContext) ChangeMyPassword(gc *gin.Context) { + var req ChangeMyPasswordDTO + gc.BindJSON(&req) + if req.Old == "" || req.New == "" { + respondBool(400, false, gc) + } + validation := app.validator.validate(req.New) + for _, val := range validation { + if !val { + app.debug.Printf("%s: Change password failed: Invalid password", gc.GetString("jfId")) + gc.JSON(400, validation) + return + } + } + user, status, err := app.jf.UserByID(gc.GetString("jfId"), false) + if status != 200 || err != nil { + app.err.Printf("Failed to change password: couldn't find user (%d): %+v", status, err) + respondBool(500, false, gc) + return + } + // Authenticate as user to confirm old password. + user, status, err = app.authJf.Authenticate(user.Name, req.Old) + if status != 200 || err != nil { + respondBool(401, false, gc) + return + } + status, err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New) + if (status != 200 && status != 204) || err != nil { + respondBool(500, false, gc) + return + } + if app.config.Section("ombi").Key("enabled").MustBool(false) { + ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId")) + if status != 200 || err != nil { + app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err) + respondBool(204, true, gc) + return + } + ombiUser["password"] = req.New + status, err = app.ombi.ModifyUser(ombiUser) + if status != 200 || err != nil { + app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err) + respondBool(204, true, gc) + return + } + app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"]) + } + respondBool(204, true, gc) +} diff --git a/api-users.go b/api-users.go index b73f591..18b4a10 100644 --- a/api-users.go +++ b/api-users.go @@ -463,6 +463,7 @@ func (app *appContext) NewUser(gc *gin.Context) { for _, val := range validation { if !val { valid = false + break } } if !valid { diff --git a/html/form.html b/html/form.html index 15e83e2..ae881b6 100644 --- a/html/form.html +++ b/html/form.html @@ -124,7 +124,7 @@
- {{ .strings.passwordRequirementsHeader }} + {{ .strings.passwordRequirementsHeader }}
    {{ range $key, $value := .requirements }}
  • diff --git a/lang/form/en-us.json b/lang/form/en-us.json index 13ef671..5fd7b4c 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -50,6 +50,8 @@ "errorCaptcha": "Captcha incorrect.", "errorPassword": "Check password requirements.", "errorNoMatch": "Passwords don't match.", + "errorOldPassword": "Old password incorrect.", + "passwordChanged": "Password Changed.", "verified": "Account verified." }, "validationStrings": { diff --git a/models.go b/models.go index bc2204b..3f6502d 100644 --- a/models.go +++ b/models.go @@ -408,3 +408,8 @@ const ( type GetMyPINDTO struct { PIN string `json:"pin"` } + +type ChangeMyPasswordDTO struct { + Old string `json:"old"` + New string `json:"new"` +} diff --git a/router.go b/router.go index ef6254b..56754df 100644 --- a/router.go +++ b/router.go @@ -241,6 +241,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) { user.DELETE(p+"/discord", app.UnlinkMyDiscord) user.DELETE(p+"/telegram", app.UnlinkMyTelegram) user.DELETE(p+"/matrix", app.UnlinkMyMatrix) + user.POST(p+"/password", app.ChangeMyPassword) } } } diff --git a/ts/form.ts b/ts/form.ts index 8707aa9..e0d3a57 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -2,7 +2,7 @@ import { Modal } from "./modules/modal.js"; import { notificationBox, whichAnimationEvent } from "./modules/common.js"; import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from "./modules/common.js"; import { loadLangSelector } from "./modules/lang.js"; -import { Validator, ValidatorConf } from "./modules/validator.js"; +import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; interface formWindow extends Window { @@ -257,11 +257,6 @@ if (window.emailRequired) { emailField.addEventListener("keyup", validator.validate) } -interface respDTO { - response: boolean; - error: string; -} - interface sendDTO { code: string; email: string; @@ -340,11 +335,11 @@ const create = (event: SubmitEvent) => { } _post("/newUser", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { - let vals = req.response as respDTO; + let vals = req.response as ValidatorRespDTO; let valid = true; for (let type in vals) { - if (requirements[type]) { requirements[type].valid = vals[type]; } - if (!vals[type]) { valid = false; } + if (requirements[type]) requirements[type].valid = vals[type]; + if (!vals[type]) valid = false; } if (req.status == 200 && valid) { if (window.redirectToJellyfin == true) { diff --git a/ts/modules/validator.ts b/ts/modules/validator.ts index e535070..3a10843 100644 --- a/ts/modules/validator.ts +++ b/ts/modules/validator.ts @@ -9,6 +9,11 @@ interface pwValString { plural: string; } +export interface ValidatorRespDTO { + response: boolean; + error: string; +} + interface pwValStrings { length: pwValString; uppercase: pwValString; diff --git a/ts/user.ts b/ts/user.ts index a48c0f2..b21f555 100644 --- a/ts/user.ts +++ b/ts/user.ts @@ -1,10 +1,10 @@ import { ThemeManager } from "./modules/theme.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { Modal } from "./modules/modal.js"; -import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader } from "./modules/common.js"; +import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader, addLoader, removeLoader } from "./modules/common.js"; import { Login } from "./modules/login.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; -import { Validator, ValidatorConf } from "./modules/validator.js"; +import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js"; interface userWindow extends Window { jellyfinID: string; @@ -385,7 +385,7 @@ const matrixConf: MatrixConfiguration = { }; let matrix = new Matrix(matrixConf); - + const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement; const newPasswordField = document.getElementById("user-new-password") as HTMLInputElement; @@ -405,9 +405,31 @@ let validatorConf: ValidatorConf = { }; let validator = new Validator(validatorConf); -let requirements = validator.requirements; +// let requirements = validator.requirements; oldPasswordField.addEventListener("keyup", validator.validate); +changePasswordButton.addEventListener("click", () => { + addLoader(changePasswordButton); + _post("/my/password", { old: oldPasswordField.value, new: newPasswordField.value }, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(changePasswordButton); + if (req.status == 400) { + window.notifications.customError("errorPassword", window.lang.notif("errorPassword")); + } else if (req.status == 500) { + window.notifications.customError("errorUnknown", window.lang.notif("errorUnknown")); + } else if (req.status == 204) { + window.notifications.customSuccess("passwordChanged", window.lang.notif("passwordChanged")); + setTimeout(() => { window.location.reload() }, 2000); + } + }, true, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + if (req.status == 401) { + removeLoader(changePasswordButton); + window.notifications.customError("oldPasswordError", window.lang.notif("errorOldPassword")); + return; + } + }); +}); // FIXME: Submit & Validate document.addEventListener("details-reload", () => {