Add PDF reports, file uploads, admin auth, and comprehensive tests

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>
This commit is contained in:
Trey t
2025-11-27 23:36:20 -06:00
parent 2817deee3c
commit 469f21a833
50 changed files with 6795 additions and 582 deletions

View File

@@ -8,6 +8,7 @@ import (
"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"
@@ -21,11 +22,13 @@ 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
PushClient interface{} // *push.GorushClient - optional
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
@@ -49,6 +52,11 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
// 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)
@@ -70,6 +78,7 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
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)
@@ -86,14 +95,31 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
// Initialize handlers
authHandler := handlers.NewAuthHandler(authService, deps.EmailService, deps.Cache)
userHandler := handlers.NewUserHandler(userService)
residenceHandler := handlers.NewResidenceHandler(residenceService)
taskHandler := handlers.NewTaskHandler(taskService)
residenceHandler := handlers.NewResidenceHandler(residenceService, deps.PDFService, deps.EmailService)
taskHandler := handlers.NewTaskHandler(taskService, deps.StorageService)
contractorHandler := handlers.NewContractorHandler(contractorService)
documentHandler := handlers.NewDocumentHandler(documentService)
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")
{
@@ -115,6 +141,11 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
setupNotificationRoutes(protected, notificationHandler)
setupSubscriptionRoutes(protected, subscriptionHandler)
setupUserRoutes(protected, userHandler)
// Upload routes (only if storage service is configured)
if uploadHandler != nil {
setupUploadRoutes(protected, uploadHandler)
}
}
}
@@ -225,6 +256,7 @@ func setupTaskRoutes(api *gin.RouterGroup, taskHandler *handlers.TaskHandler) {
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
@@ -311,3 +343,14 @@ func setupUserRoutes(api *gin.RouterGroup, userHandler *handlers.UserHandler) {
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)
}
}