diff --git a/internal/api/handlers_users.go b/internal/api/handlers_users.go index 549e213..8269d3c 100644 --- a/internal/api/handlers_users.go +++ b/internal/api/handlers_users.go @@ -4,34 +4,38 @@ import ( "net/http" "strings" + "github.com/jackc/pgx/v5/pgtype" + "git.omukk.dev/wrenn/sandbox/internal/auth" - "git.omukk.dev/wrenn/sandbox/internal/service" + "git.omukk.dev/wrenn/sandbox/internal/db" ) type usersHandler struct { - svc *service.TeamService + db *db.Queries } -func newUsersHandler(svc *service.TeamService) *usersHandler { - return &usersHandler{svc: svc} +func newUsersHandler(db *db.Queries) *usersHandler { + return &usersHandler{db: db} } // Search handles GET /v1/users/search?email= // Returns up to 10 users whose email starts with the given prefix. -// The prefix must be at least 3 characters long. +// The prefix must be at least 3 characters long and contain "@". func (h *usersHandler) Search(w http.ResponseWriter, r *http.Request) { auth.MustFromContext(r.Context()) // ensure authenticated prefix := strings.TrimSpace(r.URL.Query().Get("email")) - if len(prefix) < 3 { - writeError(w, http.StatusBadRequest, "invalid_request", "email prefix must be at least 3 characters") + if len(prefix) < 3 || !strings.Contains(prefix, "@") { + writeError(w, http.StatusBadRequest, "invalid_request", "email prefix must be at least 3 characters and contain '@'") return } - results, err := h.svc.SearchUsersByEmailPrefix(r.Context(), prefix) + // Escape LIKE metacharacters to prevent pattern injection. + escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(prefix) + + results, err := h.db.SearchUsersByEmailPrefix(r.Context(), pgtype.Text{String: escaped, Valid: true}) if err != nil { - status, code, msg := serviceErrToHTTP(err) - writeError(w, status, code, msg) + writeError(w, http.StatusInternalServerError, "internal", "search failed") return } diff --git a/internal/api/server.go b/internal/api/server.go index 67043e8..918476b 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -61,7 +61,7 @@ func New( apiKeys := newAPIKeyHandler(apiKeySvc, al) hostH := newHostHandler(hostSvc, queries, al) teamH := newTeamHandler(teamSvc, al) - usersH := newUsersHandler(teamSvc) + usersH := newUsersHandler(queries) auditH := newAuditHandler(auditSvc) statsH := newStatsHandler(statsSvc) metricsH := newSandboxMetricsHandler(queries, pool) diff --git a/internal/service/team.go b/internal/service/team.go index 0c1739e..d4c911c 100644 --- a/internal/service/team.go +++ b/internal/service/team.go @@ -9,7 +9,6 @@ import ( "connectrpc.com/connect" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "git.omukk.dev/wrenn/sandbox/internal/db" @@ -369,12 +368,6 @@ func (s *TeamService) LeaveTeam(ctx context.Context, teamID, callerUserID string return nil } -// SearchUsersByEmailPrefix returns up to 10 users whose email starts with the given prefix. -// The prefix must contain "@" to prevent broad enumeration. -func (s *TeamService) SearchUsersByEmailPrefix(ctx context.Context, prefix string) ([]db.SearchUsersByEmailPrefixRow, error) { - return s.DB.SearchUsersByEmailPrefix(ctx, pgtype.Text{String: prefix, Valid: true}) -} - // SetBYOC enables the BYOC feature flag for a team. Once enabled, BYOC cannot // be disabled — it is a one-way transition. // Admin-only — the caller must verify admin status before invoking this.