164 lines
4.9 KiB
Go
164 lines
4.9 KiB
Go
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"math"
|
|
|
|
"mail_go/config"
|
|
"mail_go/internal/storage"
|
|
"mail_go/internal/store"
|
|
"mail_go/internal/web/handlers"
|
|
"mail_go/internal/web/middleware"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// formatBytes converts a file size in bytes to a human-readable string.
|
|
func formatBytes(b int64) string {
|
|
const unit = 1024
|
|
if b < unit {
|
|
return fmt.Sprintf("%d B", b)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
// WebServer wraps the Gin engine and its dependencies.
|
|
type WebServer struct {
|
|
engine *gin.Engine
|
|
stores *store.Stores
|
|
storage *storage.AttachmentStorage
|
|
cfg config.WebConfig
|
|
}
|
|
|
|
// templateFuncs returns custom template functions for rendering.
|
|
func templateFuncs() template.FuncMap {
|
|
return template.FuncMap{
|
|
"add": func(a, b int) int { return a + b },
|
|
"sub": func(a, b int) int { return a - b },
|
|
"mul": func(a, b int) int { return a * b },
|
|
"div": func(a, b int) int { return a / b },
|
|
"mod": func(a, b int) int { return a % b },
|
|
"ceilDiv": func(a, b int) int { return int(math.Ceil(float64(a) / float64(b))) },
|
|
"seq": func(n int) []int {
|
|
result := make([]int, n)
|
|
for i := 0; i < n; i++ {
|
|
result[i] = i + 1
|
|
}
|
|
return result
|
|
},
|
|
"domainName": func(domainID uint, domains []interface{}) string {
|
|
return fmt.Sprintf("Domain #%d", domainID)
|
|
},
|
|
"safeHTML": func(s string) template.HTML {
|
|
return template.HTML(s)
|
|
},
|
|
"formatBytes": func(b int64) string {
|
|
return formatBytes(b)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewWebServer creates a new WebServer, initializes the Gin engine,
|
|
// configures sessions, middleware, and registers all routes.
|
|
func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storage.AttachmentStorage) *WebServer {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
engine := gin.New()
|
|
engine.Use(gin.Logger())
|
|
engine.Use(gin.Recovery())
|
|
|
|
// Session store (cookie-based)
|
|
cookieStore := cookie.NewStore([]byte("mail-go-secret-key-change-in-production"))
|
|
cookieStore.Options(sessions.Options{
|
|
HttpOnly: true,
|
|
SameSite: 3, // SameSiteLaxMode
|
|
MaxAge: 86400,
|
|
Path: "/",
|
|
})
|
|
engine.Use(sessions.Sessions("mail_go_session", cookieStore))
|
|
|
|
// Load HTML templates with custom functions
|
|
// Note: Go's filepath.Glob doesn't support **, so we load in two passes
|
|
tmpl := template.Must(template.New("").Funcs(templateFuncs()).ParseGlob("internal/web/templates/*.html"))
|
|
template.Must(tmpl.ParseGlob("internal/web/templates/admin/*.html"))
|
|
engine.SetHTMLTemplate(tmpl)
|
|
|
|
ws := &WebServer{
|
|
engine: engine,
|
|
stores: stores,
|
|
storage: attStorage,
|
|
cfg: cfg,
|
|
}
|
|
|
|
ws.registerRoutes()
|
|
return ws
|
|
}
|
|
|
|
// registerRoutes sets up all HTTP routes with their handlers and middleware.
|
|
func (ws *WebServer) registerRoutes() {
|
|
authHandler := handlers.NewAuthHandler(ws.stores)
|
|
mailHandler := handlers.NewMailHandler(ws.stores, ws.storage)
|
|
adminHandler := handlers.NewAdminHandler(ws.stores)
|
|
|
|
// Public routes (no auth required)
|
|
ws.engine.GET("/login", authHandler.ShowLogin)
|
|
ws.engine.POST("/login", authHandler.DoLogin)
|
|
|
|
// Auth-protected routes
|
|
auth := ws.engine.Group("")
|
|
auth.Use(middleware.AuthMiddleware(ws.stores))
|
|
{
|
|
auth.POST("/logout", authHandler.DoLogout)
|
|
auth.GET("/", func(c *gin.Context) {
|
|
c.Redirect(302, "/inbox")
|
|
})
|
|
|
|
// Mail routes
|
|
auth.GET("/inbox", mailHandler.Inbox)
|
|
auth.GET("/inbox/:id", mailHandler.View)
|
|
auth.GET("/compose", mailHandler.Compose)
|
|
auth.POST("/compose", mailHandler.DoSend)
|
|
auth.GET("/drafts", mailHandler.Drafts)
|
|
auth.GET("/drafts/:id", mailHandler.View)
|
|
auth.GET("/sent", mailHandler.Sent)
|
|
auth.GET("/sent/:id", mailHandler.View)
|
|
auth.GET("/settings", mailHandler.Settings)
|
|
auth.POST("/settings", mailHandler.UpdateSettings)
|
|
auth.POST("/mail/delete/:id", mailHandler.Delete)
|
|
auth.POST("/mail/read/:id", mailHandler.MarkRead)
|
|
auth.GET("/attachment/:id", mailHandler.DownloadAttachment)
|
|
}
|
|
|
|
// Admin routes (auth + admin required)
|
|
admin := ws.engine.Group("/admin")
|
|
admin.Use(middleware.AuthMiddleware(ws.stores))
|
|
admin.Use(middleware.AdminMiddleware())
|
|
{
|
|
admin.GET("", adminHandler.Dashboard)
|
|
admin.GET("/", adminHandler.Dashboard)
|
|
admin.GET("/domains", adminHandler.ListDomains)
|
|
admin.GET("/domains/new", adminHandler.NewDomain)
|
|
admin.POST("/domains", adminHandler.CreateDomain)
|
|
admin.POST("/domains/:id/delete", adminHandler.DeleteDomain)
|
|
admin.GET("/domains/:id/dns", adminHandler.DNSHint)
|
|
admin.GET("/users", adminHandler.ListUsers)
|
|
admin.GET("/users/new", adminHandler.NewUser)
|
|
admin.POST("/users", adminHandler.CreateUser)
|
|
admin.POST("/users/:id/delete", adminHandler.DeleteUser)
|
|
admin.GET("/users/:id/edit", adminHandler.EditUser)
|
|
admin.POST("/users/:id", adminHandler.UpdateUser)
|
|
}
|
|
}
|
|
|
|
// Start launches the HTTP server on the configured address.
|
|
func (ws *WebServer) Start() error {
|
|
return ws.engine.Run(ws.cfg.Addr)
|
|
}
|