← back to jearl4__PhotoGallery

Function bodies 1,000 total

All specs Real LLM only Function bodies
adapters.S3Adapter.Upload method · go · L57-L68 (12 LOC)
backend/internal/adapters/s3_adapter.go
func (a *S3Adapter) Upload(ctx context.Context, bucket, key string, data []byte, contentType string) error {
	log.Printf("[S3Adapter] Uploading to %s/%s (%d bytes)", bucket, key, len(data))

	_, err := a.client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(bucket),
		Key:         aws.String(key),
		Body:        bytes.NewReader(data),
		ContentType: aws.String(contentType),
	})

	return err
}
handlers.NewAdminHandler function · go · L32-L38 (7 LOC)
backend/internal/api/handlers/admin.go
func NewAdminHandler(priceToTier map[string]subscription.Tier, subService AdminSubscriptionService, subRepo AdminSubscriptionRepository) *AdminHandler {
	return &AdminHandler{
		priceToTier: priceToTier,
		subService:  subService,
		subRepo:     subRepo,
	}
}
handlers.AdminHandler.FixSubscriptionStripeID method · go · L44-L127 (84 LOC)
backend/internal/api/handlers/admin.go
func (h *AdminHandler) FixSubscriptionStripeID(w http.ResponseWriter, r *http.Request) {
	if h.subRepo == nil {
		respondWithError(w, http.StatusInternalServerError, "Admin repository not configured")
		return
	}

	// Get authenticated user ID from context
	ctx := r.Context()
	authenticatedUserID, ok := ctx.Value("userID").(string)
	if !ok || authenticatedUserID == "" {
		respondWithError(w, http.StatusUnauthorized, "Authentication required")
		return
	}

	var req struct {
		StripeCustomerID string `json:"stripe_customer_id"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondWithError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if req.StripeCustomerID == "" {
		respondWithError(w, http.StatusBadRequest, "stripe_customer_id is required")
		return
	}

	// Get the subscription for the authenticated user
	sub, err := h.subRepo.GetByUserID(r.Context(), authenticatedUserID)
	if err != nil {
		logger.Error("Failed to get subscription for admin fix"
handlers.AdminHandler.GetPriceMappings method · go · L131-L143 (13 LOC)
backend/internal/api/handlers/admin.go
func (h *AdminHandler) GetPriceMappings(w http.ResponseWriter, r *http.Request) {
	info := subscription.GetPriceMappingInfo(h.priceToTier)

	// Add validation status
	err := subscription.ValidatePriceToTierMapping(h.priceToTier)
	info["validation_status"] = "valid"
	if err != nil {
		info["validation_status"] = "invalid"
		info["validation_error"] = err.Error()
	}

	respondWithJSON(w, http.StatusOK, info)
}
handlers.AdminHandler.TestPriceMapping method · go · L147-L182 (36 LOC)
backend/internal/api/handlers/admin.go
func (h *AdminHandler) TestPriceMapping(w http.ResponseWriter, r *http.Request) {
	priceID := r.URL.Query().Get("price_id")
	if priceID == "" {
		respondWithError(w, http.StatusBadRequest, "price_id parameter is required")
		return
	}

	tier, found := h.priceToTier[priceID]

	response := map[string]interface{}{
		"price_id": priceID,
		"found":    found,
	}

	if found {
		response["tier"] = string(tier)
		response["tier_details"] = subscription.GetTierLimits(tier)
	} else {
		response["error"] = "Price ID not found in mapping"

		// Suggest which env var might need this price ID
		requiredMappings := subscription.RequiredPriceMappings()
		suggestions := []string{}
		for _, mapping := range requiredMappings {
			suggestions = append(suggestions, mapping.EnvVar)
		}
		response["suggestions"] = "Check if this price ID should be set in one of: " + joinStrings(suggestions, ", ")
	}

	status := http.StatusOK
	if !found {
		status = http.StatusNotFound
	}

	respondWithJSON(w, status, response
handlers.AdminHandler.ValidatePriceMappings method · go · L186-L201 (16 LOC)
backend/internal/api/handlers/admin.go
func (h *AdminHandler) ValidatePriceMappings(w http.ResponseWriter, r *http.Request) {
	err := subscription.ValidatePriceToTierMapping(h.priceToTier)

	response := map[string]interface{}{
		"valid": err == nil,
	}

	if err != nil {
		response["error"] = err.Error()
		respondWithJSON(w, http.StatusOK, response) // 200 with validation result
	} else {
		response["message"] = "All price mappings are valid"
		response["total_mappings"] = len(h.priceToTier)
		respondWithJSON(w, http.StatusOK, response)
	}
}
handlers.joinStrings function · go · L204-L213 (10 LOC)
backend/internal/api/handlers/admin.go
func joinStrings(strs []string, sep string) string {
	if len(strs) == 0 {
		return ""
	}
	result := strs[0]
	for i := 1; i < len(strs); i++ {
		result += sep + strs[i]
	}
	return result
}
Powered by Repobility — scan your code at https://repobility.com
handlers.NewAnalyticsHandler function · go · L34-L39 (6 LOC)
backend/internal/api/handlers/analytics.go
func NewAnalyticsHandler(analyticsService AnalyticsServiceInterface, subscriptionService SubscriptionGetter) *AnalyticsHandler {
	return &AnalyticsHandler{
		analyticsService:    analyticsService,
		subscriptionService: subscriptionService,
	}
}
handlers.AnalyticsHandler.getAnalyticsAccess method · go · L42-L62 (21 LOC)
backend/internal/api/handlers/analytics.go
func (h *AnalyticsHandler) getAnalyticsAccess(ctx context.Context, userID string) analytics.AnalyticsAccess {
	sub, err := h.subscriptionService.GetSubscription(ctx, userID)
	if err != nil {
		// Log subscription lookup errors - this could indicate a service issue
		// affecting paying users who should have access
		logger.Error("Failed to get subscription for analytics access, defaulting to no access", map[string]interface{}{
			"userID": userID,
			"error":  err.Error(),
		})
		return analytics.GetAnalyticsAccess("none", false)
	}
	if sub == nil {
		// No subscription is a legitimate state - user is on free tier
		return analytics.GetAnalyticsAccess("none", false)
	}

	limits := subscription.GetTierLimits(sub.Tier)
	isStudio := sub.Tier == subscription.TierStudio

	return analytics.GetAnalyticsAccess(limits.AnalyticsLevel, isStudio)
}
handlers.AnalyticsHandler.GetDashboardSummary method · go · L65-L91 (27 LOC)
backend/internal/api/handlers/analytics.go
func (h *AnalyticsHandler) GetDashboardSummary(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get photographer ID from context (set by auth middleware)
	photographerID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	// Get tier-based access
	access := h.getAnalyticsAccess(ctx, photographerID)

	summary, err := h.analyticsService.GetDashboardSummary(ctx, photographerID)
	if err != nil {
		respondError(w, err)
		return
	}

	// Filter summary based on tier access
	filteredSummary := analytics.FilterDashboardSummary(summary, access)

	respondJSON(w, http.StatusOK, analytics.TierFilteredSummaryResponse{
		DashboardSummary: filteredSummary,
		Access:           access,
	})
}
handlers.AnalyticsHandler.GetGalleriesAnalytics method · go · L94-L133 (40 LOC)
backend/internal/api/handlers/analytics.go
func (h *AnalyticsHandler) GetGalleriesAnalytics(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get photographer ID from context
	photographerID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	// Get tier-based access
	access := h.getAnalyticsAccess(ctx, photographerID)

	// Parse query parameters
	limit := 20
	if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
		if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 {
			limit = parsed
		}
	}

	sortBy := r.URL.Query().Get("sortBy")
	if sortBy == "" {
		sortBy = "views"
	}

	galleries, err := h.analyticsService.GetGalleriesAnalytics(ctx, photographerID, limit, sortBy)
	if err != nil {
		respondError(w, err)
		return
	}

	// Filter galleries based on tier access
	filteredGalleries := analytics.FilterGalleryAnalytics(galleries, access)

	respondJSON(w, http.StatusOK, analytics.TierFilteredGalleriesResponse{
		Galleries: fil
handlers.AnalyticsHandler.GetTopPhotos method · go · L136-L175 (40 LOC)
backend/internal/api/handlers/analytics.go
func (h *AnalyticsHandler) GetTopPhotos(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get photographer ID from context
	photographerID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	// Get tier-based access
	access := h.getAnalyticsAccess(ctx, photographerID)

	// Parse query parameters
	limit := 10
	if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
		if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 {
			limit = parsed
		}
	}

	metric := r.URL.Query().Get("metric")
	if metric == "" {
		metric = "favorites"
	}

	photos, err := h.analyticsService.GetTopPhotos(ctx, photographerID, limit, metric)
	if err != nil {
		respondError(w, err)
		return
	}

	// Filter photos based on tier access
	filteredPhotos := analytics.FilterTopPhotos(photos, access)

	respondJSON(w, http.StatusOK, analytics.TierFilteredTopPhotosResponse{
		Photos: filteredPhotos,
		Access: access,
	})
}
handlers.AnalyticsHandler.GetClientBehavior method · go · L178-L213 (36 LOC)
backend/internal/api/handlers/analytics.go
func (h *AnalyticsHandler) GetClientBehavior(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get photographer ID from context
	photographerID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	// Get tier-based access
	access := h.getAnalyticsAccess(ctx, photographerID)

	// If user doesn't have client behavior access (Studio only), return empty data with access info
	if !access.ClientBehavior {
		respondJSON(w, http.StatusOK, analytics.TierFilteredClientBehaviorResponse{
			ClientBehaviorAnalytics: &analytics.ClientBehaviorAnalytics{
				Devices:  analytics.DeviceDistribution{},
				Browsers: []analytics.BrowserDistribution{},
			},
			Access: access,
		})
		return
	}

	behavior, err := h.analyticsService.GetClientBehavior(ctx, photographerID)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, analytics.TierFilteredClientBehaviorResponse{
		ClientBehaviorAnalytic
handlers.AuthHandler.GetMe method · go · L30-L80 (51 LOC)
backend/internal/api/handlers/auth.go
func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) {
	// Get user ID from context (set by auth middleware)
	userID, ok := r.Context().Value("userID").(string)
	if !ok || userID == "" {
		logger.Error("No user ID in context", nil)
		respondWithError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	// Get email from context (from Cognito JWT)
	email, _ := r.Context().Value("email").(string)
	name, _ := r.Context().Value("name").(string)

	// Try to get photographer from database
	p, err := h.photographerRepo.GetByID(r.Context(), userID)

	// If photographer doesn't exist, create them (first time login)
	if err == photographer.ErrNotFound {
		// SECURITY FIX: Don't log PII (email) - only log anonymized identifiers
		logger.Info("Creating new photographer profile", map[string]interface{}{
			"userID": userID,
		})

		// Create new photographer with defaults
		p = &photographer.Photographer{
			UserID:      userID,
			Email:       email,
			Name:        name,
			Pr
handlers.respondWithJSON function · go · L83-L94 (12 LOC)
backend/internal/api/handlers/auth.go
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
	response, err := json.Marshal(payload)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(`{"error": "Failed to marshal response"}`))
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	w.Write(response)
}
Repobility · code-quality intelligence platform · https://repobility.com
handlers.BaseHandler.Handle method · go · L28-L45 (18 LOC)
backend/internal/api/handlers/base_handler.go
func (h *BaseHandler) Handle(handler RequestHandler, w http.ResponseWriter, r *http.Request) {
	// Step 1: Validate the request
	if err := handler.ValidateRequest(r); err != nil {
		h.respondError(w, err)
		return
	}

	// Step 2: Execute the business logic
	result, err := handler.Execute(r)
	if err != nil {
		h.respondError(w, err)
		return
	}

	// Step 3: Format and send the response
	status, response := handler.FormatResponse(result)
	h.respondJSON(w, status, response)
}
handlers.BaseHandler.respondJSON method · go · L48-L54 (7 LOC)
backend/internal/api/handlers/base_handler.go
func (h *BaseHandler) respondJSON(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if data != nil {
		json.NewEncoder(w).Encode(data)
	}
}
handlers.BaseHandler.respondError method · go · L57-L72 (16 LOC)
backend/internal/api/handlers/base_handler.go
func (h *BaseHandler) respondError(w http.ResponseWriter, err error) {
	if appErr, ok := err.(*errors.AppError); ok {
		// Sanitize the error message before sending to client
		sanitizedErr := security.SanitizeForClient(appErr.Code, appErr.Message)
		h.respondJSON(w, appErr.Code, map[string]string{
			"error": sanitizedErr.Message,
		})
	} else {
		logger.Error("Unexpected error", map[string]interface{}{"error": err.Error()})
		// Use sanitizer for generic errors too
		sanitizedErr := security.SanitizeForClient(http.StatusInternalServerError, err.Error())
		h.respondJSON(w, http.StatusInternalServerError, map[string]string{
			"error": sanitizedErr.Message,
		})
	}
}
handlers.WrapHandler function · go · L119-L124 (6 LOC)
backend/internal/api/handlers/base_handler.go
func WrapHandler(handler RequestHandler) http.HandlerFunc {
	base := &BaseHandler{}
	return func(w http.ResponseWriter, r *http.Request) {
		base.Handle(handler, w, r)
	}
}
handlers.NewClientHandler function · go · L32-L48 (17 LOC)
backend/internal/api/handlers/client.go
func NewClientHandler(
	galleryService *gallery.Service,
	photoService *photo.Service,
	sessionService *auth.SessionService,
	zipJobService *zipjob.Service,
	subscriptionService *subscription.Service,
) *ClientHandler {
	return &ClientHandler{
		galleryService:      galleryService,
		photoService:        photoService,
		sessionService:      sessionService,
		zipJobService:       zipJobService,
		subscriptionService: subscriptionService,
		cookieDomain:        "",             // Set via WithCookieConfig
		sessionTTL:          24 * time.Hour, // Default 24 hours
	}
}
handlers.ClientHandler.shouldShowBadge method · go · L59-L73 (15 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) shouldShowBadge(ctx context.Context, photographerID string) bool {
	if h.subscriptionService == nil {
		// If subscription service not configured, default to showing badge (safer)
		return true
	}

	sub, err := h.subscriptionService.GetSubscription(ctx, photographerID)
	if err != nil {
		// On error, default to showing badge (conservative approach)
		return true
	}

	// Show badge only for free tier users
	return !sub.IsPaid()
}
handlers.ClientHandler.VerifyPassword method · go · L84-L141 (58 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) VerifyPassword(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	var req VerifyPasswordRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, errors.NewBadRequest("Invalid request body"))
		return
	}

	if req.CustomURL == "" {
		respondError(w, errors.NewBadRequest("customUrl is required"))
		return
	}
	if req.Password == "" {
		respondError(w, errors.NewBadRequest("password is required"))
		return
	}

	// Verify gallery password
	g, err := h.galleryService.VerifyPassword(ctx, req.CustomURL, req.Password)
	if err != nil {
		respondError(w, err)
		return
	}

	// Set showBadge based on photographer's subscription
	g.ShowBadge = h.shouldShowBadge(ctx, g.PhotographerID)

	// Get client info from request
	ipAddress := r.Header.Get("X-Forwarded-For")
	if ipAddress == "" {
		ipAddress = r.RemoteAddr
	}
	userAgent := r.Header.Get("User-Agent")

	// Create session
	sessionToken, err := h.sessionService.CreateSession(ctx, g.G
handlers.ClientHandler.ValidateSession method · go · L146-L166 (21 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) ValidateSession(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get session info from context (set by middleware)
	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Invalid or expired session"))
		return
	}
	sessionID, ok := ctx.Value("sessionID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Invalid or expired session"))
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"valid":     true,
		"galleryId": galleryID,
		"sessionId": sessionID,
	})
}
All rows scored by the Repobility analyzer (https://repobility.com)
handlers.ClientHandler.ClearSession method · go · L170-L178 (9 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) ClearSession(w http.ResponseWriter, r *http.Request) {
	// Clear the cookie
	clearCookie := security.BuildClearCookie(security.SessionCookieName, h.cookieDomain)
	w.Header().Set("Set-Cookie", clearCookie)

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"message": "Session cleared",
	})
}
handlers.ClientHandler.GetGallery method · go · L181-L199 (19 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) GetGallery(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	customURL := getURLParam(r, "customUrl")

	// Session should be verified by middleware
	g, err := h.galleryService.GetByCustomURL(ctx, customURL)
	if err != nil {
		respondError(w, err)
		return
	}

	// Don't expose password
	g.Password = ""

	// Set showBadge based on photographer's subscription
	g.ShowBadge = h.shouldShowBadge(ctx, g.PhotographerID)

	respondJSON(w, http.StatusOK, g)
}
handlers.ClientHandler.ListPhotos method · go · L202-L225 (24 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) ListPhotos(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get gallery ID from session context (set by middleware)
	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	limit := 50 // default
	var lastKey map[string]interface{}

	photos, nextKey, err := h.photoService.ListByGallery(ctx, galleryID, limit, lastKey)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"photos":  photos,
		"lastKey": nextKey,
	})
}
handlers.ClientHandler.GetDownloadURL method · go · L228-L241 (14 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) GetDownloadURL(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	photoID := getURLParam(r, "photoId")

	url, err := h.photoService.GetDownloadURL(ctx, photoID)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"downloadUrl": url,
	})
}
handlers.ClientHandler.ToggleFavorite method · go · L244-L269 (26 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) ToggleFavorite(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	photoID := getURLParam(r, "photoId")

	// Get session info from context (set by middleware)
	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}
	sessionID, ok := ctx.Value("sessionID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Session ID not found"))
		return
	}

	favorited, err := h.photoService.ToggleFavorite(ctx, galleryID, sessionID, photoID)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"isFavorited": favorited,
	})
}
handlers.ClientHandler.GetSessionFavorites method · go · L272-L296 (25 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) GetSessionFavorites(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get session info from context
	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}
	sessionID, ok := ctx.Value("sessionID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Session ID not found"))
		return
	}

	favorites, err := h.photoService.ListFavoritesBySession(ctx, galleryID, sessionID)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"favorites": favorites,
	})
}
handlers.ClientHandler.InitiateZipDownload method · go · L300-L340 (41 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) InitiateZipDownload(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get gallery ID from session context (set by middleware)
	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	// Get gallery for the name (for filename)
	customURL := getURLParam(r, "customUrl")
	g, err := h.galleryService.GetByCustomURL(ctx, customURL)
	if err != nil {
		respondError(w, err)
		return
	}

	// Block downloads when not enabled for the current phase
	if !g.DownloadsEnabled {
		respondError(w, errors.NewForbidden("Downloads are not available for this gallery"))
		return
	}

	// Generate a safe filename from gallery name
	filename := sanitizeFilename(g.Name) + ".zip"

	// Initiate async ZIP job
	job, err := h.zipJobService.InitiateZipJob(ctx, galleryID, filename)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusAccepted, map[string]interfa
handlers.ClientHandler.GetZipJobStatus method · go · L344-L397 (54 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) GetZipJobStatus(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get gallery ID from session context (set by middleware)
	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	jobID := getURLParam(r, "jobId")

	if jobID == "" {
		respondError(w, errors.NewBadRequest("Job ID is required"))
		return
	}

	job, err := h.zipJobService.GetJobStatus(ctx, jobID)
	if err != nil {
		respondError(w, err)
		return
	}

	if job == nil {
		respondError(w, errors.NewNotFound("Job"))
		return
	}

	// Verify the job belongs to the session's gallery (prevent IDOR)
	if job.GalleryID != galleryID {
		respondError(w, errors.NewNotFound("Job"))
		return
	}

	response := map[string]interface{}{
		"jobId":      job.JobID,
		"status":     job.Status,
		"progress":   job.Progress,
		"photoCount": job.PhotoCount,
		"createdAt":  job.CreatedAt,
	}

	// Include download URL if co
Repobility · open methodology · https://repobility.com/research/
handlers.ClientHandler.DownloadGalleryZip method · go · L402-L439 (38 LOC)
backend/internal/api/handlers/client.go
func (h *ClientHandler) DownloadGalleryZip(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// Get gallery ID from session context (set by middleware)
	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	// Get gallery for the name (for filename)
	customURL := getURLParam(r, "customUrl")
	g, err := h.galleryService.GetByCustomURL(ctx, customURL)
	if err != nil {
		respondError(w, err)
		return
	}

	// Block downloads when not enabled for the current phase
	if !g.DownloadsEnabled {
		respondError(w, errors.NewForbidden("Downloads are not available for this gallery"))
		return
	}

	// Generate a safe filename from gallery name
	filename := sanitizeFilename(g.Name) + ".zip"

	// Generate the ZIP, upload to S3, and get download URL (sync - may timeout)
	downloadURL, err := h.photoService.GenerateGalleryZipURL(ctx, galleryID, filename)
	if err != nil {
		respondError(w, err)
		re
handlers.sanitizeFilename function · go · L442-L461 (20 LOC)
backend/internal/api/handlers/client.go
func sanitizeFilename(name string) string {
	// Replace spaces with hyphens
	name = strings.ReplaceAll(name, " ", "-")

	// Remove any characters that aren't alphanumeric, hyphens, or underscores
	reg := regexp.MustCompile(`[^a-zA-Z0-9\-_]`)
	name = reg.ReplaceAllString(name, "")

	// Limit length
	if len(name) > 50 {
		name = name[:50]
	}

	// Default if empty
	if name == "" {
		name = "gallery"
	}

	return name
}
handlers.ClientProofingHandler.CheckProofingEnabled method · go · L36-L54 (19 LOC)
backend/internal/api/handlers/client_proofing.go
func (h *ClientProofingHandler) CheckProofingEnabled(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	enabled, err := h.proofingService.IsProofingEnabled(ctx, galleryID)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"proofingEnabled": enabled,
	})
}
handlers.ClientProofingHandler.CreateComment method · go · L63-L105 (43 LOC)
backend/internal/api/handlers/client_proofing.go
func (h *ClientProofingHandler) CreateComment(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	sessionID, ok := ctx.Value("sessionID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Session ID not found"))
		return
	}

	var req CreateCommentRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, errors.NewBadRequest("Invalid request body"))
		return
	}

	if req.PhotoID == "" {
		respondError(w, errors.NewBadRequest("photoId is required"))
		return
	}
	if req.Text == "" {
		respondError(w, errors.NewBadRequest("text is required"))
		return
	}

	comment, err := h.proofingService.CreateComment(ctx, proofing.CreateCommentRequest{
		PhotoID:   req.PhotoID,
		GalleryID: galleryID,
		SessionID: sessionID,
		Text:      req.Text,
	})
	if err != nil {
		respondError(w, err)
		return
	}

	
handlers.ClientProofingHandler.GetMyComments method · go · L108-L133 (26 LOC)
backend/internal/api/handlers/client_proofing.go
func (h *ClientProofingHandler) GetMyComments(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	sessionID, ok := ctx.Value("sessionID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Session ID not found"))
		return
	}

	comments, err := h.proofingService.ListCommentsForSession(ctx, galleryID, sessionID)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"comments": comments,
		"count":    len(comments),
	})
}
handlers.ClientProofingHandler.DeleteComment method · go · L136-L166 (31 LOC)
backend/internal/api/handlers/client_proofing.go
func (h *ClientProofingHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	sessionID, ok := ctx.Value("sessionID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Session ID not found"))
		return
	}

	photoID := getURLParam(r, "photoId")
	commentID := getURLParam(r, "commentId")

	if photoID == "" || commentID == "" {
		respondError(w, errors.NewBadRequest("photoId and commentId are required"))
		return
	}

	err := h.proofingService.DeleteComment(ctx, galleryID, photoID, commentID, sessionID)
	if err != nil {
		respondError(w, err)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}
handlers.ClientProofingHandler.CreateEditRequest method · go · L176-L223 (48 LOC)
backend/internal/api/handlers/client_proofing.go
func (h *ClientProofingHandler) CreateEditRequest(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	sessionID, ok := ctx.Value("sessionID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Session ID not found"))
		return
	}

	var req CreateEditRequestRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, errors.NewBadRequest("Invalid request body"))
		return
	}

	if req.PhotoID == "" {
		respondError(w, errors.NewBadRequest("photoId is required"))
		return
	}
	if req.RequestType == "" {
		respondError(w, errors.NewBadRequest("requestType is required"))
		return
	}
	if req.Description == "" {
		respondError(w, errors.NewBadRequest("description is required"))
		return
	}

	editRequest, err := h.proofingService.CreateEditRequest(ctx, proofing.CreateEditRequestInput{
		PhotoID:     r
handlers.ClientProofingHandler.GetMyEditRequests method · go · L226-L251 (26 LOC)
backend/internal/api/handlers/client_proofing.go
func (h *ClientProofingHandler) GetMyEditRequests(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	galleryID, ok := ctx.Value("galleryID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Gallery ID not found in session"))
		return
	}

	sessionID, ok := ctx.Value("sessionID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("Session ID not found"))
		return
	}

	requests, err := h.proofingService.ListEditRequestsForSession(ctx, galleryID, sessionID)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"editRequests": requests,
		"count":        len(requests),
	})
}
Powered by Repobility — scan your code at https://repobility.com
handlers.DomainHandler.GetDomainConfig method · go · L25-L41 (17 LOC)
backend/internal/api/handlers/domain.go
func (h *DomainHandler) GetDomainConfig(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	userID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	config, err := h.domainService.GetDomainConfig(ctx, userID)
	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, config)
}
handlers.DomainHandler.RequestSubdomain method · go · L44-L81 (38 LOC)
backend/internal/api/handlers/domain.go
func (h *DomainHandler) RequestSubdomain(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	userID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	var req customdomain.RequestSubdomainInput
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, errors.NewBadRequest("Invalid request body"))
		return
	}

	if req.Subdomain == "" {
		respondError(w, errors.NewBadRequest("Subdomain is required"))
		return
	}

	config, err := h.domainService.RequestSubdomain(ctx, userID, req.Subdomain)
	if err != nil {
		switch err {
		case customdomain.ErrSubdomainTaken:
			respondError(w, errors.NewConflict("Subdomain is already taken"))
		case customdomain.ErrSubdomainInvalid:
			respondError(w, errors.NewBadRequest("Subdomain is invalid: must be 3-63 lowercase alphanumeric characters or hyphens"))
		case customdomain.ErrSubdomainReserved:
			respondError(w, errors.NewBadRequest("Subdomain is r
handlers.DomainHandler.RequestCustomDomain method · go · L84-L119 (36 LOC)
backend/internal/api/handlers/domain.go
func (h *DomainHandler) RequestCustomDomain(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	userID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	var req customdomain.RequestCustomDomainInput
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, errors.NewBadRequest("Invalid request body"))
		return
	}

	if req.Domain == "" {
		respondError(w, errors.NewBadRequest("Domain is required"))
		return
	}

	config, err := h.domainService.RequestCustomDomain(ctx, userID, req.Domain)
	if err != nil {
		switch err {
		case customdomain.ErrCustomDomainTaken:
			respondError(w, errors.NewConflict("Domain is already registered by another user"))
		case customdomain.ErrCustomDomainInvalid:
			respondError(w, errors.NewBadRequest("Domain format is invalid"))
		default:
			logger.Error("Failed to request custom domain", map[string]interface{}{"error": err.Error()})
			respondError(w, e
handlers.DomainHandler.VerifyDomain method · go · L122-L157 (36 LOC)
backend/internal/api/handlers/domain.go
func (h *DomainHandler) VerifyDomain(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	userID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	config, err := h.domainService.VerifyDomain(ctx, userID)
	if err != nil {
		switch err {
		case customdomain.ErrVerificationFailed:
			// Return the config with pending status and instructions
			respondJSON(w, http.StatusOK, map[string]interface{}{
				"verified":        false,
				"message":         "DNS record not found. Please add the TXT record and try again.",
				"dnsInstructions": config.DNSInstructions,
			})
		case customdomain.ErrNoPendingDomain:
			respondError(w, errors.NewBadRequest("No pending domain to verify"))
		case customdomain.ErrDomainAlreadyActive:
			respondError(w, errors.NewBadRequest("Domain is already active"))
		default:
			logger.Error("Failed to verify domain", map[string]interface{}{"error": err.Error()})
			respondError(w, err
handlers.DomainHandler.RemoveDomain method · go · L160-L176 (17 LOC)
backend/internal/api/handlers/domain.go
func (h *DomainHandler) RemoveDomain(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	userID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	if err := h.domainService.RemoveDomain(ctx, userID); err != nil {
		logger.Error("Failed to remove domain", map[string]interface{}{"error": err.Error()})
		respondError(w, errors.NewInternalServer("Failed to remove domain"))
		return
	}

	w.WriteHeader(http.StatusNoContent)
}
handlers.NewGalleryHandler function · go · L52-L59 (8 LOC)
backend/internal/api/handlers/gallery.go
func NewGalleryHandler(galleryService GalleryService, tierGate *middleware.TierGateMiddleware, photographerGetter PhotographerGetter, subscriptionChecker SubscriptionFeatureChecker) *GalleryHandler {
	return &GalleryHandler{
		galleryService:      galleryService,
		tierGate:            tierGate,
		photographerGetter:  photographerGetter,
		subscriptionChecker: subscriptionChecker,
	}
}
handlers.GalleryHandler.validateExpirationLimit method · go · L73-L116 (44 LOC)
backend/internal/api/handlers/gallery.go
func (h *GalleryHandler) validateExpirationLimit(ctx context.Context, w http.ResponseWriter, photographerID string, expiresAt *time.Time) bool {
	if expiresAt == nil || h.subscriptionChecker == nil {
		return true
	}

	// Use Ceil to round up partial days - a gallery expiring in 30.5 days
	// should be checked against the 31-day limit, not the 30-day limit
	hours := time.Until(*expiresAt).Hours()
	if hours < 0 {
		hours = 0
	}
	days := int(math.Ceil(hours / 24))

	gateResult, err := h.subscriptionChecker.GateGalleryExpiration(ctx, photographerID, days)
	if err != nil {
		logger.Error("Failed to check gallery expiration limit", map[string]interface{}{
			"photographerId": photographerID,
			"days":           days,
			"error":          err.Error(),
		})
		respondError(w, errors.NewInternalServer("Failed to check expiration limit"))
		return false
	}

	if !gateResult.Allowed {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusPaymentRequired)
		if err := json
handlers.GalleryHandler.gateFeature method · go · L120-L158 (39 LOC)
backend/internal/api/handlers/gallery.go
func (h *GalleryHandler) gateFeature(ctx context.Context, w http.ResponseWriter, photographerID, feature, message string) bool {
	if h.subscriptionChecker == nil {
		logger.Error("subscriptionChecker is nil, denying feature access", map[string]interface{}{
			"photographerId": photographerID,
			"feature":        feature,
		})
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusServiceUnavailable)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"error":   "service_unavailable",
			"message": "Unable to verify feature access. Please try again later.",
		})
		return false
	}

	hasFeature, err := h.subscriptionChecker.HasFeature(ctx, photographerID, feature)
	if err != nil {
		logger.Error("Failed to check feature", map[string]interface{}{
			"photographerId": photographerID,
			"feature":        feature,
			"error":          err.Error(),
		})
		respondError(w, errors.NewInternalServer("Failed to check feature access"))
		return false
	}

	if !hasFeature
Repobility · code-quality intelligence platform · https://repobility.com
handlers.GalleryHandler.GetGallery method · go · L402-L423 (22 LOC)
backend/internal/api/handlers/gallery.go
func (h *GalleryHandler) GetGallery(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	galleryID := getURLParam(r, "id")

	// Use strongly consistent read when photographer ID is available (authenticated request)
	// This avoids GSI eventual consistency issues when fetching immediately after creation
	var g interface{}
	var err error

	if photographerID, ok := ctx.Value("userID").(string); ok && photographerID != "" {
		g, err = h.galleryService.GetByIDForPhotographer(ctx, galleryID, photographerID)
	} else {
		g, err = h.galleryService.GetByID(ctx, galleryID)
	}

	if err != nil {
		respondError(w, err)
		return
	}

	respondJSON(w, http.StatusOK, g)
}
handlers.GalleryHandler.ListGalleries method · go · L426-L450 (25 LOC)
backend/internal/api/handlers/gallery.go
func (h *GalleryHandler) ListGalleries(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	photographerID, ok := ctx.Value("userID").(string)
	if !ok {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	limit := 20 // default limit
	var lastKey map[string]interface{}

	galleries, nextKey, err := h.galleryService.ListByPhotographer(ctx, photographerID, limit, lastKey)
	if err != nil {
		respondError(w, err)
		return
	}

	response := map[string]interface{}{
		"galleries": galleries,
		"lastKey":   nextKey,
	}

	respondJSON(w, http.StatusOK, response)
}
handlers.GalleryHandler.DeleteGallery method · go · L682-L715 (34 LOC)
backend/internal/api/handlers/gallery.go
func (h *GalleryHandler) DeleteGallery(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	galleryID := getURLParam(r, "id")

	// Get photographer ID from context (set by auth middleware)
	photographerID, ok := ctx.Value("userID").(string)
	if !ok || photographerID == "" {
		respondError(w, errors.NewUnauthorized("User ID not found"))
		return
	}

	// SECURITY: Verify ownership before allowing delete (IDOR prevention)
	existingGallery, err := h.galleryService.GetByID(ctx, galleryID)
	if err != nil {
		respondError(w, err)
		return
	}
	if existingGallery.PhotographerID != photographerID {
		logger.Warn("IDOR attempt: user tried to delete gallery they don't own", map[string]interface{}{
			"userId":    photographerID,
			"galleryId": galleryID,
			"ownerId":   existingGallery.PhotographerID,
		})
		respondError(w, errors.NewForbidden("You do not have permission to delete this gallery"))
		return
	}

	if err := h.galleryService.Delete(ctx, galleryID); err != nil {
		respondErro
‹ prevpage 2 / 20next ›