From 711394232b320b3b8b0f34d2cbe747abead64643 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 1 Aug 2024 20:17:05 +0100 Subject: [PATCH] logmessages: all log strings in one file EXCEPT: migrations.go, log strings there aren't gonna be repeated anywhere else, are very specific, and will probably change a lot. --- :w | 1503 ------------------------------------ api-jellyseerr.go | 2 +- api-users.go | 6 +- api.go | 63 +- auth.go | 65 +- backups.go | 31 +- config.go | 14 +- email.go | 5 +- generic-d.go | 8 +- housekeeping-d.go | 17 +- jellyseerr-d.go | 13 +- logmessages/logmessages.go | 208 ++++- matrix.go | 11 +- matrix_crypto.go | 15 +- migrations.go | 2 + pwreset.go | 34 +- restart_windows.go | 8 +- router.go | 15 +- setup.go | 3 +- storage.go | 5 +- telegram.go | 13 +- updater.go | 9 +- user-auth.go | 18 +- user-d.go | 22 +- views.go | 60 +- 25 files changed, 410 insertions(+), 1740 deletions(-) delete mode 100644 :w diff --git a/:w b/:w deleted file mode 100644 index 2934bb7..0000000 --- a/:w +++ /dev/null @@ -1,1503 +0,0 @@ -package main - -import ( - "fmt" - "net/url" - "os" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt" - "github.com/hrfee/jfa-go/jellyseerr" - lm "github.com/hrfee/jfa-go/logmessages" - "github.com/hrfee/mediabrowser" - "github.com/lithammer/shortuuid/v3" - "github.com/timshannon/badgerhold/v4" -) - -// @Summary Creates a new Jellyfin user without an invite. -// @Produce json -// @Param newUserDTO body newUserDTO true "New user request object" -// @Success 200 -// @Router /users [post] -// @Security Bearer -// @tags Users -func (app *appContext) NewUserAdmin(gc *gin.Context) { - respondUser := func(code int, user, email bool, msg string, gc *gin.Context) { - resp := newUserResponse{ - User: user, - Email: email, - Error: msg, - } - gc.JSON(code, resp) - gc.Abort() - } - var req newUserDTO - gc.BindJSON(&req) - existingUser, _, _ := app.jf.UserByName(req.Username, false) - if existingUser.Name != "" { - msg := lm.UserExists - app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg) - respondUser(401, false, false, msg, gc) - return - } - user, status, err := app.jf.NewUser(req.Username, req.Password) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err) - respondUser(401, false, false, err.Error(), gc) - return - } - id := user.ID - - // Record activity - app.storage.SetActivityKey(shortuuid.New(), Activity{ - Type: ActivityCreation, - UserID: id, - SourceType: ActivityAdmin, - Source: gc.GetString("jfId"), - Value: user.Name, - Time: time.Now(), - }, gc, false) - - profile := app.storage.GetDefaultProfile() - if req.Profile != "" && req.Profile != "none" { - if p, ok := app.storage.GetProfileKey(req.Profile); ok { - profile = p - } else { - app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, req.Profile) - } - - status, err = app.jf.SetPolicy(id, profile.Policy) - if !(status == 200 || status == 204 || err == nil) { - app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, req.Username, err) - } - status, err = app.jf.SetConfiguration(id, profile.Configuration) - if (status == 200 || status == 204) && err == nil { - status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) - } - if !((status == 200 || status == 204) && err == nil) { - app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, req.Username, err) - } - } - app.jf.CacheExpiry = time.Now() - if emailEnabled { - app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true}) - } - if app.config.Section("ombi").Key("enabled").MustBool(false) { - if profile.Ombi == nil { - profile.Ombi = map[string]interface{}{} - } - errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi) - if err != nil || code != 200 { - app.err.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err) - app.debug.Println("Errors reported by Ombi: " + strings.Join(errors, ", ")) - } else { - app.info.Printf(lm.CreateUser, lm.Ombi, req.Username) - } - } - if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { - // Gets existing user (not possible) or imports the given user. - _, err := app.js.MustGetUser(id) - if err != nil { - app.err.Printf(lm.FailedCreateUser, lm.Jellyseerr, req.Username, err) - } else { - app.info.Printf(lm.CreateUser, lm.Jellyseerr, req.Username) - } - err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User) - if err != nil { - app.err.Printf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, req.Username, err) - } - err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications) - if err != nil { - app.err.Printf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, req.Username, err) - } - if emailEnabled { - err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email}) - if err != nil { - app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, id, err) - } - } - } - if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { - msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false) - if err != nil { - app.err.Printf(lm.FailedConstructWelcomeMessage, id, err) - respondUser(500, true, false, err.Error(), gc) - return - } else if err := app.email.send(msg, req.Email); err != nil { - app.err.Printf(lm.FailedSendWelcomeMessage, req.Username, req.Email, err) - respondUser(500, true, false, err.Error(), gc) - return - } else { - app.info.Printf(lm.SentWelcomeMessage, req.Username, req.Email) - } - } - respondUser(200, true, true, "", gc) -} - -type errorFunc func(gc *gin.Context) - -// Used on the form & when a users email has been confirmed. -func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) (f errorFunc, success bool) { - existingUser, _, _ := app.jf.UserByName(req.Username, false) - if existingUser.Name != "" { - f = func(gc *gin.Context) { - msg := lm.UserExists - app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg) - respond(401, "errorUserExists", gc) - } - success = false - return - } - var discordUser DiscordUser - discordVerified := false - if discordEnabled { - if req.DiscordPIN == "" { - if app.config.Section("discord").Key("required").MustBool(false) { - f = func(gc *gin.Context) { - app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, lm.AccountUnverified) - respond(401, "errorDiscordVerification", gc) - } - success = false - return - } - } else { - discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN) - if !discordVerified { - f = func(gc *gin.Context) { - app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.DiscordPIN)) - respond(401, "errorInvalidPIN", gc) - } - success = false - return - } - if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(discordUser.ID) { - f = func(gc *gin.Context) { - app.debug.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, lm.AccountLinked) - respond(400, "errorAccountLinked", gc) - } - success = false - return - } - err := app.discord.ApplyRole(discordUser.ID) - if err != nil { - f = func(gc *gin.Context) { - app.err.Printf("%s: New user failed: Failed to set member role: %v", req.Code, err) - respond(401, "error", gc) - } - success = false - return - } - } - } - var matrixUser MatrixUser - matrixVerified := false - if matrixEnabled { - if req.MatrixPIN == "" { - if app.config.Section("matrix").Key("required").MustBool(false) { - f = func(gc *gin.Context) { - app.info.Printf(lm.FailedLinkUser, lm.Matrix, "?", req.Code, lm.AccountUnverified) - respond(401, "errorMatrixVerification", gc) - } - success = false - return - } - } else { - user, ok := app.matrix.tokens[req.MatrixPIN] - if !ok || !user.Verified { - matrixVerified = false - f = func(gc *gin.Context) { - app.info.Printf(lm.FailedLinkUser, lm.Matrix, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.MatrixPIN)) - respond(401, "errorInvalidPIN", gc) - } - success = false - return - } - if app.config.Section("matrix").Key("require_unique").MustBool(false) && app.matrix.UserExists(user.User.UserID) { - f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code) - respond(400, "errorAccountLinked", gc) - } - success = false - return - } - matrixVerified = user.Verified - matrixUser = *user.User - - } - } - var tgToken TelegramVerifiedToken - telegramVerified := false - if telegramEnabled { - if req.TelegramPIN == "" { - if app.config.Section("telegram").Key("required").MustBool(false) { - f = func(gc *gin.Context) { - app.info.Printf(lm.FailedLinkUser, lm.Telegram, "?", req.Code, lm.AccountUnverified) - respond(401, "errorTelegramVerification", gc) - } - success = false - return - } - } else { - tgToken, telegramVerified = app.telegram.TokenVerified(req.TelegramPIN) - if !telegramVerified { - f = func(gc *gin.Context) { - app.info.Printf(lm.FailedLinkUser, lm.Telegram, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.TelegramPIN)) - respond(401, "errorInvalidPIN", gc) - } - success = false - return - } - if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(tgToken.Username) { - f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code) - respond(400, "errorAccountLinked", gc) - } - success = false - return - } - } - } - if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed { - claims := jwt.MapClaims{ - "valid": true, - "invite": req.Code, - "exp": time.Now().Add(30 * time.Minute).Unix(), - "type": "confirmation", - } - tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) - if err != nil { - f = func(gc *gin.Context) { - app.info.Printf("Failed to generate confirmation token: %v", err) - respond(500, "errorUnknown", gc) - } - success = false - return - } - if app.ConfirmationKeys == nil { - app.ConfirmationKeys = map[string]map[string]newUserDTO{} - } - cKeys, ok := app.ConfirmationKeys[req.Code] - if !ok { - cKeys = map[string]newUserDTO{} - } - cKeys[key] = req - app.confirmationKeysLock.Lock() - app.ConfirmationKeys[req.Code] = cKeys - app.confirmationKeysLock.Unlock() - f = func(gc *gin.Context) { - app.debug.Printf("%s: Email confirmation required", req.Code) - respond(401, "confirmEmail", gc) - msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false) - if err != nil { - app.err.Printf("%s: Failed to construct confirmation email: %v", req.Code, err) - } else if err := app.email.send(msg, req.Email); err != nil { - app.err.Printf("%s: Failed to send user confirmation email: %v", req.Code, err) - } else { - app.info.Printf("%s: Sent user confirmation email to \"%s\"", req.Code, req.Email) - } - } - success = false - return - } - - user, status, err := app.jf.NewUser(req.Username, req.Password) - if !(status == 200 || status == 204) || err != nil { - f = func(gc *gin.Context) { - app.err.Printf("%s New user failed (%d): %v", req.Code, status, err) - respond(401, app.storage.lang.Admin[app.storage.lang.chosenAdminLang].Notifications.get("errorUnknown"), gc) - } - success = false - return - } - invite, _ := app.storage.GetInvitesKey(req.Code) - app.checkInvite(req.Code, true, req.Username) - if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) { - for address, settings := range invite.Notify { - if settings["notify-creation"] { - go func(addr string) { - msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false) - if err != nil { - app.err.Printf("%s: Failed to construct user creation notification: %v", req.Code, err) - } else { - // Check whether notify "addr" is an email address of Jellyfin ID - if strings.Contains(addr, "@") { - err = app.email.send(msg, addr) - } else { - err = app.sendByID(msg, addr) - } - if err != nil { - app.err.Printf("%s: Failed to send user creation notification: %v", req.Code, err) - } else { - app.info.Printf("Sent user creation notification to %s", addr) - } - } - }(address) - } - } - } - id := user.ID - - // Record activity - sourceType := ActivityAnon - source := "" - if invite.ReferrerJellyfinID != "" { - sourceType = ActivityUser - source = invite.ReferrerJellyfinID - } - - app.storage.SetActivityKey(shortuuid.New(), Activity{ - Type: ActivityCreation, - UserID: id, - SourceType: sourceType, - Source: source, - InviteCode: invite.Code, - Value: user.Name, - Time: time.Now(), - }, gc, true) - - emailStore := EmailAddress{ - Addr: req.Email, - Contact: (req.Email != ""), - } - // Only allow disabling of email contact if some other method is available. - if req.DiscordContact || req.TelegramContact || req.MatrixContact { - emailStore.Contact = req.EmailContact - } - - if invite.UserLabel != "" { - emailStore.Label = invite.UserLabel - } - - var profile Profile - if invite.Profile != "" { - app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile) - var ok bool - profile, ok = app.storage.GetProfileKey(invite.Profile) - if !ok { - profile = app.storage.GetDefaultProfile() - } - app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) - status, err = app.jf.SetPolicy(id, profile.Policy) - if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set user policy (%d): %v", req.Code, status, err) - } - app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile) - status, err = app.jf.SetConfiguration(id, profile.Configuration) - if (status == 200 || status == 204) && err == nil { - status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) - } - if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Code, status, err) - } - if app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false) && profile.ReferralTemplateKey != "" { - emailStore.ReferralTemplateKey = profile.ReferralTemplateKey - // Store here, just incase email are disabled (whether this is even possible, i don't know) - app.storage.SetEmailsKey(id, emailStore) - - // If UseReferralExpiry is enabled, create the ref now so the clock starts ticking - refInv := Invite{} - err = app.storage.db.Get(profile.ReferralTemplateKey, &refInv) - if refInv.UseReferralExpiry { - refInv.Code = GenerateInviteCode() - expiryDelta := refInv.ValidTill.Sub(refInv.Created) - refInv.Created = time.Now() - refInv.ValidTill = refInv.Created.Add(expiryDelta) - refInv.IsReferral = true - refInv.ReferrerJellyfinID = id - app.storage.SetInvitesKey(refInv.Code, refInv) - } - } - } - // if app.config.Section("password_resets").Key("enabled").MustBool(false) { - if req.Email != "" || invite.UserLabel != "" { - app.storage.SetEmailsKey(id, emailStore) - } - expiry := time.Time{} - if invite.UserExpiry { - expiry = time.Now().AddDate(0, invite.UserMonths, invite.UserDays).Add(time.Duration((60*invite.UserHours)+invite.UserMinutes) * time.Minute) - app.storage.SetUserExpiryKey(id, UserExpiry{Expiry: expiry}) - } - if discordVerified { - discordUser.Contact = req.DiscordContact - if app.storage.deprecatedDiscord == nil { - app.storage.deprecatedDiscord = discordStore{} - } - // Note we don't log an activity here, since it's part of creating a user. - app.storage.SetDiscordKey(user.ID, discordUser) - delete(app.discord.verifiedTokens, req.DiscordPIN) - } - if telegramVerified { - tgUser := TelegramUser{ - ChatID: tgToken.ChatID, - Username: tgToken.Username, - Contact: req.TelegramContact, - } - if lang, ok := app.telegram.languages[tgToken.ChatID]; ok { - tgUser.Lang = lang - } - if app.storage.deprecatedTelegram == nil { - app.storage.deprecatedTelegram = telegramStore{} - } - app.telegram.DeleteVerifiedToken(req.TelegramPIN) - app.storage.SetTelegramKey(user.ID, tgUser) - } - if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) { - if profile.Ombi != nil && len(profile.Ombi) != 0 { - template := profile.Ombi - errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, template) - accountExists := false - var ombiUser map[string]interface{} - if err != nil || code != 200 { - // Check if on the off chance, Ombi's user importer has already added the account. - ombiUser, status, err = app.getOmbiImportedUser(req.Username) - if status == 200 && err == nil { - app.info.Println("Found existing Ombi user, applying changes") - accountExists = true - template["password"] = req.Password - status, err = app.applyOmbiProfile(ombiUser, template) - if status != 200 || err != nil { - app.err.Printf("Failed to modify existing Ombi user (%d): %v\n", status, err) - } - } else { - app.info.Printf("Failed to create Ombi user (%d): %s", code, err) - app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) - } - } else { - ombiUser, status, err = app.getOmbiUser(id) - if status != 200 || err != nil { - app.err.Printf("Failed to get Ombi user (%d): %v", status, err) - } else { - app.info.Println("Created Ombi user") - accountExists = true - } - } - if accountExists { - if discordVerified || telegramVerified { - dID := "" - tUser := "" - if discordVerified { - dID = discordUser.ID - } - if telegramVerified { - u, _ := app.storage.GetTelegramKey(user.ID) - tUser = u.Username - } - resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to link Telegram/Discord to Ombi (%d): %v", status, err) - app.debug.Printf("Response: %v", resp) - } - } - } - } else { - app.debug.Printf("Skipping Ombi: Profile \"%s\" was empty", invite.Profile) - } - } - if invite.Profile != "" && app.config.Section("jellyseerr").Key("enabled").MustBool(false) { - if profile.Jellyseerr.Enabled { - // Gets existing user (not possible) or imports the given user. - _, err := app.js.MustGetUser(id) - if err != nil { - app.err.Printf("Failed to create Jellyseerr user: %v", err) - } else { - app.info.Println("Created Jellyseerr user") - } - err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User) - if err != nil { - app.err.Printf("Failed to apply Jellyseerr user template: %v\n", err) - } - err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications) - if err != nil { - app.err.Printf("Failed to apply Jellyseerr notifications template: %v\n", err) - } - contactMethods := map[jellyseerr.NotificationsField]any{} - if emailEnabled { - err = app.js.ModifyMainUserSettings(id, jellyseerr.MainUserSettings{Email: req.Email}) - if err != nil { - app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) - } else { - contactMethods[jellyseerr.FieldEmailEnabled] = req.EmailContact - } - } - if discordVerified { - contactMethods[jellyseerr.FieldDiscord] = discordUser.ID - contactMethods[jellyseerr.FieldDiscordEnabled] = req.DiscordContact - } - if telegramVerified { - u, _ := app.storage.GetTelegramKey(user.ID) - contactMethods[jellyseerr.FieldTelegram] = u.ChatID - contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact - } - if emailEnabled || discordVerified || telegramVerified { - err := app.js.ModifyNotifications(id, contactMethods) - if err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) - } - } - } else { - app.debug.Printf("Skipping Jellyseerr: Profile \"%s\" was empty", invite.Profile) - } - } - if matrixVerified { - matrixUser.Contact = req.MatrixContact - delete(app.matrix.tokens, req.MatrixPIN) - if app.storage.deprecatedMatrix == nil { - app.storage.deprecatedMatrix = matrixStore{} - } - app.storage.SetMatrixKey(user.ID, matrixUser) - } - if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified { - name := app.getAddressOrName(user.ID) - app.debug.Printf("%s: Sending welcome message to %s", req.Username, name) - msg, err := app.email.constructWelcome(req.Username, expiry, app, false) - if err != nil { - app.err.Printf("%s: Failed to construct welcome message: %v", req.Username, err) - } else if err := app.sendByID(msg, user.ID); err != nil { - app.err.Printf("%s: Failed to send welcome message: %v", req.Username, err) - } else { - app.info.Printf("%s: Sent welcome message to \"%s\"", req.Username, name) - } - } - app.jf.CacheExpiry = time.Now() - success = true - return -} - -// @Summary Creates a new Jellyfin user via invite code -// @Produce json -// @Param newUserDTO body newUserDTO true "New user request object" -// @Success 200 {object} PasswordValidation -// @Failure 400 {object} PasswordValidation -// @Router /newUser [post] -// @tags Users -func (app *appContext) NewUser(gc *gin.Context) { - var req newUserDTO - gc.BindJSON(&req) - app.debug.Printf("%s: New user attempt", req.Code) - if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) { - app.info.Printf("%s: New user failed: Captcha Incorrect", req.Code) - respond(400, "errorCaptcha", gc) - return - } - if !app.checkInvite(req.Code, false, "") { - app.info.Printf("%s New user failed: invalid code", req.Code) - respond(401, "errorInvalidCode", gc) - return - } - validation := app.validator.validate(req.Password) - valid := true - for _, val := range validation { - if !val { - valid = false - break - } - } - if !valid { - // 200 bcs idk what i did in js - app.info.Printf("%s: New user failed: Invalid password", req.Code) - gc.JSON(200, validation) - return - } - if emailEnabled { - if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") { - app.info.Printf("%s: New user failed: Email Required", req.Code) - respond(400, "errorNoEmail", gc) - return - } - if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) { - app.info.Printf("%s: New user failed: Email already in use", req.Code) - respond(400, "errorEmailLinked", gc) - return - } - } - f, success := app.newUser(req, false, gc) - if !success { - f(gc) - return - } - code := 200 - for _, val := range validation { - if !val { - code = 400 - } - } - gc.JSON(code, validation) -} - -// @Summary Enable/Disable a list of users, optionally notifying them why. -// @Produce json -// @Param enableDisableUserDTO body enableDisableUserDTO true "User enable/disable request object" -// @Success 200 {object} boolResponse -// @Failure 400 {object} stringResponse -// @Failure 500 {object} errorListDTO "List of errors" -// @Router /users/enable [post] -// @Security Bearer -// @tags Users -func (app *appContext) EnableDisableUsers(gc *gin.Context) { - var req enableDisableUserDTO - gc.BindJSON(&req) - errors := errorListDTO{ - "GetUser": map[string]string{}, - "SetPolicy": map[string]string{}, - } - sendMail := messagesEnabled - var msg *Message - var err error - if sendMail { - if req.Enabled { - msg, err = app.email.constructEnabled(req.Reason, app, false) - } else { - msg, err = app.email.constructDisabled(req.Reason, app, false) - } - if err != nil { - app.err.Printf("Failed to construct account enabled/disabled emails: %v", err) - sendMail = false - } - } - activityType := ActivityDisabled - if req.Enabled { - activityType = ActivityEnabled - } - for _, userID := range req.Users { - user, status, err := app.jf.UserByID(userID, false) - if status != 200 || err != nil { - errors["GetUser"][userID] = fmt.Sprintf("%d %v", status, err) - app.err.Printf("Failed to get user \"%s\" (%d): %v", userID, status, err) - continue - } - user.Policy.IsDisabled = !req.Enabled - status, err = app.jf.SetPolicy(userID, user.Policy) - if !(status == 200 || status == 204) || err != nil { - errors["SetPolicy"][userID] = fmt.Sprintf("%d %v", status, err) - app.err.Printf("Failed to set policy for user \"%s\" (%d): %v", userID, status, err) - continue - } - - // Record activity - app.storage.SetActivityKey(shortuuid.New(), Activity{ - Type: activityType, - UserID: userID, - SourceType: ActivityAdmin, - Source: gc.GetString("jfId"), - Time: time.Now(), - }, gc, false) - - if sendMail && req.Notify { - if err := app.sendByID(msg, userID); err != nil { - app.err.Printf("Failed to send account enabled/disabled email: %v", err) - continue - } - } - } - app.jf.CacheExpiry = time.Now() - if len(errors["GetUser"]) != 0 || len(errors["SetPolicy"]) != 0 { - gc.JSON(500, errors) - return - } - respondBool(200, true, gc) -} - -// @Summary Delete a list of users, optionally notifying them why. -// @Produce json -// @Param deleteUserDTO body deleteUserDTO true "User deletion request object" -// @Success 200 {object} boolResponse -// @Failure 400 {object} stringResponse -// @Failure 500 {object} errorListDTO "List of errors" -// @Router /users [delete] -// @Security Bearer -// @tags Users -func (app *appContext) DeleteUsers(gc *gin.Context) { - var req deleteUserDTO - gc.BindJSON(&req) - errors := map[string]string{} - ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) - sendMail := messagesEnabled - var msg *Message - var err error - if sendMail { - msg, err = app.email.constructDeleted(req.Reason, app, false) - if err != nil { - app.err.Printf("Failed to construct account deletion emails: %v", err) - sendMail = false - } - } - for _, userID := range req.Users { - if ombiEnabled { - ombiUser, code, err := app.getOmbiUser(userID) - if code == 200 && err == nil { - if id, ok := ombiUser["id"]; ok { - status, err := app.ombi.DeleteUser(id.(string)) - if err != nil || status != 200 { - app.err.Printf("Failed to delete ombi user (%d): %v", status, err) - errors[userID] = fmt.Sprintf("Ombi: %d %v, ", status, err) - } - } - } - } - - username := "" - if user, status, err := app.jf.UserByID(userID, false); status == 200 && err == nil { - username = user.Name - } - - status, err := app.jf.DeleteUser(userID) - if !(status == 200 || status == 204) || err != nil { - msg := fmt.Sprintf("%d: %v", status, err) - if _, ok := errors[userID]; !ok { - errors[userID] = msg - } else { - errors[userID] += msg - } - } - - // Record activity - app.storage.SetActivityKey(shortuuid.New(), Activity{ - Type: ActivityDeletion, - UserID: userID, - SourceType: ActivityAdmin, - Source: gc.GetString("jfId"), - Value: username, - Time: time.Now(), - }, gc, false) - - if sendMail && req.Notify { - if err := app.sendByID(msg, userID); err != nil { - app.err.Printf("Failed to send account deletion email: %v", err) - } - } - } - app.jf.CacheExpiry = time.Now() - if len(errors) == len(req.Users) { - respondBool(500, false, gc) - app.err.Printf("Account deletion failed: %s", errors[req.Users[0]]) - return - } else if len(errors) != 0 { - gc.JSON(500, errors) - return - } - respondBool(200, true, gc) -} - -// @Summary Extend time before the user(s) expiry, or create an expiry if it doesn't exist. -// @Produce json -// @Param extendExpiryDTO body extendExpiryDTO true "Extend expiry object" -// @Success 200 {object} boolResponse -// @Failure 400 {object} boolResponse -// @Failure 500 {object} boolResponse -// @Router /users/extend [post] -// @tags Users -func (app *appContext) ExtendExpiry(gc *gin.Context) { - var req extendExpiryDTO - gc.BindJSON(&req) - app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users)) - if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 && req.Timestamp <= 0 { - respondBool(400, false, gc) - return - } - for _, id := range req.Users { - base := time.Now() - if expiry, ok := app.storage.GetUserExpiryKey(id); ok { - base = expiry.Expiry - app.debug.Printf("Expiry extended for \"%s\"", id) - } else { - app.debug.Printf("Created expiry for \"%s\"", id) - } - expiry := UserExpiry{} - if req.Timestamp != 0 { - expiry.Expiry = time.Unix(req.Timestamp, 0) - } else { - expiry.Expiry = base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute) - } - app.storage.SetUserExpiryKey(id, expiry) - if messagesEnabled && req.Notify { - go func(uid string, exp time.Time) { - user, status, err := app.jf.UserByID(uid, false) - if status != 200 || err != nil { - return - } - msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false) - if err != nil { - app.err.Printf("%s: Failed to construct expiry adjustment notification: %v", uid, err) - return - } - if err := app.sendByID(msg, uid); err != nil { - app.err.Printf("%s: Failed to send expiry adjustment notification: %v", uid, err) - } - }(id, expiry.Expiry) - } - } - respondBool(204, true, gc) -} - -// @Summary Remove an expiry from a user's account. -// @Produce json -// @Param id path string true "id of user to extend expiry of." -// @Success 200 {object} boolResponse -// @Router /users/{id}/expiry [delete] -// @tags Users -func (app *appContext) RemoveExpiry(gc *gin.Context) { - app.storage.DeleteUserExpiryKey(gc.Param("id")) - respondBool(200, true, gc) -} - -// @Summary Enable referrals for the given user(s) based on the rules set in the given invite code, or profile. -// @Produce json -// @Param EnableDisableReferralDTO body EnableDisableReferralDTO true "List of users" -// @Param mode path string true "mode of template sourcing from 'invite' or 'profile'." -// @Param source path string true "invite code or profile name, depending on what mode is." -// @Param useExpiry path string true "with-expiry or none." -// @Success 200 {object} boolResponse -// @Failure 400 {object} boolResponse -// @Failure 500 {object} boolResponse -// @Router /users/referral/{mode}/{source}/{useExpiry} [post] -// @Security Bearer -// @tags Users -func (app *appContext) EnableReferralForUsers(gc *gin.Context) { - var req EnableDisableReferralDTO - gc.BindJSON(&req) - mode := gc.Param("mode") - - source := gc.Param("source") - useExpiry := gc.Param("useExpiry") == "with-expiry" - baseInv := Invite{} - if mode == "profile" { - profile, ok := app.storage.GetProfileKey(source) - err := app.storage.db.Get(profile.ReferralTemplateKey, &baseInv) - if !ok || profile.ReferralTemplateKey == "" || err != nil { - app.debug.Printf("Couldn't find template to source from") - respondBool(400, false, gc) - return - - } - app.debug.Printf("Found referral template in profile: %+v\n", profile.ReferralTemplateKey) - } else if mode == "invite" { - // Get the invite, and modify it to turn it into a referral - err := app.storage.db.Get(source, &baseInv) - if err != nil { - app.debug.Printf("Couldn't find invite to source from") - respondBool(400, false, gc) - return - } - } - for _, u := range req.Users { - // 1. Wipe out any existing referral codes. - app.storage.db.DeleteMatching(Invite{}, badgerhold.Where("ReferrerJellyfinID").Eq(u)) - - // 2. Generate referral invite. - inv := baseInv - inv.Code = GenerateInviteCode() - expiryDelta := inv.ValidTill.Sub(inv.Created) - inv.Created = time.Now() - if useExpiry { - inv.ValidTill = inv.Created.Add(expiryDelta) - } else { - inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour) - } - inv.IsReferral = true - inv.ReferrerJellyfinID = u - inv.UseReferralExpiry = useExpiry - app.storage.SetInvitesKey(inv.Code, inv) - } -} - -// @Summary Disable referrals for the given user(s). -// @Produce json -// @Param EnableDisableReferralDTO body EnableDisableReferralDTO true "List of users" -// @Success 200 {object} boolResponse -// @Router /users/referral [delete] -// @Security Bearer -// @tags Users -func (app *appContext) DisableReferralForUsers(gc *gin.Context) { - var req EnableDisableReferralDTO - gc.BindJSON(&req) - for _, u := range req.Users { - // 1. Delete directly bound template - app.storage.db.DeleteMatching(Invite{}, badgerhold.Where("ReferrerJellyfinID").Eq(u)) - // 2. Check for and delete profile-attached template - user, ok := app.storage.GetEmailsKey(u) - if !ok { - continue - } - user.ReferralTemplateKey = "" - app.storage.SetEmailsKey(u, user) - } - respondBool(200, true, gc) -} - -// @Summary Send an announcement via email to a given list of users. -// @Produce json -// @Param announcementDTO body announcementDTO true "Announcement request object" -// @Success 200 {object} boolResponse -// @Failure 400 {object} boolResponse -// @Failure 500 {object} boolResponse -// @Router /users/announce [post] -// @Security Bearer -// @tags Users -func (app *appContext) Announce(gc *gin.Context) { - var req announcementDTO - gc.BindJSON(&req) - if !messagesEnabled { - respondBool(400, false, gc) - return - } - // Generally, we only need to construct once. If {username} is included, however, this needs to be done for each user. - unique := strings.Contains(req.Message, "{username}") - if unique { - for _, userID := range req.Users { - user, status, err := app.jf.UserByID(userID, false) - if status != 200 || err != nil { - app.err.Printf("Failed to get user with ID \"%s\" (%d): %v", userID, status, err) - continue - } - msg, err := app.email.constructTemplate(req.Subject, req.Message, app, user.Name) - if err != nil { - app.err.Printf("Failed to construct announcement message: %v", err) - respondBool(500, false, gc) - return - } else if err := app.sendByID(msg, userID); err != nil { - app.err.Printf("Failed to send announcement message: %v", err) - respondBool(500, false, gc) - return - } - } - } else { - msg, err := app.email.constructTemplate(req.Subject, req.Message, app) - if err != nil { - app.err.Printf("Failed to construct announcement messages: %v", err) - respondBool(500, false, gc) - return - } else if err := app.sendByID(msg, req.Users...); err != nil { - app.err.Printf("Failed to send announcement messages: %v", err) - respondBool(500, false, gc) - return - } - } - app.info.Println("Sent announcement messages") - respondBool(200, true, gc) -} - -// @Summary Save an announcement as a template for use or editing later. -// @Produce json -// @Param announcementTemplate body announcementTemplate true "Announcement request object" -// @Success 200 {object} boolResponse -// @Failure 500 {object} boolResponse -// @Router /users/announce/template [post] -// @Security Bearer -// @tags Users -func (app *appContext) SaveAnnounceTemplate(gc *gin.Context) { - var req announcementTemplate - gc.BindJSON(&req) - if !messagesEnabled { - respondBool(400, false, gc) - return - } - - app.storage.SetAnnouncementsKey(req.Name, req) - respondBool(200, true, gc) -} - -// @Summary Save an announcement as a template for use or editing later. -// @Produce json -// @Success 200 {object} getAnnouncementsDTO -// @Router /users/announce [get] -// @Security Bearer -// @tags Users -func (app *appContext) GetAnnounceTemplates(gc *gin.Context) { - resp := &getAnnouncementsDTO{make([]string, len(app.storage.GetAnnouncements()))} - for i, a := range app.storage.GetAnnouncements() { - resp.Announcements[i] = a.Name - } - gc.JSON(200, resp) -} - -// @Summary Get an announcement template. -// @Produce json -// @Success 200 {object} announcementTemplate -// @Failure 400 {object} boolResponse -// @Param name path string true "name of template (url encoded if necessary)" -// @Router /users/announce/template/{name} [get] -// @Security Bearer -// @tags Users -func (app *appContext) GetAnnounceTemplate(gc *gin.Context) { - escapedName := gc.Param("name") - name, err := url.QueryUnescape(escapedName) - if err != nil { - respondBool(400, false, gc) - return - } - if announcement, ok := app.storage.GetAnnouncementsKey(name); ok { - gc.JSON(200, announcement) - return - } - respondBool(400, false, gc) -} - -// @Summary Delete an announcement template. -// @Produce json -// @Success 200 {object} boolResponse -// @Failure 500 {object} boolResponse -// @Param name path string true "name of template" -// @Router /users/announce/template/{name} [delete] -// @Security Bearer -// @tags Users -func (app *appContext) DeleteAnnounceTemplate(gc *gin.Context) { - name := gc.Param("name") - app.storage.DeleteAnnouncementsKey(name) - 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 Get a list of Jellyfin users. -// @Produce json -// @Success 200 {object} getUsersDTO -// @Failure 500 {object} stringResponse -// @Router /users [get] -// @Security Bearer -// @tags Users -func (app *appContext) GetUsers(gc *gin.Context) { - app.debug.Println("Users requested") - var resp getUsersDTO - users, status, err := app.jf.GetUsers(false) - resp.UserList = make([]respUser, len(users)) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) - respond(500, "Couldn't get users", gc) - return - } - adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true) - allowAll := app.config.Section("ui").Key("allow_all").MustBool(false) - referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false) - i := 0 - for _, jfUser := range users { - user := respUser{ - ID: jfUser.ID, - Name: jfUser.Name, - Admin: jfUser.Policy.IsAdministrator, - Disabled: jfUser.Policy.IsDisabled, - ReferralsEnabled: false, - } - if !jfUser.LastActivityDate.IsZero() { - user.LastActive = jfUser.LastActivityDate.Unix() - } - if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok { - user.Email = email.Addr - user.NotifyThroughEmail = email.Contact - user.Label = email.Label - user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll) - } - expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID) - if ok { - user.Expiry = expiry.Expiry.Unix() - } - if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok { - user.Telegram = tgUser.Username - user.NotifyThroughTelegram = tgUser.Contact - } - if mxUser, ok := app.storage.GetMatrixKey(jfUser.ID); ok { - user.Matrix = mxUser.UserID - user.NotifyThroughMatrix = mxUser.Contact - } - if dcUser, ok := app.storage.GetDiscordKey(jfUser.ID); ok { - user.Discord = RenderDiscordUsername(dcUser) - // user.Discord = dcUser.Username + "#" + dcUser.Discriminator - user.DiscordID = dcUser.ID - user.NotifyThroughDiscord = dcUser.Contact - } - // FIXME: Send referral data - referrerInv := Invite{} - if referralsEnabled { - // 1. Directly attached invite. - err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID)) - if err == nil { - user.ReferralsEnabled = true - // 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database. - } else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" { - user.ReferralsEnabled = true - } - } - resp.UserList[i] = user - i++ - } - gc.JSON(200, resp) -} - -// @Summary Set whether or not a user can access jfa-go. Redundant if the user is a Jellyfin admin. -// @Produce json -// @Param setAccountsAdminDTO body setAccountsAdminDTO true "Map of userIDs to whether or not they have access." -// @Success 204 {object} boolResponse -// @Failure 500 {object} boolResponse -// @Router /users/accounts-admin [post] -// @Security Bearer -// @tags Users -func (app *appContext) SetAccountsAdmin(gc *gin.Context) { - var req setAccountsAdminDTO - gc.BindJSON(&req) - app.debug.Println("Admin modification requested") - users, status, err := app.jf.GetUsers(false) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) - respond(500, "Couldn't get users", gc) - return - } - for _, jfUser := range users { - id := jfUser.ID - if admin, ok := req[id]; ok { - var emailStore = EmailAddress{} - if oldEmail, ok := app.storage.GetEmailsKey(id); ok { - emailStore = oldEmail - } - emailStore.Admin = admin - app.storage.SetEmailsKey(id, emailStore) - } - } - app.info.Println("Email list modified") - respondBool(204, true, gc) -} - -// @Summary Modify user's labels, which show next to their name in the accounts tab. -// @Produce json -// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to labels" -// @Success 204 {object} boolResponse -// @Failure 500 {object} boolResponse -// @Router /users/labels [post] -// @Security Bearer -// @tags Users -func (app *appContext) ModifyLabels(gc *gin.Context) { - var req modifyEmailsDTO - gc.BindJSON(&req) - app.debug.Println("Label modification requested") - users, status, err := app.jf.GetUsers(false) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) - respond(500, "Couldn't get users", gc) - return - } - for _, jfUser := range users { - id := jfUser.ID - if label, ok := req[id]; ok { - var emailStore = EmailAddress{} - if oldEmail, ok := app.storage.GetEmailsKey(id); ok { - emailStore = oldEmail - } - emailStore.Label = label - app.storage.SetEmailsKey(id, emailStore) - } - } - app.info.Println("Email list modified") - respondBool(204, true, gc) -} - -func (app *appContext) modifyEmail(jfID string, addr string) { - contactPrefChanged := false - emailStore, ok := app.storage.GetEmailsKey(jfID) - // Auto enable contact by email for newly added addresses - if !ok || emailStore.Addr == "" { - emailStore = EmailAddress{ - Contact: true, - } - contactPrefChanged = true - } - emailStore.Addr = addr - app.storage.SetEmailsKey(jfID, emailStore) - if app.config.Section("ombi").Key("enabled").MustBool(false) { - ombiUser, code, err := app.getOmbiUser(jfID) - if code == 200 && err == nil { - ombiUser["emailAddress"] = addr - code, err = app.ombi.ModifyUser(ombiUser) - if code != 200 || err != nil { - app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err) - } - } - } - if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { - err := app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: addr}) - if err != nil { - app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) - } else if contactPrefChanged { - contactMethods := map[jellyseerr.NotificationsField]any{ - jellyseerr.FieldEmailEnabled: true, - } - err := app.js.ModifyNotifications(jfID, contactMethods) - if err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) - } - } - } -} - -// @Summary Modify user's email addresses. -// @Produce json -// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses" -// @Success 200 {object} boolResponse -// @Failure 500 {object} stringResponse -// @Router /users/emails [post] -// @Security Bearer -// @tags Users -func (app *appContext) ModifyEmails(gc *gin.Context) { - var req modifyEmailsDTO - gc.BindJSON(&req) - app.debug.Println("Email modification requested") - users, status, err := app.jf.GetUsers(false) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) - respond(500, "Couldn't get users", gc) - return - } - for _, jfUser := range users { - id := jfUser.ID - if address, ok := req[id]; ok { - app.modifyEmail(id, address) - - activityType := ActivityContactLinked - if address == "" { - activityType = ActivityContactUnlinked - } - app.storage.SetActivityKey(shortuuid.New(), Activity{ - Type: activityType, - UserID: id, - SourceType: ActivityAdmin, - Source: gc.GetString("jfId"), - Value: "email", - Time: time.Now(), - }, gc, false) - } - } - app.info.Println("Email list modified") - respondBool(200, true, gc) -} - -// @Summary Apply settings to a list of users, either from a profile or from another user. -// @Produce json -// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings" -// @Success 200 {object} errorListDTO -// @Failure 500 {object} errorListDTO "Lists of errors that occurred while applying settings" -// @Router /users/settings [post] -// @Security Bearer -// @tags Profiles & Settings -func (app *appContext) ApplySettings(gc *gin.Context) { - app.info.Println("User settings change requested") - var req userSettingsDTO - gc.BindJSON(&req) - applyingFrom := "profile" - var policy mediabrowser.Policy - var configuration mediabrowser.Configuration - var displayprefs map[string]interface{} - var ombi map[string]interface{} - var jellyseerr JellyseerrTemplate - jellyseerr.Enabled = false - if req.From == "profile" { - // Check profile exists & isn't empty - profile, ok := app.storage.GetProfileKey(req.Profile) - if !ok { - app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile) - respond(500, "Couldn't find profile", gc) - return - } - if req.Homescreen { - if !profile.Homescreen { - app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile) - respond(500, "No homescreen template available", gc) - return - } - configuration = profile.Configuration - displayprefs = profile.Displayprefs - } - if req.Policy { - policy = profile.Policy - } - if req.Ombi && app.config.Section("ombi").Key("enabled").MustBool(false) { - if profile.Ombi != nil && len(profile.Ombi) != 0 { - ombi = profile.Ombi - } - } - if req.Jellyseerr && app.config.Section("jellyseerr").Key("enabled").MustBool(false) { - if profile.Jellyseerr.Enabled { - jellyseerr = profile.Jellyseerr - } - } - - } else if req.From == "user" { - applyingFrom = "user" - app.jf.CacheExpiry = time.Now() - user, status, err := app.jf.UserByID(req.ID, false) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err) - respond(500, "Couldn't get user", gc) - return - } - applyingFrom = "\"" + user.Name + "\"" - if req.Policy { - policy = user.Policy - } - if req.Homescreen { - displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) - if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err) - respond(500, "Couldn't get displayprefs", gc) - return - } - configuration = user.Configuration - } - } - app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom) - errors := errorListDTO{ - "policy": map[string]string{}, - "homescreen": map[string]string{}, - "ombi": map[string]string{}, - "jellyseerr": map[string]string{}, - } - /* Jellyfin doesn't seem to like too many of these requests sent in succession - and can crash and mess up its database. Issue #160 says this occurs when more - than 100 users are modified. A delay totalling 500ms between requests is used - if so. */ - var shouldDelay bool = len(req.ApplyTo) >= 100 - if shouldDelay { - app.debug.Println("Adding delay between requests for large batch") - } - for _, id := range req.ApplyTo { - var status int - var err error - if req.Policy { - status, err = app.jf.SetPolicy(id, policy) - if !(status == 200 || status == 204) || err != nil { - errors["policy"][id] = fmt.Sprintf("%d: %s", status, err) - } - } - if shouldDelay { - time.Sleep(250 * time.Millisecond) - } - if req.Homescreen { - status, err = app.jf.SetConfiguration(id, configuration) - errorString := "" - if !(status == 200 || status == 204) || err != nil { - errorString += fmt.Sprintf("Configuration %d: %v ", status, err) - } else { - status, err = app.jf.SetDisplayPreferences(id, displayprefs) - if !(status == 200 || status == 204) || err != nil { - errorString += fmt.Sprintf("Displayprefs %d: %v ", status, err) - } - } - if errorString != "" { - errors["homescreen"][id] = errorString - } - } - if ombi != nil { - errorString := "" - user, status, err := app.getOmbiUser(id) - if status != 200 || err != nil { - errorString += fmt.Sprintf("Ombi GetUser %d: %v ", status, err) - } else { - // newUser := ombi - // newUser["id"] = user["id"] - // newUser["userName"] = user["userName"] - // newUser["alias"] = user["alias"] - // newUser["emailAddress"] = user["emailAddress"] - status, err = app.applyOmbiProfile(user, ombi) - if status != 200 || err != nil { - errorString += fmt.Sprintf("Apply %d: %v ", status, err) - } - } - if errorString != "" { - errors["ombi"][id] = errorString - } - } - if jellyseerr.Enabled { - errorString := "" - // newUser := ombi - // newUser["id"] = user["id"] - // newUser["userName"] = user["userName"] - // newUser["alias"] = user["alias"] - // newUser["emailAddress"] = user["emailAddress"] - err := app.js.ApplyTemplateToUser(id, jellyseerr.User) - if err != nil { - errorString += fmt.Sprintf("ApplyUser: %v ", err) - } - err = app.js.ApplyNotificationsTemplateToUser(id, jellyseerr.Notifications) - if err != nil { - errorString += fmt.Sprintf("ApplyNotifications: %v ", err) - } - if errorString != "" { - errors["jellyseerr"][id] = errorString - } - } - - if shouldDelay { - time.Sleep(250 * time.Millisecond) - } - } - code := 200 - if len(errors["policy"]) == len(req.ApplyTo) || len(errors["homescreen"]) == len(req.ApplyTo) { - code = 500 - } - gc.JSON(code, errors) -} diff --git a/api-jellyseerr.go b/api-jellyseerr.go index 31fecb5..4ddb4ec 100644 --- a/api-jellyseerr.go +++ b/api-jellyseerr.go @@ -66,7 +66,7 @@ func (app *appContext) SetJellyseerrProfile(gc *gin.Context) { profile.Jellyseerr.User = u.UserTemplate n, err := app.js.GetNotificationPreferencesByID(jellyseerrID) if err != nil { - app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, err) + app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, gc.Param("id"), err) respond(500, "Couldn't get user notification prefs", gc) return } diff --git a/api-users.go b/api-users.go index d36ef66..8873103 100644 --- a/api-users.go +++ b/api-users.go @@ -91,7 +91,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi) if err != nil || code != 200 { app.err.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err) - app.debug.Printf(lm.AdditionalOmbiErrors, strings.Join(errors, ", ")) + app.debug.Printf(lm.AdditionalErrors, lm.Ombi, strings.Join(errors, ", ")) } else { app.info.Printf(lm.CreateUser, lm.Ombi, req.Username) } @@ -465,7 +465,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) } } else { app.info.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err) - app.debug.Printf(lm.AdditionalOmbiErrors, strings.Join(errors, ", ")) + app.debug.Printf(lm.AdditionalErrors, lm.Ombi, strings.Join(errors, ", ")) } } else { ombiUser, status, err = app.getOmbiUser(id) @@ -490,7 +490,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser) if !(status == 200 || status == 204) || err != nil { app.err.Printf(lm.FailedSyncContactMethods, lm.Ombi, err) - app.debug.Printf(lm.AdditionalOmbiErrors, resp) + app.debug.Printf(lm.AdditionalErrors, lm.Ombi, resp) } } } diff --git a/api.go b/api.go index c9dde9c..696b587 100644 --- a/api.go +++ b/api.go @@ -1,10 +1,12 @@ package main import ( + "fmt" "strings" "time" "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" "github.com/itchyny/timefmt-go" "github.com/lithammer/shortuuid/v3" @@ -122,14 +124,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { } } if !valid || req.PIN == "" { - app.info.Printf("%s: Password reset failed: Invalid password", req.PIN) + app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.InvalidPassword) gc.JSON(400, validation) return } isInternal := false if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) { - app.info.Printf("%s: PWR Failed: Captcha Incorrect", req.PIN) + app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.IncorrectCaptcha) respond(400, "errorCaptcha", gc) return } @@ -138,7 +140,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { 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) + app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, reset.PIN)) respondBool(401, false, gc) delete(app.internalPWRs, req.PIN) return @@ -148,7 +150,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { status, err := app.jf.ResetPasswordAdmin(userID) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Password Reset failed (%d): %v", status, err) + app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err) respondBool(status, false, gc) return } @@ -156,7 +158,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { } 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) + app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err) respondBool(status, false, gc) return } @@ -176,7 +178,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { user, status, err = app.jf.UserByName(username, false) } if status != 200 || err != nil { - app.err.Printf("Failed to get user \"%s\" (%d): %v", username, status, err) + app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err) respondBool(500, false, gc) return } @@ -195,31 +197,33 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { } 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", username, status, err) + app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, user.ID, err) respondBool(500, false, gc) return } if app.config.Section("ombi").Key("enabled").MustBool(false) { - // Silently fail for changing ombi passwords + // This makes no sense so has been commented out. + // It probably did at some point in the past. + /* Silently fail for changing ombi passwords if (status != 200 && status != 204) || err != nil { - app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err) + app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, 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", username, status, err) + app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err) respondBool(200, true, gc) return } ombiUser["password"] = req.Password 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) + app.err.Printf(lm.FailedChangePassword, lm.Ombi, user.ID, err) respondBool(200, true, gc) return } - app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"]) + app.debug.Printf(lm.ChangePassword, lm.Ombi, user.ID) } respondBool(200, true, gc) } @@ -231,7 +235,6 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { // @Security Bearer // @tags Configuration func (app *appContext) GetConfig(gc *gin.Context) { - app.info.Println("Config requested") resp := app.configBase // Load language options formOptions := app.storage.lang.User.getOptions() @@ -341,7 +344,6 @@ func (app *appContext) GetConfig(gc *gin.Context) { // @Security Bearer // @tags Configuration func (app *appContext) ModifyConfig(gc *gin.Context) { - app.info.Println("Config modification requested") var req configDTO gc.BindJSON(&req) // Load a new config, as we set various default values in app.config that shouldn't be stored. @@ -366,26 +368,18 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { } tempConfig.Section("").Key("first_run").SetValue("false") if err := tempConfig.SaveTo(app.configPath); err != nil { - app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err) + app.err.Printf(lm.FailedWriting, app.configPath, err) respond(500, err.Error(), gc) return } - app.debug.Println("Config saved") + app.info.Printf(lm.ModifyConfig, app.configPath) gc.JSON(200, map[string]bool{"success": true}) if req["restart-program"] != nil && req["restart-program"].(bool) { - app.info.Println("Restarting...") - if TRAY { - TRAYRESTART <- true - } else { - RESTART <- true - } - // Safety Sleep (Ensure shutdown tasks get done) - time.Sleep(time.Second) + app.Restart() } app.loadConfig() // Reinitialize password validator on config change, as opposed to every applicable request like in python. if _, ok := req["password_validation"]; ok { - app.debug.Println("Reinitializing validator") validatorConf := ValidatorConf{ "length": app.config.Section("password_validation").Key("min_length").MustInt(0), "uppercase": app.config.Section("password_validation").Key("upper").MustInt(0), @@ -425,12 +419,13 @@ func (app *appContext) CheckUpdate(gc *gin.Context) { // @tags Configuration func (app *appContext) ApplyUpdate(gc *gin.Context) { if !app.update.CanUpdate { - respond(400, "Update is manual", gc) + app.info.Printf(lm.FailedApplyUpdate, lm.UpdateManual) + respond(400, lm.UpdateManual, gc) return } err := app.update.update() if err != nil { - app.err.Printf("Failed to apply update: %v", err) + app.err.Printf(lm.FailedApplyUpdate, err) respondBool(500, false, gc) return } @@ -452,8 +447,9 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) { func (app *appContext) Logout(gc *gin.Context) { cookie, err := gc.Cookie("refresh") if err != nil { - app.debug.Printf("Couldn't get cookies: %s", err) - respond(500, "Couldn't fetch cookies", gc) + msg := fmt.Sprintf(lm.FailedGetCookies, "refresh", err) + app.debug.Println(msg) + respond(500, msg, gc) return } app.invalidTokens = append(app.invalidTokens, cookie) @@ -526,11 +522,7 @@ func (app *appContext) ServeLang(gc *gin.Context) { // @Security Bearer // @tags Other func (app *appContext) restart(gc *gin.Context) { - app.info.Println("Restarting...") - err := app.Restart() - if err != nil { - app.err.Printf("Couldn't restart, try restarting manually: %v", err) - } + app.Restart() } // @Summary Returns the last 100 lines of the log. @@ -544,6 +536,7 @@ func (app *appContext) GetLog(gc *gin.Context) { // no need to syscall.exec anymore! func (app *appContext) Restart() error { + app.info.Println(lm.Restarting) if TRAY { TRAYRESTART <- true } else { diff --git a/auth.go b/auth.go index 877d7ab..6b5ea12 100644 --- a/auth.go +++ b/auth.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" "github.com/lithammer/shortuuid/v3" ) @@ -41,6 +42,8 @@ func (app *appContext) webAuth() gin.HandlerFunc { return app.authenticate } +func (app *appContext) authLog(v any) { app.debug.Printf(lm.FailedAuthRequest, v) } + // CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens. func CreateToken(userId, jfId string, admin bool) (string, string, error) { var token, refresh string @@ -72,32 +75,26 @@ func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.Map ok = false header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) if header[0] != "Bearer" { - app.debug.Println("Invalid authorization header") + app.authLog(lm.InvalidAuthHeader) respond(401, "Unauthorized", gc) return } token, err := jwt.Parse(string(header[1]), checkToken) if err != nil { - app.debug.Printf("Auth denied: %s", err) + app.authLog(fmt.Sprintf(lm.FailedParseJWT, err)) respond(401, "Unauthorized", gc) return } claims, ok = token.Claims.(jwt.MapClaims) if !ok { - app.debug.Println("Invalid JWT") + app.authLog(lm.FailedCastJWT) respond(401, "Unauthorized", gc) return } expiryUnix := int64(claims["exp"].(float64)) - if err != nil { - app.debug.Printf("Auth denied: %s", err) - respond(401, "Unauthorized", gc) - ok = false - return - } expiry := time.Unix(expiryUnix, 0) if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) { - app.debug.Printf("Auth denied: Invalid token") + app.authLog(lm.InvalidJWT) // app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string)) respond(401, "Unauthorized", gc) ok = false @@ -115,7 +112,7 @@ func (app *appContext) authenticate(gc *gin.Context) { } isAdminToken := claims["admin"].(bool) if !isAdminToken { - app.debug.Printf("Auth denied: Token was not for admin access") + app.authLog(lm.NonAdminToken) respond(401, "Unauthorized", gc) return } @@ -130,14 +127,13 @@ func (app *appContext) authenticate(gc *gin.Context) { } } if !match { - app.debug.Printf("Couldn't find user ID \"%s\"", userID) + app.authLog(fmt.Sprintf(lm.NonAdminUser, userID)) respond(401, "Unauthorized", gc) return } gc.Set("jfId", jfID) gc.Set("userId", userID) gc.Set("userMode", false) - app.debug.Println("Auth succeeded") gc.Next() } @@ -160,7 +156,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool) password = creds[1] ok = false if username == "" || password == "" { - app.logIpDebug(gc, userpage, "Auth denied: blank username/password") + app.logIpDebug(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.EmptyUserOrPass)) respond(401, "Unauthorized", gc) return } @@ -173,16 +169,16 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc user, status, err := app.authJf.Authenticate(username, password) if status != 200 || err != nil { if status == 401 || status == 400 { - app.logIpInfo(gc, userpage, "Auth denied: Invalid username/password (Jellyfin)") + app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass)) respond(401, "Unauthorized", gc) return } if status == 403 { - app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled") + app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled)) respond(403, "yourAccountWasDisabled", gc) return } - app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err) + app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, status, err)) respond(500, "Jellyfin error", gc) return } @@ -199,7 +195,7 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc // @tags Auth // @Security getTokenAuth func (app *appContext) getTokenLogin(gc *gin.Context) { - app.logIpInfo(gc, false, "Token requested (login attempt)") + app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenLoginAttempt)) username, password, ok := app.decodeValidateLoginHeader(gc, false) if !ok { return @@ -209,13 +205,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { for _, user := range app.adminUsers { if user.Username == username && user.Password == password { match = true - app.debug.Println("Found existing user") userID = user.UserID break } } if !app.jellyfinLogin && !match { - app.logIpInfo(gc, false, "Auth denied: Invalid username/password") + app.logIpInfo(gc, false, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass)) respond(401, "Unauthorized", gc) return } @@ -233,7 +228,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { } accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator) if !accountsAdmin { - app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username) + app.authLog(fmt.Sprintf(lm.NonAdminUser, username)) respond(401, "Unauthorized", gc) return } @@ -243,12 +238,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { newUser := User{ UserID: userID, } - app.debug.Printf("Token generated for user \"%s\"", username) + app.debug.Printf(lm.GenerateToken, username) app.adminUsers = append(app.adminUsers, newUser) } token, refresh, err := CreateToken(userID, jfID, true) if err != nil { - app.err.Printf("getToken failed: Couldn't generate token (%s)", err) + app.err.Printf(lm.FailedGenerateToken, err) respond(500, "Couldn't generate token", gc) return } @@ -261,35 +256,29 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s ok = false cookie, err := gc.Cookie(cookieName) if err != nil || cookie == "" { - app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err) + app.authLog(fmt.Sprintf(lm.FailedGetCookies, cookieName, err)) respond(400, "Couldn't get token", gc) return } for _, token := range app.invalidTokens { if cookie == token { - app.debug.Println("getTokenRefresh: Invalid token") - respond(401, "Invalid token", gc) + app.authLog(lm.LocallyInvalidatedJWT) + respond(401, lm.InvalidJWT, gc) return } } token, err := jwt.Parse(cookie, checkToken) if err != nil { - app.debug.Println("getTokenRefresh: Invalid token") - respond(400, "Invalid token", gc) + app.authLog(lm.FailedParseJWT) + respond(400, lm.InvalidJWT, gc) return } claims, ok = token.Claims.(jwt.MapClaims) expiryUnix := int64(claims["exp"].(float64)) - if err != nil { - app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err) - respond(401, "Invalid token", gc) - ok = false - return - } expiry := time.Unix(expiryUnix, 0) if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) { - app.debug.Printf("getTokenRefresh: Invalid token: %+v", err) - respond(401, "Invalid token", gc) + app.authLog(lm.InvalidJWT) + respond(401, lm.InvalidJWT, gc) ok = false return } @@ -304,7 +293,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s // @Router /token/refresh [get] // @tags Auth func (app *appContext) getTokenRefresh(gc *gin.Context) { - app.logIpInfo(gc, false, "Token requested (refresh token)") + app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenRefresh)) claims, ok := app.decodeValidateRefreshCookie(gc, "refresh") if !ok { return @@ -313,7 +302,7 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) { jfID := claims["jfid"].(string) jwt, refresh, err := CreateToken(userID, jfID, true) if err != nil { - app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err) + app.err.Printf(lm.FailedGenerateToken, err) respond(500, "Couldn't generate token", gc) return } diff --git a/backups.go b/backups.go index 676bbcb..8d4565c 100644 --- a/backups.go +++ b/backups.go @@ -7,6 +7,8 @@ import ( "sort" "strings" "time" + + lm "github.com/hrfee/jfa-go/logmessages" ) const ( @@ -60,12 +62,12 @@ func (app *appContext) getBackups() *BackupList { path := app.config.Section("backups").Key("path").String() err := os.MkdirAll(path, 0755) if err != nil { - app.err.Printf("Failed to create backup directory \"%s\": %v\n", path, err) + app.err.Printf(lm.FailedCreateDir, path, err) return nil } items, err := os.ReadDir(path) if err != nil { - app.err.Printf("Failed to read backup directory \"%s\": %v\n", path, err) + app.err.Printf(lm.FailedReading, path, err) return nil } backups := &BackupList{} @@ -78,7 +80,7 @@ func (app *appContext) getBackups() *BackupList { } t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(item.Name(), BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX)) if err != nil { - app.debug.Printf("Failed to parse backup filename \"%s\": %v\n", item.Name(), err) + app.debug.Printf(lm.FailedParseTime, err) continue } backups.dates[i] = t @@ -101,36 +103,36 @@ func (app *appContext) makeBackup() (fileDetails CreateBackupDTO) { sort.Sort(backups) for _, item := range backups.files[:toDelete] { fullpath := filepath.Join(path, item.Name()) - app.debug.Printf("Deleting old backup \"%s\"\n", item.Name()) err := os.Remove(fullpath) if err != nil { - app.err.Printf("Failed to delete old backup \"%s\": %v\n", fullpath, err) + app.err.Printf(lm.FailedDeleteOldBackup, fullpath, err) return } + app.debug.Printf(lm.DeleteOldBackup, fullpath) } } fullpath := filepath.Join(path, fname) f, err := os.Create(fullpath) if err != nil { - app.err.Printf("Failed to open backup file \"%s\": %v\n", fullpath, err) + app.err.Printf(lm.FailedOpen, fullpath, err) return } defer f.Close() _, err = app.storage.db.Badger().Backup(f, 0) if err != nil { - app.err.Printf("Failed to create backup: %v\n", err) + app.err.Printf(lm.FailedCreateBackup, err) return } fstat, err := f.Stat() if err != nil { - app.err.Printf("Failed to get info on new backup: %v\n", err) + app.err.Printf(lm.FailedStat, fullpath, err) return } fileDetails.Size = fileSize(fstat.Size()) fileDetails.Name = fname fileDetails.Path = fullpath - // fmt.Printf("Created backup %+v\n", fileDetails) + app.debug.Printf(lm.CreateBackup, fileDetails) return } @@ -139,25 +141,25 @@ func (app *appContext) loadPendingBackup() { return } oldPath := filepath.Join(app.dataPath, "db-"+string(time.Now().Unix())+"-pre-"+filepath.Base(LOADBAK)) - app.info.Printf("Moving existing database to \"%s\"\n", oldPath) err := os.Rename(app.storage.db_path, oldPath) if err != nil { - app.err.Fatalf("Failed to move existing database: %v\n", err) + app.err.Fatalf(lm.FailedMoveOldDB, oldPath, err) } + app.info.Printf(lm.MoveOldDB, oldPath) app.ConnectDB() defer app.storage.db.Close() f, err := os.Open(LOADBAK) if err != nil { - app.err.Fatalf("Failed to open backup file \"%s\": %v\n", LOADBAK, err) + app.err.Fatalf(lm.FailedOpen, LOADBAK, err) } err = app.storage.db.Badger().Load(f, 256) f.Close() if err != nil { - app.err.Fatalf("Failed to restore backup file \"%s\": %v\n", LOADBAK, err) + app.err.Fatalf(lm.FailedRestoreDB, LOADBAK, err) } - app.info.Printf("Restored backup \"%s\".", LOADBAK) + app.info.Printf(lm.RestoreDB, LOADBAK) LOADBAK = "" } @@ -165,7 +167,6 @@ func newBackupDaemon(app *appContext) *GenericDaemon { interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute d := NewGenericDaemon(interval, app, func(app *appContext) { - app.debug.Println("Backups: Creating backup") app.makeBackup() }, ) diff --git a/config.go b/config.go index 8426bab..7d1af76 100644 --- a/config.go +++ b/config.go @@ -7,8 +7,10 @@ import ( "path/filepath" "strconv" "strings" + "time" "github.com/hrfee/jfa-go/easyproxy" + lm "github.com/hrfee/jfa-go/logmessages" "gopkg.in/ini.v1" ) @@ -140,7 +142,7 @@ func (app *appContext) loadConfig() error { } } if allDisabled { - fmt.Println("SETALLTRUE") + app.info.Println(lm.EnableAllPWRMethods) for _, v := range pwrMethods { app.config.Section("user_page").Key(v).SetValue("true") } @@ -175,9 +177,15 @@ func (app *appContext) loadConfig() error { app.proxyConfig.Password = app.config.Section("advanced").Key("proxy_password").MustString("") app.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig) if err != nil { - app.err.Printf("Failed to initialize Proxy: %v\n", err) + app.err.Printf(lm.FailedInitProxy, app.proxyConfig.Addr, err) + // As explained in lm.FailedInitProxy, sleep here might grab the admin's attention, + // Since we don't crash on this failing. + time.Sleep(15 * time.Second) + app.proxyEnabled = false + } else { + app.proxyEnabled = true + app.info.Printf(lm.InitProxy, app.proxyConfig.Addr) } - app.proxyEnabled = true } app.MustSetValue("updates", "enabled", "true") diff --git a/email.go b/email.go index 7a21a26..7246384 100644 --- a/email.go +++ b/email.go @@ -20,6 +20,7 @@ import ( "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/hrfee/jfa-go/easyproxy" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" "github.com/itchyny/timefmt-go" "github.com/mailgun/mailgun-go/v4" @@ -95,7 +96,7 @@ func NewEmailer(app *appContext) *Emailer { authType := sMail.AuthType(app.config.Section("smtp").Key("auth_type").MustInt(4)) err := emailer.NewSMTP(app.config.Section("smtp").Key("server").String(), app.config.Section("smtp").Key("port").MustInt(465), username, password, sslTLS, app.config.Section("smtp").Key("ssl_cert").MustString(""), app.config.Section("smtp").Key("hello_hostname").String(), app.config.Section("smtp").Key("cert_validation").MustBool(true), authType, proxyConf) if err != nil { - app.err.Printf("Error while initiating SMTP mailer: %v", err) + app.err.Printf(lm.FailedInitSMTP, err) } } else if method == "mailgun" { emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String()) @@ -580,7 +581,7 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo // Only used in html email. template["pin_code"] = pwr.Pin } else { - app.info.Println("Couldn't generate PWR link: %v", err) + app.info.Println(lm.FailedGeneratePWRLink, err) template["pin"] = pwr.Pin } } else { diff --git a/generic-d.go b/generic-d.go index f16dd68..775ac0c 100644 --- a/generic-d.go +++ b/generic-d.go @@ -1,6 +1,10 @@ package main -import "time" +import ( + "time" + + lm "github.com/hrfee/jfa-go/logmessages" +) // https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS @@ -36,7 +40,7 @@ func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app func (d *GenericDaemon) Name(name string) { d.name = name } func (d *GenericDaemon) run() { - d.app.info.Printf("%s started", d.name) + d.app.info.Printf(lm.StartDaemon, d.name) for { select { case <-d.ShutdownChannel: diff --git a/housekeeping-d.go b/housekeeping-d.go index e7c8efe..2537f94 100644 --- a/housekeeping-d.go +++ b/housekeeping-d.go @@ -4,6 +4,7 @@ import ( "time" "github.com/dgraph-io/badger/v3" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" "github.com/timshannon/badgerhold/v4" ) @@ -12,7 +13,7 @@ import ( // meant to be called with other such housekeeping functions, so assumes // the user cache is fresh. func (app *appContext) clearEmails() { - app.debug.Println("Housekeeping: removing unused email addresses") + app.debug.Println(lm.HousekeepingEmail) emails := app.storage.GetEmails() for _, email := range emails { _, _, err := app.jf.UserByID(email.JellyfinID, false) @@ -28,7 +29,7 @@ func (app *appContext) clearEmails() { // clearDiscord does the same as clearEmails, but for Discord Users. func (app *appContext) clearDiscord() { - app.debug.Println("Housekeeping: removing unused Discord IDs") + app.debug.Println(lm.HousekeepingDiscord) discordUsers := app.storage.GetDiscord() for _, discordUser := range discordUsers { _, _, err := app.jf.UserByID(discordUser.JellyfinID, false) @@ -44,7 +45,7 @@ func (app *appContext) clearDiscord() { // clearMatrix does the same as clearEmails, but for Matrix Users. func (app *appContext) clearMatrix() { - app.debug.Println("Housekeeping: removing unused Matrix IDs") + app.debug.Println(lm.HousekeepingMatrix) matrixUsers := app.storage.GetMatrix() for _, matrixUser := range matrixUsers { _, _, err := app.jf.UserByID(matrixUser.JellyfinID, false) @@ -60,7 +61,7 @@ func (app *appContext) clearMatrix() { // clearTelegram does the same as clearEmails, but for Telegram Users. func (app *appContext) clearTelegram() { - app.debug.Println("Housekeeping: removing unused Telegram IDs") + app.debug.Println(lm.HousekeepingTelegram) telegramUsers := app.storage.GetTelegram() for _, telegramUser := range telegramUsers { _, _, err := app.jf.UserByID(telegramUser.JellyfinID, false) @@ -75,7 +76,7 @@ func (app *appContext) clearTelegram() { } func (app *appContext) clearPWRCaptchas() { - app.debug.Println("Housekeeping: Clearing old PWR Captchas") + app.debug.Println(lm.HousekeepingCaptcha) captchas := map[string]Captcha{} for k, capt := range app.pwrCaptchas { if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) { @@ -86,7 +87,7 @@ func (app *appContext) clearPWRCaptchas() { } func (app *appContext) clearActivities() { - app.debug.Println("Housekeeping: Cleaning up Activity log...") + app.debug.Println(lm.HousekeepingActivity) keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000) maxAgeDays := app.config.Section("activity_log").Key("delete_after_days").MustInt(90) minAge := time.Now().AddDate(0, 0, -maxAgeDays) @@ -103,7 +104,7 @@ func (app *appContext) clearActivities() { } } if err == badger.ErrTxnTooBig { - app.debug.Printf("Activities: Delete txn was too big, doing it manually.") + app.debug.Printf(lm.AcitivityLogTxnTooBig) list := []Activity{} if errorSource == 0 { app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge)) @@ -119,7 +120,7 @@ func (app *appContext) clearActivities() { func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon { d := NewGenericDaemon(interval, app, func(app *appContext) { - app.debug.Println("Housekeeping: Checking for expired invites") + app.debug.Println(lm.HousekeepingInvites) app.checkInvites() }, func(app *appContext) { app.clearActivities() }, diff --git a/jellyseerr-d.go b/jellyseerr-d.go index f42bfbc..374cdf9 100644 --- a/jellyseerr-d.go +++ b/jellyseerr-d.go @@ -5,20 +5,21 @@ import ( "time" "github.com/hrfee/jfa-go/jellyseerr" + lm "github.com/hrfee/jfa-go/logmessages" ) func (app *appContext) SynchronizeJellyseerrUser(jfID string) { user, imported, err := app.js.GetOrImportUser(jfID) if err != nil { - app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err) + app.debug.Printf(lm.FailedMustGetJellyseerrUser, jfID, err) return } if imported { - app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID) + app.debug.Printf(lm.ImportJellyseerrUser, jfID, user.ID) } notif, err := app.js.GetNotificationPreferencesByID(user.ID) if err != nil { - app.debug.Printf("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err) + app.debug.Printf(lm.FailedGetJellyseerrNotificationPrefs, jfID, err) return } @@ -27,7 +28,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) { if ok && email.Addr != "" && user.Email != email.Addr { err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr}) if err != nil { - app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) + app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err) } else { contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact } @@ -51,7 +52,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) { if len(contactMethods) != 0 { err := app.js.ModifyNotifications(jfID, contactMethods) if err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } } } @@ -59,7 +60,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) { func (app *appContext) SynchronizeJellyseerrUsers() { users, status, err := app.jf.GetUsers(false) if err != nil || status != 200 { - app.err.Printf("Failed to get users (%d): %s", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) return } // I'm sure Jellyseerr can handle it, diff --git a/logmessages/logmessages.go b/logmessages/logmessages.go index 70764a2..57c8d93 100644 --- a/logmessages/logmessages.go +++ b/logmessages/logmessages.go @@ -1,5 +1,12 @@ package logmessages +/* Log strings for (almost) all the program. + * Helps avoid writing redundant, slightly different + * strings constantly. + * Also would help if I were to ever set up translation + * for logs. Mostly split by file, but obviously there's + * re-use, and occasionally related stuff is grouped. + */ const ( Jellyseerr = "Jellyseerr" Jellyfin = "Jellyfin" @@ -12,16 +19,20 @@ const ( // main.go FailedLogging = "Failed to start log wrapper: %v\n" - NoConfig = "Couldn't find default config file" - Write = "Wrote to \"%s\"" - FailedWriting = "Failed to write to \"%s\": %v" - FailedReading = "Failed to read from \"%s\": %v" - FailedOpen = "Failed to open \"%s\": %v" + NoConfig = "Couldn't find default config file" + Write = "Wrote to \"%s\"" + FailedWriting = "Failed to write to \"%s\": %v" + FailedCreateDir = "Failed to create directory \"%s\": %v" + FailedReading = "Failed to read from \"%s\": %v" + FailedOpen = "Failed to open \"%s\": %v" + FailedStat = "Failed to stat \"%s\": %v" + PathNotFound = "Path \"%s\" not found" CopyConfig = "Copied default configuration to \"%s\"" FailedCopyConfig = "Failed to copy default configuration to \"%s\": %v" LoadConfig = "Loaded config file \"%s\"" FailedLoadConfig = "Failed to load config file \"%s\": %v" + ModifyConfig = "Config saved to \"%s\"" SocketPath = "Socket Path: \"%s\"" FailedSocketConnect = "Couldn't establish socket connection at \"%s\": %v" @@ -65,10 +76,12 @@ const ( Serving = "Loaded @ \"%s\"" - QuitReceived = "Restart/Quit signal received, please be patient." - Quitting = "Shutting down..." - Quit = "Server shut down." - FailedQuit = "Server shutdown failed: %v" + QuitReceived = "Restart/Quit signal received, please be patient." + Quitting = "Shutting down..." + Restarting = "Restarting..." + FailedHardRestartWindows = "hard restarts not available on windows" + Quit = "Server shut down." + FailedQuit = "Server shutdown failed: %v" // api-activities.go FailedDBReadActivities = "Failed to read activities from DB: %v" @@ -79,11 +92,12 @@ const ( FailedGetUpload = "Failed to retrieve file from form data: %v" // api-invites.go - DeleteOldInvite = "Deleting old invite \"%s\"" - DeleteInvite = "Deleting invite \"%s\"" - FailedDeleteInvite = "Failed to delete invite \"%s\": %v" - GenerateInvite = "Generating new invite" - InvalidInviteCode = "Invalid invite code \"%s\"" + DeleteOldInvite = "Deleting old invite \"%s\"" + DeleteInvite = "Deleting invite \"%s\"" + FailedDeleteInvite = "Failed to delete invite \"%s\": %v" + GenerateInvite = "Generating new invite" + FailedGenerateInvite = "Failed to generate new invite: %v" + InvalidInviteCode = "Invalid invite code \"%s\"" FailedSendToTooltipNoUser = "Failed: \"%s\" not found" FailedSendToTooltipMultiUser = "Failed: \"%s\" linked to multiple users" @@ -94,22 +108,25 @@ const ( SetAdminNotify = "Set \"%s\" to %t for admin address \"%s\"" - // api-jellyseerr.go + // *jellyseerr*.go FailedGetUsers = "Failed to get user(s) from %s: %v" // FIXME: Once done, look back at uses of FailedGetUsers for places where this would make more sense. FailedGetUser = "Failed to get user \"%s\" from %s: %v" - FailedGetJellyseerrNotificationPrefs = "Failed to get user's notification prefs from " + Jellyseerr + ": %v" + FailedGetJellyseerrNotificationPrefs = "Failed to get user \"%s\"'s notification prefs from " + Jellyseerr + ": %v" FailedSyncContactMethods = "Failed to sync contact methods with %s: %v" + ImportJellyseerrUser = "Triggered import for " + Jellyseerr + " user \"%s\" (New ID: %d)" + FailedMustGetJellyseerrUser = "Failed to get or trigger import for " + Jellyseerr + " user \"%s\": %v" // api-messages.go FailedGetCustomMessage = "Failed to get custom message \"%s\"" SetContactPrefForService = "Set contact preference for %s (\"%s\"): %t" // Matrix - InvalidPIN = "Invalid PIN \"%s\"" - UnauthorizedPIN = "Unauthorized PIN \"%s\"" - FailedCreateRoom = "Failed to create room: %v" - FailedGenerateToken = "Failed to generate token: %v" + InvalidPIN = "Invalid PIN \"%s\"" + ExpiredPIN = "Expired PIN \"%s\"" + InvalidPassword = "Invalid Password" + UnauthorizedPIN = "Unauthorized PIN \"%s\"" + FailedCreateRoom = "Failed to create room: %v" // api-profiles.go SetDefaultProfile = "Setting default profile to \"%s\"" @@ -123,6 +140,7 @@ const ( FailedGetJellyfinDisplayPrefs = "Failed to get DisplayPreferences for user \"%s\" from " + Jellyfin + ": %v" ProfileNoHomescreen = "No homescreen template in profile \"%s\"" Profile = "profile" + Lang = "language" User = "user" ApplyingTemplatesFrom = "Applying templates from %s: \"%s\" to %d users" DelayingRequests = "Delay will be added between requests (count = %d)" @@ -153,7 +171,7 @@ const ( FailedSetEmailAddress = "Failed to set email address for %s user \"%s\": %v" - AdditionalOmbiErrors = "Additional errors from " + Ombi + ": %v" + AdditionalErrors = "Additional errors from %s: %v" IncorrectCaptcha = "captcha incorrect" @@ -162,14 +180,148 @@ const ( UserEmailAdjusted = "Email for user \"%s\" adjusted" UserAdminAdjusted = "Admin state for user \"%s\" set to %t" UserLabelAdjusted = "Label for user \"%s\" set to \"%s\"" + + // api.go + ApplyUpdate = "Applied update" + FailedApplyUpdate = "Failed to apply update: %v" + UpdateManual = "update is manual" + + // backups.go + DeleteOldBackup = "Deleted old backup \"%s\"" + FailedDeleteOldBackup = "Failed to delete old backup \"%s\": %v" + CreateBackup = "Created database backup \"%+v\"" + FailedCreateBackup = "Faled to create database backup: %v" + MoveOldDB = "Moved existing database to \"%s\"" + FailedMoveOldDB = "Failed to move existing database to \"%s\": %v" + RestoreDB = "Restored database from \"%s\"" + FailedRestoreDB = "Failed to resotre database from \"%s\": %v" + + // config.go + EnableAllPWRMethods = "No PWR method preferences set in [user_page], all will be enabled" + InitProxy = "Initialized proxy @ \"%s\"" + FailedInitProxy = "Failed to initialize proxy @ \"%s\": %v\nStartup will pause for a bit to grab your attention." + + // discord.go + StartDaemon = "Started %s daemon" + FailedStartDaemon = "Failed to start %s daemon: %v" + FailedGetDiscordGuildMembers = "Failed to get " + Discord + " guild members: %v" + FailedGetDiscordGuild = "Failed to get " + Discord + " guild: %v" + FailedGetDiscordRoles = "Failed to get " + Discord + " roles: %v" + FailedCreateDiscordInviteChannel = "Failed to create " + Discord + " invite channel: %v" + InviteChannelEmpty = "no invite channel set in settings" + FailedGetDiscordChannels = "Failed to get " + Discord + " channel(s): %v" + FailedGetDiscordChannel = "Failed to get " + Discord + " channel \"%s\": %v" + MonitorAllDiscordChannels = "Will monitor all " + Discord + " channels" + FailedCreateDiscordDMChannel = "Failed to create " + Discord + " private DM channel with \"%s\": %v" + NotFound = "not found" + RegisterDiscordChoice = "Registered " + Discord + " %s choice \"%s\"" + FailedRegisterDiscordChoices = "Failed to register " + Discord + " %s choices: %v" + FailedDeregDiscordChoice = "Failed to deregister " + Discord + " %s choice \"%s\": %v" + RegisterDiscordCommand = "Registered " + Discord + " command \"%s\"" + FailedRegisterDiscordCommand = "Failed to register " + Discord + " command \"%s\": %v" + FailedGetDiscordCommands = "Failed to get " + Discord + " commands: %v" + FailedDeregDiscordCommand = "Failed to deregister " + Discord + " command \"%s\": %v" + + FailedReply = "Failed to reply to %s message from \"%s\": %v" + FailedMessage = "Failed to send %s message to \"%s\": %v" + + IgnoreOutOfChannelMessage = "Ignoring out-of-channel %s message" + + FailedGenerateDiscordInvite = "Failed to generate " + Discord + " invite: %v" + + // email.go + FailedInitSMTP = "Failed to initialize SMTP mailer: %v" + FailedGeneratePWRLink = "Failed to generate PWR link: %v" + + // housekeeping-d.go + hk = "Housekeeping: " + hkcu = hk + "cleaning up " + HousekeepingEmail = hkcu + Email + " addresses" + HousekeepingDiscord = hkcu + Discord + " IDs" + HousekeepingTelegram = hkcu + Telegram + " IDs" + HousekeepingMatrix = hkcu + Matrix + " IDs" + HousekeepingCaptcha = hkcu + "PWR Captchas" + HousekeepingActivity = hkcu + "Activity log" + HousekeepingInvites = hkcu + "Invites" + ActivityLogTxnTooBig = hk + "Activity log delete transaction was too big, going one-by-one" + + // matrix*.go + FailedSyncMatrix = "Failed to sync " + Matrix + " daemon: %v" + FailedCreateMatrixRoom = "Failed to create " + Matrix + " room with user \"%s\": %v" + MatrixOLMLog = "Matrix/OLM: %v" + MatrixOLMTraceLog = "Matrix/OLM [TRACE]:" + FailedDecryptMatrixMessage = "Failed to decrypt " + Matrix + " E2EE'd message: %v" + FailedEnableMatrixEncryption = "Failed to enable encryption in " + Matrix + " room \"%s\": %v" + + // NOTE: "migrations.go" is the one file where log messages are not part of logmessages/logmessages.go. + + // pwreset.go + PWRExpired = "PWR for user \"%s\" already expired @ %s, check system time!" + + // router.go + UseDefaultHTML = "Using default HTML \"%s\"" + UseCustomHTML = "Using custom HTML \"%s\"" + FailedLoadTemplates = "Failed to load %s templates: %v" + Internal = "internal" + External = "external" + RegisterPprof = "Registered pprof" + SwaggerWarning = "Warning: Swagger should not be used on a public instance." + + // storage.go + ConnectDB = "Connected to DB \"%s\"" + FailedConnectDB = "Failed to open/connect to database \"%s\": %v" + + // updater.go + NoUpdate = "No new updates available" + FoundUpdate = "Found update" + FailedGetUpdateTag = "Failed to get latest tag: %v" + FailedGetUpdate = "Failed to get update: %v" + UpdateTagDetails = "Update/Tag details: %+v" + + // user-auth.go + UserPage = "userpage" + UserPageRequiresJellyfinAuth = "Jellyfin login must be enabled for user page access." + + // user-d.go + CheckUserExpiries = "Checking for user expiry" + DeleteExpiryForOldUser = "Deleting expiry for old user \"%s\"" + DeleteExpiredUser = "Deleting expired user \"%s\"" + DisableExpiredUser = "Disabling expired user \"%s\"" + FailedDeleteOrDisableExpiredUser = "Failed to delete/disable expired user \"%s\": %v" + + // views.go + FailedServerPush = "Failed to use HTTP/2 Server Push: %v" + IgnoreBotPWR = "Ignore PWR magic link visit from bot" + ReCAPTCHA = "ReCAPTCHA" + FailedGenerateCaptcha = "Failed to generate captcha: %v" + CaptchaNotFound = "Captcha \"%s\" not found in invite \"%s\"" + FailedVerifyReCAPTCHA = "Failed to verify reCAPTCHA: %v" + InvalidHostname = "invalid hostname (wanted \"%s\", got \"%s\")" ) const ( - FailedGetCookies = "Failed to get cookie(s) \"%s\": %v" - FailedParseJWT = "Failed to parse JWT: %v" - FailedCastJWT = "JWT claims unreadable" - InvalidJWT = "JWT was invalidated, of incorrect type or has expired" - FailedSignJWT = "Failed to sign JWT: %v" + FailedGetCookies = "Failed to get cookie(s) \"%s\": %v" + FailedParseJWT = "Failed to parse JWT: %v" + FailedCastJWT = "JWT claims unreadable" + InvalidJWT = "JWT was invalidated, of incorrect type or has expired" + LocallyInvalidatedJWT = "JWT is listed as invalidated" + FailedSignJWT = "Failed to sign JWT: %v" + + RequestingToken = "Token requested (%s)" + TokenLoginAttempt = "login attempt" + TokenRefresh = "refresh token" + UserTokenLoginAttempt = UserPage + " " + TokenLoginAttempt + UserTokenRefresh = UserPage + " " + TokenRefresh + GenerateToken = "Token generated for user \"%s\"" + FailedGenerateToken = "Failed to generate token: %v" + + FailedAuthRequest = "Failed to authorize request: %v" + InvalidAuthHeader = "invalid auth header" + NonAdminToken = "token not for admin use" + NonAdminUser = "user \"%s\" not admin" + InvalidUserOrPass = "invalid user/pass" + EmptyUserOrPass = "invalid user/pass" + UserDisabled = "user is disabled" ) const ( @@ -209,6 +361,10 @@ const ( FailedSendExpiryAdjustmentMessage = "Failed to send expiry adjustment message for \"%s\" to \"%s\": %v" SentExpiryAdjustmentMessage = "Sent expiry adjustment message for \"%s\" to \"%s\"" + FailedConstructExpiryMessage = "Failed to construct expiry message for \"%s\": %v" + FailedSendExpiryMessage = "Failed to send expiry message for \"%s\" to \"%s\": %v" + SentExpiryMessage = "Sent expiry message for \"%s\" to \"%s\"" + FailedConstructAnnouncementMessage = "Failed to construct announcement message for \"%s\": %v" FailedSendAnnouncementMessage = "Failed to send announcement message for \"%s\" to \"%s\": %v" SentAnnouncementMessage = "Sent announcement message for \"%s\" to \"%s\"" diff --git a/matrix.go b/matrix.go index d20814c..f770aa6 100644 --- a/matrix.go +++ b/matrix.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gomarkdown/markdown" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/timshannon/badgerhold/v4" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" @@ -118,13 +119,13 @@ func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string func (d *MatrixDaemon) run() { startTime := d.start - d.app.info.Println("Starting Matrix bot daemon") + d.app.info.Println(lm.StartDaemon, lm.Matrix) syncer := d.bot.Syncer.(*mautrix.DefaultSyncer) HandleSyncerCrypto(startTime, d, syncer) syncer.OnEventType(event.EventMessage, d.handleMessage) if err := d.bot.Sync(); err != nil { - d.app.err.Printf("Matrix sync failed: %v", err) + d.app.err.Printf(lm.FailedSyncMatrix, err) } } @@ -170,7 +171,7 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) { list, ) if err != nil { - d.app.err.Printf("Matrix: Failed to send message to \"%s\": %v", evt.Sender, err) + d.app.err.Printf(lm.FailedReply, lm.Matrix, evt.Sender, err) } return } @@ -203,7 +204,7 @@ func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, encrypted bo func (d *MatrixDaemon) SendStart(userID string) (ok bool) { roomID, encrypted, err := d.CreateRoom(userID) if err != nil { - d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err) + d.app.err.Printf(lm.FailedCreateMatrixRoom, userID, err) return } lang := "en-us" @@ -226,7 +227,7 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) { roomID, ) if err != nil { - d.app.err.Printf("Matrix: Failed to send welcome message to \"%s\": %v", userID, err) + d.app.err.Printf(lm.FailedMessage, lm.Matrix, userID, err) return } ok = true diff --git a/matrix_crypto.go b/matrix_crypto.go index 6e76ece..e5fdbca 100644 --- a/matrix_crypto.go +++ b/matrix_crypto.go @@ -1,10 +1,13 @@ +//go:build e2ee // +build e2ee package main import ( + "fmt" "strings" + lm "github.com/hrfee/jfa-go/logmessages" "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/event" @@ -65,22 +68,22 @@ type olmLogger struct { } func (o olmLogger) Error(message string, args ...interface{}) { - o.app.err.Printf("OLM: "+message+"\n", args) + o.app.err.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args)) } func (o olmLogger) Warn(message string, args ...interface{}) { - o.app.info.Printf("OLM: "+message+"\n", args) + o.app.info.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args)) } func (o olmLogger) Debug(message string, args ...interface{}) { - o.app.debug.Printf("OLM: "+message+"\n", args) + o.app.debug.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args)) } func (o olmLogger) Trace(message string, args ...interface{}) { if strings.HasPrefix(message, "Got membership state event") { return } - o.app.debug.Printf("OLM [TRACE]: "+message+"\n", args) + o.app.debug.Printf(lm.MatrixOlmTracelog, fmt.Sprintf(message, args)) } func InitMatrixCrypto(d *MatrixDaemon) (err error) { @@ -155,7 +158,7 @@ func HandleSyncerCrypto(startTime int64, d *MatrixDaemon, syncer *mautrix.Defaul // return // } if err != nil { - d.app.err.Printf("Failed to decrypt Matrix message: %v", err) + d.app.err.Printf(lm.FailedDecryptMatrixMessage, err) return } d.handleMessage(source, decrypted) @@ -180,7 +183,7 @@ func EncryptRoom(d *MatrixDaemon, room *mautrix.RespCreateRoom, userID id.UserID if err == nil { encrypted = true } else { - d.app.debug.Printf("Matrix: Failed to enable encryption in room: %v", err) + d.app.debug.Printf(lm.FailedEnableMatrixEncryption, room.RoomID, err) return } d.isEncrypted[room.RoomID] = encrypted diff --git a/migrations.go b/migrations.go index bf16336..6edce3a 100644 --- a/migrations.go +++ b/migrations.go @@ -9,6 +9,8 @@ import ( "gopkg.in/ini.v1" ) +// NOTE: This is the one file where log messages are not part of logmessages/logmessages.go + func runMigrations(app *appContext) { migrateProfiles(app) migrateBootstrap(app) diff --git a/pwreset.go b/pwreset.go index ae387d1..96858a7 100644 --- a/pwreset.go +++ b/pwreset.go @@ -8,6 +8,7 @@ import ( "time" "github.com/fsnotify/fsnotify" + lm "github.com/hrfee/jfa-go/logmessages" ) // GenInternalReset generates a local password reset PIN, for use with the PWR option on the Admin page. @@ -39,16 +40,16 @@ func (app *appContext) GenResetLink(pin string) (string, error) { } func (app *appContext) StartPWR() { - app.info.Println("Starting password reset daemon") + app.info.Println(lm.StartDaemon, "PWR") 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) + app.err.Printf(lm.FailedStartDaemon, "PWR", fmt.Sprintf(lm.PathNotFound, path)) return } watcher, err := fsnotify.NewWatcher() if err != nil { - app.err.Printf("Couldn't initialise password reset daemon") + app.err.Printf(lm.FailedStartDaemon, "PWR", err) return } defer watcher.Close() @@ -56,7 +57,7 @@ func (app *appContext) StartPWR() { go pwrMonitor(app, watcher) err = watcher.Add(path) if err != nil { - app.err.Printf("Failed to start password reset daemon: %s", err) + app.err.Printf(lm.FailedStartDaemon, "PWR", err) } waitForRestart() @@ -84,43 +85,36 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { var pwr PasswordReset data, err := os.ReadFile(event.Name) if err != nil { - app.debug.Printf("PWR: Failed to read file: %v", err) + app.debug.Printf(lm.FailedReading, event.Name, err) return } err = json.Unmarshal(data, &pwr) if len(pwr.Pin) == 0 || err != nil { - app.debug.Printf("PWR: Failed to read PIN: %v", err) + app.debug.Printf(lm.FailedReading, event.Name, 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) + if !(status == 200 || status == 204) || err != nil || user.ID == "" { + app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, 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) + app.err.Printf(lm.FailedConstructPWRMessage, 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) + app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, name, err) } else { - app.info.Printf("Sent password reset message to \"%s\"", name) + app.err.Printf(lm.SentPWRMessage, pwr.Username, name) } } } else { - app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry) + app.err.Printf(lm.PWRExpired, pwr.Username, pwr.Expiry) } } @@ -128,7 +122,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { if !ok { return } - app.err.Printf("Password reset daemon: %s", err) + app.err.Printf(lm.FailedStartDaemon, "PWR", err) } } } diff --git a/restart_windows.go b/restart_windows.go index 54c05cf..2c2ee3a 100644 --- a/restart_windows.go +++ b/restart_windows.go @@ -1,7 +1,11 @@ package main -import "fmt" +import ( + "fmt" + + lm "github.com/hrfee/jfa-go/logmessages" +) func (app *appContext) HardRestart() error { - return fmt.Errorf("hard restarts not available on windows") + return fmt.Errorf(lm.FailedHardRestartWindows) } diff --git a/router.go b/router.go index 85900a2..221c01d 100644 --- a/router.go +++ b/router.go @@ -11,6 +11,7 @@ import ( "github.com/gin-contrib/pprof" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) @@ -21,17 +22,17 @@ func (app *appContext) loadHTML(router *gin.Engine) { templatePath := "html" htmlFiles, err := fs.ReadDir(localFS, templatePath) if err != nil { - app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) + app.err.Fatalf(lm.FailedReading, templatePath, err) return } loadInternal := []string{} loadExternal := []string{} for _, f := range htmlFiles { if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) { - app.debug.Printf("Using default \"%s\"", f.Name()) + app.debug.Printf(lm.UseDefaultHTML, f.Name()) loadInternal = append(loadInternal, FSJoin(templatePath, f.Name())) } else { - app.info.Printf("Using custom \"%s\"", f.Name()) + app.info.Printf(lm.UseCustomHTML, f.Name()) loadExternal = append(loadExternal, filepath.Join(filepath.Join(customPath, f.Name()))) } } @@ -39,13 +40,13 @@ func (app *appContext) loadHTML(router *gin.Engine) { if len(loadInternal) != 0 { tmpl, err = template.ParseFS(localFS, loadInternal...) if err != nil { - app.err.Fatalf("Failed to load templates: %v", err) + app.err.Fatalf(lm.FailedLoadTemplates, lm.Internal, err) } } if len(loadExternal) != 0 { tmpl, err = tmpl.ParseFiles(loadExternal...) if err != nil { - app.err.Fatalf("Failed to load external templates: %v", err) + app.err.Fatalf(lm.FailedLoadTemplates, lm.External, err) } } router.SetHTMLTemplate(tmpl) @@ -96,7 +97,7 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine { router.Use(static.Serve("/", app.webFS)) router.NoRoute(app.NoRouteHandler) if *PPROF { - app.debug.Println("Loading pprof") + app.debug.Println(lm.RegisterPprof) pprof.Register(router) } SRV = &http.Server{ @@ -165,7 +166,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) { } } if *SWAGGER { - app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) + app.info.Print(warning(lm.SwaggerWarning)) for _, p := range routePrefixes { router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) } diff --git a/setup.go b/setup.go index 2e23dcf..a1cb6b9 100644 --- a/setup.go +++ b/setup.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/hrfee/jfa-go/easyproxy" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" ) @@ -104,7 +105,7 @@ func (app *appContext) TestJF(gc *gin.Context) { case 404: msg = "error404" } - app.info.Printf("Auth failed with code %d (%s)", status, err) + app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err) if msg != "" { respond(status, msg, gc) } else { diff --git a/storage.go b/storage.go index 2a10352..426328d 100644 --- a/storage.go +++ b/storage.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "github.com/hrfee/jfa-go/jellyseerr" "github.com/hrfee/jfa-go/logger" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" "github.com/timshannon/badgerhold/v4" "gopkg.in/ini.v1" @@ -175,10 +176,10 @@ func (app *appContext) ConnectDB() { opts.ValueDir = app.storage.db_path db, err := badgerhold.Open(opts) if err != nil { - app.err.Fatalf("Failed to open db \"%s\": %v", app.storage.db_path, err) + app.err.Fatalf(lm.FailedConnectDB, app.storage.db_path, err) } app.storage.db = db - app.info.Printf("Connected to DB \"%s\"", app.storage.db_path) + app.info.Printf(lm.ConnectDB, app.storage.db_path) } // GetEmails returns a copy of the store. diff --git a/telegram.go b/telegram.go index d5f5a67..4429cfd 100644 --- a/telegram.go +++ b/telegram.go @@ -7,6 +7,7 @@ import ( "time" tg "github.com/go-telegram-bot-api/telegram-bot-api" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/timshannon/badgerhold/v4" ) @@ -96,12 +97,12 @@ func (t *TelegramDaemon) NewAssignedAuthToken(id string) string { } func (t *TelegramDaemon) run() { - t.app.info.Println("Starting Telegram bot daemon") + t.app.info.Println(lm.StartDaemon, lm.Telegram) u := tg.NewUpdate(0) u.Timeout = 60 updates, err := t.bot.GetUpdatesChan(u) if err != nil { - t.app.err.Printf("Failed to start Telegram daemon: %v", err) + t.app.err.Printf(lm.FailedStartDaemon, lm.Telegram, err) telegramEnabled = false return } @@ -199,7 +200,7 @@ func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang strin content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"}) err := t.Reply(upd, content) if err != nil { - t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) + t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err) } } @@ -211,7 +212,7 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string } err := t.Reply(upd, list) if err != nil { - t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) + t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err) } return } @@ -232,14 +233,14 @@ func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string) if !ok || time.Now().After(token.Expiry) { err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN")) if err != nil { - t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) + t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err) } delete(t.tokens, upd.Message.Text) return } err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess")) if err != nil { - t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) + t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err) } t.verifiedTokens[upd.Message.Text] = TelegramVerifiedToken{ ChatID: upd.Message.Chat.ID, diff --git a/updater.go b/updater.go index 4e08e1e..2ab5341 100644 --- a/updater.go +++ b/updater.go @@ -16,6 +16,8 @@ import ( "strings" "time" + lm "github.com/hrfee/jfa-go/logmessages" + "github.com/hrfee/jfa-go/common" ) @@ -560,15 +562,16 @@ func (app *appContext) checkForUpdates() { if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") { app.err.Println("No new updates available.") } else if status != -1 { // -1 means updates disabled, we don't need to log it. - app.err.Printf("Failed to get latest tag (%d): %v", status, err) + app.err.Printf(lm.FailedGetUpdateTag, err) } return } if tag != app.tag && tag.IsNew() { - app.info.Println("Update found") + app.info.Println(lm.FoundUpdate) + app.debug.Printf(lm.UpdateTagDetails, tag) update, status, err := app.updater.GetUpdate(tag) if status != 200 || err != nil { - app.err.Printf("Failed to get update (%d): %v", status, err) + app.err.Printf(lm.FailedGetUpdate, err) return } app.tag = tag diff --git a/user-auth.go b/user-auth.go index d347f08..9af1d99 100644 --- a/user-auth.go +++ b/user-auth.go @@ -1,9 +1,11 @@ package main import ( + "fmt" "strings" "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" ) func (app *appContext) userAuth() gin.HandlerFunc { @@ -13,7 +15,7 @@ func (app *appContext) userAuth() gin.HandlerFunc { func (app *appContext) userAuthenticate(gc *gin.Context) { jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true) if !jellyfinLogin { - app.err.Println("Enable Jellyfin Login to use the User Page feature.") + app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth) respond(500, "Contact Admin", gc) return } @@ -27,7 +29,6 @@ func (app *appContext) userAuthenticate(gc *gin.Context) { gc.Set("jfId", jfID) gc.Set("userMode", true) - app.debug.Println("Auth succeeded") gc.Next() } @@ -41,11 +42,11 @@ func (app *appContext) userAuthenticate(gc *gin.Context) { // @Security getUserTokenAuth func (app *appContext) getUserTokenLogin(gc *gin.Context) { if !app.config.Section("ui").Key("jellyfin_login").MustBool(true) { - app.err.Println("Enable Jellyfin Login to use the User Page feature.") + app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth) respond(500, "Contact Admin", gc) return } - app.logIpInfo(gc, true, "UserToken requested (login attempt)") + app.logIpInfo(gc, true, fmt.Sprintf(lm.RequestingToken, lm.UserTokenLoginAttempt)) username, password, ok := app.decodeValidateLoginHeader(gc, true) if !ok { return @@ -58,12 +59,11 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) { token, refresh, err := CreateToken(user.ID, user.ID, false) if err != nil { - app.err.Printf("getUserToken failed: Couldn't generate user token (%s)", err) + app.err.Printf(lm.FailedGenerateToken, err) respond(500, "Couldn't generate user token", gc) return } - app.debug.Printf("Token generated for non-admin user \"%s\"", username) uri := "/my" if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) { uri = "/accounts/my" @@ -81,12 +81,12 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) { func (app *appContext) getUserTokenRefresh(gc *gin.Context) { jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true) if !jellyfinLogin { - app.err.Println("Enable Jellyfin Login to use the User Page feature.") + app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth) respond(500, "Contact Admin", gc) return } - app.logIpInfo(gc, true, "UserToken request (refresh token)") + app.logIpInfo(gc, true, fmt.Sprintf(lm.RequestingToken, lm.UserTokenRefresh)) claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh") if !ok { return @@ -96,7 +96,7 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) { jwt, refresh, err := CreateToken(jfID, jfID, false) if err != nil { - app.err.Printf("getUserToken failed: Couldn't generate user token (%s)", err) + app.err.Printf(lm.FailedGenerateToken, err) respond(500, "Couldn't generate user token", gc) return } diff --git a/user-d.go b/user-d.go index 52bcaa8..ad83e8e 100644 --- a/user-d.go +++ b/user-d.go @@ -3,6 +3,7 @@ package main import ( "time" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" "github.com/lithammer/shortuuid/v3" ) @@ -21,17 +22,17 @@ func (app *appContext) checkUsers() { if len(app.storage.GetUserExpiries()) == 0 { return } - app.info.Println("Daemon: Checking for user expiry") + app.info.Println(lm.CheckUserExpiries) users, status, err := app.jf.GetUsers(false) if err != nil || status != 200 { - app.err.Printf("Failed to get users (%d): %s", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) return } mode := "disable" - term := "Disabling" + phrase := lm.DisableExpiredUser if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" { mode = "delete" - term = "Deleting" + phrase = lm.DeleteExpiredUser } contact := false if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) { @@ -45,7 +46,7 @@ func (app *appContext) checkUsers() { for _, expiry := range app.storage.GetUserExpiries() { id := expiry.JellyfinID if _, ok := userExists[id]; !ok { - app.info.Printf("Deleting expiry for non-existent user \"%s\"", id) + app.info.Printf(lm.DeleteExpiryForOldUser, id) app.storage.DeleteUserExpiryKey(expiry.JellyfinID) } else if time.Now().After(expiry.Expiry) { found := false @@ -58,11 +59,10 @@ func (app *appContext) checkUsers() { } } if !found { - app.info.Printf("Expired user already deleted, ignoring.") app.storage.DeleteUserExpiryKey(expiry.JellyfinID) continue } - app.info.Printf("%s expired user \"%s\"", term, user.Name) + app.info.Printf(phrase, user.Name) // Record activity activity := Activity{ @@ -83,7 +83,7 @@ func (app *appContext) checkUsers() { activity.Type = ActivityDisabled } if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err) + app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err) continue } @@ -98,11 +98,11 @@ func (app *appContext) checkUsers() { name := app.getAddressOrName(user.ID) msg, err := app.email.constructUserExpired(app, false) if err != nil { - app.err.Printf("Failed to construct expiry message for \"%s\": %s", user.Name, err) + app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err) } else if err := app.sendByID(msg, user.ID); err != nil { - app.err.Printf("Failed to send expiry message to \"%s\": %s", name, err) + app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, name, err) } else { - app.info.Printf("Sent expiry notification to \"%s\"", name) + app.err.Printf(lm.SentExpiryMessage, user.ID, name) } } } diff --git a/views.go b/views.go index dfb561a..2b66be6 100644 --- a/views.go +++ b/views.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "encoding/json" + "fmt" "html/template" "io" "io/fs" @@ -16,6 +17,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" "github.com/gomarkdown/markdown" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" "github.com/lithammer/shortuuid/v3" "github.com/steambap/captcha" @@ -66,10 +68,9 @@ func (app *appContext) pushResources(gc *gin.Context, page Page) { toPush = []string{} } if pusher := gc.Writer.Pusher(); pusher != nil { - app.debug.Println("Using HTTP2 Server push") for _, f := range toPush { if err := pusher.Push(app.URLBase+f, nil); err != nil { - app.debug.Printf("Failed HTTP2 ServerPush of \"%s\": %+v", f, err) + app.debug.Printf(lm.FailedServerPush, err) } } } @@ -139,13 +140,13 @@ func (app *appContext) AdminPage(gc *gin.Context) { var license string l, err := fs.ReadFile(localFS, "LICENSE") if err != nil { - app.debug.Printf("Failed to load LICENSE: %s", err) + app.debug.Printf(lm.FailedReading, "LICENSE", err) license = "" } license = string(l) fontLicense, err := fs.ReadFile(localFS, filepath.Join("web", "fonts", "OFL.txt")) if err != nil { - app.debug.Printf("Failed to load OFL.txt: %s", err) + app.debug.Printf(lm.FailedReading, "fontLicense", err) } license += "---Hanken Grotesk---\n\n" @@ -312,7 +313,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) { defer gcHTML(gc, http.StatusOK, "password-reset.html", data) // If it's a bot, pretend to be a success so the preview is nice. if isBot { - app.debug.Println("PWR: Ignoring magic link visit from bot") + app.debug.Println(lm.IgnoreBotPWR) data["success"] = true data["pin"] = "NO-BO-TS" return @@ -338,13 +339,13 @@ func (app *appContext) ResetPassword(gc *gin.Context) { if !isInternal && !setPassword { resp, status, err = app.jf.ResetPassword(pin) } else if time.Now().After(pwr.Expiry) { - app.debug.Printf("Ignoring PWR request due to expired internal PIN: %s", pin) + app.debug.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, pin)) app.NoRouteHandler(gc) return } else { status, err = app.jf.ResetPasswordAdmin(pwr.ID) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Password Reset failed (%d): %v", status, err) + app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err) } else { status, err = app.jf.SetPassword(pwr.ID, "", pin) } @@ -358,7 +359,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) { username = resp.UsersReset[0] } } else { - app.err.Printf("Password Reset failed (%d): %v", status, err) + app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err) } // Only log PWRs we know the user for. @@ -378,21 +379,21 @@ func (app *appContext) ResetPassword(gc *gin.Context) { if app.config.Section("ombi").Key("enabled").MustBool(false) { jfUser, status, err := app.jf.UserByName(username, false) if status != 200 || err != nil { - app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err) + app.err.Printf(lm.FailedGetUser, username, lm.Jellyfin, err) return } ombiUser, status, err := app.getOmbiUser(jfUser.ID) if status != 200 || err != nil { - app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", username, status, err) + app.err.Printf(lm.FailedGetUser, username, lm.Ombi, err) return } ombiUser["password"] = pin 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) + app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err) return } - app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"]) + app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"]) } } @@ -460,7 +461,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) { } capt, err := captcha.New(300, 100) if err != nil { - app.err.Printf("Failed to generate captcha: %v", err) + app.err.Printf(lm.FailedGenerateCaptcha, err) respondBool(500, false, gc) return } @@ -470,7 +471,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) { captchaID := genAuthToken() var buf bytes.Buffer if err := capt.WriteImage(bufio.NewWriter(&buf)); err != nil { - app.err.Printf("Failed to render captcha: %v", err) + app.err.Printf(lm.FailedGenerateCaptcha, err) respondBool(500, false, gc) return } @@ -503,8 +504,12 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool { ok := true if !isPWR { inv, ok := app.storage.GetInvitesKey(code) - if !ok || (!isPWR && inv.Captchas == nil) { - app.debug.Printf("Couldn't find invite \"%s\"", code) + if !ok { + app.debug.Printf(lm.InvalidInviteCode, code) + return false + } + if !isPWR && inv.Captchas == nil { + app.debug.Printf(lm.CaptchaNotFound, id, code) return false } c, ok = inv.Captchas[id] @@ -512,7 +517,7 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool { c, ok = app.pwrCaptchas[code] } if !ok { - app.debug.Printf("Couldn't find Captcha \"%s\"", id) + app.debug.Printf(lm.CaptchaNotFound, id, code) return false } return strings.ToLower(c.Answer) == strings.ToLower(text) @@ -534,8 +539,11 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool { req.Header.Add("Content-Type", "application/x-www-form-urlencoded") resp, err := http.DefaultClient.Do(req) - if err != nil || resp.StatusCode != 200 { - app.err.Printf("Failed to read reCAPTCHA status (%d): %+v\n", resp.Status, err) + if err == nil && resp.StatusCode != 200 { + err = fmt.Errorf("failed (error %d)", resp.StatusCode) + } + if err != nil { + app.err.Printf(lm.FailedVerifyReCAPTCHA, err) return false } defer resp.Body.Close() @@ -543,18 +551,19 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool { body, err := io.ReadAll(resp.Body) err = json.Unmarshal(body, &data) if err != nil { - app.err.Printf("Failed to unmarshal reCAPTCHA response: %+v\n", err) + app.err.Printf(lm.FailedVerifyReCAPTCHA, err) return false } hostname := app.config.Section("captcha").Key("recaptcha_hostname").MustString("") if strings.ToLower(data.Hostname) != strings.ToLower(hostname) && data.Hostname != "" { - app.debug.Printf("Invalidating reCAPTCHA request: Hostnames didn't match (Wanted \"%s\", got \"%s\"\n", hostname, data.Hostname) + err = fmt.Errorf(lm.InvalidHostname, hostname, data.Hostname) + app.err.Printf(lm.FailedVerifyReCAPTCHA, err) return false } if len(data.ErrorCodes) > 0 { - app.err.Printf("reCAPTCHA returned errors: %+v\n", data.ErrorCodes) + app.err.Printf(lm.AdditionalErrors, lm.ReCAPTCHA, data.ErrorCodes) return false } @@ -651,20 +660,19 @@ func (app *appContext) InviteProxy(gc *gin.Context) { token, err := jwt.Parse(key, checkToken) if err != nil { fail() - app.err.Printf("Failed to parse key: %s", err) + app.debug.Printf(lm.FailedParseJWT, err) return } claims, ok := token.Claims.(jwt.MapClaims) expiry := time.Unix(int64(claims["exp"].(float64)), 0) if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) { fail() - app.debug.Printf("Invalid key") + app.debug.Printf(lm.InvalidJWT) return } f, success := app.newUser(req, true, gc) if !success { - app.err.Printf("Failed to create new user") - // Not meant for us. Calling this will be a mess, but at least it might give us some information. + // Not meant for us. Calling this is bad but at least gives us log output. f(gc) fail() return