diff --git a/codex-prompt/prompt-5.txt b/codex-prompt/prompt-5.txt new file mode 100644 index 0000000..49a9cd1 --- /dev/null +++ b/codex-prompt/prompt-5.txt @@ -0,0 +1,14 @@ +Aggiungi session e middleware. + +- Usa Fiber session middleware (cookie session). Configura key da cfg.SessionKey, cookie secure in prod, SameSite Lax, HttpOnly. + +- Implementa internal/http/middleware: + - RequireAuth: se non loggato redirect /login + - RequireAdmin: se role != admin -> 403 (pagina admin/forbidden o testo) + - CurrentUser helper (legge user_id da sessione, carica user da DB con repo) + +- Implementa flash messages (success/error) in sessione: + - SetFlashSuccess/SetFlashError + - ConsumeFlash middleware che aggiunge al template data + +Aggiorna layout.html per mostrare flash e navbar diversa per public/private/admin. \ No newline at end of file diff --git a/codex-prompt/prompt-6.txt b/codex-prompt/prompt-6.txt new file mode 100644 index 0000000..e69de29 diff --git a/internal/app/app.go b/internal/app/app.go index 967677f..ea27d24 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -28,8 +28,10 @@ func NewApp(cfg *config.Config) (*fiber.App, error) { })) store := session.New(session.Config{ + KeyLookup: "cookie:" + cfg.SessionKey, CookieHTTPOnly: true, CookieSecure: cfg.Env == config.EnvProd, + CookieSameSite: fiber.CookieSameSiteLaxMode, }) database, err := db.Open(cfg) diff --git a/internal/http/middleware/auth.go b/internal/http/middleware/auth.go new file mode 100644 index 0000000..d2662b8 --- /dev/null +++ b/internal/http/middleware/auth.go @@ -0,0 +1,62 @@ +package middleware + +import ( + "errors" + + "trustcontact/internal/models" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" +) + +func RequireAuth() fiber.Handler { + return func(c *fiber.Ctx) error { + if _, ok := CurrentUserFromContext(c); !ok { + return c.Redirect("/login") + } + return c.Next() + } +} + +func RequireAdmin() fiber.Handler { + return func(c *fiber.Ctx) error { + user, ok := CurrentUserFromContext(c) + if !ok { + return c.Redirect("/login") + } + if user.Role != models.RoleAdmin { + return c.Status(fiber.StatusForbidden).SendString("forbidden") + } + return c.Next() + } +} + +func SetSessionUserID(c *fiber.Ctx, userID uint) error { + store, ok := c.Locals(contextStoreKey).(*session.Store) + if !ok || store == nil { + return errors.New("session store not available") + } + + sess, err := store.Get(c) + if err != nil { + return err + } + + sess.Set(sessionUserIDKey, userID) + return sess.Save() +} + +func ClearSessionUser(c *fiber.Ctx) error { + store, ok := c.Locals(contextStoreKey).(*session.Store) + if !ok || store == nil { + return errors.New("session store not available") + } + + sess, err := store.Get(c) + if err != nil { + return err + } + + sess.Delete(sessionUserIDKey) + return sess.Save() +} diff --git a/internal/http/middleware/current_user.go b/internal/http/middleware/current_user.go new file mode 100644 index 0000000..97aa06f --- /dev/null +++ b/internal/http/middleware/current_user.go @@ -0,0 +1,132 @@ +package middleware + +import ( + "fmt" + "strconv" + + "trustcontact/internal/models" + "trustcontact/internal/repo" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" + "gorm.io/gorm" +) + +const ( + sessionUserIDKey = "user_id" + contextUserKey = "current_user" + contextStoreKey = "session_store" + contextTemplateKey = "template_data" +) + +func SessionStoreMiddleware(store *session.Store) fiber.Handler { + return func(c *fiber.Ctx) error { + c.Locals(contextStoreKey, store) + return c.Next() + } +} + +func CurrentUserMiddleware(store *session.Store, database *gorm.DB) fiber.Handler { + userRepo := repo.NewUserRepo(database) + + return func(c *fiber.Ctx) error { + user, err := CurrentUser(c, store, userRepo) + if err != nil { + return err + } + + c.Locals(contextUserKey, user) + setTemplateData(c, "CurrentUser", user) + return c.Next() + } +} + +func CurrentUser(c *fiber.Ctx, store *session.Store, userRepo *repo.UserRepo) (*models.User, error) { + sess, err := store.Get(c) + if err != nil { + return nil, fmt.Errorf("get session: %w", err) + } + + uidRaw := sess.Get(sessionUserIDKey) + uid, ok := normalizeUserID(uidRaw) + if !ok || uid == 0 { + return nil, nil + } + + user, err := userRepo.FindByID(uid) + if err != nil { + return nil, fmt.Errorf("load current user: %w", err) + } + + if user == nil { + sess.Delete(sessionUserIDKey) + if err := sess.Save(); err != nil { + return nil, fmt.Errorf("save session: %w", err) + } + return nil, nil + } + + return user, nil +} + +func CurrentUserFromContext(c *fiber.Ctx) (*models.User, bool) { + user, ok := c.Locals(contextUserKey).(*models.User) + if !ok || user == nil { + return nil, false + } + return user, true +} + +func normalizeUserID(v any) (uint, bool) { + switch value := v.(type) { + case uint: + return value, true + case uint64: + return uint(value), true + case uint32: + return uint(value), true + case int: + if value <= 0 { + return 0, false + } + return uint(value), true + case int64: + if value <= 0 { + return 0, false + } + return uint(value), true + case int32: + if value <= 0 { + return 0, false + } + return uint(value), true + case string: + parsed, err := strconv.ParseUint(value, 10, 64) + if err != nil || parsed == 0 { + return 0, false + } + return uint(parsed), true + default: + return 0, false + } +} + +func setTemplateData(c *fiber.Ctx, key string, value any) { + data := templateData(c) + data[key] = value + c.Locals(contextTemplateKey, data) +} + +func SetTemplateData(c *fiber.Ctx, key string, value any) { + setTemplateData(c, key, value) +} + +func templateData(c *fiber.Ctx) map[string]any { + existing, ok := c.Locals(contextTemplateKey).(map[string]any) + if ok && existing != nil { + return existing + } + fresh := make(map[string]any) + c.Locals(contextTemplateKey, fresh) + return fresh +} diff --git a/internal/http/middleware/flash.go b/internal/http/middleware/flash.go new file mode 100644 index 0000000..9059a81 --- /dev/null +++ b/internal/http/middleware/flash.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "errors" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" +) + +const ( + flashSuccessKey = "flash_success" + flashErrorKey = "flash_error" +) + +func SetFlashSuccess(c *fiber.Ctx, message string) error { + return setFlash(c, flashSuccessKey, message) +} + +func SetFlashError(c *fiber.Ctx, message string) error { + return setFlash(c, flashErrorKey, message) +} + +func ConsumeFlash() fiber.Handler { + return func(c *fiber.Ctx) error { + store, ok := c.Locals(contextStoreKey).(*session.Store) + if !ok || store == nil { + return errors.New("session store not available") + } + + sess, err := store.Get(c) + if err != nil { + return err + } + + success, _ := sess.Get(flashSuccessKey).(string) + errMsg, _ := sess.Get(flashErrorKey).(string) + + sess.Delete(flashSuccessKey) + sess.Delete(flashErrorKey) + if err := sess.Save(); err != nil { + return err + } + + if success != "" { + setTemplateData(c, "FlashSuccess", success) + } + if errMsg != "" { + setTemplateData(c, "FlashError", errMsg) + } + + c.Locals("flash_success", success) + c.Locals("flash_error", errMsg) + return c.Next() + } +} + +func setFlash(c *fiber.Ctx, key, message string) error { + store, ok := c.Locals(contextStoreKey).(*session.Store) + if !ok || store == nil { + return errors.New("session store not available") + } + + sess, err := store.Get(c) + if err != nil { + return err + } + + sess.Set(key, message) + return sess.Save() +} diff --git a/internal/http/router.go b/internal/http/router.go index 49907bd..307fd6d 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -2,14 +2,45 @@ package http import ( "trustcontact/internal/config" + httpmw "trustcontact/internal/http/middleware" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/session" "gorm.io/gorm" ) -func RegisterRoutes(app *fiber.App, _ *session.Store, _ *gorm.DB, _ *config.Config) { +func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, _ *config.Config) { + app.Use(httpmw.SessionStoreMiddleware(store)) + app.Use(httpmw.CurrentUserMiddleware(store, database)) + app.Use(httpmw.ConsumeFlash()) + app.Get("/healthz", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }) + + app.Get("/", func(c *fiber.Ctx) error { + c.Locals("nav_section", "public") + httpmw.SetTemplateData(c, "NavSection", "public") + return c.SendString("public area") + }) + + app.Get("/login", func(c *fiber.Ctx) error { + c.Locals("nav_section", "public") + httpmw.SetTemplateData(c, "NavSection", "public") + return c.SendString("login page") + }) + + private := app.Group("/private", httpmw.RequireAuth()) + private.Get("/", func(c *fiber.Ctx) error { + c.Locals("nav_section", "private") + httpmw.SetTemplateData(c, "NavSection", "private") + return c.SendString("private area") + }) + + admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin()) + admin.Get("/", func(c *fiber.Ctx) error { + c.Locals("nav_section", "admin") + httpmw.SetTemplateData(c, "NavSection", "admin") + return c.SendString("admin area") + }) } diff --git a/internal/repo/user_repo.go b/internal/repo/user_repo.go new file mode 100644 index 0000000..d2ee672 --- /dev/null +++ b/internal/repo/user_repo.go @@ -0,0 +1,29 @@ +package repo + +import ( + "errors" + + "trustcontact/internal/models" + + "gorm.io/gorm" +) + +type UserRepo struct { + db *gorm.DB +} + +func NewUserRepo(db *gorm.DB) *UserRepo { + return &UserRepo{db: db} +} + +func (r *UserRepo) FindByID(id uint) (*models.User, error) { + var user models.User + if err := r.db.First(&user, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + return &user, nil +} diff --git a/web/templates/layout.html b/web/templates/layout.html new file mode 100644 index 0000000..29ebd1a --- /dev/null +++ b/web/templates/layout.html @@ -0,0 +1,42 @@ +{{- $flashSuccess := index . "FlashSuccess" -}} +{{- $flashError := index . "FlashError" -}} +{{- $nav := index . "NavSection" -}} + + + + + + {{index . "Title"}} + + + + + +
+ {{if $flashSuccess}} +
{{$flashSuccess}}
+ {{end}} + {{if $flashError}} +
{{$flashError}}
+ {{end}} + +
+ {{index . "Content"}} +
+
+ +