userpage: implement change password functionality

This commit is contained in:
Harvey Tindall 2023-06-22 20:54:52 +01:00
parent 12ce669566
commit 97db4d714a
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
9 changed files with 118 additions and 18 deletions

View File

@ -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)
}

View File

@ -463,6 +463,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
for _, val := range validation {
if !val {
valid = false
break
}
}
if !valid {

View File

@ -124,7 +124,7 @@
</div>
<div class="flex-initial">
<div class="card ~neutral @low mb-4">
<span class="label supra" for="inv-uses">{{ .strings.passwordRequirementsHeader }}</span>
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
<ul>
{{ range $key, $value := .requirements }}
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">

View File

@ -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": {

View File

@ -408,3 +408,8 @@ const (
type GetMyPINDTO struct {
PIN string `json:"pin"`
}
type ChangeMyPasswordDTO struct {
Old string `json:"old"`
New string `json:"new"`
}

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -9,6 +9,11 @@ interface pwValString {
plural: string;
}
export interface ValidatorRespDTO {
response: boolean;
error: string;
}
interface pwValStrings {
length: pwValString;
uppercase: pwValString;

View File

@ -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", () => {