Features: - PDF service for generating task reports with ReportLab-style formatting - Storage service for file uploads (local and S3-compatible) - Admin authentication middleware with JWT support - Admin user model and repository Infrastructure: - Updated Docker configuration for admin panel builds - Email service enhancements for task notifications - Updated router with admin and file upload routes - Environment configuration updates Tests: - Unit tests for handlers (auth, residence, task) - Unit tests for models (user, residence, task) - Unit tests for repositories (user, residence, task) - Unit tests for services (residence, task) - Integration test setup - Test utilities for mocking database and services Database: - Admin user seed data - Updated test data seeds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
357 lines
14 KiB
Go
357 lines
14 KiB
Go
package router
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/treytartt/mycrib-api/internal/admin"
|
|
"github.com/treytartt/mycrib-api/internal/config"
|
|
"github.com/treytartt/mycrib-api/internal/handlers"
|
|
"github.com/treytartt/mycrib-api/internal/middleware"
|
|
"github.com/treytartt/mycrib-api/internal/push"
|
|
"github.com/treytartt/mycrib-api/internal/repositories"
|
|
"github.com/treytartt/mycrib-api/internal/services"
|
|
"github.com/treytartt/mycrib-api/pkg/utils"
|
|
)
|
|
|
|
const Version = "2.0.0"
|
|
|
|
// Dependencies holds all dependencies needed by the router
|
|
type Dependencies struct {
|
|
DB *gorm.DB
|
|
Cache *services.CacheService
|
|
Config *config.Config
|
|
EmailService *services.EmailService
|
|
PDFService *services.PDFService
|
|
PushClient interface{} // *push.GorushClient - optional
|
|
StorageService *services.StorageService
|
|
}
|
|
|
|
// SetupRouter creates and configures the Gin router
|
|
func SetupRouter(deps *Dependencies) *gin.Engine {
|
|
cfg := deps.Config
|
|
|
|
// Set Gin mode based on debug setting
|
|
if cfg.Server.Debug {
|
|
gin.SetMode(gin.DebugMode)
|
|
} else {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
|
|
r := gin.New()
|
|
|
|
// Global middleware
|
|
r.Use(utils.GinRecovery())
|
|
r.Use(utils.GinLogger())
|
|
r.Use(corsMiddleware(cfg))
|
|
|
|
// Health check endpoint (no auth required)
|
|
r.GET("/api/health/", healthCheck)
|
|
|
|
// Serve static files from uploads directory
|
|
if cfg.Storage.UploadDir != "" {
|
|
r.Static("/uploads", cfg.Storage.UploadDir)
|
|
}
|
|
|
|
// Initialize repositories
|
|
userRepo := repositories.NewUserRepository(deps.DB)
|
|
residenceRepo := repositories.NewResidenceRepository(deps.DB)
|
|
taskRepo := repositories.NewTaskRepository(deps.DB)
|
|
contractorRepo := repositories.NewContractorRepository(deps.DB)
|
|
documentRepo := repositories.NewDocumentRepository(deps.DB)
|
|
notificationRepo := repositories.NewNotificationRepository(deps.DB)
|
|
subscriptionRepo := repositories.NewSubscriptionRepository(deps.DB)
|
|
|
|
// Initialize push client (optional)
|
|
var gorushClient *push.GorushClient
|
|
if deps.PushClient != nil {
|
|
if gc, ok := deps.PushClient.(*push.GorushClient); ok {
|
|
gorushClient = gc
|
|
}
|
|
}
|
|
|
|
// Initialize services
|
|
authService := services.NewAuthService(userRepo, cfg)
|
|
userService := services.NewUserService(userRepo)
|
|
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
|
|
residenceService.SetTaskRepository(taskRepo) // Wire up task repo for statistics
|
|
taskService := services.NewTaskService(taskRepo, residenceRepo)
|
|
contractorService := services.NewContractorService(contractorRepo, residenceRepo)
|
|
documentService := services.NewDocumentService(documentRepo, residenceRepo)
|
|
notificationService := services.NewNotificationService(notificationRepo, gorushClient)
|
|
|
|
// Wire up notification and email services to task service (for task completion notifications)
|
|
taskService.SetNotificationService(notificationService)
|
|
taskService.SetEmailService(deps.EmailService)
|
|
subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
|
|
|
|
// Initialize middleware
|
|
authMiddleware := middleware.NewAuthMiddleware(deps.DB, deps.Cache)
|
|
|
|
// Initialize handlers
|
|
authHandler := handlers.NewAuthHandler(authService, deps.EmailService, deps.Cache)
|
|
userHandler := handlers.NewUserHandler(userService)
|
|
residenceHandler := handlers.NewResidenceHandler(residenceService, deps.PDFService, deps.EmailService)
|
|
taskHandler := handlers.NewTaskHandler(taskService, deps.StorageService)
|
|
contractorHandler := handlers.NewContractorHandler(contractorService)
|
|
documentHandler := handlers.NewDocumentHandler(documentService, deps.StorageService)
|
|
notificationHandler := handlers.NewNotificationHandler(notificationService)
|
|
subscriptionHandler := handlers.NewSubscriptionHandler(subscriptionService)
|
|
staticDataHandler := handlers.NewStaticDataHandler(residenceService, taskService, contractorService)
|
|
|
|
// Initialize upload handler (if storage service is available)
|
|
var uploadHandler *handlers.UploadHandler
|
|
if deps.StorageService != nil {
|
|
uploadHandler = handlers.NewUploadHandler(deps.StorageService)
|
|
}
|
|
|
|
// Set up admin routes (separate auth system)
|
|
adminDeps := &admin.Dependencies{
|
|
EmailService: deps.EmailService,
|
|
}
|
|
if deps.PushClient != nil {
|
|
if gc, ok := deps.PushClient.(*push.GorushClient); ok {
|
|
adminDeps.PushClient = gc
|
|
}
|
|
}
|
|
admin.SetupRoutes(r, deps.DB, cfg, adminDeps)
|
|
|
|
// API group
|
|
api := r.Group("/api")
|
|
{
|
|
// Public auth routes (no auth required)
|
|
setupPublicAuthRoutes(api, authHandler)
|
|
|
|
// Public data routes (no auth required)
|
|
setupPublicDataRoutes(api, residenceHandler, taskHandler, contractorHandler, staticDataHandler)
|
|
|
|
// Protected routes (auth required)
|
|
protected := api.Group("")
|
|
protected.Use(authMiddleware.TokenAuth())
|
|
{
|
|
setupProtectedAuthRoutes(protected, authHandler)
|
|
setupResidenceRoutes(protected, residenceHandler)
|
|
setupTaskRoutes(protected, taskHandler)
|
|
setupContractorRoutes(protected, contractorHandler)
|
|
setupDocumentRoutes(protected, documentHandler)
|
|
setupNotificationRoutes(protected, notificationHandler)
|
|
setupSubscriptionRoutes(protected, subscriptionHandler)
|
|
setupUserRoutes(protected, userHandler)
|
|
|
|
// Upload routes (only if storage service is configured)
|
|
if uploadHandler != nil {
|
|
setupUploadRoutes(protected, uploadHandler)
|
|
}
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// corsMiddleware configures CORS - allowing all origins for API access
|
|
func corsMiddleware(cfg *config.Config) gin.HandlerFunc {
|
|
return cors.New(cors.Config{
|
|
AllowAllOrigins: true,
|
|
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
|
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"},
|
|
ExposeHeaders: []string{"Content-Length"},
|
|
AllowCredentials: false, // Must be false when AllowAllOrigins is true
|
|
MaxAge: 12 * time.Hour,
|
|
})
|
|
}
|
|
|
|
// healthCheck returns API health status
|
|
func healthCheck(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "healthy",
|
|
"version": Version,
|
|
"framework": "Gin",
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// setupPublicAuthRoutes configures public authentication routes
|
|
func setupPublicAuthRoutes(api *gin.RouterGroup, authHandler *handlers.AuthHandler) {
|
|
auth := api.Group("/auth")
|
|
{
|
|
auth.POST("/login/", authHandler.Login)
|
|
auth.POST("/register/", authHandler.Register)
|
|
auth.POST("/forgot-password/", authHandler.ForgotPassword)
|
|
auth.POST("/verify-reset-code/", authHandler.VerifyResetCode)
|
|
auth.POST("/reset-password/", authHandler.ResetPassword)
|
|
}
|
|
}
|
|
|
|
// setupProtectedAuthRoutes configures protected authentication routes
|
|
func setupProtectedAuthRoutes(api *gin.RouterGroup, authHandler *handlers.AuthHandler) {
|
|
auth := api.Group("/auth")
|
|
{
|
|
auth.POST("/logout/", authHandler.Logout)
|
|
auth.GET("/me/", authHandler.CurrentUser)
|
|
auth.PUT("/profile/", authHandler.UpdateProfile)
|
|
auth.PATCH("/profile/", authHandler.UpdateProfile)
|
|
auth.POST("/verify-email/", authHandler.VerifyEmail)
|
|
auth.POST("/resend-verification/", authHandler.ResendVerification)
|
|
}
|
|
}
|
|
|
|
// setupPublicDataRoutes configures public data routes (lookups, static data)
|
|
func setupPublicDataRoutes(api *gin.RouterGroup, residenceHandler *handlers.ResidenceHandler, taskHandler *handlers.TaskHandler, contractorHandler *handlers.ContractorHandler, staticDataHandler *handlers.StaticDataHandler) {
|
|
// Static data routes (public, cached)
|
|
staticData := api.Group("/static_data")
|
|
{
|
|
staticData.GET("/", staticDataHandler.GetStaticData)
|
|
staticData.POST("/refresh/", staticDataHandler.RefreshStaticData)
|
|
}
|
|
|
|
// Lookup routes (public)
|
|
api.GET("/residences/types/", residenceHandler.GetResidenceTypes)
|
|
api.GET("/tasks/categories/", taskHandler.GetCategories)
|
|
api.GET("/tasks/priorities/", taskHandler.GetPriorities)
|
|
api.GET("/tasks/frequencies/", taskHandler.GetFrequencies)
|
|
api.GET("/tasks/statuses/", taskHandler.GetStatuses)
|
|
api.GET("/contractors/specialties/", contractorHandler.GetSpecialties)
|
|
}
|
|
|
|
// setupResidenceRoutes configures residence routes
|
|
func setupResidenceRoutes(api *gin.RouterGroup, residenceHandler *handlers.ResidenceHandler) {
|
|
residences := api.Group("/residences")
|
|
{
|
|
residences.GET("/", residenceHandler.ListResidences)
|
|
residences.POST("/", residenceHandler.CreateResidence)
|
|
residences.GET("/my-residences/", residenceHandler.GetMyResidences)
|
|
residences.POST("/join-with-code/", residenceHandler.JoinWithCode)
|
|
|
|
residences.GET("/:id/", residenceHandler.GetResidence)
|
|
residences.PUT("/:id/", residenceHandler.UpdateResidence)
|
|
residences.PATCH("/:id/", residenceHandler.UpdateResidence)
|
|
residences.DELETE("/:id/", residenceHandler.DeleteResidence)
|
|
|
|
residences.POST("/:id/generate-share-code/", residenceHandler.GenerateShareCode)
|
|
residences.POST("/:id/generate-tasks-report/", residenceHandler.GenerateTasksReport)
|
|
residences.GET("/:id/users/", residenceHandler.GetResidenceUsers)
|
|
residences.DELETE("/:id/users/:user_id/", residenceHandler.RemoveResidenceUser)
|
|
}
|
|
}
|
|
|
|
// setupTaskRoutes configures task routes
|
|
func setupTaskRoutes(api *gin.RouterGroup, taskHandler *handlers.TaskHandler) {
|
|
tasks := api.Group("/tasks")
|
|
{
|
|
tasks.GET("/", taskHandler.ListTasks)
|
|
tasks.POST("/", taskHandler.CreateTask)
|
|
tasks.GET("/by-residence/:residence_id/", taskHandler.GetTasksByResidence)
|
|
|
|
tasks.GET("/:id/", taskHandler.GetTask)
|
|
tasks.PUT("/:id/", taskHandler.UpdateTask)
|
|
tasks.PATCH("/:id/", taskHandler.UpdateTask)
|
|
tasks.DELETE("/:id/", taskHandler.DeleteTask)
|
|
|
|
tasks.POST("/:id/mark-in-progress/", taskHandler.MarkInProgress)
|
|
tasks.POST("/:id/cancel/", taskHandler.CancelTask)
|
|
tasks.POST("/:id/uncancel/", taskHandler.UncancelTask)
|
|
tasks.POST("/:id/archive/", taskHandler.ArchiveTask)
|
|
tasks.POST("/:id/unarchive/", taskHandler.UnarchiveTask)
|
|
tasks.GET("/:id/completions/", taskHandler.GetTaskCompletions)
|
|
}
|
|
|
|
// Task Completions
|
|
completions := api.Group("/task-completions")
|
|
{
|
|
completions.GET("/", taskHandler.ListCompletions)
|
|
completions.POST("/", taskHandler.CreateCompletion)
|
|
completions.GET("/:id/", taskHandler.GetCompletion)
|
|
completions.DELETE("/:id/", taskHandler.DeleteCompletion)
|
|
}
|
|
}
|
|
|
|
// setupContractorRoutes configures contractor routes
|
|
func setupContractorRoutes(api *gin.RouterGroup, contractorHandler *handlers.ContractorHandler) {
|
|
contractors := api.Group("/contractors")
|
|
{
|
|
contractors.GET("/", contractorHandler.ListContractors)
|
|
contractors.POST("/", contractorHandler.CreateContractor)
|
|
contractors.GET("/:id/", contractorHandler.GetContractor)
|
|
contractors.PUT("/:id/", contractorHandler.UpdateContractor)
|
|
contractors.PATCH("/:id/", contractorHandler.UpdateContractor)
|
|
contractors.DELETE("/:id/", contractorHandler.DeleteContractor)
|
|
contractors.POST("/:id/toggle-favorite/", contractorHandler.ToggleFavorite)
|
|
contractors.GET("/:id/tasks/", contractorHandler.GetContractorTasks)
|
|
}
|
|
}
|
|
|
|
// setupDocumentRoutes configures document routes
|
|
func setupDocumentRoutes(api *gin.RouterGroup, documentHandler *handlers.DocumentHandler) {
|
|
documents := api.Group("/documents")
|
|
{
|
|
documents.GET("/", documentHandler.ListDocuments)
|
|
documents.POST("/", documentHandler.CreateDocument)
|
|
documents.GET("/warranties/", documentHandler.ListWarranties)
|
|
documents.GET("/:id/", documentHandler.GetDocument)
|
|
documents.PUT("/:id/", documentHandler.UpdateDocument)
|
|
documents.PATCH("/:id/", documentHandler.UpdateDocument)
|
|
documents.DELETE("/:id/", documentHandler.DeleteDocument)
|
|
documents.POST("/:id/activate/", documentHandler.ActivateDocument)
|
|
documents.POST("/:id/deactivate/", documentHandler.DeactivateDocument)
|
|
}
|
|
}
|
|
|
|
// setupNotificationRoutes configures notification routes
|
|
func setupNotificationRoutes(api *gin.RouterGroup, notificationHandler *handlers.NotificationHandler) {
|
|
notifications := api.Group("/notifications")
|
|
{
|
|
notifications.GET("/", notificationHandler.ListNotifications)
|
|
notifications.GET("/unread-count/", notificationHandler.GetUnreadCount)
|
|
notifications.POST("/mark-all-read/", notificationHandler.MarkAllAsRead)
|
|
notifications.POST("/:id/read/", notificationHandler.MarkAsRead)
|
|
|
|
notifications.POST("/devices/", notificationHandler.RegisterDevice)
|
|
notifications.GET("/devices/", notificationHandler.ListDevices)
|
|
notifications.DELETE("/devices/:id/", notificationHandler.DeleteDevice)
|
|
|
|
notifications.GET("/preferences/", notificationHandler.GetPreferences)
|
|
notifications.PUT("/preferences/", notificationHandler.UpdatePreferences)
|
|
notifications.PATCH("/preferences/", notificationHandler.UpdatePreferences)
|
|
}
|
|
}
|
|
|
|
// setupSubscriptionRoutes configures subscription routes
|
|
func setupSubscriptionRoutes(api *gin.RouterGroup, subscriptionHandler *handlers.SubscriptionHandler) {
|
|
subscription := api.Group("/subscription")
|
|
{
|
|
subscription.GET("/", subscriptionHandler.GetSubscription)
|
|
subscription.GET("/status/", subscriptionHandler.GetSubscriptionStatus)
|
|
subscription.GET("/upgrade-trigger/:key/", subscriptionHandler.GetUpgradeTrigger)
|
|
subscription.GET("/features/", subscriptionHandler.GetFeatureBenefits)
|
|
subscription.GET("/promotions/", subscriptionHandler.GetPromotions)
|
|
subscription.POST("/purchase/", subscriptionHandler.ProcessPurchase)
|
|
subscription.POST("/cancel/", subscriptionHandler.CancelSubscription)
|
|
subscription.POST("/restore/", subscriptionHandler.RestoreSubscription)
|
|
}
|
|
}
|
|
|
|
// setupUserRoutes configures user routes
|
|
func setupUserRoutes(api *gin.RouterGroup, userHandler *handlers.UserHandler) {
|
|
users := api.Group("/users")
|
|
{
|
|
users.GET("/", userHandler.ListUsers)
|
|
users.GET("/:id/", userHandler.GetUser)
|
|
users.GET("/profiles/", userHandler.ListProfiles)
|
|
}
|
|
}
|
|
|
|
// setupUploadRoutes configures file upload routes
|
|
func setupUploadRoutes(api *gin.RouterGroup, uploadHandler *handlers.UploadHandler) {
|
|
uploads := api.Group("/uploads")
|
|
{
|
|
uploads.POST("/image/", uploadHandler.UploadImage)
|
|
uploads.POST("/document/", uploadHandler.UploadDocument)
|
|
uploads.POST("/completion/", uploadHandler.UploadCompletion)
|
|
uploads.DELETE("/", uploadHandler.DeleteFile)
|
|
}
|
|
}
|