← back to jearl4__PhotoGallery

Function bodies 1,000 total

All specs Real LLM only Function bodies
config.ProcessorConfigBuilder.validate method · go · L81-L100 (20 LOC)
backend/internal/config/processor_config.go
func (b *ProcessorConfigBuilder) validate() {
	if b.config.AWSRegion == "" {
		b.errors = append(b.errors, "AWS_REGION_NAME is required")
	}
	if b.config.DynamoDBTablePrefix == "" {
		b.errors = append(b.errors, "DYNAMODB_TABLE_PREFIX is required")
	}
	if b.config.S3BucketOriginal == "" {
		b.errors = append(b.errors, "S3_BUCKET_ORIGINAL is required")
	}
	if b.config.S3BucketOptimized == "" {
		b.errors = append(b.errors, "S3_BUCKET_OPTIMIZED is required")
	}
	if b.config.S3BucketThumbnail == "" {
		b.errors = append(b.errors, "S3_BUCKET_THUMBNAIL is required")
	}
	if b.config.APIStage == "" {
		b.errors = append(b.errors, "STAGE is required")
	}
}
config.NewZipProcessorConfigBuilder function · go · L24-L29 (6 LOC)
backend/internal/config/zip_processor_config.go
func NewZipProcessorConfigBuilder() *ZipProcessorConfigBuilder {
	return &ZipProcessorConfigBuilder{
		config: &ZipProcessorConfig{},
		errors: []string{},
	}
}
config.ZipProcessorConfigBuilder.FromEnvironment method · go · L32-L39 (8 LOC)
backend/internal/config/zip_processor_config.go
func (b *ZipProcessorConfigBuilder) FromEnvironment() *ZipProcessorConfigBuilder {
	b.config.AWSRegion = os.Getenv("AWS_REGION_NAME")
	b.config.DynamoDBTablePrefix = os.Getenv("DYNAMODB_TABLE_PREFIX")
	b.config.S3BucketOptimized = os.Getenv("S3_BUCKET_OPTIMIZED")
	b.config.APIStage = os.Getenv("STAGE")
	b.config.ZipJobsTable = os.Getenv("ZIP_JOBS_TABLE")
	return b
}
config.ZipProcessorConfigBuilder.Build method · go · L42-L50 (9 LOC)
backend/internal/config/zip_processor_config.go
func (b *ZipProcessorConfigBuilder) Build() (*ZipProcessorConfig, error) {
	b.validate()

	if len(b.errors) > 0 {
		return nil, fmt.Errorf("configuration errors: %v", b.errors)
	}

	return b.config, nil
}
config.ZipProcessorConfigBuilder.validate method · go · L52-L65 (14 LOC)
backend/internal/config/zip_processor_config.go
func (b *ZipProcessorConfigBuilder) validate() {
	if b.config.AWSRegion == "" {
		b.errors = append(b.errors, "AWS_REGION_NAME is required")
	}
	if b.config.DynamoDBTablePrefix == "" {
		b.errors = append(b.errors, "DYNAMODB_TABLE_PREFIX is required")
	}
	if b.config.S3BucketOptimized == "" {
		b.errors = append(b.errors, "S3_BUCKET_OPTIMIZED is required")
	}
	if b.config.APIStage == "" {
		b.errors = append(b.errors, "STAGE is required")
	}
}
config.ZipProcessorConfig.ZipJobsTableName method · go · L73-L78 (6 LOC)
backend/internal/config/zip_processor_config.go
func (c *ZipProcessorConfig) ZipJobsTableName() string {
	if c.ZipJobsTable != "" {
		return c.ZipJobsTable
	}
	return fmt.Sprintf("%s-zip-jobs-%s", c.DynamoDBTablePrefix, c.APIStage)
}
analytics.NewEventHandler function · go · L47-L57 (11 LOC)
backend/internal/domain/analytics/event_handler.go
func NewEventHandler(
	photographerRepo PhotographerAnalyticsRepo,
	galleryRepo GalleryAnalyticsRepo,
) *EventHandler {
	return &EventHandler{
		photographerRepo:   photographerRepo,
		galleryRepo:        galleryRepo,
		processedGalleries: make(map[string]bool),
		processedViews:     make(map[string]bool),
	}
}
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
analytics.EventHandler.RegisterHandlers method · go · L60-L69 (10 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) RegisterHandlers(bus events.EventBus) {
	bus.Subscribe(events.PhotoUploaded, h.handlePhotoUploaded)
	bus.Subscribe(events.PhotoDeleted, h.handlePhotoDeleted)
	bus.Subscribe(events.GalleryCreated, h.handleGalleryCreated)
	bus.Subscribe(events.GalleryDeleted, h.handleGalleryDeleted)
	bus.Subscribe(events.FavoriteToggled, h.handleFavoriteToggled)
	bus.Subscribe(events.PhotoDownloaded, h.handlePhotoDownloaded)
	bus.Subscribe(events.ClientSessionCreated, h.handleSessionCreated)
	bus.Subscribe(events.GalleryViewed, h.handleGalleryViewed)
}
analytics.EventHandler.handlePhotoUploaded method · go · L72-L101 (30 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) handlePhotoUploaded(ctx context.Context, event events.Event) error {
	payload, ok := event.Payload().(*events.PhotoUploadedPayload)
	if !ok {
		logger.Error("Invalid payload type for PhotoUploaded event", map[string]interface{}{
			"eventType":    string(event.Type()),
			"payloadType":  fmt.Sprintf("%T", event.Payload()),
			"expectedType": "*events.PhotoUploadedPayload",
		})
		return nil
	}

	gallery, err := h.galleryRepo.GetByID(ctx, payload.GalleryID)
	if err != nil || gallery == nil {
		logger.Error("Failed to get gallery for photo upload analytics", map[string]interface{}{
			"galleryId": payload.GalleryID,
			"error":     err,
		})
		return nil // Don't fail the upload for analytics errors
	}

	// Increment photographer's total photos
	if err := h.photographerRepo.IncrementTotalPhotos(ctx, gallery.PhotographerID, 1); err != nil {
		logger.Error("Failed to increment total photos", map[string]interface{}{
			"photographerId": gallery.PhotographerID,
			"err
analytics.EventHandler.handlePhotoDeleted method · go · L104-L134 (31 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) handlePhotoDeleted(ctx context.Context, event events.Event) error {
	payload, ok := event.Payload().(*events.PhotoUploadedPayload) // Reusing same payload structure
	if !ok {
		logger.Error("Invalid payload type for PhotoDeleted event", map[string]interface{}{
			"eventType":    string(event.Type()),
			"payloadType":  fmt.Sprintf("%T", event.Payload()),
			"expectedType": "*events.PhotoUploadedPayload",
		})
		return nil
	}

	gallery, err := h.galleryRepo.GetByID(ctx, payload.GalleryID)
	if err != nil || gallery == nil {
		logger.Error("Failed to get gallery for photo delete analytics", map[string]interface{}{
			"galleryId": payload.GalleryID,
			"photoId":   payload.PhotoID,
			"error":     err,
		})
		return nil
	}

	// Decrement photographer's total photos
	if err := h.photographerRepo.IncrementTotalPhotos(ctx, gallery.PhotographerID, -1); err != nil {
		logger.Error("Failed to decrement total photos", map[string]interface{}{
			"photographerId": gallery.Pho
analytics.EventHandler.handleGalleryCreated method · go · L137-L177 (41 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) handleGalleryCreated(ctx context.Context, event events.Event) error {
	payload, ok := event.Payload().(*events.GalleryCreatedPayload)
	if !ok {
		logger.Error("Invalid payload type for GalleryCreated event", map[string]interface{}{
			"eventType":    string(event.Type()),
			"payloadType":  fmt.Sprintf("%T", event.Payload()),
			"expectedType": "*events.GalleryCreatedPayload",
		})
		return nil
	}

	// Idempotency check: only increment if this gallery hasn't been processed
	h.mu.Lock()
	if h.processedGalleries[payload.GalleryID] {
		h.mu.Unlock()
		logger.Info("Skipping duplicate gallery created event", map[string]interface{}{
			"galleryId":      payload.GalleryID,
			"photographerId": payload.PhotographerID,
		})
		return nil
	}
	// Prevent unbounded memory growth: clear map if it exceeds limit
	if len(h.processedGalleries) >= maxIdempotencyEntries {
		h.processedGalleries = make(map[string]bool)
		logger.Info("Cleared idempotency cache (reached max entries)", 
analytics.EventHandler.handleGalleryDeleted method · go · L180-L216 (37 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) handleGalleryDeleted(ctx context.Context, event events.Event) error {
	payload, ok := event.Payload().(*events.GalleryDeletedPayload)
	if !ok {
		logger.Error("Invalid payload type for GalleryDeleted event", map[string]interface{}{
			"eventType":    string(event.Type()),
			"payloadType":  fmt.Sprintf("%T", event.Payload()),
			"expectedType": "*events.GalleryDeletedPayload",
		})
		return nil
	}

	// Remove from idempotency tracking (allows re-creation with same ID if needed)
	h.mu.Lock()
	delete(h.processedGalleries, payload.GalleryID)
	h.mu.Unlock()

	// Decrement total galleries
	if err := h.photographerRepo.IncrementTotalGalleries(ctx, payload.PhotographerID, -1); err != nil {
		logger.Error("Failed to decrement total galleries", map[string]interface{}{
			"photographerId": payload.PhotographerID,
			"error":          err,
		})
	}

	// Decrement total photos by the count of photos in the deleted gallery
	if payload.PhotoCount > 0 {
		if err := h.photographe
analytics.EventHandler.handleFavoriteToggled method · go · L219-L262 (44 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) handleFavoriteToggled(ctx context.Context, event events.Event) error {
	payload, ok := event.Payload().(*events.FavoriteToggledPayload)
	if !ok {
		logger.Error("Invalid payload type for FavoriteToggled event", map[string]interface{}{
			"eventType":    string(event.Type()),
			"payloadType":  fmt.Sprintf("%T", event.Payload()),
			"expectedType": "*events.FavoriteToggledPayload",
		})
		return nil
	}

	gallery, err := h.galleryRepo.GetByID(ctx, payload.GalleryID)
	if err != nil || gallery == nil {
		logger.Error("Failed to get gallery for favorite toggle analytics", map[string]interface{}{
			"galleryId": payload.GalleryID,
			"photoId":   payload.PhotoID,
			"error":     err,
		})
		return nil
	}

	delta := 1
	if !payload.Favorited {
		delta = -1
	}

	// Update gallery favorite count
	if err := h.galleryRepo.IncrementFavoriteCount(ctx, payload.GalleryID, delta); err != nil {
		logger.Error("Failed to update gallery favorite count", map[string]interface{}{
			"g
analytics.EventHandler.handlePhotoDownloaded method · go · L265-L293 (29 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) handlePhotoDownloaded(ctx context.Context, event events.Event) error {
	payload, ok := event.Payload().(*events.PhotoDownloadedPayload)
	if !ok {
		logger.Error("Invalid payload type for PhotoDownloaded event", map[string]interface{}{
			"eventType":    string(event.Type()),
			"payloadType":  fmt.Sprintf("%T", event.Payload()),
			"expectedType": "*events.PhotoDownloadedPayload",
		})
		return nil
	}

	// Update gallery download count
	if err := h.galleryRepo.IncrementDownloadCount(ctx, payload.GalleryID, 1); err != nil {
		logger.Error("Failed to update gallery download count", map[string]interface{}{
			"galleryId": payload.GalleryID,
			"error":     err,
		})
	}

	// Update photographer total downloads
	if err := h.photographerRepo.IncrementTotalDownloads(ctx, payload.PhotographerID, 1); err != nil {
		logger.Error("Failed to update photographer total downloads", map[string]interface{}{
			"photographerId": payload.PhotographerID,
			"error":          err,
		
analytics.EventHandler.handleSessionCreated method · go · L296-L332 (37 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) handleSessionCreated(ctx context.Context, event events.Event) error {
	payload, ok := event.Payload().(*events.ClientSessionCreatedPayload)
	if !ok {
		logger.Error("Invalid payload type for ClientSessionCreated event", map[string]interface{}{
			"eventType":    string(event.Type()),
			"payloadType":  fmt.Sprintf("%T", event.Payload()),
			"expectedType": "*events.ClientSessionCreatedPayload",
		})
		return nil
	}

	// Update gallery unique clients
	if err := h.galleryRepo.IncrementUniqueClients(ctx, payload.GalleryID); err != nil {
		logger.Error("Failed to update gallery unique clients", map[string]interface{}{
			"galleryId": payload.GalleryID,
			"error":     err,
		})
	}

	// Update gallery last client access
	if err := h.galleryRepo.UpdateLastClientAccess(ctx, payload.GalleryID); err != nil {
		logger.Error("Failed to update gallery last client access", map[string]interface{}{
			"galleryId": payload.GalleryID,
			"error":     err,
		})
	}

	// Update phot
All rows scored by the Repobility analyzer (https://repobility.com)
analytics.EventHandler.handleGalleryViewed method · go · L335-L385 (51 LOC)
backend/internal/domain/analytics/event_handler.go
func (h *EventHandler) handleGalleryViewed(ctx context.Context, event events.Event) error {
	payload, ok := event.Payload().(*events.GalleryViewedPayload)
	if !ok {
		logger.Error("Invalid payload type for GalleryViewed event", map[string]interface{}{
			"eventType":    string(event.Type()),
			"payloadType":  fmt.Sprintf("%T", event.Payload()),
			"expectedType": "*events.GalleryViewedPayload",
		})
		return nil
	}

	// Idempotency check: deduplicate by session+gallery to prevent inflated counts from retries
	viewKey := fmt.Sprintf("%s:%s", payload.SessionID, payload.GalleryID)
	h.mu.Lock()
	if h.processedViews[viewKey] {
		h.mu.Unlock()
		logger.Info("Skipping duplicate gallery viewed event", map[string]interface{}{
			"galleryId":      payload.GalleryID,
			"sessionId":      payload.SessionID,
			"photographerId": payload.PhotographerID,
		})
		return nil
	}
	// Prevent unbounded memory growth: clear map if it exceeds limit
	if len(h.processedViews) >= maxIdempotencyEntries {
		h.pr
analytics.NewService function · go · L26-L38 (13 LOC)
backend/internal/domain/analytics/service.go
func NewService(
	photographerRepo *PhotographerRepoAdapter,
	galleryRepo repository.GalleryRepository,
	photoRepo repository.PhotoRepository,
	sessionRepo repository.ClientSessionRepository,
) *Service {
	return &Service{
		photographerRepo: photographerRepo,
		galleryRepo:      galleryRepo,
		photoRepo:        photoRepo,
		sessionRepo:      sessionRepo,
	}
}
analytics.Service.GetDashboardSummary method · go · L41-L59 (19 LOC)
backend/internal/domain/analytics/service.go
func (s *Service) GetDashboardSummary(ctx context.Context, photographerID string) (*DashboardSummary, error) {
	p, err := s.photographerRepo.GetByID(ctx, photographerID)
	if err != nil {
		return nil, err
	}
	if p == nil {
		return nil, fmt.Errorf("photographer not found: %s", photographerID)
	}

	return &DashboardSummary{
		TotalViews:        p.TotalViews,
		TotalDownloads:    p.TotalDownloads,
		TotalFavorites:    p.TotalFavorites,
		TotalPhotos:       p.TotalPhotos,
		TotalGalleries:    p.TotalGalleries,
		TotalStorageBytes: p.StorageUsed,
		TotalClients:      p.TotalClients,
	}, nil
}
analytics.Service.GetGalleriesAnalytics method · go · L62-L112 (51 LOC)
backend/internal/domain/analytics/service.go
func (s *Service) GetGalleriesAnalytics(ctx context.Context, photographerID string, limit int, sortBy string) ([]*GalleryAnalytics, error) {
	galleries, _, err := s.galleryRepo.ListByPhotographer(ctx, photographerID, 100, nil)
	if err != nil {
		return nil, err
	}

	analytics := make([]*GalleryAnalytics, 0, len(galleries))
	for _, g := range galleries {
		analytics = append(analytics, &GalleryAnalytics{
			GalleryID:          g.GalleryID,
			Name:               g.Name,
			PhotoCount:         g.PhotoCount,
			TotalSize:          g.TotalSize,
			ViewCount:          g.ViewCount,
			DownloadCount:      g.TotalDownloads,
			FavoriteCount:      g.TotalFavorites,
			UniqueClients:      g.UniqueClients,
			ClientAccessCount:  g.ClientAccessCount,
			LastClientAccessAt: g.LastClientAccessAt,
			CreatedAt:          g.CreatedAt,
			Status:             g.Status,
		})
	}

	// Sort based on sortBy parameter
	switch sortBy {
	case "downloads":
		sort.Slice(analytics, func(i, j int) bool {
			return a
analytics.Service.GetTopPhotos method · go · L115-L168 (54 LOC)
backend/internal/domain/analytics/service.go
func (s *Service) GetTopPhotos(ctx context.Context, photographerID string, limit int, metric string) ([]*TopPhoto, error) {
	// Get all galleries for this photographer
	galleries, _, err := s.galleryRepo.ListByPhotographer(ctx, photographerID, 100, nil)
	if err != nil {
		return nil, err
	}

	// Build gallery name map
	galleryNames := make(map[string]string)
	for _, g := range galleries {
		galleryNames[g.GalleryID] = g.Name
	}

	// Collect all photos from all galleries
	var allPhotos []*repository.Photo
	for _, g := range galleries {
		photos, _, err := s.photoRepo.ListByGallery(ctx, g.GalleryID, 100, nil)
		if err != nil {
			continue // Skip galleries with errors
		}
		allPhotos = append(allPhotos, photos...)
	}

	// Sort photos by metric
	if metric == "downloads" {
		sort.Slice(allPhotos, func(i, j int) bool {
			return allPhotos[i].DownloadCount > allPhotos[j].DownloadCount
		})
	} else { // "favorites" or default
		sort.Slice(allPhotos, func(i, j int) bool {
			return allPhotos[i
analytics.Service.GetClientBehavior method · go · L171-L229 (59 LOC)
backend/internal/domain/analytics/service.go
func (s *Service) GetClientBehavior(ctx context.Context, photographerID string) (*ClientBehaviorAnalytics, error) {
	// Get all galleries for this photographer
	galleries, _, err := s.galleryRepo.ListByPhotographer(ctx, photographerID, 100, nil)
	if err != nil {
		return nil, err
	}

	// Collect gallery IDs
	galleryIDs := make([]string, 0, len(galleries))
	for _, g := range galleries {
		galleryIDs = append(galleryIDs, g.GalleryID)
	}

	// Get device distribution
	deviceDist, err := s.sessionRepo.GetDeviceDistribution(ctx, galleryIDs)
	if err != nil {
		return nil, err
	}

	// Get browser distribution
	browserDist, err := s.sessionRepo.GetBrowserDistribution(ctx, galleryIDs)
	if err != nil {
		return nil, err
	}

	// Calculate total for percentages
	var total int64
	for _, count := range browserDist {
		total += count
	}

	// Convert browser distribution to slice with percentages
	browsers := make([]BrowserDistribution, 0, len(browserDist))
	for browser, count := range browserDist {
		
analytics.GetAnalyticsAccess function · go · L126-L169 (44 LOC)
backend/internal/domain/analytics/types.go
func GetAnalyticsAccess(analyticsLevel string, isStudio bool) AnalyticsAccess {
	switch analyticsLevel {
	case string(AnalyticsLevelNone):
		return AnalyticsAccess{
			Level:          AnalyticsLevelNone,
			Summary:        false,
			Downloads:      false,
			Favorites:      false,
			Galleries:      false,
			TopPhotos:      false,
			ClientBehavior: false,
		}
	case string(AnalyticsLevelBasic):
		return AnalyticsAccess{
			Level:          AnalyticsLevelBasic,
			Summary:        true,
			Downloads:      false,
			Favorites:      false,
			Galleries:      false,
			TopPhotos:      false,
			ClientBehavior: false,
		}
	case string(AnalyticsLevelFull):
		return AnalyticsAccess{
			Level:          AnalyticsLevelFull,
			Summary:        true,
			Downloads:      true,
			Favorites:      true,
			Galleries:      true,
			TopPhotos:      true,
			ClientBehavior: isStudio,
		}
	default:
		return AnalyticsAccess{
			Level:          AnalyticsLevelNone,
			Summary:        false,
			Downloads:     
analytics.FilterDashboardSummary function · go · L172-L198 (27 LOC)
backend/internal/domain/analytics/types.go
func FilterDashboardSummary(summary *DashboardSummary, access AnalyticsAccess) *DashboardSummary {
	if summary == nil || !access.Summary {
		// Return zeroed summary for nil input or users without access
		return &DashboardSummary{}
	}

	filtered := &DashboardSummary{
		TotalViews:        summary.TotalViews,
		TotalPhotos:       summary.TotalPhotos,
		TotalGalleries:    summary.TotalGalleries,
		TotalStorageBytes: summary.TotalStorageBytes,
	}

	// Only include downloads/favorites for Pro+ (full analytics)
	if access.Downloads {
		filtered.TotalDownloads = summary.TotalDownloads
	}
	if access.Favorites {
		filtered.TotalFavorites = summary.TotalFavorites
	}
	// Only include client count for Pro+ (full analytics)
	if access.Galleries {
		filtered.TotalClients = summary.TotalClients
	}

	return filtered
}
Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
analytics.FilterGalleryAnalytics function · go · L201-L232 (32 LOC)
backend/internal/domain/analytics/types.go
func FilterGalleryAnalytics(galleries []*GalleryAnalytics, access AnalyticsAccess) []*GalleryAnalytics {
	if !access.Galleries {
		return []*GalleryAnalytics{}
	}

	filtered := make([]*GalleryAnalytics, len(galleries))
	for i, g := range galleries {
		filteredGallery := &GalleryAnalytics{
			GalleryID:          g.GalleryID,
			Name:               g.Name,
			PhotoCount:         g.PhotoCount,
			TotalSize:          g.TotalSize,
			ViewCount:          g.ViewCount,
			CreatedAt:          g.CreatedAt,
			Status:             g.Status,
			UniqueClients:      g.UniqueClients,
			ClientAccessCount:  g.ClientAccessCount,
			LastClientAccessAt: g.LastClientAccessAt,
		}

		if access.Downloads {
			filteredGallery.DownloadCount = g.DownloadCount
		}
		if access.Favorites {
			filteredGallery.FavoriteCount = g.FavoriteCount
		}

		filtered[i] = filteredGallery
	}

	return filtered
}
analytics.FilterTopPhotos function · go · L235-L261 (27 LOC)
backend/internal/domain/analytics/types.go
func FilterTopPhotos(photos []*TopPhoto, access AnalyticsAccess) []*TopPhoto {
	if !access.TopPhotos {
		return []*TopPhoto{}
	}

	filtered := make([]*TopPhoto, len(photos))
	for i, p := range photos {
		filteredPhoto := &TopPhoto{
			PhotoID:      p.PhotoID,
			GalleryID:    p.GalleryID,
			GalleryName:  p.GalleryName,
			FileName:     p.FileName,
			ThumbnailKey: p.ThumbnailKey,
		}

		if access.Favorites {
			filteredPhoto.FavoriteCount = p.FavoriteCount
		}
		if access.Downloads {
			filteredPhoto.DownloadCount = p.DownloadCount
		}

		filtered[i] = filteredPhoto
	}

	return filtered
}
auth.NewSessionService function · go · L28-L36 (9 LOC)
backend/internal/domain/auth/session.go
func NewSessionService(sessionRepo repository.ClientSessionRepository, galleryRepo repository.GalleryRepository, jwtSecret string, sessionTTLHours int, eventBus events.EventBus) *SessionService {
	return &SessionService{
		sessionRepo: sessionRepo,
		galleryRepo: galleryRepo,
		jwtSecret:   []byte(jwtSecret),
		sessionTTL:  time.Duration(sessionTTLHours) * time.Hour,
		eventBus:    eventBus,
	}
}
auth.SessionService.CreateSession method · go · L46-L114 (69 LOC)
backend/internal/domain/auth/session.go
func (s *SessionService) CreateSession(ctx context.Context, galleryID, ipAddress, userAgent string) (string, error) {
	// Generate session ID
	sessionID := utils.GenerateID("session")

	// Parse user agent for device info
	parsedUA := utils.ParseUserAgent(userAgent)

	// Calculate TTL
	now := time.Now()
	expiresAt := now.Add(s.sessionTTL)

	// Create session record
	session := &repository.ClientSession{
		SessionID:     sessionID,
		GalleryID:     galleryID,
		IPAddressHash: utils.HashIPAddress(ipAddress),
		UserAgent:     userAgent,
		DeviceType:    parsedUA.DeviceType,
		BrowserFamily: parsedUA.BrowserFamily,
		OSFamily:      parsedUA.OSFamily,
		FirstAccessAt: now,
		LastAccessAt:  now,
		AccessCount:   1,
		TTL:           expiresAt.Unix(),
	}

	if err := s.sessionRepo.Create(ctx, session); err != nil {
		logger.Error("Failed to create session", map[string]interface{}{"error": err.Error()})
		return "", errors.Wrap(err, 500, "Failed to create session")
	}

	// Generate JWT token
	to
auth.SessionService.VerifySession method · go · L117-L159 (43 LOC)
backend/internal/domain/auth/session.go
func (s *SessionService) VerifySession(ctx context.Context, tokenString string) (*SessionClaims, error) {
	// Parse token
	token, err := jwt.ParseWithClaims(tokenString, &SessionClaims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, errors.NewUnauthorized("Invalid signing method")
		}
		return s.jwtSecret, nil
	})

	if err != nil {
		logger.Warn("Failed to parse session token", map[string]interface{}{"error": err.Error()})
		return nil, errors.NewUnauthorized("Invalid session token")
	}

	claims, ok := token.Claims.(*SessionClaims)
	if !ok || !token.Valid {
		return nil, errors.NewUnauthorized("Invalid session claims")
	}

	// Verify expiration
	if claims.ExpiresAt != nil && claims.ExpiresAt.Before(time.Now()) {
		return nil, errors.NewUnauthorized("Session expired")
	}

	// Verify session exists in database
	session, err := s.sessionRepo.GetByID(ctx, claims.GalleryID, claims.SessionID)
	if err != nil {
		return
auth.SessionService.generateToken method · go · L162-L175 (14 LOC)
backend/internal/domain/auth/session.go
func (s *SessionService) generateToken(galleryID, sessionID string, expiresAt time.Time) (string, error) {
	claims := SessionClaims{
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(expiresAt),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Issuer:    "photographer-gallery",
		},
		GalleryID: galleryID,
		SessionID: sessionID,
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(s.jwtSecret)
}
auth.GenerateJWTSecret function · go · L178-L184 (7 LOC)
backend/internal/domain/auth/session.go
func GenerateJWTSecret() (string, error) {
	bytes := make([]byte, 32)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(bytes), nil
}
customdomain.NewService function · go · L31-L36 (6 LOC)
backend/internal/domain/customdomain/service.go
func NewService(photographerRepo PhotographerRepository, baseDomain string) *Service {
	return &Service{
		photographerRepo: photographerRepo,
		baseDomain:       baseDomain,
	}
}
Repobility · MCP-ready · https://repobility.com
customdomain.Service.GetDomainConfig method · go · L45-L75 (31 LOC)
backend/internal/domain/customdomain/service.go
func (s *Service) GetDomainConfig(ctx context.Context, userID string) (*DomainConfig, error) {
	p, err := s.photographerRepo.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}

	config := &DomainConfig{}

	if p.Subdomain != "" {
		config.Type = TypeSubdomain
		config.Subdomain = p.Subdomain
		config.FullDomain = fmt.Sprintf("%s.%s", p.Subdomain, s.baseDomain)
		config.Status = StatusActive // Subdomains are immediately active
	}

	if p.CustomDomain != "" {
		config.Type = TypeCustomDomain
		config.CustomDomain = p.CustomDomain
		config.FullDomain = p.CustomDomain
		config.Status = DomainStatus(p.DomainStatus)
		config.VerifiedAt = p.DomainVerifiedAt

		// If pending verification, include the token for display
		if p.DomainStatus == string(StatusPendingVerification) {
			config.VerificationToken = p.VerificationToken
			config.DNSInstructions = s.getVerificationInstructions(p.CustomDomain, p.VerificationToken)
		}
	}

	return config, nil
}
customdomain.Service.RequestSubdomain method · go · L78-L118 (41 LOC)
backend/internal/domain/customdomain/service.go
func (s *Service) RequestSubdomain(ctx context.Context, userID, subdomain string) (*DomainConfig, error) {
	// Normalize subdomain
	subdomain = strings.ToLower(strings.TrimSpace(subdomain))

	// Validate format
	if !subdomainPattern.MatchString(subdomain) || len(subdomain) < 3 || len(subdomain) > 63 {
		return nil, ErrSubdomainInvalid
	}

	// Check if reserved
	if IsReservedSubdomain(subdomain) {
		return nil, ErrSubdomainReserved
	}

	// Check for banned/inappropriate words
	if ContainsBannedWord(subdomain) {
		return nil, ErrSubdomainBanned
	}

	// Check if already taken by another user
	existing, err := s.photographerRepo.GetBySubdomain(ctx, subdomain)
	if err != nil && err != photographer.ErrNotFound {
		return nil, fmt.Errorf("failed to check subdomain availability: %w", err)
	}
	if existing != nil && existing.UserID != userID {
		return nil, ErrSubdomainTaken
	}

	// Update the photographer's subdomain
	err = s.photographerRepo.UpdateDomain(ctx, userID, subdomain, "", "", "", "")
customdomain.Service.RequestCustomDomain method · go · L121-L168 (48 LOC)
backend/internal/domain/customdomain/service.go
func (s *Service) RequestCustomDomain(ctx context.Context, userID, domain string) (*DomainConfig, error) {
	// Normalize domain
	domain = strings.ToLower(strings.TrimSpace(domain))
	domain = strings.TrimPrefix(domain, "http://")
	domain = strings.TrimPrefix(domain, "https://")
	domain = strings.TrimPrefix(domain, "www.")
	domain = strings.TrimSuffix(domain, "/")

	// Validate format
	if !customDomainPattern.MatchString(domain) {
		return nil, ErrCustomDomainInvalid
	}

	// Prevent using the base domain or its subdomains
	if domain == s.baseDomain || strings.HasSuffix(domain, "."+s.baseDomain) {
		return nil, ErrCustomDomainInvalid
	}

	// Check if already taken by another user
	existing, err := s.photographerRepo.GetByCustomDomain(ctx, domain)
	if err != nil && err != photographer.ErrNotFound {
		return nil, fmt.Errorf("failed to check domain availability: %w", err)
	}
	if existing != nil && existing.UserID != userID {
		return nil, ErrCustomDomainTaken
	}

	// Generate verification to
customdomain.Service.VerifyDomain method · go · L171-L211 (41 LOC)
backend/internal/domain/customdomain/service.go
func (s *Service) VerifyDomain(ctx context.Context, userID string) (*DomainConfig, error) {
	p, err := s.photographerRepo.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}

	if p.CustomDomain == "" || p.VerificationToken == "" {
		return nil, ErrNoPendingDomain
	}

	if p.DomainStatus == string(StatusActive) {
		return nil, ErrDomainAlreadyActive
	}

	// Check DNS TXT record
	verified := s.checkDNSVerification(p.CustomDomain, p.VerificationToken)

	if !verified {
		return &DomainConfig{
			Type:              TypeCustomDomain,
			CustomDomain:      p.CustomDomain,
			FullDomain:        p.CustomDomain,
			Status:            StatusPendingVerification,
			VerificationToken: p.VerificationToken,
			DNSInstructions:   s.getVerificationInstructions(p.CustomDomain, p.VerificationToken),
		}, ErrVerificationFailed
	}

	// Domain verified! Update status to verified (SSL will be handled separately)
	err = s.photographerRepo.UpdateDomain(ctx, userID, "", p.CustomDomain, string(StatusVerifi
customdomain.Service.ResolvePhotographerByHost method · go · L219-L240 (22 LOC)
backend/internal/domain/customdomain/service.go
func (s *Service) ResolvePhotographerByHost(ctx context.Context, host string) (*photographer.Photographer, error) {
	host = strings.ToLower(strings.TrimSpace(host))
	host = strings.Split(host, ":")[0] // Remove port if present

	// Check if it's a subdomain of our base domain
	if strings.HasSuffix(host, "."+s.baseDomain) {
		subdomain := strings.TrimSuffix(host, "."+s.baseDomain)
		// Ignore www or empty subdomains
		if subdomain == "" || subdomain == "www" {
			return nil, photographer.ErrNotFound
		}
		return s.photographerRepo.GetBySubdomain(ctx, subdomain)
	}

	// Check if it's the base domain itself
	if host == s.baseDomain || host == "www."+s.baseDomain {
		return nil, photographer.ErrNotFound
	}

	// Check if it's a custom domain
	return s.photographerRepo.GetByCustomDomain(ctx, host)
}
customdomain.Service.checkDNSVerification method · go · L243-L259 (17 LOC)
backend/internal/domain/customdomain/service.go
func (s *Service) checkDNSVerification(domain, expectedToken string) bool {
	recordName := fmt.Sprintf("_framefocal-verify.%s", domain)

	txtRecords, err := net.LookupTXT(recordName)
	if err != nil {
		// DNS lookup failed - record doesn't exist yet
		return false
	}

	for _, record := range txtRecords {
		if strings.TrimSpace(record) == expectedToken {
			return true
		}
	}

	return false
}
customdomain.Service.getVerificationInstructions method · go · L262-L271 (10 LOC)
backend/internal/domain/customdomain/service.go
func (s *Service) getVerificationInstructions(domain, token string) []DNSRecord {
	return []DNSRecord{
		{
			Type:    "TXT",
			Name:    fmt.Sprintf("_framefocal-verify.%s", domain),
			Value:   token,
			Purpose: "verification",
		},
	}
}
customdomain.generateVerificationToken function · go · L274-L280 (7 LOC)
backend/internal/domain/customdomain/service.go
func generateVerificationToken() (string, error) {
	bytes := make([]byte, 16)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return "pg-verify-" + hex.EncodeToString(bytes), nil
}
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
gallery.getPhaseBehaviors function · go · L21-L34 (14 LOC)
backend/internal/domain/gallery/service.go
func getPhaseBehaviors(phase string) (bool, bool, bool) {
	switch phase {
	case repository.GalleryPhaseUploading:
		return false, false, false // No watermarks, downloads, or comments during upload
	case repository.GalleryPhaseProofing:
		return true, false, true // Watermarks ON, downloads OFF, comments ON
	case repository.GalleryPhaseEditing:
		return true, false, true // Watermarks ON, downloads OFF, comments ON
	case repository.GalleryPhaseDelivered:
		return false, true, false // Watermarks OFF, downloads ON, comments OFF
	default:
		return false, false, false
	}
}
gallery.Service.Create method · go · L78-L154 (77 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) Create(ctx context.Context, req CreateGalleryRequest) (*repository.Gallery, error) {
	if req.CustomURL == "" {
		req.CustomURL = utils.GenerateCustomURL(req.Name)
	} else if !utils.ValidateCustomURL(req.CustomURL) {
		return nil, errors.NewBadRequest("Invalid custom URL format")
	}

	if existing, _ := s.galleryRepo.GetByCustomURL(ctx, req.CustomURL); existing != nil {
		return nil, errors.New(409, "Custom URL already exists")
	}

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		return nil, errors.NewInternalServer("Failed to hash password")
	}

	// Default template to "grid" if not specified
	template := req.Template
	if template == "" {
		template = "grid"
	}

	// Determine status based on publishAt
	status := "active"
	if req.PublishAt != nil && req.PublishAt.After(time.Now()) {
		status = "scheduled"
	}

	// Default phase to "uploading" if not specified
	phase := req.Phase
	if phase == "" {
		phase = r
gallery.Service.GetByID method · go · L157-L166 (10 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) GetByID(ctx context.Context, galleryID string) (*repository.Gallery, error) {
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}
	return gallery, nil
}
gallery.Service.GetByIDForPhotographer method · go · L171-L180 (10 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) GetByIDForPhotographer(ctx context.Context, galleryID, photographerID string) (*repository.Gallery, error) {
	gallery, err := s.galleryRepo.GetByIDForPhotographer(ctx, galleryID, photographerID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}
	return gallery, nil
}
gallery.Service.GetByCustomURL method · go · L183-L192 (10 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) GetByCustomURL(ctx context.Context, customURL string) (*repository.Gallery, error) {
	gallery, err := s.galleryRepo.GetByCustomURL(ctx, customURL)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}
	return gallery, nil
}
gallery.Service.Update method · go · L204-L219 (16 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) Update(ctx context.Context, galleryID string, req UpdateGalleryRequest) (*repository.Gallery, error) {
	gallery, err := s.GetByID(ctx, galleryID)
	if err != nil {
		return nil, err
	}

	if err := s.applyUpdates(gallery, req); err != nil {
		return nil, errors.Wrap(err, 400, "Failed to apply updates")
	}

	if err := s.galleryRepo.Update(ctx, gallery); err != nil {
		return nil, errors.Wrap(err, 500, "Failed to update gallery")
	}
	logger.Info("Gallery updated", map[string]interface{}{"galleryId": gallery.GalleryID})
	return gallery, nil
}
gallery.Service.applyUpdates method · go · L221-L284 (64 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) applyUpdates(gallery *repository.Gallery, req UpdateGalleryRequest) error {
	if req.Name != nil {
		gallery.Name = *req.Name
	}
	if req.Description != nil {
		gallery.Description = *req.Description
	}
	if req.Password != nil {
		hash, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
		if err != nil {
			return fmt.Errorf("failed to hash password: %w", err)
		}
		gallery.Password = string(hash)
	}
	if req.ExpiresAt != nil {
		gallery.ExpiresAt = req.ExpiresAt
	}
	// Handle publishAt updates
	if req.ClearPublishAt {
		// Clear publishAt and set status to active (publish immediately)
		gallery.PublishAt = nil
		if gallery.Status == "scheduled" {
			gallery.Status = "active"
		}
	} else if req.PublishAt != nil {
		gallery.PublishAt = req.PublishAt
		// Update status based on new publishAt
		if req.PublishAt.After(time.Now()) {
			gallery.Status = "scheduled"
		} else {
			gallery.Status = "active"
		}
	}
	if req.WatermarkType != nil {
		validT
gallery.Service.Delete method · go · L287-L321 (35 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) Delete(ctx context.Context, galleryID string) error {
	gallery, err := s.GetByID(ctx, galleryID)
	if err != nil {
		return err
	}

	photos, err := s.fetchAllPhotos(ctx, galleryID)
	if err != nil {
		return errors.Wrap(err, 500, "Failed to list photos for deletion")
	}

	failedS3, failedDB := s.deletePhotos(ctx, photos)
	if failedS3 > 0 || failedDB > 0 {
		logger.Warn("Gallery deletion completed with failures", map[string]interface{}{
			"galleryId": gallery.GalleryID, "failedS3": failedS3, "failedDB": failedDB,
		})
	}

	if err := s.galleryRepo.Delete(ctx, galleryID); err != nil {
		return errors.Wrap(err, 500, "Failed to delete gallery")
	}
	logger.Info("Gallery deleted", map[string]interface{}{"galleryId": gallery.GalleryID, "photos": len(photos)})

	// Publish GalleryDeleted event for analytics
	if s.eventBus != nil {
		event := events.NewEvent(events.GalleryDeleted, &events.GalleryDeletedPayload{
			GalleryID:      gallery.GalleryID,
			PhotographerID: gallery.Pho
All rows scored by the Repobility analyzer (https://repobility.com)
gallery.Service.fetchAllPhotos method · go · L323-L338 (16 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) fetchAllPhotos(ctx context.Context, galleryID string) ([]*repository.Photo, error) {
	var all []*repository.Photo
	var lastKey map[string]interface{}
	for {
		photos, nextKey, err := s.photoRepo.ListByGallery(ctx, galleryID, photoDeletionBatchSize, lastKey)
		if err != nil {
			return nil, err
		}
		all = append(all, photos...)
		if nextKey == nil {
			break
		}
		lastKey = nextKey
	}
	return all, nil
}
gallery.Service.deletePhotos method · go · L340-L352 (13 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) deletePhotos(ctx context.Context, photos []*repository.Photo) (failedS3, failedDB int) {
	for _, photo := range photos {
		if s.storageService != nil {
			if err := s.storageService.DeletePhoto(ctx, photo.OriginalKey, photo.OptimizedKey, photo.ThumbnailKey); err != nil {
				failedS3++
			}
		}
		if err := s.photoRepo.Delete(ctx, photo.PhotoID); err != nil {
			failedDB++
		}
	}
	return
}
gallery.Service.VerifyPassword method · go · L355-L380 (26 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) VerifyPassword(ctx context.Context, customURL, password string) (*repository.Gallery, error) {
	gallery, err := s.GetByCustomURL(ctx, customURL)
	if err != nil {
		return nil, err
	}

	// Return NotFound for scheduled galleries to hide their existence from clients
	if gallery.Status == "scheduled" {
		return nil, errors.NewNotFound("Gallery")
	}
	if gallery.Status != "active" {
		return nil, errors.NewBadRequest("Gallery is not active")
	}
	if gallery.ExpiresAt != nil && gallery.ExpiresAt.Before(time.Now()) {
		return nil, errors.NewBadRequest("Gallery has expired")
	}
	if gallery.Phase == repository.GalleryPhaseUploading {
		return nil, errors.NewForbidden("This gallery is not yet available for viewing")
	}
	if err := bcrypt.CompareHashAndPassword([]byte(gallery.Password), []byte(password)); err != nil {
		return nil, errors.NewUnauthorized("Invalid password")
	}

	_ = s.galleryRepo.IncrementClientAccessCount(ctx, gallery.GalleryID)
	return gallery, nil
}
‹ prevpage 5 / 20next ›