1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-11-09 20:00:12 +00:00

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.
This commit is contained in:
Harvey Tindall 2024-08-01 20:17:05 +01:00
parent 15a317f84f
commit 711394232b
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
25 changed files with 410 additions and 1740 deletions

1503
:w

File diff suppressed because it is too large Load Diff

View File

@ -66,7 +66,7 @@ func (app *appContext) SetJellyseerrProfile(gc *gin.Context) {
profile.Jellyseerr.User = u.UserTemplate profile.Jellyseerr.User = u.UserTemplate
n, err := app.js.GetNotificationPreferencesByID(jellyseerrID) n, err := app.js.GetNotificationPreferencesByID(jellyseerrID)
if err != nil { 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) respond(500, "Couldn't get user notification prefs", gc)
return return
} }

View File

@ -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) errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
if err != nil || code != 200 { if err != nil || code != 200 {
app.err.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err) 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 { } else {
app.info.Printf(lm.CreateUser, lm.Ombi, req.Username) 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 { } else {
app.info.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err) 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 { } else {
ombiUser, status, err = app.getOmbiUser(id) 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) resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Ombi, err) app.err.Printf(lm.FailedSyncContactMethods, lm.Ombi, err)
app.debug.Printf(lm.AdditionalOmbiErrors, resp) app.debug.Printf(lm.AdditionalErrors, lm.Ombi, resp)
} }
} }
} }

63
api.go
View File

@ -1,10 +1,12 @@
package main package main
import ( import (
"fmt"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/itchyny/timefmt-go" "github.com/itchyny/timefmt-go"
"github.com/lithammer/shortuuid/v3" "github.com/lithammer/shortuuid/v3"
@ -122,14 +124,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
} }
} }
if !valid || req.PIN == "" { 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) gc.JSON(400, validation)
return return
} }
isInternal := false isInternal := false
if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) { 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) respond(400, "errorCaptcha", gc)
return return
} }
@ -138,7 +140,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
if reset, ok := app.internalPWRs[req.PIN]; ok { if reset, ok := app.internalPWRs[req.PIN]; ok {
isInternal = true isInternal = true
if time.Now().After(reset.Expiry) { 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) respondBool(401, false, gc)
delete(app.internalPWRs, req.PIN) delete(app.internalPWRs, req.PIN)
return return
@ -148,7 +150,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
status, err := app.jf.ResetPasswordAdmin(userID) status, err := app.jf.ResetPasswordAdmin(userID)
if !(status == 200 || status == 204) || err != nil { 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) respondBool(status, false, gc)
return return
} }
@ -156,7 +158,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
} else { } else {
resp, status, err := app.jf.ResetPassword(req.PIN) resp, status, err := app.jf.ResetPassword(req.PIN)
if status != 200 || err != nil || !resp.Success { 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) respondBool(status, false, gc)
return return
} }
@ -176,7 +178,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
user, status, err = app.jf.UserByName(username, false) user, status, err = app.jf.UserByName(username, false)
} }
if status != 200 || err != nil { 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) respondBool(500, false, gc)
return return
} }
@ -195,31 +197,33 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
} }
status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password) status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
if !(status == 200 || status == 204) || err != nil { 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) respondBool(500, false, gc)
return return
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { 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 { 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) respondBool(200, true, gc)
return return
} } */
ombiUser, status, err := app.getOmbiUser(user.ID) ombiUser, status, err := app.getOmbiUser(user.ID)
if status != 200 || err != nil { 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) respondBool(200, true, gc)
return return
} }
ombiUser["password"] = req.Password ombiUser["password"] = req.Password
status, err = app.ombi.ModifyUser(ombiUser) status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil { 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) respondBool(200, true, gc)
return 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) respondBool(200, true, gc)
} }
@ -231,7 +235,6 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
// @Security Bearer // @Security Bearer
// @tags Configuration // @tags Configuration
func (app *appContext) GetConfig(gc *gin.Context) { func (app *appContext) GetConfig(gc *gin.Context) {
app.info.Println("Config requested")
resp := app.configBase resp := app.configBase
// Load language options // Load language options
formOptions := app.storage.lang.User.getOptions() formOptions := app.storage.lang.User.getOptions()
@ -341,7 +344,6 @@ func (app *appContext) GetConfig(gc *gin.Context) {
// @Security Bearer // @Security Bearer
// @tags Configuration // @tags Configuration
func (app *appContext) ModifyConfig(gc *gin.Context) { func (app *appContext) ModifyConfig(gc *gin.Context) {
app.info.Println("Config modification requested")
var req configDTO var req configDTO
gc.BindJSON(&req) gc.BindJSON(&req)
// Load a new config, as we set various default values in app.config that shouldn't be stored. // 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") tempConfig.Section("").Key("first_run").SetValue("false")
if err := tempConfig.SaveTo(app.configPath); err != nil { 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) respond(500, err.Error(), gc)
return return
} }
app.debug.Println("Config saved") app.info.Printf(lm.ModifyConfig, app.configPath)
gc.JSON(200, map[string]bool{"success": true}) gc.JSON(200, map[string]bool{"success": true})
if req["restart-program"] != nil && req["restart-program"].(bool) { if req["restart-program"] != nil && req["restart-program"].(bool) {
app.info.Println("Restarting...") app.Restart()
if TRAY {
TRAYRESTART <- true
} else {
RESTART <- true
}
// Safety Sleep (Ensure shutdown tasks get done)
time.Sleep(time.Second)
} }
app.loadConfig() app.loadConfig()
// Reinitialize password validator on config change, as opposed to every applicable request like in python. // Reinitialize password validator on config change, as opposed to every applicable request like in python.
if _, ok := req["password_validation"]; ok { if _, ok := req["password_validation"]; ok {
app.debug.Println("Reinitializing validator")
validatorConf := ValidatorConf{ validatorConf := ValidatorConf{
"length": app.config.Section("password_validation").Key("min_length").MustInt(0), "length": app.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase": app.config.Section("password_validation").Key("upper").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 // @tags Configuration
func (app *appContext) ApplyUpdate(gc *gin.Context) { func (app *appContext) ApplyUpdate(gc *gin.Context) {
if !app.update.CanUpdate { if !app.update.CanUpdate {
respond(400, "Update is manual", gc) app.info.Printf(lm.FailedApplyUpdate, lm.UpdateManual)
respond(400, lm.UpdateManual, gc)
return return
} }
err := app.update.update() err := app.update.update()
if err != nil { if err != nil {
app.err.Printf("Failed to apply update: %v", err) app.err.Printf(lm.FailedApplyUpdate, err)
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} }
@ -452,8 +447,9 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
func (app *appContext) Logout(gc *gin.Context) { func (app *appContext) Logout(gc *gin.Context) {
cookie, err := gc.Cookie("refresh") cookie, err := gc.Cookie("refresh")
if err != nil { if err != nil {
app.debug.Printf("Couldn't get cookies: %s", err) msg := fmt.Sprintf(lm.FailedGetCookies, "refresh", err)
respond(500, "Couldn't fetch cookies", gc) app.debug.Println(msg)
respond(500, msg, gc)
return return
} }
app.invalidTokens = append(app.invalidTokens, cookie) app.invalidTokens = append(app.invalidTokens, cookie)
@ -526,11 +522,7 @@ func (app *appContext) ServeLang(gc *gin.Context) {
// @Security Bearer // @Security Bearer
// @tags Other // @tags Other
func (app *appContext) restart(gc *gin.Context) { func (app *appContext) restart(gc *gin.Context) {
app.info.Println("Restarting...") app.Restart()
err := app.Restart()
if err != nil {
app.err.Printf("Couldn't restart, try restarting manually: %v", err)
}
} }
// @Summary Returns the last 100 lines of the log. // @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! // no need to syscall.exec anymore!
func (app *appContext) Restart() error { func (app *appContext) Restart() error {
app.info.Println(lm.Restarting)
if TRAY { if TRAY {
TRAYRESTART <- true TRAYRESTART <- true
} else { } else {

65
auth.go
View File

@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3" "github.com/lithammer/shortuuid/v3"
) )
@ -41,6 +42,8 @@ func (app *appContext) webAuth() gin.HandlerFunc {
return app.authenticate 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. // 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) { func CreateToken(userId, jfId string, admin bool) (string, string, error) {
var token, refresh string var token, refresh string
@ -72,32 +75,26 @@ func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.Map
ok = false ok = false
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
if header[0] != "Bearer" { if header[0] != "Bearer" {
app.debug.Println("Invalid authorization header") app.authLog(lm.InvalidAuthHeader)
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
token, err := jwt.Parse(string(header[1]), checkToken) token, err := jwt.Parse(string(header[1]), checkToken)
if err != nil { if err != nil {
app.debug.Printf("Auth denied: %s", err) app.authLog(fmt.Sprintf(lm.FailedParseJWT, err))
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
claims, ok = token.Claims.(jwt.MapClaims) claims, ok = token.Claims.(jwt.MapClaims)
if !ok { if !ok {
app.debug.Println("Invalid JWT") app.authLog(lm.FailedCastJWT)
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
expiryUnix := int64(claims["exp"].(float64)) 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) expiry := time.Unix(expiryUnix, 0)
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) { 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)) // app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
ok = false ok = false
@ -115,7 +112,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
} }
isAdminToken := claims["admin"].(bool) isAdminToken := claims["admin"].(bool)
if !isAdminToken { if !isAdminToken {
app.debug.Printf("Auth denied: Token was not for admin access") app.authLog(lm.NonAdminToken)
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
@ -130,14 +127,13 @@ func (app *appContext) authenticate(gc *gin.Context) {
} }
} }
if !match { if !match {
app.debug.Printf("Couldn't find user ID \"%s\"", userID) app.authLog(fmt.Sprintf(lm.NonAdminUser, userID))
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
gc.Set("jfId", jfID) gc.Set("jfId", jfID)
gc.Set("userId", userID) gc.Set("userId", userID)
gc.Set("userMode", false) gc.Set("userMode", false)
app.debug.Println("Auth succeeded")
gc.Next() gc.Next()
} }
@ -160,7 +156,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool)
password = creds[1] password = creds[1]
ok = false ok = false
if username == "" || password == "" { 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) respond(401, "Unauthorized", gc)
return return
} }
@ -173,16 +169,16 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
user, status, err := app.authJf.Authenticate(username, password) user, status, err := app.authJf.Authenticate(username, password)
if status != 200 || err != nil { if status != 200 || err != nil {
if status == 401 || status == 400 { 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) respond(401, "Unauthorized", gc)
return return
} }
if status == 403 { 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) respond(403, "yourAccountWasDisabled", gc)
return 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) respond(500, "Jellyfin error", gc)
return return
} }
@ -199,7 +195,7 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
// @tags Auth // @tags Auth
// @Security getTokenAuth // @Security getTokenAuth
func (app *appContext) getTokenLogin(gc *gin.Context) { 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) username, password, ok := app.decodeValidateLoginHeader(gc, false)
if !ok { if !ok {
return return
@ -209,13 +205,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
for _, user := range app.adminUsers { for _, user := range app.adminUsers {
if user.Username == username && user.Password == password { if user.Username == username && user.Password == password {
match = true match = true
app.debug.Println("Found existing user")
userID = user.UserID userID = user.UserID
break break
} }
} }
if !app.jellyfinLogin && !match { 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) respond(401, "Unauthorized", gc)
return return
} }
@ -233,7 +228,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
} }
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator) accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
if !accountsAdmin { if !accountsAdmin {
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username) app.authLog(fmt.Sprintf(lm.NonAdminUser, username))
respond(401, "Unauthorized", gc) respond(401, "Unauthorized", gc)
return return
} }
@ -243,12 +238,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
newUser := User{ newUser := User{
UserID: userID, UserID: userID,
} }
app.debug.Printf("Token generated for user \"%s\"", username) app.debug.Printf(lm.GenerateToken, username)
app.adminUsers = append(app.adminUsers, newUser) app.adminUsers = append(app.adminUsers, newUser)
} }
token, refresh, err := CreateToken(userID, jfID, true) token, refresh, err := CreateToken(userID, jfID, true)
if err != nil { 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) respond(500, "Couldn't generate token", gc)
return return
} }
@ -261,35 +256,29 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
ok = false ok = false
cookie, err := gc.Cookie(cookieName) cookie, err := gc.Cookie(cookieName)
if err != nil || cookie == "" { 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) respond(400, "Couldn't get token", gc)
return return
} }
for _, token := range app.invalidTokens { for _, token := range app.invalidTokens {
if cookie == token { if cookie == token {
app.debug.Println("getTokenRefresh: Invalid token") app.authLog(lm.LocallyInvalidatedJWT)
respond(401, "Invalid token", gc) respond(401, lm.InvalidJWT, gc)
return return
} }
} }
token, err := jwt.Parse(cookie, checkToken) token, err := jwt.Parse(cookie, checkToken)
if err != nil { if err != nil {
app.debug.Println("getTokenRefresh: Invalid token") app.authLog(lm.FailedParseJWT)
respond(400, "Invalid token", gc) respond(400, lm.InvalidJWT, gc)
return return
} }
claims, ok = token.Claims.(jwt.MapClaims) claims, ok = token.Claims.(jwt.MapClaims)
expiryUnix := int64(claims["exp"].(float64)) 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) expiry := time.Unix(expiryUnix, 0)
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) { if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err) app.authLog(lm.InvalidJWT)
respond(401, "Invalid token", gc) respond(401, lm.InvalidJWT, gc)
ok = false ok = false
return return
} }
@ -304,7 +293,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
// @Router /token/refresh [get] // @Router /token/refresh [get]
// @tags Auth // @tags Auth
func (app *appContext) getTokenRefresh(gc *gin.Context) { 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") claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
if !ok { if !ok {
return return
@ -313,7 +302,7 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
jfID := claims["jfid"].(string) jfID := claims["jfid"].(string)
jwt, refresh, err := CreateToken(userID, jfID, true) jwt, refresh, err := CreateToken(userID, jfID, true)
if err != nil { 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) respond(500, "Couldn't generate token", gc)
return return
} }

View File

@ -7,6 +7,8 @@ import (
"sort" "sort"
"strings" "strings"
"time" "time"
lm "github.com/hrfee/jfa-go/logmessages"
) )
const ( const (
@ -60,12 +62,12 @@ func (app *appContext) getBackups() *BackupList {
path := app.config.Section("backups").Key("path").String() path := app.config.Section("backups").Key("path").String()
err := os.MkdirAll(path, 0755) err := os.MkdirAll(path, 0755)
if err != nil { 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 return nil
} }
items, err := os.ReadDir(path) items, err := os.ReadDir(path)
if err != nil { 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 return nil
} }
backups := &BackupList{} 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)) t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(item.Name(), BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX))
if err != nil { if err != nil {
app.debug.Printf("Failed to parse backup filename \"%s\": %v\n", item.Name(), err) app.debug.Printf(lm.FailedParseTime, err)
continue continue
} }
backups.dates[i] = t backups.dates[i] = t
@ -101,36 +103,36 @@ func (app *appContext) makeBackup() (fileDetails CreateBackupDTO) {
sort.Sort(backups) sort.Sort(backups)
for _, item := range backups.files[:toDelete] { for _, item := range backups.files[:toDelete] {
fullpath := filepath.Join(path, item.Name()) fullpath := filepath.Join(path, item.Name())
app.debug.Printf("Deleting old backup \"%s\"\n", item.Name())
err := os.Remove(fullpath) err := os.Remove(fullpath)
if err != nil { if err != nil {
app.err.Printf("Failed to delete old backup \"%s\": %v\n", fullpath, err) app.err.Printf(lm.FailedDeleteOldBackup, fullpath, err)
return return
} }
app.debug.Printf(lm.DeleteOldBackup, fullpath)
} }
} }
fullpath := filepath.Join(path, fname) fullpath := filepath.Join(path, fname)
f, err := os.Create(fullpath) f, err := os.Create(fullpath)
if err != nil { if err != nil {
app.err.Printf("Failed to open backup file \"%s\": %v\n", fullpath, err) app.err.Printf(lm.FailedOpen, fullpath, err)
return return
} }
defer f.Close() defer f.Close()
_, err = app.storage.db.Badger().Backup(f, 0) _, err = app.storage.db.Badger().Backup(f, 0)
if err != nil { if err != nil {
app.err.Printf("Failed to create backup: %v\n", err) app.err.Printf(lm.FailedCreateBackup, err)
return return
} }
fstat, err := f.Stat() fstat, err := f.Stat()
if err != nil { if err != nil {
app.err.Printf("Failed to get info on new backup: %v\n", err) app.err.Printf(lm.FailedStat, fullpath, err)
return return
} }
fileDetails.Size = fileSize(fstat.Size()) fileDetails.Size = fileSize(fstat.Size())
fileDetails.Name = fname fileDetails.Name = fname
fileDetails.Path = fullpath fileDetails.Path = fullpath
// fmt.Printf("Created backup %+v\n", fileDetails) app.debug.Printf(lm.CreateBackup, fileDetails)
return return
} }
@ -139,25 +141,25 @@ func (app *appContext) loadPendingBackup() {
return return
} }
oldPath := filepath.Join(app.dataPath, "db-"+string(time.Now().Unix())+"-pre-"+filepath.Base(LOADBAK)) 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) err := os.Rename(app.storage.db_path, oldPath)
if err != nil { 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() app.ConnectDB()
defer app.storage.db.Close() defer app.storage.db.Close()
f, err := os.Open(LOADBAK) f, err := os.Open(LOADBAK)
if err != nil { 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) err = app.storage.db.Badger().Load(f, 256)
f.Close() f.Close()
if err != nil { 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 = "" 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 interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
d := NewGenericDaemon(interval, app, d := NewGenericDaemon(interval, app,
func(app *appContext) { func(app *appContext) {
app.debug.Println("Backups: Creating backup")
app.makeBackup() app.makeBackup()
}, },
) )

View File

@ -7,8 +7,10 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/hrfee/jfa-go/easyproxy" "github.com/hrfee/jfa-go/easyproxy"
lm "github.com/hrfee/jfa-go/logmessages"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
) )
@ -140,7 +142,7 @@ func (app *appContext) loadConfig() error {
} }
} }
if allDisabled { if allDisabled {
fmt.Println("SETALLTRUE") app.info.Println(lm.EnableAllPWRMethods)
for _, v := range pwrMethods { for _, v := range pwrMethods {
app.config.Section("user_page").Key(v).SetValue("true") 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.proxyConfig.Password = app.config.Section("advanced").Key("proxy_password").MustString("")
app.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig) app.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig)
if err != nil { 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") app.MustSetValue("updates", "enabled", "true")

View File

@ -20,6 +20,7 @@ import (
"github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/html"
"github.com/hrfee/jfa-go/easyproxy" "github.com/hrfee/jfa-go/easyproxy"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/itchyny/timefmt-go" "github.com/itchyny/timefmt-go"
"github.com/mailgun/mailgun-go/v4" "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)) 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) 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 { if err != nil {
app.err.Printf("Error while initiating SMTP mailer: %v", err) app.err.Printf(lm.FailedInitSMTP, err)
} }
} else if method == "mailgun" { } else if method == "mailgun" {
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String()) 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. // Only used in html email.
template["pin_code"] = pwr.Pin template["pin_code"] = pwr.Pin
} else { } else {
app.info.Println("Couldn't generate PWR link: %v", err) app.info.Println(lm.FailedGeneratePWRLink, err)
template["pin"] = pwr.Pin template["pin"] = pwr.Pin
} }
} else { } else {

View File

@ -1,6 +1,10 @@
package main 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 // 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) Name(name string) { d.name = name }
func (d *GenericDaemon) run() { func (d *GenericDaemon) run() {
d.app.info.Printf("%s started", d.name) d.app.info.Printf(lm.StartDaemon, d.name)
for { for {
select { select {
case <-d.ShutdownChannel: case <-d.ShutdownChannel:

View File

@ -4,6 +4,7 @@ import (
"time" "time"
"github.com/dgraph-io/badger/v3" "github.com/dgraph-io/badger/v3"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
) )
@ -12,7 +13,7 @@ import (
// meant to be called with other such housekeeping functions, so assumes // meant to be called with other such housekeeping functions, so assumes
// the user cache is fresh. // the user cache is fresh.
func (app *appContext) clearEmails() { func (app *appContext) clearEmails() {
app.debug.Println("Housekeeping: removing unused email addresses") app.debug.Println(lm.HousekeepingEmail)
emails := app.storage.GetEmails() emails := app.storage.GetEmails()
for _, email := range emails { for _, email := range emails {
_, _, err := app.jf.UserByID(email.JellyfinID, false) _, _, 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. // clearDiscord does the same as clearEmails, but for Discord Users.
func (app *appContext) clearDiscord() { func (app *appContext) clearDiscord() {
app.debug.Println("Housekeeping: removing unused Discord IDs") app.debug.Println(lm.HousekeepingDiscord)
discordUsers := app.storage.GetDiscord() discordUsers := app.storage.GetDiscord()
for _, discordUser := range discordUsers { for _, discordUser := range discordUsers {
_, _, err := app.jf.UserByID(discordUser.JellyfinID, false) _, _, 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. // clearMatrix does the same as clearEmails, but for Matrix Users.
func (app *appContext) clearMatrix() { func (app *appContext) clearMatrix() {
app.debug.Println("Housekeeping: removing unused Matrix IDs") app.debug.Println(lm.HousekeepingMatrix)
matrixUsers := app.storage.GetMatrix() matrixUsers := app.storage.GetMatrix()
for _, matrixUser := range matrixUsers { for _, matrixUser := range matrixUsers {
_, _, err := app.jf.UserByID(matrixUser.JellyfinID, false) _, _, 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. // clearTelegram does the same as clearEmails, but for Telegram Users.
func (app *appContext) clearTelegram() { func (app *appContext) clearTelegram() {
app.debug.Println("Housekeeping: removing unused Telegram IDs") app.debug.Println(lm.HousekeepingTelegram)
telegramUsers := app.storage.GetTelegram() telegramUsers := app.storage.GetTelegram()
for _, telegramUser := range telegramUsers { for _, telegramUser := range telegramUsers {
_, _, err := app.jf.UserByID(telegramUser.JellyfinID, false) _, _, err := app.jf.UserByID(telegramUser.JellyfinID, false)
@ -75,7 +76,7 @@ func (app *appContext) clearTelegram() {
} }
func (app *appContext) clearPWRCaptchas() { func (app *appContext) clearPWRCaptchas() {
app.debug.Println("Housekeeping: Clearing old PWR Captchas") app.debug.Println(lm.HousekeepingCaptcha)
captchas := map[string]Captcha{} captchas := map[string]Captcha{}
for k, capt := range app.pwrCaptchas { for k, capt := range app.pwrCaptchas {
if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) { if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) {
@ -86,7 +87,7 @@ func (app *appContext) clearPWRCaptchas() {
} }
func (app *appContext) clearActivities() { 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) keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
maxAgeDays := app.config.Section("activity_log").Key("delete_after_days").MustInt(90) maxAgeDays := app.config.Section("activity_log").Key("delete_after_days").MustInt(90)
minAge := time.Now().AddDate(0, 0, -maxAgeDays) minAge := time.Now().AddDate(0, 0, -maxAgeDays)
@ -103,7 +104,7 @@ func (app *appContext) clearActivities() {
} }
} }
if err == badger.ErrTxnTooBig { if err == badger.ErrTxnTooBig {
app.debug.Printf("Activities: Delete txn was too big, doing it manually.") app.debug.Printf(lm.AcitivityLogTxnTooBig)
list := []Activity{} list := []Activity{}
if errorSource == 0 { if errorSource == 0 {
app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge)) 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 { func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
d := NewGenericDaemon(interval, app, d := NewGenericDaemon(interval, app,
func(app *appContext) { func(app *appContext) {
app.debug.Println("Housekeeping: Checking for expired invites") app.debug.Println(lm.HousekeepingInvites)
app.checkInvites() app.checkInvites()
}, },
func(app *appContext) { app.clearActivities() }, func(app *appContext) { app.clearActivities() },

View File

@ -5,20 +5,21 @@ import (
"time" "time"
"github.com/hrfee/jfa-go/jellyseerr" "github.com/hrfee/jfa-go/jellyseerr"
lm "github.com/hrfee/jfa-go/logmessages"
) )
func (app *appContext) SynchronizeJellyseerrUser(jfID string) { func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
user, imported, err := app.js.GetOrImportUser(jfID) user, imported, err := app.js.GetOrImportUser(jfID)
if err != nil { 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 return
} }
if imported { 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) notif, err := app.js.GetNotificationPreferencesByID(user.ID)
if err != nil { 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 return
} }
@ -27,7 +28,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
if ok && email.Addr != "" && user.Email != email.Addr { if ok && email.Addr != "" && user.Email != email.Addr {
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr}) err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
if err != nil { 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 { } else {
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
} }
@ -51,7 +52,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
if len(contactMethods) != 0 { if len(contactMethods) != 0 {
err := app.js.ModifyNotifications(jfID, contactMethods) err := app.js.ModifyNotifications(jfID, contactMethods)
if err != nil { 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() { func (app *appContext) SynchronizeJellyseerrUsers() {
users, status, err := app.jf.GetUsers(false) users, status, err := app.jf.GetUsers(false)
if err != nil || status != 200 { 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 return
} }
// I'm sure Jellyseerr can handle it, // I'm sure Jellyseerr can handle it,

View File

@ -1,5 +1,12 @@
package logmessages 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 ( const (
Jellyseerr = "Jellyseerr" Jellyseerr = "Jellyseerr"
Jellyfin = "Jellyfin" Jellyfin = "Jellyfin"
@ -12,16 +19,20 @@ const (
// main.go // main.go
FailedLogging = "Failed to start log wrapper: %v\n" FailedLogging = "Failed to start log wrapper: %v\n"
NoConfig = "Couldn't find default config file" NoConfig = "Couldn't find default config file"
Write = "Wrote to \"%s\"" Write = "Wrote to \"%s\""
FailedWriting = "Failed to write to \"%s\": %v" FailedWriting = "Failed to write to \"%s\": %v"
FailedReading = "Failed to read from \"%s\": %v" FailedCreateDir = "Failed to create directory \"%s\": %v"
FailedOpen = "Failed to open \"%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\"" CopyConfig = "Copied default configuration to \"%s\""
FailedCopyConfig = "Failed to copy default configuration to \"%s\": %v" FailedCopyConfig = "Failed to copy default configuration to \"%s\": %v"
LoadConfig = "Loaded config file \"%s\"" LoadConfig = "Loaded config file \"%s\""
FailedLoadConfig = "Failed to load config file \"%s\": %v" FailedLoadConfig = "Failed to load config file \"%s\": %v"
ModifyConfig = "Config saved to \"%s\""
SocketPath = "Socket Path: \"%s\"" SocketPath = "Socket Path: \"%s\""
FailedSocketConnect = "Couldn't establish socket connection at \"%s\": %v" FailedSocketConnect = "Couldn't establish socket connection at \"%s\": %v"
@ -65,10 +76,12 @@ const (
Serving = "Loaded @ \"%s\"" Serving = "Loaded @ \"%s\""
QuitReceived = "Restart/Quit signal received, please be patient." QuitReceived = "Restart/Quit signal received, please be patient."
Quitting = "Shutting down..." Quitting = "Shutting down..."
Quit = "Server shut down." Restarting = "Restarting..."
FailedQuit = "Server shutdown failed: %v" FailedHardRestartWindows = "hard restarts not available on windows"
Quit = "Server shut down."
FailedQuit = "Server shutdown failed: %v"
// api-activities.go // api-activities.go
FailedDBReadActivities = "Failed to read activities from DB: %v" FailedDBReadActivities = "Failed to read activities from DB: %v"
@ -79,11 +92,12 @@ const (
FailedGetUpload = "Failed to retrieve file from form data: %v" FailedGetUpload = "Failed to retrieve file from form data: %v"
// api-invites.go // api-invites.go
DeleteOldInvite = "Deleting old invite \"%s\"" DeleteOldInvite = "Deleting old invite \"%s\""
DeleteInvite = "Deleting invite \"%s\"" DeleteInvite = "Deleting invite \"%s\""
FailedDeleteInvite = "Failed to delete invite \"%s\": %v" FailedDeleteInvite = "Failed to delete invite \"%s\": %v"
GenerateInvite = "Generating new invite" GenerateInvite = "Generating new invite"
InvalidInviteCode = "Invalid invite code \"%s\"" FailedGenerateInvite = "Failed to generate new invite: %v"
InvalidInviteCode = "Invalid invite code \"%s\""
FailedSendToTooltipNoUser = "Failed: \"%s\" not found" FailedSendToTooltipNoUser = "Failed: \"%s\" not found"
FailedSendToTooltipMultiUser = "Failed: \"%s\" linked to multiple users" FailedSendToTooltipMultiUser = "Failed: \"%s\" linked to multiple users"
@ -94,22 +108,25 @@ const (
SetAdminNotify = "Set \"%s\" to %t for admin address \"%s\"" SetAdminNotify = "Set \"%s\" to %t for admin address \"%s\""
// api-jellyseerr.go // *jellyseerr*.go
FailedGetUsers = "Failed to get user(s) from %s: %v" 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. // 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" 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" 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 // api-messages.go
FailedGetCustomMessage = "Failed to get custom message \"%s\"" FailedGetCustomMessage = "Failed to get custom message \"%s\""
SetContactPrefForService = "Set contact preference for %s (\"%s\"): %t" SetContactPrefForService = "Set contact preference for %s (\"%s\"): %t"
// Matrix // Matrix
InvalidPIN = "Invalid PIN \"%s\"" InvalidPIN = "Invalid PIN \"%s\""
UnauthorizedPIN = "Unauthorized PIN \"%s\"" ExpiredPIN = "Expired PIN \"%s\""
FailedCreateRoom = "Failed to create room: %v" InvalidPassword = "Invalid Password"
FailedGenerateToken = "Failed to generate token: %v" UnauthorizedPIN = "Unauthorized PIN \"%s\""
FailedCreateRoom = "Failed to create room: %v"
// api-profiles.go // api-profiles.go
SetDefaultProfile = "Setting default profile to \"%s\"" SetDefaultProfile = "Setting default profile to \"%s\""
@ -123,6 +140,7 @@ const (
FailedGetJellyfinDisplayPrefs = "Failed to get DisplayPreferences for user \"%s\" from " + Jellyfin + ": %v" FailedGetJellyfinDisplayPrefs = "Failed to get DisplayPreferences for user \"%s\" from " + Jellyfin + ": %v"
ProfileNoHomescreen = "No homescreen template in profile \"%s\"" ProfileNoHomescreen = "No homescreen template in profile \"%s\""
Profile = "profile" Profile = "profile"
Lang = "language"
User = "user" User = "user"
ApplyingTemplatesFrom = "Applying templates from %s: \"%s\" to %d users" ApplyingTemplatesFrom = "Applying templates from %s: \"%s\" to %d users"
DelayingRequests = "Delay will be added between requests (count = %d)" 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" 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" IncorrectCaptcha = "captcha incorrect"
@ -162,14 +180,148 @@ const (
UserEmailAdjusted = "Email for user \"%s\" adjusted" UserEmailAdjusted = "Email for user \"%s\" adjusted"
UserAdminAdjusted = "Admin state for user \"%s\" set to %t" UserAdminAdjusted = "Admin state for user \"%s\" set to %t"
UserLabelAdjusted = "Label for user \"%s\" set to \"%s\"" 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 ( const (
FailedGetCookies = "Failed to get cookie(s) \"%s\": %v" FailedGetCookies = "Failed to get cookie(s) \"%s\": %v"
FailedParseJWT = "Failed to parse JWT: %v" FailedParseJWT = "Failed to parse JWT: %v"
FailedCastJWT = "JWT claims unreadable" FailedCastJWT = "JWT claims unreadable"
InvalidJWT = "JWT was invalidated, of incorrect type or has expired" InvalidJWT = "JWT was invalidated, of incorrect type or has expired"
FailedSignJWT = "Failed to sign JWT: %v" 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 ( const (
@ -209,6 +361,10 @@ const (
FailedSendExpiryAdjustmentMessage = "Failed to send expiry adjustment message for \"%s\" to \"%s\": %v" FailedSendExpiryAdjustmentMessage = "Failed to send expiry adjustment message for \"%s\" to \"%s\": %v"
SentExpiryAdjustmentMessage = "Sent expiry adjustment message for \"%s\" to \"%s\"" 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" FailedConstructAnnouncementMessage = "Failed to construct announcement message for \"%s\": %v"
FailedSendAnnouncementMessage = "Failed to send announcement message for \"%s\" to \"%s\": %v" FailedSendAnnouncementMessage = "Failed to send announcement message for \"%s\" to \"%s\": %v"
SentAnnouncementMessage = "Sent announcement message for \"%s\" to \"%s\"" SentAnnouncementMessage = "Sent announcement message for \"%s\" to \"%s\""

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
@ -118,13 +119,13 @@ func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string
func (d *MatrixDaemon) run() { func (d *MatrixDaemon) run() {
startTime := d.start 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) syncer := d.bot.Syncer.(*mautrix.DefaultSyncer)
HandleSyncerCrypto(startTime, d, syncer) HandleSyncerCrypto(startTime, d, syncer)
syncer.OnEventType(event.EventMessage, d.handleMessage) syncer.OnEventType(event.EventMessage, d.handleMessage)
if err := d.bot.Sync(); err != nil { 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, list,
) )
if err != nil { 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 return
} }
@ -203,7 +204,7 @@ func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, encrypted bo
func (d *MatrixDaemon) SendStart(userID string) (ok bool) { func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
roomID, encrypted, err := d.CreateRoom(userID) roomID, encrypted, err := d.CreateRoom(userID)
if err != nil { 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 return
} }
lang := "en-us" lang := "en-us"
@ -226,7 +227,7 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
roomID, roomID,
) )
if err != nil { 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 return
} }
ok = true ok = true

View File

@ -1,10 +1,13 @@
//go:build e2ee
// +build e2ee // +build e2ee
package main package main
import ( import (
"fmt"
"strings" "strings"
lm "github.com/hrfee/jfa-go/logmessages"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
@ -65,22 +68,22 @@ type olmLogger struct {
} }
func (o olmLogger) Error(message string, args ...interface{}) { 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{}) { 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{}) { 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{}) { func (o olmLogger) Trace(message string, args ...interface{}) {
if strings.HasPrefix(message, "Got membership state event") { if strings.HasPrefix(message, "Got membership state event") {
return 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) { func InitMatrixCrypto(d *MatrixDaemon) (err error) {
@ -155,7 +158,7 @@ func HandleSyncerCrypto(startTime int64, d *MatrixDaemon, syncer *mautrix.Defaul
// return // return
// } // }
if err != nil { if err != nil {
d.app.err.Printf("Failed to decrypt Matrix message: %v", err) d.app.err.Printf(lm.FailedDecryptMatrixMessage, err)
return return
} }
d.handleMessage(source, decrypted) d.handleMessage(source, decrypted)
@ -180,7 +183,7 @@ func EncryptRoom(d *MatrixDaemon, room *mautrix.RespCreateRoom, userID id.UserID
if err == nil { if err == nil {
encrypted = true encrypted = true
} else { } else {
d.app.debug.Printf("Matrix: Failed to enable encryption in room: %v", err) d.app.debug.Printf(lm.FailedEnableMatrixEncryption, room.RoomID, err)
return return
} }
d.isEncrypted[room.RoomID] = encrypted d.isEncrypted[room.RoomID] = encrypted

View File

@ -9,6 +9,8 @@ import (
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
) )
// NOTE: This is the one file where log messages are not part of logmessages/logmessages.go
func runMigrations(app *appContext) { func runMigrations(app *appContext) {
migrateProfiles(app) migrateProfiles(app)
migrateBootstrap(app) migrateBootstrap(app)

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/fsnotify/fsnotify" "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. // 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() { 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() path := app.config.Section("password_resets").Key("watch_directory").String()
if _, err := os.Stat(path); os.IsNotExist(err) { 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 return
} }
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
app.err.Printf("Couldn't initialise password reset daemon") app.err.Printf(lm.FailedStartDaemon, "PWR", err)
return return
} }
defer watcher.Close() defer watcher.Close()
@ -56,7 +57,7 @@ func (app *appContext) StartPWR() {
go pwrMonitor(app, watcher) go pwrMonitor(app, watcher)
err = watcher.Add(path) err = watcher.Add(path)
if err != nil { if err != nil {
app.err.Printf("Failed to start password reset daemon: %s", err) app.err.Printf(lm.FailedStartDaemon, "PWR", err)
} }
waitForRestart() waitForRestart()
@ -84,43 +85,36 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
var pwr PasswordReset var pwr PasswordReset
data, err := os.ReadFile(event.Name) data, err := os.ReadFile(event.Name)
if err != nil { if err != nil {
app.debug.Printf("PWR: Failed to read file: %v", err) app.debug.Printf(lm.FailedReading, event.Name, err)
return return
} }
err = json.Unmarshal(data, &pwr) err = json.Unmarshal(data, &pwr)
if len(pwr.Pin) == 0 || err != nil { 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 continue
} }
app.info.Printf("New password reset for user \"%s\"", pwr.Username) app.info.Printf("New password reset for user \"%s\"", pwr.Username)
if currentTime := time.Now(); pwr.Expiry.After(currentTime) { if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
user, status, err := app.jf.UserByName(pwr.Username, false) user, status, err := app.jf.UserByName(pwr.Username, false)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil || user.ID == "" {
app.err.Printf("Failed to get users from Jellyfin: Code %d", status) app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
app.debug.Printf("Error: %s", err)
return return
} }
uid := user.ID uid := user.ID
if uid == "" {
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)
return
}
name := app.getAddressOrName(uid) name := app.getAddressOrName(uid)
if name != "" { if name != "" {
msg, err := app.email.constructReset(pwr, app, false) msg, err := app.email.constructReset(pwr, app, false)
if err != nil { if err != nil {
app.err.Printf("Failed to construct password reset message for \"%s\"", pwr.Username) app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else if err := app.sendByID(msg, uid); err != nil { } else if err := app.sendByID(msg, uid); err != nil {
app.err.Printf("Failed to send password reset message to \"%s\"", name) app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, name, err)
app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else { } else {
app.info.Printf("Sent password reset message to \"%s\"", name) app.err.Printf(lm.SentPWRMessage, pwr.Username, name)
} }
} }
} else { } 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 { if !ok {
return return
} }
app.err.Printf("Password reset daemon: %s", err) app.err.Printf(lm.FailedStartDaemon, "PWR", err)
} }
} }
} }

View File

@ -1,7 +1,11 @@
package main package main
import "fmt" import (
"fmt"
lm "github.com/hrfee/jfa-go/logmessages"
)
func (app *appContext) HardRestart() error { func (app *appContext) HardRestart() error {
return fmt.Errorf("hard restarts not available on windows") return fmt.Errorf(lm.FailedHardRestartWindows)
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/gin-contrib/pprof" "github.com/gin-contrib/pprof"
"github.com/gin-contrib/static" "github.com/gin-contrib/static"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
) )
@ -21,17 +22,17 @@ func (app *appContext) loadHTML(router *gin.Engine) {
templatePath := "html" templatePath := "html"
htmlFiles, err := fs.ReadDir(localFS, templatePath) htmlFiles, err := fs.ReadDir(localFS, templatePath)
if err != nil { if err != nil {
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) app.err.Fatalf(lm.FailedReading, templatePath, err)
return return
} }
loadInternal := []string{} loadInternal := []string{}
loadExternal := []string{} loadExternal := []string{}
for _, f := range htmlFiles { for _, f := range htmlFiles {
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) { 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())) loadInternal = append(loadInternal, FSJoin(templatePath, f.Name()))
} else { } 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()))) 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 { if len(loadInternal) != 0 {
tmpl, err = template.ParseFS(localFS, loadInternal...) tmpl, err = template.ParseFS(localFS, loadInternal...)
if err != nil { if err != nil {
app.err.Fatalf("Failed to load templates: %v", err) app.err.Fatalf(lm.FailedLoadTemplates, lm.Internal, err)
} }
} }
if len(loadExternal) != 0 { if len(loadExternal) != 0 {
tmpl, err = tmpl.ParseFiles(loadExternal...) tmpl, err = tmpl.ParseFiles(loadExternal...)
if err != nil { if err != nil {
app.err.Fatalf("Failed to load external templates: %v", err) app.err.Fatalf(lm.FailedLoadTemplates, lm.External, err)
} }
} }
router.SetHTMLTemplate(tmpl) router.SetHTMLTemplate(tmpl)
@ -96,7 +97,7 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
router.Use(static.Serve("/", app.webFS)) router.Use(static.Serve("/", app.webFS))
router.NoRoute(app.NoRouteHandler) router.NoRoute(app.NoRouteHandler)
if *PPROF { if *PPROF {
app.debug.Println("Loading pprof") app.debug.Println(lm.RegisterPprof)
pprof.Register(router) pprof.Register(router)
} }
SRV = &http.Server{ SRV = &http.Server{
@ -165,7 +166,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
} }
} }
if *SWAGGER { 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 { for _, p := range routePrefixes {
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/easyproxy" "github.com/hrfee/jfa-go/easyproxy"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
) )
@ -104,7 +105,7 @@ func (app *appContext) TestJF(gc *gin.Context) {
case 404: case 404:
msg = "error404" msg = "error404"
} }
app.info.Printf("Auth failed with code %d (%s)", status, err) app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
if msg != "" { if msg != "" {
respond(status, msg, gc) respond(status, msg, gc)
} else { } else {

View File

@ -13,6 +13,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/jellyseerr" "github.com/hrfee/jfa-go/jellyseerr"
"github.com/hrfee/jfa-go/logger" "github.com/hrfee/jfa-go/logger"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
@ -175,10 +176,10 @@ func (app *appContext) ConnectDB() {
opts.ValueDir = app.storage.db_path opts.ValueDir = app.storage.db_path
db, err := badgerhold.Open(opts) db, err := badgerhold.Open(opts)
if err != nil { 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.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. // GetEmails returns a copy of the store.

View File

@ -7,6 +7,7 @@ import (
"time" "time"
tg "github.com/go-telegram-bot-api/telegram-bot-api" tg "github.com/go-telegram-bot-api/telegram-bot-api"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
) )
@ -96,12 +97,12 @@ func (t *TelegramDaemon) NewAssignedAuthToken(id string) string {
} }
func (t *TelegramDaemon) run() { 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 := tg.NewUpdate(0)
u.Timeout = 60 u.Timeout = 60
updates, err := t.bot.GetUpdatesChan(u) updates, err := t.bot.GetUpdatesChan(u)
if err != nil { 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 telegramEnabled = false
return 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"}) content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
err := t.Reply(upd, content) err := t.Reply(upd, content)
if err != nil { 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) err := t.Reply(upd, list)
if err != nil { 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 return
} }
@ -232,14 +233,14 @@ func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string)
if !ok || time.Now().After(token.Expiry) { if !ok || time.Now().After(token.Expiry) {
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN")) err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
if err != nil { 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) delete(t.tokens, upd.Message.Text)
return return
} }
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess")) err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
if err != nil { 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{ t.verifiedTokens[upd.Message.Text] = TelegramVerifiedToken{
ChatID: upd.Message.Chat.ID, ChatID: upd.Message.Chat.ID,

View File

@ -16,6 +16,8 @@ import (
"strings" "strings"
"time" "time"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/jfa-go/common" "github.com/hrfee/jfa-go/common"
) )
@ -560,15 +562,16 @@ func (app *appContext) checkForUpdates() {
if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") { if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
app.err.Println("No new updates available.") app.err.Println("No new updates available.")
} else if status != -1 { // -1 means updates disabled, we don't need to log it. } 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 return
} }
if tag != app.tag && tag.IsNew() { 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) update, status, err := app.updater.GetUpdate(tag)
if status != 200 || err != nil { if status != 200 || err != nil {
app.err.Printf("Failed to get update (%d): %v", status, err) app.err.Printf(lm.FailedGetUpdate, err)
return return
} }
app.tag = tag app.tag = tag

View File

@ -1,9 +1,11 @@
package main package main
import ( import (
"fmt"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
) )
func (app *appContext) userAuth() gin.HandlerFunc { func (app *appContext) userAuth() gin.HandlerFunc {
@ -13,7 +15,7 @@ func (app *appContext) userAuth() gin.HandlerFunc {
func (app *appContext) userAuthenticate(gc *gin.Context) { func (app *appContext) userAuthenticate(gc *gin.Context) {
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true) jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true)
if !jellyfinLogin { 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) respond(500, "Contact Admin", gc)
return return
} }
@ -27,7 +29,6 @@ func (app *appContext) userAuthenticate(gc *gin.Context) {
gc.Set("jfId", jfID) gc.Set("jfId", jfID)
gc.Set("userMode", true) gc.Set("userMode", true)
app.debug.Println("Auth succeeded")
gc.Next() gc.Next()
} }
@ -41,11 +42,11 @@ func (app *appContext) userAuthenticate(gc *gin.Context) {
// @Security getUserTokenAuth // @Security getUserTokenAuth
func (app *appContext) getUserTokenLogin(gc *gin.Context) { func (app *appContext) getUserTokenLogin(gc *gin.Context) {
if !app.config.Section("ui").Key("jellyfin_login").MustBool(true) { 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) respond(500, "Contact Admin", gc)
return 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) username, password, ok := app.decodeValidateLoginHeader(gc, true)
if !ok { if !ok {
return return
@ -58,12 +59,11 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
token, refresh, err := CreateToken(user.ID, user.ID, false) token, refresh, err := CreateToken(user.ID, user.ID, false)
if err != nil { 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) respond(500, "Couldn't generate user token", gc)
return return
} }
app.debug.Printf("Token generated for non-admin user \"%s\"", username)
uri := "/my" uri := "/my"
if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) { if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) {
uri = "/accounts/my" uri = "/accounts/my"
@ -81,12 +81,12 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
func (app *appContext) getUserTokenRefresh(gc *gin.Context) { func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true) jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true)
if !jellyfinLogin { 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) respond(500, "Contact Admin", gc)
return 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") claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh")
if !ok { if !ok {
return return
@ -96,7 +96,7 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
jwt, refresh, err := CreateToken(jfID, jfID, false) jwt, refresh, err := CreateToken(jfID, jfID, false)
if err != nil { 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) respond(500, "Couldn't generate user token", gc)
return return
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"time" "time"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3" "github.com/lithammer/shortuuid/v3"
) )
@ -21,17 +22,17 @@ func (app *appContext) checkUsers() {
if len(app.storage.GetUserExpiries()) == 0 { if len(app.storage.GetUserExpiries()) == 0 {
return return
} }
app.info.Println("Daemon: Checking for user expiry") app.info.Println(lm.CheckUserExpiries)
users, status, err := app.jf.GetUsers(false) users, status, err := app.jf.GetUsers(false)
if err != nil || status != 200 { 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 return
} }
mode := "disable" mode := "disable"
term := "Disabling" phrase := lm.DisableExpiredUser
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" { if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
mode = "delete" mode = "delete"
term = "Deleting" phrase = lm.DeleteExpiredUser
} }
contact := false contact := false
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) { 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() { for _, expiry := range app.storage.GetUserExpiries() {
id := expiry.JellyfinID id := expiry.JellyfinID
if _, ok := userExists[id]; !ok { 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) app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
} else if time.Now().After(expiry.Expiry) { } else if time.Now().After(expiry.Expiry) {
found := false found := false
@ -58,11 +59,10 @@ func (app *appContext) checkUsers() {
} }
} }
if !found { if !found {
app.info.Printf("Expired user already deleted, ignoring.")
app.storage.DeleteUserExpiryKey(expiry.JellyfinID) app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
continue continue
} }
app.info.Printf("%s expired user \"%s\"", term, user.Name) app.info.Printf(phrase, user.Name)
// Record activity // Record activity
activity := Activity{ activity := Activity{
@ -83,7 +83,7 @@ func (app *appContext) checkUsers() {
activity.Type = ActivityDisabled activity.Type = ActivityDisabled
} }
if !(status == 200 || status == 204) || err != nil { 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 continue
} }
@ -98,11 +98,11 @@ func (app *appContext) checkUsers() {
name := app.getAddressOrName(user.ID) name := app.getAddressOrName(user.ID)
msg, err := app.email.constructUserExpired(app, false) msg, err := app.email.constructUserExpired(app, false)
if err != nil { 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 { } 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 { } else {
app.info.Printf("Sent expiry notification to \"%s\"", name) app.err.Printf(lm.SentExpiryMessage, user.ID, name)
} }
} }
} }

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"html/template" "html/template"
"io" "io"
"io/fs" "io/fs"
@ -16,6 +17,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3" "github.com/lithammer/shortuuid/v3"
"github.com/steambap/captcha" "github.com/steambap/captcha"
@ -66,10 +68,9 @@ func (app *appContext) pushResources(gc *gin.Context, page Page) {
toPush = []string{} toPush = []string{}
} }
if pusher := gc.Writer.Pusher(); pusher != nil { if pusher := gc.Writer.Pusher(); pusher != nil {
app.debug.Println("Using HTTP2 Server push")
for _, f := range toPush { for _, f := range toPush {
if err := pusher.Push(app.URLBase+f, nil); err != nil { 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 var license string
l, err := fs.ReadFile(localFS, "LICENSE") l, err := fs.ReadFile(localFS, "LICENSE")
if err != nil { if err != nil {
app.debug.Printf("Failed to load LICENSE: %s", err) app.debug.Printf(lm.FailedReading, "LICENSE", err)
license = "" license = ""
} }
license = string(l) license = string(l)
fontLicense, err := fs.ReadFile(localFS, filepath.Join("web", "fonts", "OFL.txt")) fontLicense, err := fs.ReadFile(localFS, filepath.Join("web", "fonts", "OFL.txt"))
if err != nil { 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" 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) 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 it's a bot, pretend to be a success so the preview is nice.
if isBot { if isBot {
app.debug.Println("PWR: Ignoring magic link visit from bot") app.debug.Println(lm.IgnoreBotPWR)
data["success"] = true data["success"] = true
data["pin"] = "NO-BO-TS" data["pin"] = "NO-BO-TS"
return return
@ -338,13 +339,13 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
if !isInternal && !setPassword { if !isInternal && !setPassword {
resp, status, err = app.jf.ResetPassword(pin) resp, status, err = app.jf.ResetPassword(pin)
} else if time.Now().After(pwr.Expiry) { } 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) app.NoRouteHandler(gc)
return return
} else { } else {
status, err = app.jf.ResetPasswordAdmin(pwr.ID) status, err = app.jf.ResetPasswordAdmin(pwr.ID)
if !(status == 200 || status == 204) || err != nil { 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 { } else {
status, err = app.jf.SetPassword(pwr.ID, "", pin) status, err = app.jf.SetPassword(pwr.ID, "", pin)
} }
@ -358,7 +359,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
username = resp.UsersReset[0] username = resp.UsersReset[0]
} }
} else { } 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. // 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) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
jfUser, status, err := app.jf.UserByName(username, false) jfUser, status, err := app.jf.UserByName(username, false)
if status != 200 || err != nil { 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 return
} }
ombiUser, status, err := app.getOmbiUser(jfUser.ID) ombiUser, status, err := app.getOmbiUser(jfUser.ID)
if status != 200 || err != nil { 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 return
} }
ombiUser["password"] = pin ombiUser["password"] = pin
status, err = app.ombi.ModifyUser(ombiUser) status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil { 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 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) capt, err := captcha.New(300, 100)
if err != nil { if err != nil {
app.err.Printf("Failed to generate captcha: %v", err) app.err.Printf(lm.FailedGenerateCaptcha, err)
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} }
@ -470,7 +471,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
captchaID := genAuthToken() captchaID := genAuthToken()
var buf bytes.Buffer var buf bytes.Buffer
if err := capt.WriteImage(bufio.NewWriter(&buf)); err != nil { 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) respondBool(500, false, gc)
return return
} }
@ -503,8 +504,12 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
ok := true ok := true
if !isPWR { if !isPWR {
inv, ok := app.storage.GetInvitesKey(code) inv, ok := app.storage.GetInvitesKey(code)
if !ok || (!isPWR && inv.Captchas == nil) { if !ok {
app.debug.Printf("Couldn't find invite \"%s\"", code) app.debug.Printf(lm.InvalidInviteCode, code)
return false
}
if !isPWR && inv.Captchas == nil {
app.debug.Printf(lm.CaptchaNotFound, id, code)
return false return false
} }
c, ok = inv.Captchas[id] 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] c, ok = app.pwrCaptchas[code]
} }
if !ok { if !ok {
app.debug.Printf("Couldn't find Captcha \"%s\"", id) app.debug.Printf(lm.CaptchaNotFound, id, code)
return false return false
} }
return strings.ToLower(c.Answer) == strings.ToLower(text) 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") req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 { if err == nil && resp.StatusCode != 200 {
app.err.Printf("Failed to read reCAPTCHA status (%d): %+v\n", resp.Status, err) err = fmt.Errorf("failed (error %d)", resp.StatusCode)
}
if err != nil {
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
return false return false
} }
defer resp.Body.Close() 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) body, err := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &data) err = json.Unmarshal(body, &data)
if err != nil { if err != nil {
app.err.Printf("Failed to unmarshal reCAPTCHA response: %+v\n", err) app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
return false return false
} }
hostname := app.config.Section("captcha").Key("recaptcha_hostname").MustString("") hostname := app.config.Section("captcha").Key("recaptcha_hostname").MustString("")
if strings.ToLower(data.Hostname) != strings.ToLower(hostname) && data.Hostname != "" { 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 return false
} }
if len(data.ErrorCodes) > 0 { 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 return false
} }
@ -651,20 +660,19 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
token, err := jwt.Parse(key, checkToken) token, err := jwt.Parse(key, checkToken)
if err != nil { if err != nil {
fail() fail()
app.err.Printf("Failed to parse key: %s", err) app.debug.Printf(lm.FailedParseJWT, err)
return return
} }
claims, ok := token.Claims.(jwt.MapClaims) claims, ok := token.Claims.(jwt.MapClaims)
expiry := time.Unix(int64(claims["exp"].(float64)), 0) expiry := time.Unix(int64(claims["exp"].(float64)), 0)
if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) { if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
fail() fail()
app.debug.Printf("Invalid key") app.debug.Printf(lm.InvalidJWT)
return return
} }
f, success := app.newUser(req, true, gc) f, success := app.newUser(req, true, gc)
if !success { if !success {
app.err.Printf("Failed to create new user") // Not meant for us. Calling this is bad but at least gives us log output.
// Not meant for us. Calling this will be a mess, but at least it might give us some information.
f(gc) f(gc)
fail() fail()
return return