diff --git a/backend/cmd/api/api.go b/backend/cmd/api/api.go index 7e7d316..4bff235 100644 --- a/backend/cmd/api/api.go +++ b/backend/cmd/api/api.go @@ -1,30 +1,151 @@ package main import ( - "log" + "context" + "errors" "net/http" + "os" + "os/signal" + "syscall" "time" - "git.ewellenr.ca/receipt_indexer/backend/internal/auth" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/httprate" + + // "git.ewellenr.ca/receipt_indexer/backend/internal/auth" + "git.ewellenr.ca/receipt_indexer/backend/internal/env" + "git.ewellenr.ca/receipt_indexer/backend/internal/logger" + "git.ewellenr.ca/receipt_indexer/backend/internal/ratelimiter" + "git.ewellenr.ca/receipt_indexer/backend/internal/storage" + auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth" + "git.ewellenr.ca/receipt_indexer/backend/internal/storage/cache" ) type application struct { // set up the configs stuff here - config config - auth auth.Authenticator + config config + auth auth_storage.AuthStorage + store storage.Storage + logger logger.Logger + cacheStorage cache.Storage + rateLimiter ratelimiter.Limiter + environment env.Environment } type config struct { - addr string + addr string + rateLimiter ratelimiter.Config + redisCfg redisConfig + // holds the different stuff like rate limiter, store, authenticator } -func (app *application) mount() *http.ServeMux { - mux := http.NewServeMux() +type redisConfig struct { + addr string + pw string + db int + enabled bool +} - mux.HandleFunc("GET /v1/health", app.healthCheckHandler) +func (app *application) mount() http.Handler { + r := chi.NewRouter() - return mux + r.Use(middleware.Logger) + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.CleanPath) + r.Use(middleware.Recoverer) + + r.Use(middleware.Throttle(100)) // temporary or removable. throttles the whole thing to 1000 concurrent requests + // r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{env.GetString("CORS_ALLOWED_ORIGIN", "http://localhost:5174")}, + // AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + // AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + // ExposedHeaders: []string{"Link"}, + // AllowCredentials: false, + // MaxAge: 300, // Maximum value not ignored by any of major browsers + // })) + + // use either the chi built rate limiter or the custom built one + if app.config.rateLimiter.Enabled { + // r.Use(app.RateLimiterMiddleware) + r.Use(httprate.LimitByRealIP(app.config.rateLimiter.RequestsPerTimeFrame, app.config.rateLimiter.TimeFrame)) + } + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Use(middleware.Heartbeat("/ping")) + + // v1 of api + r.Route("/v1", func(r chi.Router) { + + // FIX API. NEED TO ALSO CONSIDER GROUPS AND STUFF + // Operations + // r.Get("/health", app.healthCheckHandler) + // r.With(app.BasicAuthMiddleware()).Get("/debug/vars", expvar.Handler().ServeHTTP) + + // docsURL := fmt.Sprintf("%s/swagger/doc.json", app.config.addr) + // r.Get("/swagger/*", httpSwagger.Handler(httpSwagger.URL(docsURL))) + + r.Route("/users", func(r chi.Router) { + // r.Put("/activate/{token}", app.activateUserHandler) + + // r.Get("/", app.check) + + r.Route("/{userID}", func(r chi.Router) { + r.Use(app.AuthSessionMiddleware) + r.Use(app.CSRFCheckMiddleware) + r.Use(app.CheckUserMatchingMiddleware) + + r.Get("/", app.getUserHandler) + + r.Route("/receipts", func(r chi.Router) { + r.With(app.Paginate).Get("/", app.getReceiptsHandler) + + r.Post("/", app.createReceiptHandler) + + r.Route("/{receiptID}", func(r chi.Router) { + r.Use(app.receiptsContextMiddleware) + + r.Get("/", app.getReceiptHandler) + r.Patch("/", app.updateReceiptHandler) + r.Delete("/", app.checkReceiptOwnership("admin", app.deleteReceiptHandler)) + + r.Route("/images", func(r chi.Router) { + r.Post("/", app.addImageHandler) + r.Delete("/{imageID}", app.deleteImageHandler) + }) + }) + }) + }) + + }) + + // Admin page routes + r.Route("/admin", func(r chi.Router) { + r.Use(app.AuthSessionMiddleware) + r.Use(app.CheckRoleMiddleware("admin")) + + r.Route("/users", func(r chi.Router) { + r.Get("/", app.getUsersHandler) + r.Delete("/{userID}", app.deleteUserHandler) + }) + + }) + + // Public routes + r.Route("/auth", func(r chi.Router) { + r.Post("/login", app.loginHandler) + r.Post("/newuser", app.registerUserHandler) + r.Post("/refreshtoken", app.refreshTokenHandler) + r.Post("/logout", app.logoutHandler) + }) + }) + + return r } func (app *application) run(mux http.Handler) error { @@ -32,12 +153,41 @@ func (app *application) run(mux http.Handler) error { srv := &http.Server{ Addr: app.config.addr, Handler: mux, - WriteTimeout: time.Second * 30, // if the server takes this much time to read or write, time it out. + WriteTimeout: time.Second * 30, ReadTimeout: time.Second * 10, IdleTimeout: time.Minute, } - log.Printf("Server has started at %s", app.config.addr) // temporary + shutdown := make(chan error) - return srv.ListenAndServe() + go func() { + quit := make(chan os.Signal, 1) + + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + s := <-quit + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + app.logger.Info("signal caught", "signal", s.String()) + + shutdown <- srv.Shutdown(ctx) + }() + + app.logger.Info("server has started", "addr", app.config.addr) //, "env", app.config.env) + + err := srv.ListenAndServe() + + if !errors.Is(err, http.ErrServerClosed) { + return err + } + + err = <-shutdown + if err != nil { + return err + } + + app.logger.Info("server has stopped", "addr", app.config.addr) //, "env", app.config.env) + + return nil }