package user import ( "context" "errors" "fmt" "log" "os" "path/filepath" "time" "dance-lessons-coach/pkg/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // SQLiteRepository implements UserRepository using SQLite type SQLiteRepository struct { db *gorm.DB dbPath string config *config.Config spanPrefix string } // NewSQLiteRepository creates a new SQLite repository func NewSQLiteRepository(dbPath string, config *config.Config) (*SQLiteRepository, error) { repo := &SQLiteRepository{ dbPath: dbPath, config: config, spanPrefix: "user.repo.", } if err := repo.initializeDatabase(); err != nil { return nil, fmt.Errorf("failed to initialize database: %w", err) } return repo, nil } // initializeDatabase sets up the SQLite database and runs migrations func (r *SQLiteRepository) initializeDatabase() error { // Create directory if it doesn't exist dir := filepath.Dir(r.dbPath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create directory: %w", err) } // Configure GORM logger to use standard log gormLogger := logger.New( log.New(os.Stdout, "\n", log.LstdFlags), logger.Config{ SlowThreshold: time.Second, LogLevel: logger.Warn, IgnoreRecordNotFoundError: true, Colorful: true, }, ) var err error r.db, err = gorm.Open(sqlite.Open(r.dbPath), &gorm.Config{ Logger: gormLogger, }) if err != nil { return fmt.Errorf("failed to connect to database: %w", err) } // Auto-migrate the User model if err := r.db.AutoMigrate(&User{}); err != nil { return fmt.Errorf("failed to auto-migrate: %w", err) } return nil } // CreateUser creates a new user in the database func (r *SQLiteRepository) CreateUser(ctx context.Context, user *User) error { // Create telemetry span ctx, span := r.createSpan(ctx, "create_user") if span != nil { defer span.End() } result := r.db.WithContext(ctx).Create(user) if result.Error != nil { if span != nil { span.RecordError(result.Error) } return fmt.Errorf("failed to create user: %w", result.Error) } return nil } // GetUserByUsername retrieves a user by username func (r *SQLiteRepository) GetUserByUsername(ctx context.Context, username string) (*User, error) { // Create telemetry span ctx, span := r.createSpan(ctx, "get_user_by_username") if span != nil { defer span.End() span.SetAttributes(attribute.String("username", username)) } var user User result := r.db.WithContext(ctx).Where("username = ?", username).First(&user) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, nil } if span != nil { span.RecordError(result.Error) } return nil, fmt.Errorf("failed to get user by username: %w", result.Error) } return &user, nil } // GetUserByID retrieves a user by ID func (r *SQLiteRepository) GetUserByID(ctx context.Context, id uint) (*User, error) { var user User result := r.db.WithContext(ctx).First(&user, id) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, nil } return nil, fmt.Errorf("failed to get user by ID: %w", result.Error) } return &user, nil } // UpdateUser updates a user in the database func (r *SQLiteRepository) UpdateUser(ctx context.Context, user *User) error { result := r.db.WithContext(ctx).Save(user) if result.Error != nil { return fmt.Errorf("failed to update user: %w", result.Error) } return nil } // DeleteUser deletes a user from the database func (r *SQLiteRepository) DeleteUser(ctx context.Context, id uint) error { result := r.db.WithContext(ctx).Delete(&User{}, id) if result.Error != nil { return fmt.Errorf("failed to delete user: %w", result.Error) } return nil } // AllowPasswordReset flags a user for password reset func (r *SQLiteRepository) AllowPasswordReset(ctx context.Context, username string) error { user, err := r.GetUserByUsername(ctx, username) if err != nil { return fmt.Errorf("failed to get user for password reset: %w", err) } if user == nil { return fmt.Errorf("user not found: %s", username) } user.AllowPasswordReset = true return r.UpdateUser(ctx, user) } // CompletePasswordReset completes the password reset process func (r *SQLiteRepository) CompletePasswordReset(ctx context.Context, username, newPasswordHash string) error { user, err := r.GetUserByUsername(ctx, username) if err != nil { return fmt.Errorf("failed to get user for password reset completion: %w", err) } if user == nil { return fmt.Errorf("user not found: %s", username) } if !user.AllowPasswordReset { return fmt.Errorf("password reset not allowed for user: %s", username) } user.PasswordHash = newPasswordHash user.AllowPasswordReset = false return r.UpdateUser(ctx, user) } // UserExists checks if a user exists by username func (r *SQLiteRepository) UserExists(ctx context.Context, username string) (bool, error) { var count int64 result := r.db.WithContext(ctx).Model(&User{}).Where("username = ?", username).Count(&count) if result.Error != nil { return false, fmt.Errorf("failed to check if user exists: %w", result.Error) } return count > 0, nil } // Close closes the database connection func (r *SQLiteRepository) Close() error { sqlDB, err := r.db.DB() if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } return sqlDB.Close() } // CheckDatabaseHealth checks if the database is healthy and responsive func (r *SQLiteRepository) CheckDatabaseHealth(ctx context.Context) error { // Simple query to test database connectivity var count int64 result := r.db.WithContext(ctx).Model(&User{}).Count(&count) if result.Error != nil { return fmt.Errorf("database health check failed: %w", result.Error) } return nil } // createSpan creates a new telemetry span if persistence telemetry is enabled func (r *SQLiteRepository) createSpan(ctx context.Context, operation string) (context.Context, trace.Span) { if r.config == nil || !r.config.GetPersistenceTelemetryEnabled() { return ctx, trace.SpanFromContext(ctx) } // Create a new span with the operation name spanName := r.spanPrefix + operation tr := otel.Tracer("user-repository") return tr.Start(ctx, spanName) }