package main import ( "encoding/json" "fmt" "os" "strings" "time" "github.com/fsnotify/fsnotify" ) // GenInternalReset generates a local password reset PIN, for use with the PWR option on the Admin page. func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) { pin := genAuthToken() user, status, err := app.jf.UserByID(userID, false) if err != nil || status != 200 { return InternalPWR{}, err } pwr := InternalPWR{ PIN: pin, Username: user.Name, ID: userID, Expiry: time.Now().Add(30 * time.Minute), } 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() if _, err := os.Stat(path); os.IsNotExist(err) { app.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path) return } watcher, err := fsnotify.NewWatcher() if err != nil { app.err.Printf("Couldn't initialise password reset daemon") return } defer watcher.Close() go pwrMonitor(app, watcher) err = watcher.Add(path) if err != nil { app.err.Printf("Failed to start password reset daemon: %s", err) } waitForRestart() } // PasswordReset represents a passwordreset-xyz.json file generated by Jellyfin. type PasswordReset struct { Pin string `json:"Pin"` Username string `json:"UserName"` Expiry time.Time `json:"ExpirationDate"` Internal bool `json:"Internal,omitempty"` } func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { if !emailEnabled { return } for { select { case event, ok := <-watcher.Events: if !ok { return } if event.Op&fsnotify.Write == fsnotify.Write && strings.Contains(event.Name, "passwordreset") { var pwr PasswordReset data, err := os.ReadFile(event.Name) if err != nil { app.debug.Printf("PWR: Failed to read file: %v", err) return } err = json.Unmarshal(data, &pwr) if len(pwr.Pin) == 0 || err != nil { app.debug.Printf("PWR: Failed to read PIN: %v", err) continue } app.info.Printf("New password reset for user \"%s\"", pwr.Username) if currentTime := time.Now(); pwr.Expiry.After(currentTime) { user, status, err := app.jf.UserByName(pwr.Username, false) if !(status == 200 || status == 204) || err != nil { app.err.Printf("Failed to get users from Jellyfin: Code %d", status) app.debug.Printf("Error: %s", err) return } uid := user.ID if uid == "" { app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username) return } name := app.getAddressOrName(uid) if name != "" { msg, err := app.email.constructReset(pwr, app, false) if err != nil { app.err.Printf("Failed to construct password reset message for \"%s\"", pwr.Username) app.debug.Printf("%s: Error: %s", pwr.Username, err) } else if err := app.sendByID(msg, uid); err != nil { app.err.Printf("Failed to send password reset message to \"%s\"", name) app.debug.Printf("%s: Error: %s", pwr.Username, err) } else { app.info.Printf("Sent password reset message to \"%s\"", name) } } } else { app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry) } } case err, ok := <-watcher.Errors: if !ok { return } app.err.Printf("Password reset daemon: %s", err) } } }