prompt-5
This commit is contained in:
parent
ae48383dc8
commit
722dd85fc6
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue