diff --git a/api.go b/api.go index 3502c27..af24370 100644 --- a/api.go +++ b/api.go @@ -918,6 +918,74 @@ func (app *appContext) DeleteAnnounceTemplate(gc *gin.Context) { respondBool(200, false, gc) } +// @Summary Generate password reset links for a list of users, sending the links to them if possible. +// @Produce json +// @Param AdminPasswordResetDTO body AdminPasswordResetDTO true "List of user IDs" +// @Success 204 {object} boolResponse +// @Success 200 {object} AdminPasswordResetRespDTO +// @Failure 400 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/password-reset [post] +// @Security Bearer +// @tags Users +func (app *appContext) AdminPasswordReset(gc *gin.Context) { + var req AdminPasswordResetDTO + gc.BindJSON(&req) + if req.Users == nil || len(req.Users) == 0 { + app.debug.Println("Ignoring empty request for PWR") + respondBool(400, false, gc) + return + } + linkCount := 0 + var pwr InternalPWR + var err error + resp := AdminPasswordResetRespDTO{} + for _, id := range req.Users { + pwr, err = app.GenInternalReset(id) + if err != nil { + app.err.Printf("Failed to get user from Jellyfin: %v", err) + respondBool(500, false, gc) + return + } + if app.internalPWRs == nil { + app.internalPWRs = map[string]InternalPWR{} + } + app.internalPWRs[pwr.PIN] = pwr + sendAddress := app.getAddressOrName(id) + if sendAddress == "" || len(req.Users) == 1 { + resp.Link, err = app.GenResetLink(pwr.PIN) + linkCount++ + if sendAddress == "" { + resp.Manual = true + } + } + if sendAddress != "" { + msg, err := app.email.constructReset( + PasswordReset{ + Pin: pwr.PIN, + Username: pwr.Username, + Expiry: pwr.Expiry, + Internal: true, + }, app, false, + ) + if err != nil { + app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err) + respondBool(500, false, gc) + return + } else if err := app.sendByID(msg, id); err != nil { + app.err.Printf("Failed to send password reset message to \"%s\": %v", sendAddress, err) + } else { + app.info.Printf("Sent password reset message to \"%s\"", sendAddress) + } + } + } + if resp.Link != "" && linkCount == 1 { + gc.JSON(200, resp) + return + } + respondBool(204, true, gc) +} + // @Summary Create a new invite. // @Produce json // @Param generateInviteDTO body generateInviteDTO true "New invite request object" @@ -1505,38 +1573,70 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { gc.JSON(400, validation) return } - resp, status, err := app.jf.ResetPassword(req.PIN) - if status != 200 || err != nil || !resp.Success { - app.err.Printf("Password Reset failed (%d): %v", status, err) - respondBool(status, false, gc) - return + isInternal := false + var userID, username string + if reset, ok := app.internalPWRs[req.PIN]; ok { + isInternal = true + if time.Now().After(reset.Expiry) { + app.info.Printf("Password reset failed: PIN \"%s\" has expired", reset.PIN) + respondBool(401, false, gc) + delete(app.internalPWRs, req.PIN) + return + } + userID = reset.ID + username = reset.Username + status, err := app.jf.ResetPasswordAdmin(userID) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Password Reset failed (%d): %v", status, err) + respondBool(status, false, gc) + return + } + } else { + resp, status, err := app.jf.ResetPassword(req.PIN) + if status != 200 || err != nil || !resp.Success { + app.err.Printf("Password Reset failed (%d): %v", status, err) + respondBool(status, false, gc) + return + } + if req.Password == "" || len(resp.UsersReset) == 0 { + respondBool(200, false, gc) + return + } + username = resp.UsersReset[0] } - if req.Password == "" || len(resp.UsersReset) == 0 { - respondBool(200, false, gc) - return + var user mediabrowser.User + var status int + var err error + if isInternal { + user, status, err = app.jf.UserByID(userID, false) + } else { + user, status, err = app.jf.UserByName(username, false) } - user, status, err := app.jf.UserByName(resp.UsersReset[0], false) if status != 200 || err != nil { - app.err.Printf("Failed to get user \"%s\" (%d): %v", resp.UsersReset[0], status, err) + app.err.Printf("Failed to get user \"%s\" (%d): %v", username, status, err) respondBool(500, false, gc) return } - status, err = app.jf.SetPassword(user.ID, req.PIN, req.Password) + prevPassword := req.PIN + if isInternal { + prevPassword = "" + } + status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to change password for \"%s\" (%d): %v", resp.UsersReset[0], status, err) + app.err.Printf("Failed to change password for \"%s\" (%d): %v", username, status, err) respondBool(500, false, gc) return } if app.config.Section("ombi").Key("enabled").MustBool(false) { // Silently fail for changing ombi passwords if status != 200 || err != nil { - app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", resp.UsersReset[0], status, err) + app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err) respondBool(200, true, gc) return } ombiUser, status, err := app.getOmbiUser(user.ID) if status != 200 || err != nil { - app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", resp.UsersReset[0], status, err) + app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", username, status, err) respondBool(200, true, gc) return } diff --git a/email.go b/email.go index e65f84c..3f880df 100644 --- a/email.go +++ b/email.go @@ -514,6 +514,18 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite return email, nil } +// GenResetLink generates and returns a password reset link. +func (app *appContext) GenResetLink(pin string) (string, error) { + url := app.config.Section("password_resets").Key("url_base").String() + var pinLink string + if url == "" { + return pinLink, fmt.Errorf("disabled as no URL Base provided. Set in Settings > Password Resets.") + } + // Strip /invite from end of this URL, ik it's ugly. + pinLink = fmt.Sprintf("%s/reset?pin=%s", url, pin) + return pinLink, nil +} + func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} { d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) message := app.config.Section("messages").Key("message").String() @@ -544,17 +556,16 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo } else { template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": pwr.Username}) template["codeExpiry"] = emailer.lang.PasswordReset.template("codeExpiry", tmpl{"date": d, "time": t, "expiresInMinutes": expiresIn}) - url := app.config.Section("password_resets").Key("url_base").String() if linkResetEnabled { - if url != "" { + pinLink, err := app.GenResetLink(pwr.Pin) + if err == nil { // Strip /invite form end of this URL, ik its ugly. template["link_reset"] = true - pinLink := fmt.Sprintf("%s/reset?pin=%s", url, pwr.Pin) template["pin"] = pinLink // Only used in html email. template["pin_code"] = pwr.Pin } else { - app.info.Println("Password Reset link disabled as no URL Base provided. Set in Settings > Password Resets.") + app.info.Println("Couldn't generate PWR link: %v", err) template["pin"] = pwr.Pin } } else { diff --git a/go.mod b/go.mod index 1c229c8..214724f 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ replace github.com/hrfee/jfa-go/logger => ./logger replace github.com/hrfee/jfa-go/linecache => ./linecache +replace github.com/hrfee/mediabrowser => ../mediabrowser + require ( github.com/bwmarrin/discordgo v0.23.2 github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a @@ -38,7 +40,7 @@ require ( github.com/hrfee/jfa-go/linecache v0.0.0-20211003145958-a220ba8dfb58 github.com/hrfee/jfa-go/logger v0.0.0-20211003145958-a220ba8dfb58 github.com/hrfee/jfa-go/ombi v0.0.0-20211003145958-a220ba8dfb58 - github.com/hrfee/mediabrowser v0.3.5 + github.com/hrfee/mediabrowser v0.0.0-00010101000000-000000000000 github.com/itchyny/timefmt-go v0.1.3 github.com/json-iterator/go v1.1.12 // indirect github.com/lib/pq v1.10.3 // indirect diff --git a/go.sum b/go.sum index f04dd61..3ee3d47 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,7 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4= github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -58,6 +59,7 @@ github.com/getlantern/ops v0.0.0-20200403153110-8476b16edcd6 h1:QthAQCekS1YOeYWS github.com/getlantern/ops v0.0.0-20200403153110-8476b16edcd6/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= github.com/getlantern/systray v1.1.0 h1:U0wCEqseLi2ok1fE6b88gJklzriavPJixZysZPkZd/Y= github.com/getlantern/systray v1.1.0/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc= github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= @@ -138,8 +140,6 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hrfee/mediabrowser v0.3.5 h1:bOJlI2HLvw7v0c7mcRw5XDRMUHReQzk5z0EJYRyYjpo= -github.com/hrfee/mediabrowser v0.3.5/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U= github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -216,6 +216,7 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -263,8 +264,10 @@ github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2t github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= +github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw= github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= diff --git a/html/admin.html b/html/admin.html index 568f5c7..92f37ec 100644 --- a/html/admin.html +++ b/html/admin.html @@ -12,6 +12,7 @@ window.ombiEnabled = {{ .ombiEnabled }}; window.usernameEnabled = {{ .username }}; window.langFile = JSON.parse({{ .language }}); + window.linkResetEnabled = {{ .linkResetEnabled }}; window.language = "{{ .langName }}"; {{ template "header.html" . }} @@ -253,6 +254,13 @@

{{ .strings.settingsRefreshPage }}

+ diff --git a/html/form.html b/html/form.html index 27d7ed2..04e2ed5 100644 --- a/html/form.html +++ b/html/form.html @@ -14,9 +14,9 @@