Function bodies 1,000 total
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, responsehandlers.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: filhandlers.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{
ClientBehaviorAnalytichandlers.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,
Prhandlers.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.Ghandlers.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]interfahandlers.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 coRepobility · 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)
rehandlers.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: rhandlers.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 rhandlers.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, ehandlers.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, errhandlers.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 := jsonhandlers.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 !hasFeatureRepobility · 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