oups
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user