package main import ( "context" "errors" "expvar" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/FernandoVideira/LK_API_Temp/docs" // This is required to autogenerate swagger docs "github.com/FernandoVideira/LK_API_Temp/internal/auth" "github.com/FernandoVideira/LK_API_Temp/internal/mailer" "github.com/FernandoVideira/LK_API_Temp/internal/ratelimiter" "github.com/FernandoVideira/LK_API_Temp/internal/store" "github.com/FernandoVideira/LK_API_Temp/internal/store/cache" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" httpSwagger "github.com/swaggo/http-swagger/v2" // http-swagger middleware "go.uber.org/zap" ) type api struct { config config store store.Storage cacheStore cache.Storage logger *zap.SugaredLogger mailer mailer.Client authenticator auth.Authenticator rateLimiter ratelimiter.Limiter } type config struct { addr string db dbConfig env string apiURL string mail mailConfig frontEndURL string auth authConfig redisCfg redisConfig rateLimiter ratelimiter.Config } type mailConfig struct { inviteExp time.Duration passwordRefreshExp time.Duration fromEmail string sendGrid sendGridConfig } type authConfig struct { basic basicConfig token tokenConfig } type basicConfig struct { username string password string } type tokenConfig struct { secret string exp time.Duration refreshExp time.Duration iss string aud string } type redisConfig struct { addr string pw string db int enabled bool } // TODO: Switch to previously made SMTP mailer type sendGridConfig struct { apiKey string } type dbConfig struct { addr string maxOpenConns int maxIdleConns int maxIdleTime time.Duration } // This `mount` function is responsible for setting up the routing for your API // using the Chi router. Here's a breakdown of what it does: func (api *api) mount() http.Handler { r := chi.NewRouter() // *CORS r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"https://*", "http://*"}, //* Use this for development //AllowedOrigins: []string{env.GetList("ALLOWED_ORIGINS", []string{"https://lk-api-temp.onrender.com"})}, //* Use this for production AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, ExposedHeaders: []string{"Link"}, AllowCredentials: false, MaxAge: 300, })) r.Use(api.rateLimiterMiddleware) r.Use(middleware.Timeout(60 * time.Second)) r.Route("/v1", func(r chi.Router) { //r.With(api.BasicAuthMiddleware()).Get("/health", api.healthCheckHandler) r.Get("/health", api.healthCheckHandler) r.With(api.BasicAuthMiddleware()).Get("/debug/vars", expvar.Handler().ServeHTTP) docsURL := fmt.Sprintf("%s/swagger/doc.json", api.config.addr) r.Get("/swagger/*", httpSwagger.Handler(httpSwagger.URL(docsURL))) r.Route("/posts", func(r chi.Router) { r.Use(api.AuthTokenMiddleware) r.Post("/", api.createPostHandler) r.Route("/{id}", func(r chi.Router) { r.Use(api.postsContextMiddleware) r.Get("/", api.getPostHandler) r.Patch("/", api.checkPostOwnership("moderator", api.updatePostHandler)) r.Delete("/", api.checkPostOwnership("admin", api.deletePostHandler)) r.Post("/comments", api.createCommentHandler) }) }) r.Route("/users", func(r chi.Router) { r.Put("/activate/{token}", api.activateUserHandler) r.Put("/reset-password/{token}", api.resetPasswordHandler) r.Route("/me", func(r chi.Router) { r.Use(api.AuthTokenMiddleware) r.Get("/", api.getUserHandler) r.Patch("/", api.checkProfileOwnership(api.updateUserHandler)) }) }) r.Route("/authentication", func(r chi.Router) { r.Post("/register", api.registerUserHandler) r.Post("/token", api.createTokenHandler) r.Post("/forgot-password", api.forgotPasswordHandler) r.Post("/refresh", api.refreshTokenHandler) }) r.Route("/admin", func(r chi.Router) { r.Use(api.AuthTokenMiddleware) r.Route("/users", func(r chi.Router) { r.Get("/", api.protectAdminRoutes("moderator", api.getUsersHandler)) r.Route("/{id}", func(r chi.Router) { //r.Get("/", api.protectAdminRoutes("moderator", api.getUserByIdHandler)) }) }) }) }) return r } func (api *api) run(mux http.Handler) error { //Docs docs.SwaggerInfo.Version = "0.0.1" docs.SwaggerInfo.Host = api.config.apiURL docs.SwaggerInfo.BasePath = "/v1" srv := http.Server{ Addr: api.config.addr, Handler: mux, WriteTimeout: 30 * time.Second, ReadTimeout: 10 * time.Second, IdleTimeout: time.Minute, } shutdown := make(chan error) go func() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) s := <-quit ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() api.logger.Infow("shutting down server", "signal", s.String()) shutdown <- srv.Shutdown(ctx) }() api.logger.Infow("server has started", "addr", api.config.addr, "env", api.config.env) err := srv.ListenAndServe() if !errors.Is(err, http.ErrServerClosed) { return err } err = <-shutdown if err != nil { return err } if userStore, ok := api.store.Users.(*store.UserStore); ok { if err := userStore.Close(); err != nil { api.logger.Errorw("error closing userstore", err) } } api.logger.Infow("server has been shutdown") return nil }