From 3b9e37d12b112c462e46d9b9d5b9ac85e7131f19 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 6 Dec 2025 18:55:48 -0600 Subject: [PATCH] Add generate-share-package endpoint for residence sharing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add POST /api/residences/:id/generate-share-package/ endpoint - Add SharePackageResponse DTO with share code and metadata - Add GenerateSharePackage service method to create one-time share codes - Update JoinWithCode to deactivate share code after successful use 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../casera-api/environments/Dev.bru | 2 +- internal/dto/responses/residence.go | 9 ++++ internal/handlers/residence_handler.go | 31 ++++++++++++ internal/router/router.go | 1 + internal/services/residence_service.go | 48 +++++++++++++++++++ 5 files changed, 90 insertions(+), 1 deletion(-) diff --git a/bruno-collections/casera-api/environments/Dev.bru b/bruno-collections/casera-api/environments/Dev.bru index 71c1c10..1851f1e 100644 --- a/bruno-collections/casera-api/environments/Dev.bru +++ b/bruno-collections/casera-api/environments/Dev.bru @@ -1,5 +1,5 @@ vars { base_url: https://casera.treytartt.com api_url: {{base_url}}/api - auth_token: your-auth-token-here + auth_token: 64eea3e59ecdf58a35a4fb45f4797413a3f96456 } diff --git a/internal/dto/responses/residence.go b/internal/dto/responses/residence.go index d3a6028..cb6c2aa 100644 --- a/internal/dto/responses/residence.go +++ b/internal/dto/responses/residence.go @@ -91,6 +91,15 @@ type GenerateShareCodeResponse struct { ShareCode ShareCodeResponse `json:"share_code"` } +// SharePackageResponse represents the response for generating a share package +// This contains the share code plus metadata for the .casera file +type SharePackageResponse struct { + ShareCode string `json:"share_code"` + ResidenceName string `json:"residence_name"` + SharedBy string `json:"shared_by"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` +} + // === Factory Functions === // NewResidenceUserResponse creates a ResidenceUserResponse from a User model diff --git a/internal/handlers/residence_handler.go b/internal/handlers/residence_handler.go index 1827013..d93072e 100644 --- a/internal/handlers/residence_handler.go +++ b/internal/handlers/residence_handler.go @@ -207,6 +207,37 @@ func (h *ResidenceHandler) GenerateShareCode(c *gin.Context) { c.JSON(http.StatusOK, response) } +// GenerateSharePackage handles POST /api/residences/:id/generate-share-package/ +// Returns a share code with metadata for creating a .casera package file +func (h *ResidenceHandler) GenerateSharePackage(c *gin.Context) { + user := c.MustGet(middleware.AuthUserKey).(*models.User) + + residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")}) + return + } + + var req requests.GenerateShareCodeRequest + // Request body is optional (for expires_in_hours) + c.ShouldBindJSON(&req) + + response, err := h.residenceService.GenerateSharePackage(uint(residenceID), user.ID, req.ExpiresInHours) + if err != nil { + switch { + case errors.Is(err, services.ErrResidenceNotFound): + c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")}) + case errors.Is(err, services.ErrNotResidenceOwner): + c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + return + } + + c.JSON(http.StatusOK, response) +} + // JoinWithCode handles POST /api/residences/join-with-code/ func (h *ResidenceHandler) JoinWithCode(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) diff --git a/internal/router/router.go b/internal/router/router.go index 7f7f606..377c8f9 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -272,6 +272,7 @@ func setupResidenceRoutes(api *gin.RouterGroup, residenceHandler *handlers.Resid residences.DELETE("/:id/", residenceHandler.DeleteResidence) residences.POST("/:id/generate-share-code/", residenceHandler.GenerateShareCode) + residences.POST("/:id/generate-share-package/", residenceHandler.GenerateSharePackage) residences.POST("/:id/generate-tasks-report/", residenceHandler.GenerateTasksReport) residences.GET("/:id/users/", residenceHandler.GetResidenceUsers) residences.DELETE("/:id/users/:user_id/", residenceHandler.RemoveResidenceUser) diff --git a/internal/services/residence_service.go b/internal/services/residence_service.go index 7709fbd..7227995 100644 --- a/internal/services/residence_service.go +++ b/internal/services/residence_service.go @@ -337,6 +337,48 @@ func (s *ResidenceService) GenerateShareCode(residenceID, userID uint, expiresIn }, nil } +// GenerateSharePackage generates a share code and returns package metadata for .casera file +func (s *ResidenceService) GenerateSharePackage(residenceID, userID uint, expiresInHours int) (*responses.SharePackageResponse, error) { + // Check ownership (only owners can share residences) + isOwner, err := s.residenceRepo.IsOwner(residenceID, userID) + if err != nil { + return nil, err + } + if !isOwner { + return nil, ErrNotResidenceOwner + } + + // Get residence details for the package + residence, err := s.residenceRepo.FindByID(residenceID) + if err != nil { + return nil, err + } + + // Get the user who's sharing + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, err + } + + // Default to 24 hours if not specified + if expiresInHours <= 0 { + expiresInHours = 24 + } + + // Generate the share code + shareCode, err := s.residenceRepo.CreateShareCode(residenceID, userID, time.Duration(expiresInHours)*time.Hour) + if err != nil { + return nil, err + } + + return &responses.SharePackageResponse{ + ShareCode: shareCode.Code, + ResidenceName: residence.Name, + SharedBy: user.Email, + ExpiresAt: shareCode.ExpiresAt, + }, nil +} + // JoinWithCode allows a user to join a residence using a share code func (s *ResidenceService) JoinWithCode(code string, userID uint) (*responses.JoinResidenceResponse, error) { // Find the share code @@ -362,6 +404,12 @@ func (s *ResidenceService) JoinWithCode(code string, userID uint) (*responses.Jo return nil, err } + // Mark share code as used (one-time use) + if err := s.residenceRepo.DeactivateShareCode(shareCode.ID); err != nil { + // Log the error but don't fail the join - the user has already been added + // The code will just be usable by others until it expires + } + // Get the residence with full details residence, err := s.residenceRepo.FindByID(shareCode.ResidenceID) if err != nil {