This commit is contained in:
fabio 2026-02-22 17:43:04 +01:00
parent ae48383dc8
commit 722dd85fc6
9 changed files with 383 additions and 1 deletions

14
codex-prompt/prompt-5.txt Normal file
View File

@ -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.

View File

View File

@ -28,8 +28,10 @@ func NewApp(cfg *config.Config) (*fiber.App, error) {
})) }))
store := session.New(session.Config{ store := session.New(session.Config{
KeyLookup: "cookie:" + cfg.SessionKey,
CookieHTTPOnly: true, CookieHTTPOnly: true,
CookieSecure: cfg.Env == config.EnvProd, CookieSecure: cfg.Env == config.EnvProd,
CookieSameSite: fiber.CookieSameSiteLaxMode,
}) })
database, err := db.Open(cfg) database, err := db.Open(cfg)

View File

@ -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()
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -2,14 +2,45 @@ package http
import ( import (
"trustcontact/internal/config" "trustcontact/internal/config"
httpmw "trustcontact/internal/http/middleware"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/session" "github.com/gofiber/fiber/v2/middleware/session"
"gorm.io/gorm" "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 { app.Get("/healthz", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK) 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")
})
} }

View File

@ -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
}

42
web/templates/layout.html Normal file
View File

@ -0,0 +1,42 @@
{{- $flashSuccess := index . "FlashSuccess" -}}
{{- $flashError := index . "FlashError" -}}
{{- $nav := index . "NavSection" -}}
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{index . "Title"}}</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f7fb; color: #1f2937; }
nav { background: #111827; color: #fff; padding: 12px 16px; display: flex; gap: 12px; }
nav a { color: #e5e7eb; text-decoration: none; }
nav a.active { color: #fff; font-weight: 600; }
.container { max-width: 960px; margin: 20px auto; padding: 0 16px; }
.flash { padding: 12px 14px; border-radius: 8px; margin-bottom: 12px; }
.flash.success { background: #dcfce7; color: #166534; }
.flash.error { background: #fee2e2; color: #991b1b; }
main { background: #fff; border-radius: 10px; padding: 20px; }
</style>
</head>
<body>
<nav>
<a href="/" class="{{if eq $nav "public"}}active{{end}}">Public</a>
<a href="/private" class="{{if eq $nav "private"}}active{{end}}">Private</a>
<a href="/admin" class="{{if eq $nav "admin"}}active{{end}}">Admin</a>
</nav>
<div class="container">
{{if $flashSuccess}}
<div class="flash success">{{$flashSuccess}}</div>
{{end}}
{{if $flashError}}
<div class="flash error">{{$flashError}}</div>
{{end}}
<main>
{{index . "Content"}}
</main>
</div>
</body>
</html>