diff --git a/backend/internal/repository/user_repo.go b/backend/internal/repository/user_repo.go new file mode 100644 index 0000000..fd567ef --- /dev/null +++ b/backend/internal/repository/user_repo.go @@ -0,0 +1,290 @@ +package repository + +import ( + "context" + "database/sql" + "fmt" + + "github.com/yourusername/victorialogs-manager/internal/models" +) + +// UserRepository handles database operations for users +type UserRepository struct { + db *sql.DB +} + +// NewUserRepository creates a new user repository +func NewUserRepository(db *sql.DB) *UserRepository { + return &UserRepository{db: db} +} + +// Create creates a new user +func (r *UserRepository) Create(ctx context.Context, user *models.User) error { + query := ` + INSERT INTO users (id, username, email, password_hash, role, is_active) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING created_at, updated_at + ` + + err := r.db.QueryRowContext( + ctx, + query, + user.ID, + user.Username, + user.Email, + user.PasswordHash, + user.Role, + user.IsActive, + ).Scan(&user.CreatedAt, &user.UpdatedAt) + + if err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + + return nil +} + +// GetByID retrieves a user by ID +func (r *UserRepository) GetByID(ctx context.Context, id string) (*models.User, error) { + query := ` + SELECT id, username, email, password_hash, role, is_active, created_at, updated_at + FROM users + WHERE id = $1 + ` + + user := &models.User{} + err := r.db.QueryRowContext(ctx, query, id).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.PasswordHash, + &user.Role, + &user.IsActive, + &user.CreatedAt, + &user.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, fmt.Errorf("user not found") + } + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + return user, nil +} + +// GetByUsername retrieves a user by username +func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.User, error) { + query := ` + SELECT id, username, email, password_hash, role, is_active, created_at, updated_at + FROM users + WHERE username = $1 + ` + + user := &models.User{} + err := r.db.QueryRowContext(ctx, query, username).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.PasswordHash, + &user.Role, + &user.IsActive, + &user.CreatedAt, + &user.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, fmt.Errorf("user not found") + } + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + return user, nil +} + +// GetByEmail retrieves a user by email +func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*models.User, error) { + query := ` + SELECT id, username, email, password_hash, role, is_active, created_at, updated_at + FROM users + WHERE email = $1 + ` + + user := &models.User{} + err := r.db.QueryRowContext(ctx, query, email).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.PasswordHash, + &user.Role, + &user.IsActive, + &user.CreatedAt, + &user.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, fmt.Errorf("user not found") + } + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + return user, nil +} + +// List retrieves all users with optional filters +func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]*models.User, error) { + query := ` + SELECT id, username, email, password_hash, role, is_active, created_at, updated_at + FROM users + ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + ` + + rows, err := r.db.QueryContext(ctx, query, limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list users: %w", err) + } + defer rows.Close() + + var users []*models.User + for rows.Next() { + user := &models.User{} + err := rows.Scan( + &user.ID, + &user.Username, + &user.Email, + &user.PasswordHash, + &user.Role, + &user.IsActive, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan user: %w", err) + } + users = append(users, user) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating users: %w", err) + } + + return users, nil +} + +// Update updates a user +func (r *UserRepository) Update(ctx context.Context, user *models.User) error { + query := ` + UPDATE users + SET username = $1, email = $2, role = $3, is_active = $4, updated_at = CURRENT_TIMESTAMP + WHERE id = $5 + RETURNING updated_at + ` + + err := r.db.QueryRowContext( + ctx, + query, + user.Username, + user.Email, + user.Role, + user.IsActive, + user.ID, + ).Scan(&user.UpdatedAt) + + if err == sql.ErrNoRows { + return fmt.Errorf("user not found") + } + if err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + + return nil +} + +// UpdatePassword updates a user's password hash +func (r *UserRepository) UpdatePassword(ctx context.Context, userID, passwordHash string) error { + query := ` + UPDATE users + SET password_hash = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2 + ` + + result, err := r.db.ExecContext(ctx, query, passwordHash, userID) + if err != nil { + return fmt.Errorf("failed to update password: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("user not found") + } + + return nil +} + +// Delete deletes a user +func (r *UserRepository) Delete(ctx context.Context, id string) error { + query := `DELETE FROM users WHERE id = $1` + + result, err := r.db.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to delete user: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("user not found") + } + + return nil +} + +// Count returns the total number of users +func (r *UserRepository) Count(ctx context.Context) (int, error) { + query := `SELECT COUNT(*) FROM users` + + var count int + err := r.db.QueryRowContext(ctx, query).Scan(&count) + if err != nil { + return 0, fmt.Errorf("failed to count users: %w", err) + } + + return count, nil +} + +// UsernameExists checks if a username already exists +func (r *UserRepository) UsernameExists(ctx context.Context, username string) (bool, error) { + query := `SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)` + + var exists bool + err := r.db.QueryRowContext(ctx, query, username).Scan(&exists) + if err != nil { + return false, fmt.Errorf("failed to check username: %w", err) + } + + return exists, nil +} + +// EmailExists checks if an email already exists +func (r *UserRepository) EmailExists(ctx context.Context, email string) (bool, error) { + query := `SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)` + + var exists bool + err := r.db.QueryRowContext(ctx, query, email).Scan(&exists) + if err != nil { + return false, fmt.Errorf("failed to check email: %w", err) + } + + return exists, nil +}