Change logging to be json comaptible

This commit is contained in:
2026-05-03 22:42:43 +02:00
parent d1f6782c96
commit 1a82f21202
14 changed files with 501 additions and 35 deletions

View File

@@ -19,6 +19,9 @@ type Claims struct {
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
log := StructuredLog(c).With().
Str("event", "auth_middleware").
Logger()
var tokenString string
@@ -39,6 +42,7 @@ func AuthMiddleware() gin.HandlerFunc {
}
if tokenString == "" {
log.Warn().Str("reason", "no_token").Msg("Auth failed - no token provided")
abortUnauthorized(c)
return
}
@@ -51,13 +55,26 @@ func AuthMiddleware() gin.HandlerFunc {
return jwtSecret, nil
})
if err != nil || !token.Valid {
if err != nil {
log.Warn().
Str("reason", "token_parse_error").
Err(err).
Msg("Auth failed - token parse error")
abortUnauthorized(c)
return
}
if !token.Valid {
log.Warn().Str("reason", "invalid_token").Msg("Auth failed - invalid token")
abortUnauthorized(c)
return
}
c.Set("user_id", claims.UserID)
c.Set("role", claims.Role)
c.Set("username", claims.UserID)
log.Debug().Str("user_id", claims.UserID).Str("role", claims.Role).Msg("Auth successful")
c.Next()
}
@@ -76,26 +93,37 @@ func abortUnauthorized(c *gin.Context) {
func RequireRole(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
log := StructuredLog(c).With().
Str("event", "role_check").
Logger()
roleValue, exists := c.Get("role")
if !exists {
log.Warn().Str("reason", "no_role").Msg("Role check failed - no role in context")
abortForbidden(c)
return
}
userRole, ok := roleValue.(string)
if !ok {
log.Warn().Str("reason", "invalid_role_type").Msg("Role check failed - invalid role type")
abortForbidden(c)
return
}
for _, allowed := range roles {
if userRole == allowed {
log.Debug().Str("required_roles", strings.Join(roles, ",")).Str("user_role", userRole).Msg("Role check passed")
c.Next()
return
}
}
log.Warn().
Str("required_roles", strings.Join(roles, ",")).
Str("user_role", userRole).
Msg("Role check failed - insufficient permissions")
abortForbidden(c)
}
}

View File

@@ -1 +1,87 @@
package middleware
import (
"strconv"
"time"
"ResendIt/internal/logger"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
)
// StructuredLogger returns a gin middleware that logs HTTP requests in JSON format
func StructuredLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
method := c.Request.Method
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
clientIP := c.ClientIP()
userAgent := c.Request.UserAgent()
requestID := c.GetString("request_id")
evt := logger.Log.Info().
Str("type", "http_request").
Str("method", method).
Str("path", path).
Str("query", query).
Int("status", status).
Dur("latency_ms", latency).
Str("client_ip", clientIP).
Str("user_agent", userAgent).
Str("request_id", requestID)
if len(c.Errors) > 0 {
evt = evt.Str("error", c.Errors.ByType(gin.ErrorTypePrivate).String())
}
if userID, exists := c.Get("user_id"); exists {
evt = evt.Str("user_id", userID.(string))
}
if username, exists := c.Get("username"); exists {
evt = evt.Str("username", username.(string))
}
evt.Msg("")
}
}
// RequestIDMiddleware adds a unique request ID to each request
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
func generateRequestID() string {
// Simple request ID generation - could use uuid package for more entropy
return strconv.FormatInt(time.Now().UnixNano(), 36)
}
// StructuredLog returns a child logger with HTTP context for use in handlers
func StructuredLog(c *gin.Context) zerolog.Logger {
log := logger.Log.With().
Str("type", "app_log").
Str("request_id", c.GetString("request_id"))
if userID, exists := c.Get("user_id"); exists {
log = log.Str("user_id", userID.(string))
}
if username, exists := c.Get("username"); exists {
log = log.Str("username", username.(string))
}
return log.Logger()
}

View File

@@ -114,6 +114,9 @@ func RateLimitByIPDynamic(maxFn func() int, per time.Duration, burstFn func() in
}
return func(c *gin.Context) {
log := StructuredLog(c).With().
Str("event", "rate_limit_check").
Logger()
// Kinda a shitty fix
if c.FullPath() == "/api/files/upload/chunk" || c.FullPath() == "/api/files/upload/init" || c.FullPath() == "/api/files/upload/complete" {
@@ -136,6 +139,12 @@ func RateLimitByIPDynamic(maxFn func() int, per time.Duration, burstFn func() in
client := getClient(ip, now, max, burst)
if !client.bucket.allow() {
log.Warn().
Str("ip", ip).
Int("max", max).
Int("burst", burst).
Msg("Rate limit exceeded")
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "rate limit exceeded",
})
@@ -144,4 +153,4 @@ func RateLimitByIPDynamic(maxFn func() int, per time.Duration, burstFn func() in
c.Next()
}
}
}

View File

@@ -2,6 +2,9 @@ package auth
import (
"os"
"time"
"ResendIt/internal/api/middleware"
"github.com/gin-gonic/gin"
)
@@ -37,16 +40,44 @@ func (h *Handler) Login(c *gin.Context) {
}
if err := c.ShouldBindJSON(&req); err != nil {
log := middleware.StructuredLog(c)
log.Warn().
Str("event", "login_failed").
Str("reason", "invalid_request").
Str("username", req.Username).
Msg("Login attempt with invalid request")
c.JSON(400, gin.H{"error": "Invalid request body"})
return
}
log := middleware.StructuredLog(c).With().
Str("event", "login_attempt").
Str("username", req.Username).
Str("ip", c.ClientIP()).
Logger()
start := time.Now()
token, err := h.service.Login(req.Username, req.Password)
latency := time.Since(start)
if err != nil {
log.Warn().
Str("result", "failed").
Dur("latency_ms", latency).
Err(err).
Msg("Login failed")
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}
log.Info().
Str("result", "success").
Dur("latency_ms", latency).
Msg("Login successful")
isSecure := os.Getenv("USE_HTTPS") == "true"
c.SetCookie(
@@ -56,7 +87,7 @@ func (h *Handler) Login(c *gin.Context) {
"/",
os.Getenv("DOMAIN"),
isSecure,
true, // httpOnly (IMPORTANT)
true,
)
c.JSON(200, gin.H{"token": token})

View File

@@ -5,6 +5,8 @@ import (
"os"
"path/filepath"
"ResendIt/internal/logger"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
@@ -21,19 +23,43 @@ func Connect() (*gorm.DB, error) {
dsn = "./data/database.db"
}
logger.Log.Info().
Str("type", "db_connect").
Str("db_type", dbType).
Msg("Connecting to database")
var db *gorm.DB
var err error
switch dbType {
case "sqlite":
return connectSQLite(dsn)
db, err = connectSQLite(dsn)
case "postgres":
return connectPostgres(dsn)
db, err = connectPostgres(dsn)
case "mysql":
return connectMySQL(dsn)
db, err = connectMySQL(dsn)
default:
return nil, fmt.Errorf("unsupported DB_TYPE: %s", dbType)
}
if err != nil {
logger.Log.Error().
Str("type", "db_connect").
Str("db_type", dbType).
Err(err).
Msg("Failed to connect to database")
return nil, err
}
logger.Log.Info().
Str("type", "db_connect").
Str("db_type", dbType).
Msg("Database connected successfully")
return db, nil
}
func connectSQLite(filePath string) (*gorm.DB, error) {
@@ -51,6 +77,12 @@ func connectSQLite(filePath string) (*gorm.DB, error) {
return nil, fmt.Errorf("failed to open SQLite database: %w", err)
}
sqlDB, err := db.DB()
if err == nil {
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
}
return db, nil
}
@@ -64,6 +96,12 @@ func connectPostgres(dsn string) (*gorm.DB, error) {
return nil, fmt.Errorf("failed to connect to Postgres: %w", err)
}
sqlDB, err := db.DB()
if err == nil {
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
}
return db, nil
}
@@ -77,5 +115,11 @@ func connectMySQL(dsn string) (*gorm.DB, error) {
return nil, fmt.Errorf("failed to connect to MySQL: %w", err)
}
sqlDB, err := db.DB()
if err == nil {
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
}
return db, nil
}
}

View File

@@ -1,12 +1,13 @@
package file
import (
"ResendIt/internal/api/middleware"
"ResendIt/internal/config"
"ResendIt/internal/logger"
"ResendIt/internal/notify"
"ResendIt/internal/util"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
@@ -34,26 +35,38 @@ func NewHandler(s *Service, cfg ConfigService) *Handler {
}
func (h *Handler) Upload(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "file_upload").
Logger()
err := c.Request.ParseMultipartForm(0)
if err != nil {
log.Warn().Str("reason", "parse_error").Err(err).Msg("Upload failed")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
file, err := c.FormFile("file")
if err != nil {
log.Warn().Str("reason", "missing_file").Msg("Upload failed")
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
return
}
maxSize := h.configService.GetInt64Default(config.KeyUploadMaxFileSizeBytes, config.DefaultUploadMaxFileSizeBytes)
if file.Size > maxSize {
log.Warn().
Str("reason", "file_too_large").
Int64("file_size", file.Size).
Int64("max_size", maxSize).
Msg("Upload rejected")
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "file too large"})
return
}
f, err := file.Open()
if err != nil {
log.Error().Err(err).Msg("Failed to open uploaded file")
c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot open file"})
return
}
@@ -80,10 +93,18 @@ func (h *Handler) Upload(c *gin.Context) {
duration,
)
if err != nil {
log.Error().Err(err).Msg("Upload failed")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
log.Info().
Str("file_id", record.ID).
Str("filename", record.Filename).
Int64("file_size", record.Size).
Bool("once", once).
Msg("File uploaded successfully")
enabled := h.configService.GetIntDefault(config.KeyUseNtfy, config.DefaultUseNtfy)
if enabled == 1 {
ntfyURL := h.configService.GetStringDefault(config.KeyNtfyUrl, "")
@@ -94,7 +115,7 @@ func (h *Handler) Upload(c *gin.Context) {
msg := fmt.Sprintf("%s (%s)\nID: %s", record.Filename, util.HumanSize(record.Size), record.ID)
clickUrl := fmt.Sprintf("f/%s", record.ViewID)
if err := notify.Publish(ntfyURL, topic, title, msg, clickUrl); err != nil {
log.Printf("ntfy publish failed: %v", err)
logger.Log.Warn().Err(err).Str("type", "ntfy").Msg("ntfy publish failed")
}
}()
}
@@ -110,13 +131,21 @@ func (h *Handler) Upload(c *gin.Context) {
}
func (h *Handler) View(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "file_view").
Logger()
id := c.Param("id")
record, err := h.service.DownloadFile(id)
if err != nil {
log.Warn().Str("file_id", id).Err(err).Msg("File view failed - not found")
c.HTML(http.StatusOK, "error.html", nil)
return
}
log.Info().Str("file_id", id).Str("filename", record.Filename).Msg("File viewed")
name := util.SafeFilename(record.Filename)
c.Header("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
c.Header("X-Content-Type-Options", "nosniff")
@@ -134,31 +163,46 @@ func isXSSRisk(filename string) bool {
}
func (h *Handler) Download(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "file_download").
Logger()
id := c.Param("id")
record, err := h.service.DownloadFile(id)
if err != nil {
log.Warn().Str("file_id", id).Err(err).Msg("File download failed - not found")
c.HTML(http.StatusOK, "error.html", nil)
return
}
log.Info().Str("file_id", id).Str("filename", record.Filename).Int64("size", record.Size).Msg("File downloaded")
name := util.SafeFilename(record.Filename)
c.Header("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
c.Header("X-Content-Type-Options", "nosniff")
//c.Header("Content-Security-Policy", "default-src 'none'; img-src 'self'; media-src 'self'; script-src 'none'; style-src 'none';")
//c.Header("Content-Type", "application/octet-stream")
c.File(record.Path)
}
func (h *Handler) Delete(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "file_delete").
Logger()
id := c.Param("del_id")
_, err := h.service.DeleteFileByDeletionID(id)
record, err := h.service.DeleteFileByDeletionID(id)
if err != nil {
log.Warn().Str("deletion_id", id).Err(err).Msg("File delete failed")
c.HTML(http.StatusOK, "error.html", nil)
return
}
//c.JSON(http.StatusOK, gin.H{"status": "deleted"})
log.Info().
Str("file_id", record.ID).
Str("filename", record.Filename).
Msg("File deleted")
c.HTML(http.StatusOK, "deleted.html", nil)
}
@@ -185,18 +229,29 @@ func (h *Handler) AdminGet(c *gin.Context) {
}
func (h *Handler) AdminDelete(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "admin_file_delete").
Logger()
id := c.Param("id")
_, err := h.service.DeleteFileByID(id)
record, err := h.service.DeleteFileByID(id)
if err != nil {
log.Warn().Str("file_id", id).Err(err).Msg("Admin file delete failed")
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
return
}
log.Info().Str("file_id", record.ID).Str("filename", record.Filename).Msg("Admin deleted file")
c.Redirect(301, "/admin")
}
func (h *Handler) AdminForceDelete(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "admin_file_force_delete").
Logger()
id := c.Param("id")
_, err := h.service.GetFileByID(id)
@@ -206,14 +261,21 @@ func (h *Handler) AdminForceDelete(c *gin.Context) {
}
if _, err := h.service.ForceDelete(id); err != nil {
log.Error().Err(err).Msg("Admin force delete failed")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
log.Info().Str("file_id", id).Msg("Admin force deleted file")
c.Redirect(301, "/admin")
}
func (h *Handler) AdminReinstate(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "admin_file_reinstate").
Logger()
id := c.Param("id")
_, err := h.service.GetFileByID(id)
@@ -223,10 +285,13 @@ func (h *Handler) AdminReinstate(c *gin.Context) {
}
if _, err := h.service.ReinstateFile(id); err != nil {
log.Error().Err(err).Msg("Admin reinstate failed")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
log.Info().Str("file_id", id).Msg("Admin reinstated file")
c.Redirect(301, "/admin")
}
@@ -331,6 +396,10 @@ func (h *Handler) UploadChunk(c *gin.Context) {
}
func (h *Handler) UploadComplete(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "chunked_upload_complete").
Logger()
var req struct {
FileID string `json:"fileId"`
Filename string `json:"filename"`
@@ -377,6 +446,7 @@ func (h *Handler) UploadComplete(c *gin.Context) {
24*time.Hour,
)
if err != nil {
log.Error().Err(err).Msg("Chunked upload failed")
c.JSON(500, gin.H{"error": err.Error()})
return
}
@@ -384,6 +454,12 @@ func (h *Handler) UploadComplete(c *gin.Context) {
// cleanup temp
_ = os.RemoveAll(tmpDir)
log.Info().
Str("file_id", record.ID).
Str("filename", record.Filename).
Int("chunks", req.TotalChunks).
Msg("Chunked upload completed")
c.JSON(200, gin.H{
"id": record.ID,
"view_key": record.ViewID,
@@ -414,4 +490,4 @@ func (h *Handler) UploadStatus(c *gin.Context) {
c.JSON(200, gin.H{
"uploadedChunks": uploaded,
})
}
}

40
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,40 @@
package logger
import (
"os"
"time"
"github.com/rs/zerolog"
)
var Log zerolog.Logger
func init() {
output := os.Stderr
logFormat := os.Getenv("LOG_FORMAT")
if logFormat == "json" {
Log = zerolog.New(output).With().Timestamp().Logger()
} else {
Log = zerolog.New(zerolog.ConsoleWriter{
Out: output,
TimeFormat: time.RFC3339,
}).With().Timestamp().Logger()
}
level := os.Getenv("LOG_LEVEL")
switch level {
case "debug":
Log = Log.Level(zerolog.DebugLevel)
case "warn":
Log = Log.Level(zerolog.WarnLevel)
case "error":
Log = Log.Level(zerolog.ErrorLevel)
default:
Log = Log.Level(zerolog.InfoLevel)
}
}
func With() zerolog.Context {
return Log.With()
}

View File

@@ -2,6 +2,9 @@ package user
import (
"fmt"
"time"
"ResendIt/internal/api/middleware"
"github.com/gin-gonic/gin"
)
@@ -15,6 +18,10 @@ func NewHandler(service *Service) *Handler {
}
func (h *Handler) Register(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "user_register").
Logger()
var req struct {
Username string `json:"username"`
Password string `json:"password"`
@@ -22,20 +29,43 @@ func (h *Handler) Register(c *gin.Context) {
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Warn().
Str("reason", "invalid_request").
Msg("Registration failed")
c.JSON(400, gin.H{"error": "invalid request"})
return
}
user, err := h.service.CreateUser(req.Username, req.Password, req.Role)
if err != nil {
log.Error().
Err(err).
Str("username", req.Username).
Msg("Registration failed")
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(201, gin.H{"id": user.ID, "username": user.Username, "role": user.Role})
log.Info().
Str("user_id", fmt.Sprint(user.ID)).
Str("username", user.Username).
Str("role", user.Role).
Msg("User registered successfully")
c.JSON(201, gin.H{
"id": user.ID,
"username": user.Username,
"role": user.Role,
})
}
func (h *Handler) ChangePassword(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "password_change").
Logger()
var req struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
@@ -43,41 +73,75 @@ func (h *Handler) ChangePassword(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
fmt.Println("User ID not found in context")
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Warn().
Str("reason", "invalid_request").
Msg("Password change failed")
c.JSON(400, gin.H{"error": "invalid request"})
return
}
err := h.service.ChangePassword(userID.(string), req.OldPassword, req.NewPassword)
uid := fmt.Sprint(userID)
start := time.Now()
err := h.service.ChangePassword(uid, req.OldPassword, req.NewPassword)
latency := time.Since(start)
if err != nil {
log.Warn().
Str("user_id", uid).
Str("reason", err.Error()).
Dur("latency_ms", latency).
Msg("Password change failed")
c.JSON(500, gin.H{"error": err.Error()})
return
}
log.Info().
Str("user_id", uid).
Dur("latency_ms", latency).
Msg("Password changed successfully")
c.JSON(200, gin.H{"message": "password changed successfully"})
}
func ForcePasswordChangeMiddleware(userService *Service) gin.HandlerFunc {
return func(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "force_password_check").
Logger()
userID, exists := c.Get("user_id")
if !exists {
c.Next()
return
}
user, err := userService.FindByID(userID.(string))
uid := fmt.Sprint(userID)
user, err := userService.FindByID(uid)
if err != nil {
log.Error().
Err(err).
Str("user_id", uid).
Msg("Failed to find user for password check")
c.AbortWithStatus(500)
return
}
// Allow access to change password page itself
if user.ForceChangePassword && c.Request.URL.Path != "/change-password" {
log.Warn().
Str("user_id", uid).
Str("path", c.Request.URL.Path).
Msg("Access denied - force password change required")
c.Redirect(302, "/change-password")
c.Abort()
return

View File

@@ -1,6 +1,7 @@
package web
import (
"ResendIt/internal/api/middleware"
"ResendIt/internal/config"
"net/http"
"strconv"
@@ -30,6 +31,10 @@ type ConfigPageData struct {
// ConfigPage renders a modular admin config screen.
func (h *Handler) ConfigPage(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "config_page_view").
Logger()
cfg := h.configService
maxBytes := cfg.GetInt64Default(config.KeyUploadMaxFileSizeBytes, config.DefaultUploadMaxFileSizeBytes)
@@ -48,10 +53,16 @@ func (h *Handler) ConfigPage(c *gin.Context) {
NtfyTopic: cfg.GetStringDefault(config.KeyNtfyTopic, config.DefaultNtfyTopic),
}
log.Debug().Msg("Config page viewed")
c.HTML(http.StatusOK, "config.html", data)
}
func (h *Handler) ConfigSave(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "config_save").
Logger()
cfg := h.configService
// Parse + validate.
@@ -87,22 +98,26 @@ func (h *Handler) ConfigSave(c *gin.Context) {
newMODT, err := strconv.Unquote(`"` + c.PostForm("site_modt") + `"`)
if err != nil {
log.Warn().Str("reason", "invalid_modtext").Msg("Config save failed")
h.renderConfigError(c, "invalid modtext")
return
}
maxMB, err := parseInt64("upload_max_file_size_mb", 1, 1024*1024)
if err != nil {
log.Warn().Str("key", "upload_max_file_size_mb").Msg("Config save failed - invalid value")
h.renderConfigError(c, "invalid max file size")
return
}
maxFiles, err := parseInt("upload_multi_max_files", 1, 500)
if err != nil {
log.Warn().Str("key", "upload_multi_max_files").Msg("Config save failed - invalid value")
h.renderConfigError(c, "invalid max files")
return
}
maxHours, err := parseInt("upload_max_hours", 1, 24*365)
if err != nil {
log.Warn().Str("key", "upload_max_hours").Msg("Config save failed - invalid value")
h.renderConfigError(c, "invalid max hours")
return
}
@@ -110,27 +125,32 @@ func (h *Handler) ConfigSave(c *gin.Context) {
// Rate limits: stored, but not applied dynamically yet.
loginPerMin, err := parseInt("ratelimit_login_per_minute", 1, 10000)
if err != nil {
log.Warn().Str("key", "ratelimit_login_per_minute").Msg("Config save failed - invalid value")
h.renderConfigError(c, "invalid login rate")
return
}
loginBurst, err := parseInt("ratelimit_login_burst", 1, 10000)
if err != nil {
log.Warn().Str("key", "ratelimit_login_burst").Msg("Config save failed - invalid value")
h.renderConfigError(c, "invalid login burst")
return
}
apiPerMin, err := parseInt("ratelimit_api_per_minute", 1, 100000)
if err != nil {
log.Warn().Str("key", "ratelimit_api_per_minute").Msg("Config save failed - invalid value")
h.renderConfigError(c, "invalid api rate")
return
}
apiBurst, err := parseInt("ratelimit_api_burst", 1, 100000)
if err != nil {
log.Warn().Str("key", "ratelimit_api_burst").Msg("Config save failed - invalid value")
h.renderConfigError(c, "invalid api burst")
return
}
useNTFY, err := strconv.ParseBool(c.PostForm("ntfy_use"))
if err != nil {
log.Warn().Str("key", "ntfy_use").Msg("Config save failed - invalid value")
h.renderConfigError(c, "invalid ntfy use value")
return
}
@@ -139,14 +159,17 @@ func (h *Handler) ConfigSave(c *gin.Context) {
// Persist.
if err := cfg.SetString(config.KeyUploadMaxFileSizeBytes, strconv.FormatInt(maxMB*1024*1024, 10)); err != nil {
log.Error().Err(err).Str("key", config.KeyUploadMaxFileSizeBytes).Msg("Config save failed")
h.renderConfigError(c, err.Error())
return
}
if err := cfg.SetString(config.KeyUploadMultiMaxFiles, strconv.Itoa(maxFiles)); err != nil {
log.Error().Err(err).Str("key", config.KeyUploadMultiMaxFiles).Msg("Config save failed")
h.renderConfigError(c, err.Error())
return
}
if err := cfg.SetString(config.KeyUploadMaxHours, strconv.Itoa(maxHours)); err != nil {
log.Error().Err(err).Str("key", config.KeyUploadMaxHours).Msg("Config save failed")
h.renderConfigError(c, err.Error())
return
}
@@ -167,6 +190,13 @@ func (h *Handler) ConfigSave(c *gin.Context) {
_ = cfg.SetString(config.KeyNtfyUrl, ntfyUrl)
_ = cfg.SetString(config.KeyNtfyTopic, ntfyTopic)
log.Info().
Str("modtext", newMODT).
Int64("max_file_size_mb", maxMB).
Int("max_files", maxFiles).
Int("max_hours", maxHours).
Msg("Config saved successfully")
c.Redirect(http.StatusFound, "/config?saved=1")
}
@@ -183,4 +213,4 @@ func (h *Handler) renderConfigError(c *gin.Context, msg string) {
RateLimitApiBurst: h.configService.GetIntDefault(config.KeyRateLimitApiBurst, config.DefaultRateLimitApiBurst),
}
c.HTML(http.StatusBadRequest, "config.html", data)
}
}

View File

@@ -1,6 +1,7 @@
package web
import (
"ResendIt/internal/api/middleware"
"ResendIt/internal/buildinfo"
"ResendIt/internal/config"
"ResendIt/internal/file"
@@ -51,16 +52,23 @@ func (h *Handler) LoginPage(c *gin.Context) {
}
func (h *Handler) FileView(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "file_view_page").
Logger()
id := c.Param("id")
fileRecord, err := h.fileService.GetFileByViewID(id)
if err != nil {
log.Warn().Str("view_id", id).Err(err).Msg("File view failed - not found")
c.HTML(404, "error.html", gin.H{
"MODT": h.configService.GetStringDefault(config.KeyModtext, config.DefaultModt),
})
return
}
log.Info().Str("view_id", id).Str("filename", fileRecord.Filename).Msg("File view page rendered")
downloadKey := fileRecord.ID
deleteKey := fileRecord.DeletionID
@@ -73,6 +81,10 @@ func (h *Handler) FileView(c *gin.Context) {
}
func (h *Handler) AdminPage(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "admin_page_view").
Logger()
pageStr := c.Query("page")
page, err := strconv.Atoi(pageStr)
if err != nil || page < 1 {
@@ -84,6 +96,7 @@ func (h *Handler) AdminPage(c *gin.Context) {
files, totalCount, err := h.fileService.GetPaginatedFiles(limit, offset)
if err != nil {
log.Error().Err(err).Msg("Failed to load files for admin page")
c.HTML(500, "admin.html", gin.H{
"error": err.Error(),
})
@@ -117,6 +130,8 @@ func (h *Handler) AdminPage(c *gin.Context) {
totalPages := (totalCount + limit - 1) / limit
log.Debug().Int("page", page).Int("total_files", totalCount).Msg("Admin page viewed")
c.HTML(200, "admin.html", gin.H{
"Files": adminFiles,
"Page": page,
@@ -126,10 +141,16 @@ func (h *Handler) AdminPage(c *gin.Context) {
}
func (h *Handler) Logout(c *gin.Context) {
log := middleware.StructuredLog(c).With().
Str("event", "logout").
Logger()
log.Info().Msg("User logged out")
c.SetCookie("auth_token", "", -1, "/", "", false, true)
c.Redirect(302, "/")
}
func (h *Handler) ChangePasswordPage(c *gin.Context) {
c.HTML(200, "changePassword.html", nil)
}
}