diff --git a/api-userpage.go b/api-userpage.go
index 93f856a..189b8aa 100644
--- a/api-userpage.go
+++ b/api-userpage.go
@@ -477,3 +477,58 @@ func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
app.storage.DeleteMatrixKey(gc.GetString("jfId"))
respondBool(200, true, gc)
}
+
+// @Summary Generate & send a password reset link if the given email/contact method exists. Doesn't give you any info about it's success.
+// @Produce json
+// @Param address path string true "address/contact method associated w/ your account."
+// @Success 204 {object} boolResponse
+// @Failure 400 {object} boolResponse
+// @Failure 500 {object} boolResponse
+// @Router /my/password/reset/{address} [post]
+// @tags Users
+func (app *appContext) ResetMyPassword(gc *gin.Context) {
+ address := gc.Param("address")
+ if address == "" {
+ app.debug.Println("Ignoring empty request for PWR")
+ respondBool(400, false, gc)
+ return
+ }
+ var pwr InternalPWR
+ var err error
+
+ jfID := app.reverseUserSearch(address)
+ if jfID == "" {
+ app.debug.Printf("Ignoring PWR request: User not found")
+ respondBool(204, true, gc)
+ return
+ }
+ pwr, err = app.GenInternalReset(jfID)
+ 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
+ // FIXME: Send to all contact methods
+ 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, jfID); err != nil {
+ app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err)
+ } else {
+ app.info.Printf("Sent password reset message to \"%s\"", address)
+ }
+ respondBool(204, true, gc)
+}
diff --git a/email.go b/email.go
index 777ed25..dda9428 100644
--- a/email.go
+++ b/email.go
@@ -522,18 +522,6 @@ 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()
@@ -835,38 +823,37 @@ func (emailer *Emailer) send(email *Message, address ...string) error {
return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...)
}
-func (app *appContext) sendByID(email *Message, ID ...string) error {
+func (app *appContext) sendByID(email *Message, ID ...string) (err error) {
for _, id := range ID {
- var err error
if tgChat, ok := app.storage.GetTelegramKey(id); ok && tgChat.Contact && telegramEnabled {
err = app.telegram.Send(email, tgChat.ChatID)
- if err != nil {
- return err
- }
+ // if err != nil {
+ // return err
+ // }
}
if dcChat, ok := app.storage.GetDiscordKey(id); ok && dcChat.Contact && discordEnabled {
err = app.discord.Send(email, dcChat.ChannelID)
- if err != nil {
- return err
- }
+ // if err != nil {
+ // return err
+ // }
}
if mxChat, ok := app.storage.GetMatrixKey(id); ok && mxChat.Contact && matrixEnabled {
err = app.matrix.Send(email, mxChat)
- if err != nil {
- return err
- }
+ // if err != nil {
+ // return err
+ // }
}
if address, ok := app.storage.GetEmailsKey(id); ok && address.Contact && emailEnabled {
err = app.email.send(email, address.Addr)
- if err != nil {
- return err
- }
- }
- if err != nil {
- return err
+ // if err != nil {
+ // return err
+ // }
}
+ // if err != nil {
+ // return err
+ // }
}
- return nil
+ return
}
func (app *appContext) getAddressOrName(jfID string) string {
@@ -879,5 +866,33 @@ func (app *appContext) getAddressOrName(jfID string) string {
if addr, ok := app.storage.GetEmailsKey(jfID); ok {
return addr.Addr
}
+ if mxChat, ok := app.storage.GetMatrixKey(jfID); ok && mxChat.Contact && matrixEnabled {
+ return mxChat.UserID
+ }
+ return ""
+}
+
+func (app *appContext) reverseUserSearch(address string) string {
+ for id, email := range app.storage.GetEmails() {
+ if strings.ToLower(address) == strings.ToLower(email.Addr) {
+ return id
+ }
+ }
+ for id, dcUser := range app.storage.GetDiscord() {
+ if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
+ return id
+ }
+ }
+ tgUsername := strings.TrimPrefix(address, "@")
+ for id, tgUser := range app.storage.GetTelegram() {
+ if tgUsername == tgUser.Username {
+ return id
+ }
+ }
+ for id, mxUser := range app.storage.GetMatrix() {
+ if address == mxUser.UserID {
+ return id
+ }
+ }
return ""
}
diff --git a/html/admin.html b/html/admin.html
index bd17f32..b062065 100644
--- a/html/admin.html
+++ b/html/admin.html
@@ -410,6 +410,9 @@
+
diff --git a/html/user.html b/html/user.html
index 4c62619..b709969 100644
--- a/html/user.html
+++ b/html/user.html
@@ -6,6 +6,7 @@
window.notificationsEnabled = {{ .notifications }};
window.ombiEnabled = {{ .ombiEnabled }};
window.langFile = JSON.parse({{ .language }});
+ window.pwrEnabled = {{ .pwrEnabled }};
window.linkResetEnabled = {{ .linkResetEnabled }};
window.language = "{{ .langName }}";
window.telegramEnabled = {{ .telegramEnabled }};
@@ -43,6 +44,30 @@
+ {{ if .pwrEnabled }}
+
+
+
{{ .strings.resetPassword }}
+
+ {{ if .linkResetEnabled }}
+ {{ .strings.resetPasswordThroughLink }}
+ {{ else }}
+ {{ .strings.resetPasswordThroughJellyfin }}
+ {{ end }}
+
+
+
+
+ {{ if .linkResetEnabled }}
+
+ {{ .strings.submit }}
+
+ {{ else }}
+
{{ .strings.continue }}
+ {{ end }}
+
+
+ {{ end }}
{{ template "login-modal.html" . }}
{{ template "account-linking.html" . }}
@@ -68,6 +93,8 @@
{{ .strings.logout }}
+
+
diff --git a/lang/common/en-us.json b/lang/common/en-us.json
index 266216f..43539db 100644
--- a/lang/common/en-us.json
+++ b/lang/common/en-us.json
@@ -38,7 +38,8 @@
"expiry": "Expiry",
"add": "Add",
"edit": "Edit",
- "delete": "Delete"
+ "delete": "Delete",
+ "myAccount": "My Account"
},
"notifications": {
"errorLoginBlank": "The username and/or password were left blank.",
diff --git a/lang/form/en-us.json b/lang/form/en-us.json
index 946af1f..5b30b33 100644
--- a/lang/form/en-us.json
+++ b/lang/form/en-us.json
@@ -27,7 +27,11 @@
"customMessagePlaceholderHeader": "Customize this card",
"customMessagePlaceholderContent": "Click the user page edit button in settings to customize this card, or show one on the login screen, and don't worry, the user can't see this.",
"userPageSuccessMessage": "You can see and change details about your account later on the {myAccount} page.",
- "myAccount": "My Account"
+ "resetPassword": "Reset Password",
+ "resetPasswordThroughJellyfin": "To reset your password, visit {jfLink} and press the \"Forgot Password\" button.",
+ "resetPasswordThroughLink": "To reset your password, enter your email address or a linked contact method username, and submit. A link will be sent to reset your password.",
+ "resetSent": "Reset Sent.",
+ "resetSentDescription": "If an account with the given contact method exists, a password reset link has been sent to all contact methods available. The code will expire in 30 minutes."
},
"notifications": {
"errorUserExists": "User already exists.",
diff --git a/pwreset.go b/pwreset.go
index 22e9a74..be3d3fb 100644
--- a/pwreset.go
+++ b/pwreset.go
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
+ "fmt"
"os"
"strings"
"time"
@@ -25,6 +26,18 @@ func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
return pwr, 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 (app *appContext) StartPWR() {
app.info.Println("Starting password reset daemon")
path := app.config.Section("password_resets").Key("watch_directory").String()
diff --git a/router.go b/router.go
index f0fd890..ef6254b 100644
--- a/router.go
+++ b/router.go
@@ -148,6 +148,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.GET(p+"/my/token/login", app.getUserTokenLogin)
router.GET(p+"/my/token/refresh", app.getUserTokenRefresh)
router.GET(p+"/my/confirm/:jwt", app.ConfirmMyAction)
+ router.POST(p+"/my/password/reset/:address", app.ResetMyPassword)
}
}
if *SWAGGER {
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index 4c174da..cae4f54 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -110,6 +110,7 @@ declare interface Modals {
discord: Modal;
matrix: Modal;
sendPWR?: Modal;
+ pwr?: Modal;
logs: Modal;
email?: Modal;
}
diff --git a/ts/user.ts b/ts/user.ts
index ed71747..03e8f28 100644
--- a/ts/user.ts
+++ b/ts/user.ts
@@ -16,6 +16,7 @@ interface userWindow extends Window {
discordInviteLink: boolean;
matrixUserID: string;
discordSendPINMessage: string;
+ pwrEnabled: string;
}
declare var window: userWindow;
@@ -44,10 +45,38 @@ window.modals = {} as Modals;
if (window.matrixEnabled) {
window.modals.matrix = new Modal(document.getElementById("modal-matrix"), false);
}
+ if (window.pwrEnabled) {
+ window.modals.pwr = new Modal(document.getElementById("modal-pwr"), false);
+ window.modals.pwr.onclose = () => {
+ window.modals.login.show();
+ };
+ const resetButton = document.getElementById("modal-login-pwr");
+ resetButton.onclick = () => {
+ window.modals.login.close();
+ window.modals.pwr.show();
+ }
+ }
})();
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
+if (window.pwrEnabled && window.linkResetEnabled) {
+ const submitButton = document.getElementById("pwr-submit");
+ const input = document.getElementById("pwr-address") as HTMLInputElement;
+ submitButton.onclick = () => _post("/my/password/reset/" + input.value, null, (req: XMLHttpRequest) => {
+ if (req.readyState != 4) return;
+ if (req.status != 204) {
+ window.notifications.customError("unkownError", window.lang.notif("errorUnknown"));;
+ window.modals.pwr.close();
+ return;
+ }
+ window.modals.pwr.modal.querySelector(".heading").textContent = window.lang.strings("resetSent");
+ window.modals.pwr.modal.querySelector(".content").textContent = window.lang.strings("resetSentDescription");
+ submitButton.classList.add("unfocused");
+ input.classList.add("unfocused");
+ });
+}
+
const grid = document.querySelector(".grid");
var rootCard = document.getElementById("card-user");
var contactCard = document.getElementById("card-contact");
diff --git a/views.go b/views.go
index 62f2ed1..43f6fb1 100644
--- a/views.go
+++ b/views.go
@@ -157,6 +157,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"jellyfinLogin": app.jellyfinLogin,
"jfAdminOnly": jfAdminOnly,
"jfAllowAll": jfAllowAll,
+ "userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
})
}
@@ -177,6 +178,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
"discordEnabled": discordEnabled,
"matrixEnabled": matrixEnabled,
"ombiEnabled": ombiEnabled,
+ "pwrEnabled": app.config.Section("password_resets").Key("enabled").MustBool(false),
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"notifications": notificationsEnabled,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
@@ -184,6 +186,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
"validationStrings": app.storage.lang.User[lang].ValidationStrings,
"language": app.storage.lang.User[lang].JSON,
"langName": lang,
+ "jfLink": app.config.Section("ui").Key("redirect_url").String(),
}
if telegramEnabled {
data["telegramUsername"] = app.telegram.username