From 94cf9531fa9cd709de1e98aee1124ea78a65258d Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Mon, 1 Jun 2026 11:30:38 +0300 Subject: [PATCH] refactor(handlers): standardize logging using request attributes helper - Replace manual IP logging with the `withRequestLogAttrs` helper in authentication handlers. - Add user activity logging for API documentation and login page views. - Clean up log calls to use variadic expansion of request attributes. --- backend/libs/handlers/api_docs.go | 12 ++++++++++ backend/libs/handlers/auth.go | 37 ++++++++++++++++-------------- backend/libs/handlers/dashboard.go | 24 +++++++++++-------- backend/libs/handlers/download.go | 32 +++++++++++++------------- backend/libs/handlers/logging.go | 29 +++++++++++++++++++++++ backend/libs/handlers/pages.go | 11 +++++++++ backend/libs/handlers/upload.go | 24 +++++++++---------- backend/libs/httpserver/server.go | 1 - backend/static/css/50-admin.css | 4 ++++ 9 files changed, 119 insertions(+), 55 deletions(-) create mode 100644 backend/libs/handlers/logging.go diff --git a/backend/libs/handlers/api_docs.go b/backend/libs/handlers/api_docs.go index 30e7188..2fed685 100644 --- a/backend/libs/handlers/api_docs.go +++ b/backend/libs/handlers/api_docs.go @@ -21,6 +21,18 @@ type apiDocsData struct { } func (a *App) APIDocs(w http.ResponseWriter, r *http.Request) { + user, loggedIn := a.currentUser(r) + actor := "anonymous" + if loggedIn { + actor = "user" + } + a.logger.Info("api docs viewed", withRequestLogAttrs(r, + "source", "page", + "severity", "user_activity", + "code", 2501, + "actor", actor, + "user_id", user.ID, + )...) a.renderPage(w, r, http.StatusOK, "api.html", web.PageData{ Title: "API documentation", Description: "Curl and ShareX upload examples for Warpbox.", diff --git a/backend/libs/handlers/auth.go b/backend/libs/handlers/auth.go index e1023f1..70c2167 100644 --- a/backend/libs/handlers/auth.go +++ b/backend/libs/handlers/auth.go @@ -35,7 +35,7 @@ func (a *App) Register(w http.ResponseWriter, r *http.Request) { func (a *App) RegisterPost(w http.ResponseWriter, r *http.Request) { if !a.rateLimiter.Allow("register:"+uploadClientIP(r), 10, time.Minute, time.Now().UTC()) { - a.logger.Warn("registration rate limited", "source", "auth", "severity", "warn", "code", 4291, "ip", uploadClientIP(r)) + a.logger.Warn("registration rate limited", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4291)...) a.renderAuth(w, r, http.StatusTooManyRequests, authPageData{Mode: "register", Error: "Too many registration attempts."}) return } @@ -45,11 +45,11 @@ func (a *App) RegisterPost(w http.ResponseWriter, r *http.Request) { } user, err := a.authService.CreateBootstrapUser(r.FormValue("username"), r.FormValue("email"), r.FormValue("password")) if err != nil { - a.logger.Warn("bootstrap registration failed", "source", "auth", "severity", "warn", "code", 4400, "ip", uploadClientIP(r), "email", r.FormValue("email"), "error", err.Error()) + a.logger.Warn("bootstrap registration failed", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4400, "email", r.FormValue("email"), "error", err.Error())...) a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "register", Error: err.Error()}) return } - a.logger.Info("first admin created", "source", "auth", "severity", "user_activity", "code", 2401, "user_id", user.ID, "ip", uploadClientIP(r)) + a.logger.Info("first admin created", withRequestLogAttrs(r, "source", "auth", "severity", "user_activity", "code", 2401, "user_id", user.ID)...) a.loginAndRedirect(w, r, user.Email, r.FormValue("password"), "/app") } @@ -58,12 +58,13 @@ func (a *App) Login(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/app", http.StatusSeeOther) return } + a.logger.Info("login page viewed", withRequestLogAttrs(r, "source", "page", "severity", "user_activity", "code", 2503, "actor", "anonymous")...) a.renderAuth(w, r, http.StatusOK, authPageData{Mode: "login", ReturnPath: r.URL.Query().Get("next")}) } func (a *App) LoginPost(w http.ResponseWriter, r *http.Request) { if !a.rateLimiter.Allow("login:"+uploadClientIP(r), 10, time.Minute, time.Now().UTC()) { - a.logger.Warn("login rate limited", "source", "auth", "severity", "warn", "code", 4292, "ip", uploadClientIP(r), "email", r.FormValue("email")) + a.logger.Warn("login rate limited", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4292, "email", r.FormValue("email"))...) a.renderAuth(w, r, http.StatusTooManyRequests, authPageData{Mode: "login", Error: "Too many login attempts."}) return } @@ -77,13 +78,13 @@ func (a *App) LoginPost(w http.ResponseWriter, r *http.Request) { } user, token, err := a.authService.Login(r.FormValue("email"), r.FormValue("password")) if err != nil { - a.logger.Warn("login failed", "source", "auth", "severity", "warn", "code", 4401, "email", r.FormValue("email"), "ip", uploadClientIP(r)) + a.logger.Warn("login failed", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4401, "email", r.FormValue("email"))...) a.recordLoginAbuse(r, services.AbuseKindUserLogin, "user login failed") a.renderAuth(w, r, http.StatusUnauthorized, authPageData{Mode: "login", Error: "Invalid email or password.", ReturnPath: next}) return } a.setUserSessionCookie(w, r, token) - a.logger.Info("user login", "source", "auth", "severity", "user_activity", "code", 2402, "user_id", user.ID, "ip", uploadClientIP(r)) + a.logger.Info("user login", withRequestLogAttrs(r, "source", "auth", "severity", "user_activity", "code", 2402, "user_id", user.ID)...) http.Redirect(w, r, safeReturnPath(next), http.StatusSeeOther) } @@ -92,7 +93,7 @@ func (a *App) Logout(w http.ResponseWriter, r *http.Request) { return } if user, ok := a.currentUser(r); ok { - a.logger.Info("user logout", "source", "auth", "severity", "user_activity", "code", 2405, "user_id", user.ID, "ip", uploadClientIP(r)) + a.logger.Info("user logout", withRequestLogAttrs(r, "source", "auth", "severity", "user_activity", "code", 2405, "user_id", user.ID)...) } if cookie, err := r.Cookie(userSessionCookieName); err == nil { _ = a.authService.Logout(cookie.Value) @@ -107,6 +108,7 @@ func (a *App) Invite(w http.ResponseWriter, r *http.Request) { a.renderAuth(w, r, http.StatusNotFound, authPageData{Mode: "invite", Error: "This invite is invalid or expired."}) return } + a.logger.Info("invite page viewed", withRequestLogAttrs(r, "source", "page", "severity", "user_activity", "code", 2504, "invite_email", invite.Email, "reset", invite.UserID != "")...) a.renderAuth(w, r, http.StatusOK, authPageData{Mode: "invite", Token: r.PathValue("token"), Email: invite.Email, IsReset: invite.UserID != ""}) } @@ -114,7 +116,7 @@ func (a *App) InvitePost(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") invite, err := a.authService.InviteByToken(token) if err != nil { - a.logger.Warn("invite accept invalid", "source", "auth", "severity", "warn", "code", 4404, "ip", uploadClientIP(r)) + a.logger.Warn("invite accept invalid", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4404)...) a.renderAuth(w, r, http.StatusNotFound, authPageData{Mode: "invite", Error: "This invite is invalid or expired."}) return } @@ -124,11 +126,11 @@ func (a *App) InvitePost(w http.ResponseWriter, r *http.Request) { } user, err := a.authService.AcceptInvite(token, r.FormValue("username"), r.FormValue("password")) if err != nil { - a.logger.Warn("invite accept failed", "source", "auth", "severity", "warn", "code", 4405, "ip", uploadClientIP(r), "invite_email", invite.Email, "error", err.Error()) + a.logger.Warn("invite accept failed", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4405, "invite_email", invite.Email, "error", err.Error())...) a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "invite", Token: token, Email: invite.Email, IsReset: invite.UserID != "", Error: err.Error()}) return } - a.logger.Info("invite accepted", "source", "auth", "severity", "user_activity", "code", 2403, "user_id", user.ID, "ip", uploadClientIP(r), "invite_email", invite.Email) + a.logger.Info("invite accepted", withRequestLogAttrs(r, "source", "auth", "severity", "user_activity", "code", 2403, "user_id", user.ID, "invite_email", invite.Email)...) a.loginAndRedirect(w, r, user.Email, r.FormValue("password"), "/app") } @@ -153,6 +155,7 @@ func (a *App) AccountSettings(w http.ResponseWriter, r *http.Request) { if !ok { return } + a.logger.Info("account settings viewed", withRequestLogAttrs(r, "source", "page", "severity", "user_activity", "code", 2505, "user_id", user.ID)...) a.renderAccount(w, r, http.StatusOK, user, accountData{}) } @@ -170,11 +173,11 @@ func (a *App) CreateUserToken(w http.ResponseWriter, r *http.Request) { } result, err := a.authService.CreateAPIToken(user.ID, r.FormValue("name")) if err != nil { - a.logger.Warn("api token create failed", "source", "user_activity", "severity", "warn", "code", 4420, "user_id", user.ID, "error", err.Error()) + a.logger.Warn("api token create failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4420, "user_id", user.ID, "error", err.Error())...) a.renderAccount(w, r, http.StatusBadRequest, user, accountData{Error: "Could not create token."}) return } - a.logger.Info("api token created", "source", "user_activity", "severity", "user_activity", "code", 2420, "user_id", user.ID, "token_id", result.Token.ID) + a.logger.Info("api token created", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2420, "user_id", user.ID, "token_id", result.Token.ID)...) a.renderAccount(w, r, http.StatusOK, user, accountData{NewToken: result.Plaintext}) } @@ -184,9 +187,9 @@ func (a *App) DeleteUserToken(w http.ResponseWriter, r *http.Request) { return } if err := a.authService.DeleteAPIToken(user.ID, r.PathValue("tokenID")); err != nil { - a.logger.Warn("api token delete failed", "source", "user_activity", "severity", "warn", "code", 4421, "user_id", user.ID, "error", err.Error()) + a.logger.Warn("api token delete failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4421, "user_id", user.ID, "error", err.Error())...) } else { - a.logger.Info("api token deleted", "source", "user_activity", "severity", "user_activity", "code", 2421, "user_id", user.ID, "token_id", r.PathValue("tokenID")) + a.logger.Info("api token deleted", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2421, "user_id", user.ID, "token_id", r.PathValue("tokenID"))...) } http.Redirect(w, r, "/account/settings", http.StatusSeeOther) } @@ -233,16 +236,16 @@ func (a *App) ChangePassword(w http.ResponseWriter, r *http.Request) { return } if !services.VerifyPasswordHash(user.PasswordHash, r.FormValue("current_password")) { - a.logger.Warn("password change failed current password", "source", "user_activity", "severity", "warn", "code", 4422, "user_id", user.ID, "ip", uploadClientIP(r)) + a.logger.Warn("password change failed current password", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4422, "user_id", user.ID)...) http.Redirect(w, r, "/account/settings", http.StatusSeeOther) return } if err := a.authService.SetPassword(user.ID, r.FormValue("new_password")); err != nil { - a.logger.Warn("password change failed", "source", "user_activity", "severity", "warn", "code", 4423, "user_id", user.ID, "error", err.Error()) + a.logger.Warn("password change failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4423, "user_id", user.ID, "error", err.Error())...) http.Redirect(w, r, "/account/settings", http.StatusSeeOther) return } - a.logger.Info("password changed", "source", "user_activity", "severity", "user_activity", "code", 2422, "user_id", user.ID, "ip", uploadClientIP(r)) + a.logger.Info("password changed", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2422, "user_id", user.ID)...) http.Redirect(w, r, "/account/settings", http.StatusSeeOther) } diff --git a/backend/libs/handlers/dashboard.go b/backend/libs/handlers/dashboard.go index 7f1fad9..84a77bd 100644 --- a/backend/libs/handlers/dashboard.go +++ b/backend/libs/handlers/dashboard.go @@ -42,6 +42,12 @@ func (a *App) Dashboard(w http.ResponseWriter, r *http.Request) { if !ok { return } + a.logger.Info("user dashboard viewed", withRequestLogAttrs(r, + "source", "page", + "severity", "user_activity", + "code", 2502, + "user_id", user.ID, + )...) collections, err := a.authService.ListCollections(user.ID) if err != nil { http.Error(w, "unable to load collections", http.StatusInternalServerError) @@ -112,9 +118,9 @@ func (a *App) CreateCollection(w http.ResponseWriter, r *http.Request) { return } if _, err := a.authService.CreateCollection(user.ID, r.FormValue("name")); err != nil { - a.logger.Warn("collection create failed", "source", "user_activity", "severity", "warn", "code", 4410, "user_id", user.ID, "error", err.Error()) + a.logger.Warn("collection create failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4410, "user_id", user.ID, "error", err.Error())...) } else { - a.logger.Info("collection created", "source", "user_activity", "severity", "user_activity", "code", 2410, "user_id", user.ID, "name", r.FormValue("name")) + a.logger.Info("collection created", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2410, "user_id", user.ID, "name", r.FormValue("name"))...) } http.Redirect(w, r, "/app", http.StatusSeeOther) } @@ -129,11 +135,11 @@ func (a *App) RenameUserBox(w http.ResponseWriter, r *http.Request) { return } if err := a.uploadService.RenameOwnedBox(r.PathValue("boxID"), user.ID, r.FormValue("title")); err != nil { - a.logger.Warn("owned box rename failed", "source", "user_activity", "severity", "warn", "code", 4411, "user_id", user.ID, "box_id", r.PathValue("boxID"), "error", err.Error()) + a.logger.Warn("owned box rename failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4411, "user_id", user.ID, "box_id", r.PathValue("boxID"), "error", err.Error())...) a.handleUserBoxError(w, r, err) return } - a.logger.Info("owned box renamed", "source", "user_activity", "severity", "user_activity", "code", 2411, "user_id", user.ID, "box_id", r.PathValue("boxID")) + a.logger.Info("owned box renamed", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2411, "user_id", user.ID, "box_id", r.PathValue("boxID"))...) http.Redirect(w, r, "/app", http.StatusSeeOther) } @@ -148,16 +154,16 @@ func (a *App) MoveUserBox(w http.ResponseWriter, r *http.Request) { } collectionID := r.FormValue("collection_id") if !a.authService.CollectionOwnedBy(collectionID, user.ID) { - a.logger.Warn("owned box move invalid collection", "source", "user_activity", "severity", "warn", "code", 4412, "user_id", user.ID, "box_id", r.PathValue("boxID"), "collection_id", collectionID) + a.logger.Warn("owned box move invalid collection", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4412, "user_id", user.ID, "box_id", r.PathValue("boxID"), "collection_id", collectionID)...) http.Error(w, "collection not found", http.StatusForbidden) return } if err := a.uploadService.MoveOwnedBox(r.PathValue("boxID"), user.ID, collectionID); err != nil { - a.logger.Warn("owned box move failed", "source", "user_activity", "severity", "warn", "code", 4413, "user_id", user.ID, "box_id", r.PathValue("boxID"), "error", err.Error()) + a.logger.Warn("owned box move failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4413, "user_id", user.ID, "box_id", r.PathValue("boxID"), "error", err.Error())...) a.handleUserBoxError(w, r, err) return } - a.logger.Info("owned box moved", "source", "user_activity", "severity", "user_activity", "code", 2412, "user_id", user.ID, "box_id", r.PathValue("boxID"), "collection_id", collectionID) + a.logger.Info("owned box moved", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2412, "user_id", user.ID, "box_id", r.PathValue("boxID"), "collection_id", collectionID)...) http.Redirect(w, r, "/app", http.StatusSeeOther) } @@ -167,11 +173,11 @@ func (a *App) DeleteUserBox(w http.ResponseWriter, r *http.Request) { return } if err := a.uploadService.DeleteOwnedBox(r.PathValue("boxID"), user.ID); err != nil { - a.logger.Warn("owned box delete failed", "source", "user_activity", "severity", "warn", "code", 4414, "user_id", user.ID, "box_id", r.PathValue("boxID"), "error", err.Error()) + a.logger.Warn("owned box delete failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4414, "user_id", user.ID, "box_id", r.PathValue("boxID"), "error", err.Error())...) a.handleUserBoxError(w, r, err) return } - a.logger.Info("owned box deleted", "source", "user_activity", "severity", "user_activity", "code", 2413, "user_id", user.ID, "box_id", r.PathValue("boxID")) + a.logger.Info("owned box deleted", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2413, "user_id", user.ID, "box_id", r.PathValue("boxID"))...) http.Redirect(w, r, "/app", http.StatusSeeOther) } diff --git a/backend/libs/handlers/download.go b/backend/libs/handlers/download.go index 57991e5..da1a41e 100644 --- a/backend/libs/handlers/download.go +++ b/backend/libs/handlers/download.go @@ -53,12 +53,12 @@ type previewPageData struct { func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { - a.logger.Warn("download page missing box", "source", "download", "severity", "warn", "code", 4040, "box_id", r.PathValue("boxID"), "ip", uploadClientIP(r)) + a.logger.Warn("download page missing box", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4040, "box_id", r.PathValue("boxID"))...) http.NotFound(w, r) return } if err := a.uploadService.CanDownload(box); err != nil { - a.logger.Warn("download page unavailable", "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "ip", uploadClientIP(r), "error", err.Error()) + a.logger.Warn("download page unavailable", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "error", err.Error())...) a.renderPage(w, r, http.StatusForbidden, "download.html", web.PageData{ Title: "Download unavailable", Description: "This Warpbox link is no longer available.", @@ -101,7 +101,7 @@ func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) { ExpiresLabel: expiresLabel, }, }) - a.logger.Info("download page viewed", "source", "download", "severity", "user_activity", "code", 2003, "box_id", box.ID, "ip", uploadClientIP(r), "locked", locked) + a.logger.Info("download page viewed", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2003, "box_id", box.ID, "locked", locked)...) } func plural(n int) string { @@ -139,7 +139,7 @@ func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) { DownloadURL: view.DownloadURL, }, }) - a.logger.Info("file preview page viewed", "source", "download", "severity", "user_activity", "code", 2004, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r)) + a.logger.Info("file preview page viewed", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2004, "box_id", box.ID, "file_id", file.ID)...) } func (a *App) DownloadFileContent(w http.ResponseWriter, r *http.Request) { @@ -148,13 +148,13 @@ func (a *App) DownloadFileContent(w http.ResponseWriter, r *http.Request) { return } if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) { - a.logger.Warn("protected file download blocked", "source", "download", "severity", "warn", "code", 4013, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r)) + a.logger.Warn("protected file download blocked", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4013, "box_id", box.ID, "file_id", file.ID)...) http.Error(w, "password required", http.StatusUnauthorized) return } a.serveFileContent(w, r, box, file, r.URL.Query().Get("inline") != "1") - a.logger.Info("file content served", "source", "download", "severity", "user_activity", "code", 2005, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r), "attachment", r.URL.Query().Get("inline") != "1") + a.logger.Info("file content served", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2005, "box_id", box.ID, "file_id", file.ID, "attachment", r.URL.Query().Get("inline") != "1")...) } func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) { @@ -202,7 +202,7 @@ func (a *App) UnlockBox(w http.ResponseWriter, r *http.Request) { return } if !a.uploadService.VerifyPassword(box, r.FormValue("password")) { - a.logger.Warn("box unlock failed", "source", "user_activity", "severity", "warn", "code", 4011, "box_id", box.ID, "ip", uploadClientIP(r)) + a.logger.Warn("box unlock failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4011, "box_id", box.ID)...) http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther) return } @@ -215,26 +215,26 @@ func (a *App) UnlockBox(w http.ResponseWriter, r *http.Request) { Secure: r.TLS != nil, Expires: box.ExpiresAt, }) - a.logger.Info("box unlocked", "source", "user_activity", "severity", "user_activity", "code", 2002, "box_id", box.ID, "ip", uploadClientIP(r)) + a.logger.Info("box unlocked", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2002, "box_id", box.ID)...) http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther) } func (a *App) loadFileForRequest(w http.ResponseWriter, r *http.Request) (services.Box, services.File, bool) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { - a.logger.Warn("file request missing box", "source", "download", "severity", "warn", "code", 4041, "box_id", r.PathValue("boxID"), "file_id", r.PathValue("fileID"), "ip", uploadClientIP(r)) + a.logger.Warn("file request missing box", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4041, "box_id", r.PathValue("boxID"), "file_id", r.PathValue("fileID"))...) http.NotFound(w, r) return services.Box{}, services.File{}, false } if err := a.uploadService.CanDownload(box); err != nil { - a.logger.Warn("file request unavailable", "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "file_id", r.PathValue("fileID"), "ip", uploadClientIP(r), "error", err.Error()) + a.logger.Warn("file request unavailable", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "file_id", r.PathValue("fileID"), "error", err.Error())...) http.Error(w, err.Error(), statusForDownloadError(err)) return services.Box{}, services.File{}, false } file, err := a.uploadService.FindFile(box, r.PathValue("fileID")) if err != nil { - a.logger.Warn("file request missing file", "source", "download", "severity", "warn", "code", 4042, "box_id", box.ID, "file_id", r.PathValue("fileID"), "ip", uploadClientIP(r)) + a.logger.Warn("file request missing file", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4042, "box_id", box.ID, "file_id", r.PathValue("fileID"))...) http.NotFound(w, r) return services.Box{}, services.File{}, false } @@ -244,7 +244,7 @@ func (a *App) loadFileForRequest(w http.ResponseWriter, r *http.Request) (servic func (a *App) serveFileContent(w http.ResponseWriter, r *http.Request, box services.Box, file services.File, attachment bool) { object, err := a.uploadService.OpenFileObject(r.Context(), box, file) if err != nil { - a.logger.Warn("file object missing", "source", "download", "severity", "warn", "code", 4043, "box_id", box.ID, "file_id", file.ID, "ip", uploadClientIP(r), "error", err.Error()) + a.logger.Warn("file object missing", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4043, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...) http.NotFound(w, r) return } @@ -280,17 +280,17 @@ func readSeekCloser(source io.ReadCloser) io.ReadSeeker { func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) { box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { - a.logger.Warn("zip request missing box", "source", "download", "severity", "warn", "code", 4044, "box_id", r.PathValue("boxID"), "ip", uploadClientIP(r)) + a.logger.Warn("zip request missing box", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4044, "box_id", r.PathValue("boxID"))...) http.NotFound(w, r) return } if err := a.uploadService.CanDownload(box); err != nil { - a.logger.Warn("zip request unavailable", "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "ip", uploadClientIP(r), "error", err.Error()) + a.logger.Warn("zip request unavailable", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", statusForDownloadError(err), "box_id", box.ID, "error", err.Error())...) http.Error(w, err.Error(), statusForDownloadError(err)) return } if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) { - a.logger.Warn("protected zip download blocked", "source", "download", "severity", "warn", "code", 4014, "box_id", box.ID, "ip", uploadClientIP(r)) + a.logger.Warn("protected zip download blocked", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4014, "box_id", box.ID)...) http.Error(w, "password required", http.StatusUnauthorized) return } @@ -306,7 +306,7 @@ func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) { if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) { a.logger.Warn("failed to record zip download", "source", "download", "severity", "warn", "code", 4003, "box_id", box.ID, "error", err.Error()) } - a.logger.Info("zip downloaded", "source", "download", "severity", "user_activity", "code", 2006, "box_id", box.ID, "ip", uploadClientIP(r), "files", len(box.Files)) + a.logger.Info("zip downloaded", withRequestLogAttrs(r, "source", "download", "severity", "user_activity", "code", 2006, "box_id", box.ID, "files", len(box.Files))...) } func (a *App) fileView(box services.Box, file services.File) fileView { diff --git a/backend/libs/handlers/logging.go b/backend/libs/handlers/logging.go new file mode 100644 index 0000000..8ab718e --- /dev/null +++ b/backend/libs/handlers/logging.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "net/http" + + "warpbox.dev/backend/libs/middleware" +) + +func requestLogAttrs(r *http.Request) []any { + attrs := []any{ + "ip", uploadClientIP(r), + "method", r.Method, + "path", r.URL.Path, + } + if requestID := middleware.RequestIDFromContext(r.Context()); requestID != "" { + attrs = append(attrs, "request_id", requestID) + } + if userAgent := r.UserAgent(); userAgent != "" { + attrs = append(attrs, "user_agent", userAgent) + } + return attrs +} + +func withRequestLogAttrs(r *http.Request, attrs ...any) []any { + out := make([]any, 0, len(attrs)+8) + out = append(out, attrs...) + out = append(out, requestLogAttrs(r)...) + return out +} diff --git a/backend/libs/handlers/pages.go b/backend/libs/handlers/pages.go index 8906e3b..eaeb2ce 100644 --- a/backend/libs/handlers/pages.go +++ b/backend/libs/handlers/pages.go @@ -46,6 +46,17 @@ func (a *App) Home(w http.ResponseWriter, r *http.Request) { http.Error(w, "unable to load upload policy", http.StatusInternalServerError) return } + actor := "anonymous" + if loggedIn { + actor = "user" + } + a.logger.Info("upload page viewed", withRequestLogAttrs(r, + "source", "page", + "severity", "user_activity", + "code", 2500, + "actor", actor, + "user_id", user.ID, + )...) maxUploadSize, limitSummary := a.homeUploadPolicyLabels(settings, user, loggedIn, isAdmin) expiryOptions, defaultExpiry := a.homeExpiryOptions(settings, user, loggedIn, isAdmin) a.renderPage(w, r, http.StatusOK, "home.html", web.PageData{ diff --git a/backend/libs/handlers/upload.go b/backend/libs/handlers/upload.go index cff3035..2db6dd2 100644 --- a/backend/libs/handlers/upload.go +++ b/backend/libs/handlers/upload.go @@ -18,7 +18,7 @@ import ( func (a *App) Upload(w http.ResponseWriter, r *http.Request) { user, loggedIn, authErr := a.currentUserWithAuthError(r) if authErr != nil { - a.logger.Warn("upload rejected invalid bearer token", "source", "user-upload", "severity", "warn", "code", 4010, "ip", uploadClientIP(r), "user_agent", r.UserAgent()) + a.logger.Warn("upload rejected invalid bearer token", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4010)...) helpers.WriteJSONError(w, http.StatusUnauthorized, "invalid bearer token") return } @@ -30,14 +30,14 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) { return } if !loggedIn && !settings.AnonymousUploadsEnabled { - a.logger.Warn("anonymous upload rejected disabled", "source", "user-upload", "severity", "warn", "code", 4012, "ip", uploadClientIP(r)) + a.logger.Warn("anonymous upload rejected disabled", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4012)...) helpers.WriteJSONError(w, http.StatusForbidden, "anonymous uploads are disabled") return } effectivePolicy := a.effectiveUploadPolicy(settings, user, loggedIn) rateKey := uploadRateKey(r, user, loggedIn) if !isAdminUpload && effectivePolicy.ShortRequests > 0 && !a.rateLimiter.Allow("upload:"+rateKey, effectivePolicy.ShortRequests, effectivePolicy.ShortWindow, time.Now().UTC()) { - a.logger.Warn("upload rate limited", "source", "user-upload", "severity", "warn", "code", 4290, "ip", uploadClientIP(r), "user_id", user.ID) + a.logger.Warn("upload rate limited", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4290, "user_id", user.ID)...) helpers.WriteJSONError(w, http.StatusTooManyRequests, "too many upload requests, please slow down") return } @@ -52,7 +52,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) { parseLimit = 32 << 20 } if err := r.ParseMultipartForm(parseLimit); err != nil { - a.logger.Warn("upload form parse failed", "source", "user-upload", "severity", "warn", "code", 4000, "ip", uploadClientIP(r), "user_id", user.ID, "error", err.Error()) + a.logger.Warn("upload form parse failed", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4000, "user_id", user.ID, "error", err.Error())...) helpers.WriteJSONError(w, http.StatusBadRequest, "upload form could not be read") return } @@ -65,14 +65,14 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) { ownerID = user.ID collectionID = r.FormValue("collection_id") if !a.authService.CollectionOwnedBy(collectionID, user.ID) { - a.logger.Warn("upload rejected invalid collection", "source", "user-upload", "severity", "warn", "code", 4030, "user_id", user.ID, "collection_id", collectionID) + a.logger.Warn("upload rejected invalid collection", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4030, "user_id", user.ID, "collection_id", collectionID)...) helpers.WriteJSONError(w, http.StatusForbidden, "collection not found") return } } if !isAdminUpload { if status, message := a.checkUploadPolicy(r, user, loggedIn, settings, effectivePolicy, files, totalBytes); message != "" { - a.logger.Warn("upload rejected by policy", "source", "quota", "severity", "warn", "code", status, "ip", uploadClientIP(r), "user_id", user.ID, "message", message, "bytes", totalBytes, "files", len(files)) + a.logger.Warn("upload rejected by policy", withRequestLogAttrs(r, "source", "quota", "severity", "warn", "code", status, "user_id", user.ID, "message", message, "bytes", totalBytes, "files", len(files))...) helpers.WriteJSONError(w, status, message) return } @@ -89,7 +89,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) { } } if !unlimitedExpiry && maxDays > effectivePolicy.MaxDays { - a.logger.Warn("upload rejected expiration days", "source", "user-upload", "severity", "warn", "code", 4131, "ip", uploadClientIP(r), "user_id", user.ID, "requested_days", maxDays, "max_days", effectivePolicy.MaxDays) + a.logger.Warn("upload rejected expiration days", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4131, "user_id", user.ID, "requested_days", maxDays, "max_days", effectivePolicy.MaxDays)...) helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays)) return } @@ -99,13 +99,13 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) { // Only honour it for unlimited uploaders; otherwise it's an invalid value. if expiresMinutes < 0 || rawMaxDays < 0 { if !unlimitedExpiry { - a.logger.Warn("upload rejected unlimited expiration", "source", "user-upload", "severity", "warn", "code", 4133, "ip", uploadClientIP(r), "user_id", user.ID) + a.logger.Warn("upload rejected unlimited expiration", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4133, "user_id", user.ID)...) helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays)) return } expiresMinutes = -1 } else if expiresMinutes > 0 && !unlimitedExpiry && expiresMinutes > effectivePolicy.MaxDays*24*60 { - a.logger.Warn("upload rejected expiration minutes", "source", "user-upload", "severity", "warn", "code", 4132, "ip", uploadClientIP(r), "user_id", user.ID, "requested_minutes", expiresMinutes, "max_days", effectivePolicy.MaxDays) + a.logger.Warn("upload rejected expiration minutes", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4132, "user_id", user.ID, "requested_minutes", expiresMinutes, "max_days", effectivePolicy.MaxDays)...) helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays)) return } @@ -123,12 +123,12 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) { } result, boxesAdded, status, policyMessage, err := a.createOrAppendBox(r, user, loggedIn, effectivePolicy, files, opts, !isAdminUpload) if policyMessage != "" { - a.logger.Warn("upload rejected by policy", "source", "quota", "severity", "warn", "code", status, "ip", uploadClientIP(r), "user_id", user.ID, "message", policyMessage, "bytes", totalBytes, "files", len(files)) + a.logger.Warn("upload rejected by policy", withRequestLogAttrs(r, "source", "quota", "severity", "warn", "code", status, "user_id", user.ID, "message", policyMessage, "bytes", totalBytes, "files", len(files))...) helpers.WriteJSONError(w, status, policyMessage) return } if err != nil { - a.logger.Warn("upload failed", "source", "user-upload", "severity", "warn", "code", 4001, "ip", uploadClientIP(r), "user_id", user.ID, "error", err.Error()) + a.logger.Warn("upload failed", withRequestLogAttrs(r, "source", "user-upload", "severity", "warn", "code", 4001, "user_id", user.ID, "error", err.Error())...) helpers.WriteJSONError(w, http.StatusBadRequest, err.Error()) return } @@ -141,7 +141,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) { } } jobs.GenerateThumbnailsForBoxAsync(a.uploadService, a.logger, result.BoxID) - a.logger.Info("upload response sent", "source", "user-upload", "severity", "user_activity", "code", 2001, "ip", uploadClientIP(r), "user_id", user.ID, "box_id", result.BoxID, "files", len(files), "bytes", totalBytes, "admin", isAdminUpload) + a.logger.Info("box uploaded", withRequestLogAttrs(r, "source", "user-upload", "severity", "user_activity", "code", 2001, "user_id", user.ID, "box_id", result.BoxID, "files", len(files), "bytes", totalBytes, "admin", isAdminUpload, "anonymous", !loggedIn)...) if wantsJSON(r) { helpers.WriteJSON(w, http.StatusCreated, result) diff --git a/backend/libs/httpserver/server.go b/backend/libs/httpserver/server.go index ffd4987..32da313 100644 --- a/backend/libs/httpserver/server.go +++ b/backend/libs/httpserver/server.go @@ -50,7 +50,6 @@ func New(cfg config.Config, logger *slog.Logger) (*http.Server, error) { middleware.SecurityHeaders, middleware.Gzip, middleware.ClientIP(cfg.TrustedProxies), - middleware.Logger(logger), middleware.Bans(logger, banService, cfg.TrustedProxies), ) diff --git a/backend/static/css/50-admin.css b/backend/static/css/50-admin.css index a442873..4fc12e3 100644 --- a/backend/static/css/50-admin.css +++ b/backend/static/css/50-admin.css @@ -218,12 +218,16 @@ width: 100%; max-width: 2.2rem; min-height: 0; + height: 100%; display: flex; align-items: flex-end; justify-content: center; + align-self: stretch; } .bar-chart-bar { + display: block; + flex: 0 0 auto; width: 100%; min-height: 0; border-radius: 6px 6px 0 0;