298 lines
8.8 KiB
Go
298 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"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"
|
|
"git.ewellenr.ca/receipt_indexer/backend/internal/storage/cache"
|
|
)
|
|
|
|
type application struct {
|
|
// set up the configs stuff here
|
|
config config
|
|
store storage.Storage // for user storage. also stores authentication stuff
|
|
logger logger.Logger // logging events/actions/issues
|
|
cacheStorage cache.Storage // for caching the storage
|
|
rateLimiter ratelimiter.Limiter // rate limiting users
|
|
environment env.Environment // accessing environment variables for the setup
|
|
}
|
|
|
|
type config struct {
|
|
addr string
|
|
storeCfg storeConfig
|
|
envCfg envConfig
|
|
apiUrl string
|
|
frontendUrl string
|
|
auth authConfig
|
|
mail mailConfig
|
|
cacheCfg redisConfig
|
|
rateLimiter ratelimiter.Config
|
|
|
|
// holds the different stuff like rate limiter, store, authenticator
|
|
}
|
|
|
|
type storeConfig struct {
|
|
db dbConfig
|
|
authStore authConfig
|
|
}
|
|
|
|
type redisConfig struct {
|
|
addr string
|
|
pw string
|
|
db int
|
|
enabled bool
|
|
}
|
|
|
|
func (app *application) mount() http.Handler {
|
|
r := chi.NewRouter()
|
|
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.RequestID)
|
|
r.Use(middleware.RealIP)
|
|
r.Use(middleware.CleanPath)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
/// CSRF and SESSION TOKEN STUFF:
|
|
// Store session token in https or whatever only so js can't access it, and the store csrf locally using react or whatever, remove the cookie, and then include it in the http header under 'X-CSRF-Token' or as a hidden input in an html form
|
|
|
|
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) {
|
|
|
|
// SEPERATE STUFF FOR THE LOGIN RELATED STUFF. CONSIDER A GROUP
|
|
|
|
// 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)))
|
|
|
|
// Need to sign in as a user. Then, you can see the groups you're in, your role in the groups,
|
|
|
|
r.Route("/user", func(r chi.Router) {
|
|
r.Route("/{userID}", func(r chi.Router) {
|
|
r.Use(app.AuthSessionMiddleware, app.CSRFCheckMiddleware, app.CheckUserMatchingMiddleware) // consider the actual use of the user matching middleware and whether an admin should be used instead
|
|
|
|
r.Get("/", app.getUserHandler)
|
|
|
|
r.Route("/groups", func(r chi.Router) {
|
|
r.Get("/", app.getUsersGroupsHandler)
|
|
|
|
r.Route("/{groupID}", func(r chi.Router) {
|
|
|
|
r.Use(app.addGroupToContextMiddleware)
|
|
|
|
r.Get("/", app.getUsersGroupHandler)
|
|
r.Delete("/", app.removeUserGroupHandler) // maybe this should expect authentication headers to reverify the password when deleting a group you own.
|
|
|
|
r.Put("/owner", app.setGroupOwnerHandler)
|
|
r.Get("/owner", app.getGroupOwnerHandler)
|
|
|
|
r.Put("/moderator", app.addGroupModeratorHandler)
|
|
r.Delete("/moderator/{secondaryuserID}", app.removeModeratorPriviligesHandler)
|
|
|
|
r.Route("/users", func(r chi.Router) {
|
|
|
|
r.Use(app.CheckGroupRoleMiddleware) // need to verify that they are an owner/moderator
|
|
|
|
r.Get("/", app.getGroupUsersHandler)
|
|
r.Post("/", app.addGroupUserHandler) // needs to create a new user/do the whole invite thing if they don't already exist
|
|
r.Delete("/{secondaryuserID}", app.removeUserFromGroupHandler) // needs to check if the user is an owner/moderator
|
|
})
|
|
|
|
})
|
|
})
|
|
|
|
r.Route("/receipts", func(r chi.Router) {
|
|
r.Use(app.addQueryParamsToContextMiddleware)
|
|
|
|
r.Get("/", app.getReceiptsHandler) // batch get. should allow pagination and potentially a filter of some sort in the request. Does not return images or anything like that. Just the basic data (id, amount, date, location/store)
|
|
|
|
r.Route("/{receiptID}", func(r chi.Router) {
|
|
|
|
r.Use(app.receiptContextMiddleware)
|
|
|
|
r.Get("/", app.getReceiptHandler)
|
|
r.Delete("/", app.deleteReceiptHandler)
|
|
|
|
r.Route("/images", func(r chi.Router) {
|
|
r.Get("/", app.getReceiptImagesHandler)
|
|
r.Post("/", app.addReceiptImageHandler)
|
|
r.Route("/{imageID}", func(r chi.Router) {
|
|
r.Get("/", app.getReceiptImageHandler)
|
|
r.Put("/", app.changeReceiptImageHandler)
|
|
r.Delete("/", app.deleteReceiptImageHandler)
|
|
r.Put("/confirm", app.confirmImageCreationHandler)
|
|
})
|
|
})
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
// r.Use(app.CSRFCheckMiddleware)
|
|
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(app.AuthSessionMiddleware)
|
|
r.Use(app.CSRFCheckMiddleware)
|
|
|
|
r.Route("/groups", func(r chi.Router) {
|
|
r.Get("/", app.getGroupsHandler)
|
|
r.Route("/{groupID}", func(r chi.Router) {
|
|
r.Get("/", app.getGroupHandler)
|
|
})
|
|
})
|
|
|
|
r.Route("/users", func(r chi.Router) {
|
|
r.With(app.CheckRoleMiddleware("admin")).Get("/", app.getUsersHandler)
|
|
|
|
r.Route("/{userID}", func(r chi.Router) {
|
|
|
|
r.With(app.CheckRoleMiddleware("admin")).Delete("/", app.getUserHandler)
|
|
})
|
|
})
|
|
})
|
|
|
|
// 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 {
|
|
|
|
srv := &http.Server{
|
|
Addr: app.config.addr,
|
|
Handler: mux,
|
|
WriteTimeout: time.Second * 30,
|
|
ReadTimeout: time.Second * 10,
|
|
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(), 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
|
|
}
|