package api import ( "RenewCMS/api/handlers/activity" "RenewCMS/api/handlers/apikey" "RenewCMS/api/handlers/article" "RenewCMS/api/handlers/auth" "RenewCMS/api/handlers/author" "RenewCMS/api/handlers/frontend" "RenewCMS/api/handlers/imagevariant" "RenewCMS/api/handlers/media" "RenewCMS/api/handlers/settings" "RenewCMS/api/handlers/site" "RenewCMS/api/handlers/staticimages" "RenewCMS/api/handlers/update" "RenewCMS/api/handlers/user" "RenewCMS/api/middleware" "RenewCMS/internal/infrastructure/config" "encoding/json" "net/http" "time" "github.com/MadAppGang/httplog" "github.com/go-chi/chi/v5" chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/jwtauth/v5" ) func enableLoggingIfDev(r *chi.Mux, routerName string, environment string) { if environment == "development" { r.Use(httplog.LoggerWithName(routerName)) } } func GetHelloWorld(w http.ResponseWriter, _ *http.Request) { msg, _ := json.Marshal(map[string]string{"message": "Hello World"}) _, _ = w.Write(msg) } const ( apiKeyMaxRequests = 120 apiKeyWindow = time.Minute ) func (c *UseCases) apiKeyAuth() func(http.Handler) http.Handler { c.apiKeyAuthOnce.Do(func() { c.apiKeyAuthMiddleware = middleware.APIKeyAuth(c.AuthenticateAPIKeyUseCase, apiKeyMaxRequests, apiKeyWindow) }) return c.apiKeyAuthMiddleware } func NewArticleRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() r.Use(container.apiKeyAuth()) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) articleHandler := article.NewHandler(container.CreateArticleUseCase, container.GetArticleUseCase, container.ListArticlesUseCase, container.DeleteArticleUseCase, container.UpdateArticleUseCase, container.GetUserUseCase) r.Group(func(r chi.Router) { r.Get("/", articleHandler.ListArticles) r.Get("/{id}", articleHandler.GetArticle) }) r.Group(func(r chi.Router) { r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Post("/", articleHandler.PostArticle) r.Delete("/{id}", articleHandler.DeleteArticle) r.Patch("/{id}", articleHandler.UpdateArticleBody) r.Patch("/{id}/publish", articleHandler.PublishArticle) r.Patch("/{id}/unpublish", articleHandler.UnpublishArticle) }) return r } func NewAuthRouter(container *UseCases, isProduction bool, rateLimit config.AuthRateLimitSettings, trustedProxies []string) http.Handler { r := chi.NewRouter() r.Use(jwtauth.Verifier(container.Token)) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) loginHandler := auth.NewLoginHandler(container.Token, container.CreateUserUseCase, container.AuthenticateUserUseCase, isProduction) verificationHandler := auth.NewVerificationHandler(container.VerifyUserUseCase, container.CancelRegistrationUseCase, isProduction) meHandler := auth.NewMeHandler(container.GetUserUseCase, container.GetMyAuthorUseCase) passwordResetHandler := auth.NewPasswordResetHandler(container.RequestPasswordResetUseCase, container.ResetPasswordUseCase) acceptInviteHandler := auth.NewAcceptInviteHandler(container.GetInviteUseCase, container.AcceptInviteUseCase, container.Token, isProduction) r.Post("/register", loginHandler.Register) r.Get("/registration", loginHandler.RegistrationStatus) r.Group(func(r chi.Router) { r.Use(middleware.TrustedProxyRealIP(trustedProxies)) r.Use(middleware.NewRateLimiter(rateLimit.MaxAttempts, rateLimit.Window)) r.Post("/login", loginHandler.Login) }) r.Post("/logout", loginHandler.Logout) r.Group(func(r chi.Router) { r.Use(middleware.TrustedProxyRealIP(trustedProxies)) r.Use(middleware.NewRateLimiter(rateLimit.MaxAttempts, rateLimit.Window)) r.Post("/password-reset", passwordResetHandler.RequestPasswordReset) }) r.Post("/password-reset/confirm", passwordResetHandler.ResetPassword) r.Group(func(r chi.Router) { r.Use(middleware.TrustedProxyRealIP(trustedProxies)) r.Use(middleware.NewRateLimiter(rateLimit.MaxAttempts, rateLimit.Window)) r.Get("/invite", acceptInviteHandler.GetInvite) r.Post("/invite/accept", acceptInviteHandler.AcceptInvite) }) r.Group(func(r chi.Router) { r.Use(middleware.RequireSession) r.Use(jwtauth.Verifier(container.Token)) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Post("/verify", verificationHandler.Verify) r.Delete("/register", verificationHandler.CancelRegistration) }) r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(container.Token)) r.Use(container.apiKeyAuth()) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Get("/me", meHandler.Me) }) return r } func NewUserRouter(container *UseCases, rateLimit config.AuthRateLimitSettings, trustedProxies []string) http.Handler { r := chi.NewRouter() userHandler := user.NewHandler(container.UpdateUserProfileUseCase) inviteHandler := user.NewInviteHandler(container.CreateInviteUseCase, container.CancelInviteUseCase, container.RenewInviteUseCase, container.ListUsersAndInvitesUseCase) r.Use(jwtauth.Verifier(container.Token)) r.Use(container.apiKeyAuth()) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Patch("/me", userHandler.UpdateMe) r.Get("/", inviteHandler.ListUsers) r.Group(func(r chi.Router) { r.Use(middleware.RequireOwner(container.GetUserUseCase)) r.Use(middleware.TrustedProxyRealIP(trustedProxies)) r.Use(middleware.NewRateLimiter(rateLimit.MaxAttempts, rateLimit.Window)) r.Post("/invite", inviteHandler.CreateInvite) r.Delete("/invite/{id}", inviteHandler.CancelInvite) r.Post("/invite/{id}/renew", inviteHandler.RenewInvite) }) return r } func NewSettingsRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() settingsHandler := settings.NewHandler(container.GetSettingsUseCase, container.GetActivityRetentionUseCase, container.UpdateActivityRetentionUseCase) r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(container.Token)) r.Use(container.apiKeyAuth()) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Get("/", settingsHandler.GetSettings) }) r.Group(func(r chi.Router) { r.Use(middleware.RequireSession) r.Use(jwtauth.Verifier(container.Token)) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(middleware.RequireSignedIn) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Use(middleware.RequireOwner(container.GetUserUseCase)) r.Patch("/", settingsHandler.UpdateSettings) }) return r } func NewSiteRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() siteHandler := site.NewHandler(container.ListSitesUseCase, container.CreateSiteUseCase, container.UpdateSiteUseCase, container.SetDefaultSiteUseCase, container.DeleteSiteUseCase) r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(container.Token)) r.Use(container.apiKeyAuth()) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Get("/", siteHandler.List) }) r.Group(func(r chi.Router) { r.Use(middleware.RequireSession) r.Use(jwtauth.Verifier(container.Token)) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(middleware.RequireSignedIn) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Use(middleware.RequireOwner(container.GetUserUseCase)) r.Post("/", siteHandler.Create) r.Patch("/{id}", siteHandler.Update) r.Patch("/{id}/default", siteHandler.SetDefault) r.Delete("/{id}", siteHandler.Delete) }) return r } func NewAuthorRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() authorHandler := author.NewHandler(container.ListAuthorsUseCase, container.CreateAuthorUseCase, container.CreateMyAuthorUseCase, container.UpdateAuthorUseCase, container.DeleteAuthorUseCase, container.GetUserUseCase) r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(container.Token)) r.Use(container.apiKeyAuth()) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Get("/", authorHandler.List) }) r.Group(func(r chi.Router) { r.Use(middleware.RequireSession) r.Use(jwtauth.Verifier(container.Token)) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(middleware.RequireSignedIn) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Post("/me", authorHandler.CreateMine) r.Patch("/{id}", authorHandler.Update) r.Group(func(r chi.Router) { r.Use(middleware.RequireOwnerOrAdmin(container.GetUserUseCase)) r.Post("/", authorHandler.Create) r.Delete("/{id}", authorHandler.Delete) }) }) return r } func NewActivityRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() activityHandler := activity.NewHandler(container.ListActivityUseCase, container.GetActivitySummaryUseCase, container.ExportActivityUseCase) r.Use(middleware.RequireSession) r.Use(jwtauth.Verifier(container.Token)) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(middleware.RequireSignedIn) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Get("/", activityHandler.List) r.Group(func(r chi.Router) { r.Use(middleware.RequireOwnerOrAdmin(container.GetUserUseCase)) r.Get("/summary", activityHandler.Summary) r.Get("/export", activityHandler.Export) }) return r } const ( updateCheckMaxAttempts = 5 updateCheckWindow = time.Minute updateDownloadMaxAttempts = 5 updateDownloadWindow = time.Minute ) func NewSystemRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() updateHandler := update.NewHandler(container.GetUpdateStatusUseCase, container.CheckForUpdateUseCase, container.StartUpdateDownloadUseCase, container.RequestRestartUseCase, container.GetUserUseCase) r.Use(middleware.RequireSession) r.Use(jwtauth.Verifier(container.Token)) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Get("/update", updateHandler.GetStatus) r.Group(func(r chi.Router) { r.Use(middleware.RequireOwner(container.GetUserUseCase)) r.Use(middleware.NewUserRateLimiter(updateCheckMaxAttempts, updateCheckWindow)) r.Post("/update/check", updateHandler.Check) }) r.Group(func(r chi.Router) { r.Use(middleware.RequireOwner(container.GetUserUseCase)) r.Use(middleware.NewUserRateLimiter(updateDownloadMaxAttempts, updateDownloadWindow)) r.Post("/update/download", updateHandler.Download) }) r.Group(func(r chi.Router) { r.Use(middleware.RequireOwner(container.GetUserUseCase)) r.Post("/update/restart", updateHandler.Restart) }) return r } func NewAPIKeyRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() apiKeyHandler := apikey.NewHandler(container.CreateAPIKeyUseCase, container.ListAPIKeysUseCase, container.RevokeAPIKeyUseCase) r.Use(middleware.RequireSession) r.Use(jwtauth.Verifier(container.Token)) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Post("/", apiKeyHandler.Create) r.Get("/", apiKeyHandler.List) r.Delete("/{id}", apiKeyHandler.Revoke) return r } func NewMediaRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() mediaHandler := media.NewHandler(container.CreateImageUseCase, container.ListMediaUseCase, container.RenameMediaUseCase, container.DeleteMediaUseCase, container.MergeMediaUseCase, container.GetMediaUsageUseCase, container.GetPurgeSettingsUseCase, container.UpdatePurgeSettingsUseCase, container.PreviewPurgeUseCase, container.RunPurgeUseCase, container.ListImageVariantsUseCase, container.GenerateImageVariantUseCase, container.GenerateAllImageVariantsUseCase, container.PurgeImageVariantUseCase, container.GenerateVariantsOnUploadUseCase) r.Use(jwtauth.Verifier(container.Token)) r.Use(container.apiKeyAuth()) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(middleware.RequireSignedIn) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Group(func(r chi.Router) { r.Use(middleware.RequireOwnerOrAdmin(container.GetUserUseCase)) r.Get("/purge-settings", mediaHandler.GetPurgeSettings) r.Patch("/purge-settings", mediaHandler.UpdatePurgeSettings) r.Get("/purge-preview", mediaHandler.PreviewPurge) r.Post("/purge", mediaHandler.RunPurge) }) r.Group(func(r chi.Router) { r.Use(middleware.RejectAPIKey) r.Use(middleware.RequireOwnerOrAdmin(container.GetUserUseCase)) r.Get("/{id}/variants", mediaHandler.ListVariants) r.Post("/{id}/variants", mediaHandler.GenerateAllVariants) r.Post("/{id}/variants/{name}", mediaHandler.GenerateVariant) r.Delete("/{id}/variants/{name}", mediaHandler.PurgeVariant) }) r.Get("/", mediaHandler.List) r.Post("/", mediaHandler.Upload) r.Post("/merge", mediaHandler.Merge) r.Patch("/{id}", mediaHandler.Rename) r.Delete("/{id}", mediaHandler.Delete) r.Get("/{id}/usage", mediaHandler.Usage) return r } func NewImageVariantRouter(container *UseCases, trustedProxies []string) http.Handler { r := chi.NewRouter() variantHandler := imagevariant.NewHandler(container.ListVariantsUseCase, container.CreateVariantUseCase, container.UpdateVariantUseCase, container.SetVariantEnabledUseCase, container.DeleteVariantUseCase, container.PurgeVariantUseCase, container.GetVariantCacheStatsUseCase, container.PurgeVariantCacheUseCase) r.Use(jwtauth.Verifier(container.Token)) r.Use(middleware.AuditContext(container.GetUserUseCase, trustedProxies)) r.Use(middleware.RequireSignedIn) r.Use(jwtauth.Authenticator(container.Token)) r.Use(middleware.RequireCurrentToken(container.GetUserUseCase)) r.Use(middleware.RequireVerifiedUser(container.GetUserUseCase)) r.Use(middleware.RequireOwnerOrAdmin(container.GetUserUseCase)) r.Get("/", variantHandler.List) r.Post("/", variantHandler.Create) r.Get("/cache", variantHandler.CacheStats) r.Post("/cache/purge", variantHandler.PurgeCache) r.Put("/{name}", variantHandler.Update) r.Patch("/{name}", variantHandler.SetEnabled) r.Delete("/{name}", variantHandler.Delete) r.Post("/{name}/purge", variantHandler.Purge) return r } func InitBackendRoutes(container *UseCases, settings config.Settings) *chi.Mux { r := chi.NewRouter() enableLoggingIfDev(r, "backend", settings.Environment) r.Use(chimiddleware.GetHead) r.Use(middleware.JsonContentTypeMiddleware) r.Get("/", GetHelloWorld) r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(container.Token)) r.Mount("/article", NewArticleRouter(container, settings.TrustedProxies)) }) r.Mount("/user", NewUserRouter(container, settings.AuthRateLimit, settings.TrustedProxies)) r.Mount("/settings", NewSettingsRouter(container, settings.TrustedProxies)) r.Mount("/site", NewSiteRouter(container, settings.TrustedProxies)) r.Mount("/author", NewAuthorRouter(container, settings.TrustedProxies)) r.Mount("/activity", NewActivityRouter(container, settings.TrustedProxies)) r.Mount("/media", NewMediaRouter(container, settings.TrustedProxies)) r.Mount("/image-variants", NewImageVariantRouter(container, settings.TrustedProxies)) r.Mount("/api-key", NewAPIKeyRouter(container, settings.TrustedProxies)) r.Mount("/system", NewSystemRouter(container, settings.TrustedProxies)) r.Mount("/auth", NewAuthRouter(container, settings.IsProduction(), settings.AuthRateLimit, settings.TrustedProxies)) return r } func InitFrontendRoutes(environment string) *chi.Mux { r := chi.NewRouter() enableLoggingIfDev(r, "frontend", environment) r.Mount("/", frontend.NewFrontendRouter()) return r } func GetHealth(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ok")) } func enableCorsIfConfigured(r *chi.Mux, origins []string) { if len(origins) == 0 { return } r.Use(cors.Handler(cors.Options{ AllowedOrigins: origins, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, ExposedHeaders: []string{"Link"}, AllowCredentials: false, MaxAge: 300, })) } func InitRoutes(container *UseCases, settings config.Settings) *chi.Mux { backend := InitBackendRoutes(container, settings) frontend := InitFrontendRoutes(settings.Environment) apiRouter := chi.NewRouter() apiRouter.Use(middleware.SecurityHeaders(settings.IsProduction())) enableCorsIfConfigured(apiRouter, settings.CorsAllowedOrigins) apiRouter.Use(chimiddleware.GetHead) apiRouter.Get("/healthz", GetHealth) apiRouter.Mount("/v1", backend) apiRouter.Mount("/static/uploadedImages", staticimages.NewHandler(container.OpenMediaUseCase, container.ServeVariantUseCase)) apiRouter.Mount("/", frontend) return apiRouter }