rentease/internal/server/server.go
Ruidy 146787033a
refactor(payment): extract payment logic to new service
Moves all payment-related logic (manual payments, Stripe sync, webhook
handling) from the booking service into a dedicated payment service
(`internal/service/payment`). Updates server, cron, and handler wiring
to
inject and use the new payment service. Adjusts tests, routes, and
documentation to reflect the new separation of concerns.

This improves cohesion, clarifies responsibilities, and prepares for
future payment features. No database schema changes are introduced.
2025-11-21 10:09:30 +01:00

159 lines
4 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"
"github.com/rjNemo/rentease/internal/service/payment"
)
type Server struct {
Router *chi.Mux
httpServer *http.Server
bs *booking.Service
ps *payment.Service
as *auth.Service
hc *config.Host
addr string
stripeWebhookSecret string
}
func New(bs *booking.Service, ps *payment.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,
ps: ps,
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
}