feat: add admin apis

This commit is contained in:
Krispire Admin
2026-06-16 14:47:45 +05:30
parent aa61f4f0b1
commit a49bf7591e
24 changed files with 1164 additions and 22 deletions
+46 -2
View File
@@ -67,6 +67,7 @@ type Server struct {
reminderH *handler.ReminderHandler
feedbackH *handler.FeedbackHandler
statsH *handler.StatsHandler
adminH *handler.AdminHandler
// Auth state (nil when Postgres is unavailable — /auth and /users return 503)
authIssuer *auth.Issuer
@@ -284,6 +285,13 @@ func (s *Server) initAuth(cfg *config.Config, logger *zap.Logger) {
feedbackSvc := service.NewFeedbackService(feedbackRepo, mail, cfg.FeedbackAdminEmails, logger)
s.feedbackH = handler.NewFeedbackHandler(feedbackSvc)
s.statsH = handler.NewStatsHandler(statsRepo, feedbackRepo, feedbackSvc)
// Admin panel — read-mostly listing + notify/reply/resolve actions.
// Reuses the notification service (delivery) and mailer (feedback reply).
// Gated by RequireAuth + RequireAdmin in the router.
adminRepo := repository.NewAdminRepo(s.pg)
adminSvc := service.NewAdminService(adminRepo, notifSvc, mail, logger, cfg.FrontendURL)
s.adminH = handler.NewAdminHandler(adminSvc)
}
// initShortener wires Postgres + Redis + the shorturl handler/workers.
@@ -404,9 +412,17 @@ func (s *Server) setupRouter() {
r.Use(chiMiddleware.Recoverer)
r.Use(chiMiddleware.Compress(5))
// CORS
// CORS — the main frontend plus, when configured, the admin panel (a
// separate origin). Empty origins are filtered so the list never widens
// unintentionally.
allowedOrigins := []string{}
for _, o := range []string{s.cfg.FrontendURL, s.cfg.AdminURL} {
if o != "" {
allowedOrigins = append(allowedOrigins, o)
}
}
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{s.cfg.FrontendURL},
AllowedOrigins: allowedOrigins,
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Request-ID"},
ExposedHeaders: []string{"X-Request-ID"},
@@ -961,6 +977,34 @@ func (s *Server) setupRouter() {
r.Post("/timezone", s.reminderH.SetTimezone)
}
})
// --- Admin panel --- every route gated by RequireAuth + RequireAdmin
// (role == "admin"), so the whole surface is invisible to ordinary
// accounts. Additive: no existing route or payload changes.
r.Route("/admin", func(r chi.Router) {
if s.adminH == nil {
r.HandleFunc("/*", s.authUnavailable)
return
}
r.Use(middleware.RequireAuth(s.authIssuer, s.userRepo))
r.Use(middleware.RequireAdmin(s.userRepo))
r.Get("/users", s.adminH.ListUsers)
r.Post("/users/{id}/notify", s.adminH.NotifyUser)
r.Post("/users/{id}/ban", s.adminH.BanUser)
r.Delete("/users/{id}/ban", s.adminH.UnbanUser)
r.Get("/posts", s.adminH.ListPosts)
r.Post("/posts/{id}/hide", s.adminH.HidePost)
r.Delete("/posts/{id}/hide", s.adminH.UnhidePost)
r.Delete("/posts/{id}", s.adminH.DeletePost)
r.Post("/posts/{id}/restore", s.adminH.RestorePost)
r.Delete("/comments/{id}", s.adminH.DeleteComment)
r.Post("/comments/{id}/restore", s.adminH.RestoreComment)
r.Get("/collections", s.adminH.ListCollections)
r.Get("/feedback", s.adminH.ListFeedback)
r.Post("/feedback/{id}/reply", s.adminH.ReplyFeedback)
r.Get("/reports", s.adminH.ListReports)
r.Post("/reports/{type}/{id}/resolve", s.adminH.ResolveReport)
})
})
s.router = r