mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-06 02:36:49 +00:00
Some checks failed
CI / checks (push) Has been cancelled
Refactor booking retrieval to return errors instead of nil values, enabling more robust error handling throughout the booking, payment, and PDF endpoints. Add custom HTTP error page rendering for not found and internal server errors. Update interfaces and tests to match new method signatures. This improves user feedback and code maintainability.
156 lines
3.9 KiB
Go
156 lines
3.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"time"
|
|
|
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/cors"
|
|
|
|
"github.com/rjNemo/rentease/internal/config"
|
|
"github.com/rjNemo/rentease/internal/service/auth"
|
|
"github.com/rjNemo/rentease/internal/service/booking"
|
|
)
|
|
|
|
type Server struct {
|
|
Router *chi.Mux
|
|
httpServer *http.Server
|
|
bs *booking.Service
|
|
as *auth.Service
|
|
hc *config.Host
|
|
addr string
|
|
stripeWebhookSecret string
|
|
}
|
|
|
|
func New(bs *booking.Service, as *auth.Service, hc *config.Host, opts ...Option) (*Server, error) {
|
|
option := new(options)
|
|
for _, opt := range opts {
|
|
if err := opt(option); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
router, err := NewRouter(*option.fs, *option.debug, option.origins)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s := &Server{
|
|
Router: router,
|
|
bs: bs,
|
|
as: as,
|
|
hc: hc,
|
|
addr: fmt.Sprintf("0.0.0.0:%d", *option.port),
|
|
stripeWebhookSecret: "",
|
|
}
|
|
|
|
if option.stripeWebhookSecret != nil {
|
|
s.stripeWebhookSecret = *option.stripeWebhookSecret
|
|
}
|
|
|
|
s.MountHandlers()
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Server) Start(ctx context.Context) {
|
|
s.httpServer = &http.Server{
|
|
Addr: s.addr,
|
|
Handler: s.Router,
|
|
}
|
|
|
|
go func() {
|
|
if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
slog.Error("shutting down the server", slog.Any("error", err))
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, os.Interrupt)
|
|
defer signal.Stop(quit)
|
|
|
|
select {
|
|
case <-quit:
|
|
case <-ctx.Done():
|
|
}
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := s.httpServer.Shutdown(shutdownCtx); err != nil {
|
|
slog.Error("server shutdown failed", slog.Any("error", err))
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func NewRouter(filesystem embed.FS, debug bool, origins []string) (*chi.Mux, error) {
|
|
r := chi.NewRouter()
|
|
_ = debug
|
|
|
|
r.Use(middleware.RequestID)
|
|
r.Use(middleware.RealIP)
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.Recoverer)
|
|
r.Use(middleware.Compress(5))
|
|
r.Use(CachingMiddleware(0, "js", "css", "png", "ico"))
|
|
|
|
if len(origins) == 0 {
|
|
origins = []string{"*"}
|
|
}
|
|
|
|
r.Use(cors.Handler(cors.Options{
|
|
AllowedOrigins: origins,
|
|
AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodOptions},
|
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "HX-Request", "HX-Boosted", "api-key"},
|
|
ExposedHeaders: []string{"HX-Redirect"},
|
|
AllowCredentials: true,
|
|
}))
|
|
|
|
sentryHandler := sentryhttp.New(sentryhttp.Options{
|
|
Repanic: true,
|
|
})
|
|
r.Use(sentryHandler.Handle)
|
|
r.Use(SentryTracingMiddleware)
|
|
|
|
assetsFS, err := fs.Sub(filesystem, "assets")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load static assets: %w", err)
|
|
}
|
|
|
|
fileServer := http.StripPrefix("/static/", http.FileServer(http.FS(assetsFS)))
|
|
r.Handle("/static/*", fileServer)
|
|
|
|
notFoundHandler, err := newHTTPErrorHandler(filesystem, http.StatusNotFound)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to setup not found handler: %w", err)
|
|
}
|
|
r.NotFound(notFoundHandler)
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func newHTTPErrorHandler(filesystem embed.FS, statusCode int) (http.HandlerFunc, error) {
|
|
filePath := fmt.Sprintf("assets/html/HTTP%d.html", statusCode)
|
|
page, err := fs.ReadFile(filesystem, filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read error page %s: %w", filePath, err)
|
|
}
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.WriteHeader(statusCode)
|
|
if _, err := w.Write(page); err != nil {
|
|
slog.Error("failed to write error page", slog.Any("error", err))
|
|
}
|
|
}, nil
|
|
}
|