This commit is contained in:
lfirmin
2026-05-10 18:07:51 +02:00
parent 50af2452b1
commit a5f888b3c1
59 changed files with 6682 additions and 108 deletions
+208
View File
@@ -0,0 +1,208 @@
package panel
import (
"context"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/leolionad58/ticketbot/internal/config"
"github.com/leolionad58/ticketbot/internal/db"
panelauth "github.com/leolionad58/ticketbot/internal/panel/auth"
"github.com/leolionad58/ticketbot/internal/panel/handlers"
)
// Server is the HTTP panel server.
type Server struct {
http *http.Server
}
// NewServer builds the panel HTTP server with all routes wired up.
func NewServer(
cfg config.PanelWebConfig,
sqlDB *db.TicketRepo,
adminRepo *db.PanelAdminRepo,
sessionRepo *db.PanelSessionRepo,
auditRepo *db.AuditLogRepo,
panelRepo *db.PanelConfigRepo,
convocRepo *db.ConvocationConfigRepo,
guildID string,
bot BotService,
) *Server {
authSvc := panelauth.NewService(adminRepo, sessionRepo, cfg.SessionTimeoutMinutes)
authMw := panelauth.NewMiddleware(authSvc, adminRepo)
oauthCfg := panelauth.NewOAuthConfig(
cfg.OAuthClientID,
cfg.OAuthClientSecret,
cfg.BaseURL+"/oauth/callback",
)
rnd := handlers.NewRenderer(ParseTemplates)
if bot != nil {
rnd.SetBotStatus(bot.GetBotStatus)
}
loginH := &handlers.LoginHandler{
Admins: adminRepo,
Auth: authSvc,
OAuthCfg: oauthCfg,
Renderer: rnd,
}
authH := &handlers.AuthHandler{
Admins: adminRepo,
Auth: authSvc,
AuditLog: auditRepo,
Issuer: "TicketBot",
Renderer: rnd,
}
var getBotStatus func() handlers.BotStatus
if bot != nil {
getBotStatus = bot.GetBotStatus
}
dashH := &handlers.DashboardHandler{
TicketRepo: sqlDB,
PanelRepo: panelRepo,
GetBotStatus: getBotStatus,
Renderer: rnd,
}
var discordInfo handlers.DiscordInfo
if bot != nil {
if di, ok := bot.(handlers.DiscordInfo); ok {
discordInfo = di
}
}
panelsH := &handlers.PanelsHandler{
PanelRepo: panelRepo,
AuditLog: auditRepo,
BotService: bot,
DiscordInfo: discordInfo,
Renderer: rnd,
}
var convocBot handlers.ConvocBotService
var convocInfo handlers.ConvocDiscordInfo
if bot != nil {
convocBot = bot
if di, ok := bot.(handlers.ConvocDiscordInfo); ok {
convocInfo = di
}
}
convocH := &handlers.ConvocationsHandler{
TicketRepo: sqlDB,
ConvocRepo: convocRepo,
GuildID: guildID,
BotService: convocBot,
DiscordInfo: convocInfo,
Renderer: rnd,
}
transcriptsH := &handlers.TranscriptsHandler{
TicketRepo: sqlDB,
Renderer: rnd,
DiscordInfo: discordInfo,
}
auditH := &handlers.AuditHandler{
AuditRepo: auditRepo,
Renderer: rnd,
}
sessH := &handlers.SessionsHandler{
SessionRepo: sessionRepo,
AdminRepo: adminRepo,
AuditLog: auditRepo,
Renderer: rnd,
}
r := chi.NewRouter()
r.Use(chimiddleware.Recoverer)
r.Use(securityHeaders)
// Static files
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(StaticFS())))
// Transcript view — served without CSP headers (must be outside securityHeaders group)
r.Get("/transcripts/view/{id}", transcriptsH.HandleView)
// Public auth routes
r.Get("/login", loginH.HandleLogin)
r.Get("/oauth/callback", loginH.HandleCallback)
r.Get("/auth/password-setup", authH.HandlePasswordSetupGET)
r.Post("/auth/password-setup", authH.HandlePasswordSetupPOST)
r.Get("/auth/totp-setup", authH.HandleTOTPSetupGET)
r.Post("/auth/totp-setup", authH.HandleTOTPSetupPOST)
r.Get("/auth/verify", authH.HandleVerifyGET)
r.Post("/auth/verify", authH.HandleVerifyPOST)
r.Post("/auth/logout", authH.HandleLogout)
// Protected routes
r.Group(func(r chi.Router) {
r.Use(authMw.RequireAuth)
r.Get("/", dashH.Handle)
// Panels CRUD
r.Get("/panels", panelsH.HandleList)
r.Get("/panels/new", panelsH.HandleNew)
r.Post("/panels/new", panelsH.HandleCreate)
r.Post("/panels/preview", panelsH.HandlePreview)
r.Get("/panels/{id}/edit", panelsH.HandleEdit)
r.Post("/panels/{id}/edit", panelsH.HandleUpdate)
r.Post("/panels/{id}/delete", panelsH.HandleDelete)
r.Post("/panels/{id}/duplicate", panelsH.HandleDuplicate)
r.Post("/panels/{id}/types/reorder", panelsH.HandleReorderTypes)
// Convocations
r.Get("/convocations", convocH.HandleGET)
r.Post("/convocations", convocH.HandlePOST)
r.Post("/convocations/send-panel", convocH.HandleSendPanel)
// Transcripts (list + download only; view is outside securityHeaders)
r.Get("/transcripts", transcriptsH.HandleList)
r.Get("/transcripts/download/{id}", transcriptsH.HandleDownload)
// Audit log
r.Get("/audit", auditH.HandleList)
// Sessions (superadmin only)
r.Group(func(r chi.Router) {
r.Use(authMw.RequireSuperadmin)
r.Get("/sessions", sessH.HandleList)
r.Post("/sessions/{id}/revoke", sessH.HandleRevoke)
r.Post("/sessions/revoke-all", sessH.HandleRevokeAll)
})
})
addr := fmt.Sprintf(":%d", cfg.Port)
return &Server{
http: &http.Server{
Addr: addr,
Handler: r,
},
}
}
// Start runs the HTTP server until ctx is cancelled.
func (s *Server) Start(ctx context.Context) error {
errCh := make(chan error, 1)
go func() { errCh <- s.http.ListenAndServe() }()
select {
case <-ctx.Done():
return s.http.Shutdown(context.Background())
case err := <-errCh:
return err
}
}
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.tailwindcss.com https://cdn.jsdelivr.net; "+
"style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; "+
"img-src 'self' https://cdn.discordapp.com data:; frame-src 'self'")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "0")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
next.ServeHTTP(w, r)
})
}