diff --git a/internal/api/handlers/artifacts.go b/internal/api/handlers/artifacts.go index 8e7c4ee..2d023dd 100644 --- a/internal/api/handlers/artifacts.go +++ b/internal/api/handlers/artifacts.go @@ -156,16 +156,8 @@ func (h *ArtifactHandler) Download(w http.ResponseWriter, r *http.Request) { } func (h *ArtifactHandler) resolveRunIDs(w http.ResponseWriter, r *http.Request) (repoID, runID int64, ok bool) { - owner := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - var u models.User - if found, _ := h.db.Where("username = ?", owner).Get(&u); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, 0, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) + rID, ok := resolveRepoID(h.db, w, r) + if !ok { return 0, 0, false } runID, err := strconv.ParseInt(chi.URLParam(r, "runID"), 10, 64) @@ -173,7 +165,7 @@ func (h *ArtifactHandler) resolveRunIDs(w http.ResponseWriter, r *http.Request) jsonError(w, "invalid run ID", http.StatusBadRequest) return 0, 0, false } - return repo.ID, runID, true + return rID, runID, true } func isUnder(root, path string) bool { diff --git a/internal/api/handlers/environment.go b/internal/api/handlers/environment.go index 6517181..38276b7 100644 --- a/internal/api/handlers/environment.go +++ b/internal/api/handlers/environment.go @@ -323,19 +323,7 @@ func (h *EnvironmentHandler) UpdateDeploymentStatus(w http.ResponseWriter, r *ht // ── Helpers ─────────────────────────────────────────────────────────────────── func (h *EnvironmentHandler) resolveRepo(w http.ResponseWriter, r *http.Request) (int64, bool) { - owner := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - var u models.User - if found, _ := h.db.Where("username = ?", owner).Get(&u); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - return repo.ID, true + return resolveRepoID(h.db, w, r) } func (h *EnvironmentHandler) resolveEnv(w http.ResponseWriter, r *http.Request) (*models.Environment, bool) { diff --git a/internal/api/handlers/issues.go b/internal/api/handlers/issues.go index 2e9df98..c29a961 100644 --- a/internal/api/handlers/issues.go +++ b/internal/api/handlers/issues.go @@ -132,20 +132,7 @@ func (h *IssueHandler) Reopen(w http.ResponseWriter, r *http.Request) { } func (h *IssueHandler) repoID(w http.ResponseWriter, r *http.Request) (int64, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - - var owner models.User - if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - return repo.ID, true + return resolveRepoID(h.db, w, r) } func (h *IssueHandler) lookupIssue(w http.ResponseWriter, r *http.Request) (*models.Issue, bool) { diff --git a/internal/api/handlers/keys.go b/internal/api/handlers/keys.go index 16e4cb2..fc08a80 100644 --- a/internal/api/handlers/keys.go +++ b/internal/api/handlers/keys.go @@ -48,19 +48,13 @@ type deployKeyResponse struct { } func (h *DeployKeyHandler) lookupRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, *models.User, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") + repo, ok := resolveRepo(h.db, w, r) + if !ok { + return nil, nil, false + } var owner models.User - if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, nil, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, nil, false - } - return &repo, &owner, true + h.db.ID(repo.OwnerID).Get(&owner) + return repo, &owner, true } func (h *DeployKeyHandler) canManage(repo *models.Repository, callerID int64) bool { diff --git a/internal/api/handlers/lfs.go b/internal/api/handlers/lfs.go index 260f850..d123812 100644 --- a/internal/api/handlers/lfs.go +++ b/internal/api/handlers/lfs.go @@ -4,7 +4,6 @@ import ( "encoding/json" "net/http" - "github.com/go-chi/chi/v5" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/api/middleware" @@ -16,31 +15,7 @@ type LFSHandler struct{ db *xorm.Engine } func NewLFSHandler(db *xorm.Engine) *LFSHandler { return &LFSHandler{db: db} } func (h *LFSHandler) resolveRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - - var owner models.User - found, err := h.db.Where("username = ?", ownerName).Get(&owner) - if err != nil { - jsonError(w, "database error: "+err.Error(), http.StatusInternalServerError) - return nil, false - } - if !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, false - } - - var repo models.Repository - found, err = h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo) - if err != nil { - jsonError(w, "database error: "+err.Error(), http.StatusInternalServerError) - return nil, false - } - if !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, false - } - return &repo, true + return resolveRepo(h.db, w, r) } func (h *LFSHandler) canManage(repo *models.Repository, callerID int64) bool { diff --git a/internal/api/handlers/members.go b/internal/api/handlers/members.go index 043da93..b7ab4d6 100644 --- a/internal/api/handlers/members.go +++ b/internal/api/handlers/members.go @@ -28,22 +28,15 @@ type memberResponse struct { AddedAt string `json:"addedAt"` } -// lookupRepoForMembers resolves the repo from URL params and returns the owner User. +// lookupRepoAndOwner resolves {owner}/{repo} and returns the repo + its creator user. func (h *MemberHandler) lookupRepoAndOwner(w http.ResponseWriter, r *http.Request) (*models.Repository, *models.User, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - + repo, ok := resolveRepo(h.db, w, r) + if !ok { + return nil, nil, false + } var owner models.User - if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, nil, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, nil, false - } - return &repo, &owner, true + h.db.ID(repo.OwnerID).Get(&owner) + return repo, &owner, true } // callerCanManage returns true if callerID is the repo owner or has admin permission. diff --git a/internal/api/handlers/pipelines.go b/internal/api/handlers/pipelines.go index 4df7822..d790f51 100644 --- a/internal/api/handlers/pipelines.go +++ b/internal/api/handlers/pipelines.go @@ -247,19 +247,7 @@ func (h *PipelineHandler) RetryJob(w http.ResponseWriter, r *http.Request) { // ── Helpers ─────────────────────────────────────────────────────────────────── func (h *PipelineHandler) repoID(w http.ResponseWriter, r *http.Request) (int64, bool) { - owner := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - var u models.User - if found, _ := h.db.Where("username = ?", owner).Get(&u); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - return repo.ID, true + return resolveRepoID(h.db, w, r) } func (h *PipelineHandler) lookupRun(w http.ResponseWriter, r *http.Request) (*models.PipelineRun, bool) { diff --git a/internal/api/handlers/pr_settings.go b/internal/api/handlers/pr_settings.go index 23aca5d..ae48d78 100644 --- a/internal/api/handlers/pr_settings.go +++ b/internal/api/handlers/pr_settings.go @@ -18,29 +18,7 @@ func NewPRSettingsHandler(db *xorm.Engine) *PRSettingsHandler { } func (h *PRSettingsHandler) resolveRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - var owner models.User - found, err := h.db.Where("username = ?", ownerName).Get(&owner) - if err != nil { - jsonError(w, "database error: "+err.Error(), http.StatusInternalServerError) - return nil, false - } - if !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, false - } - var repo models.Repository - found, err = h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo) - if err != nil { - jsonError(w, "database error: "+err.Error(), http.StatusInternalServerError) - return nil, false - } - if !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, false - } - return &repo, true + return resolveRepo(h.db, w, r) } func (h *PRSettingsHandler) canManage(repo *models.Repository, callerID int64) bool { diff --git a/internal/api/handlers/prs.go b/internal/api/handlers/prs.go index 5aa530d..d5e6454 100644 --- a/internal/api/handlers/prs.go +++ b/internal/api/handlers/prs.go @@ -241,23 +241,7 @@ func (h *PRHandler) Update(w http.ResponseWriter, r *http.Request) { } func (h *PRHandler) repoIDFromURL(w http.ResponseWriter, r *http.Request) (int64, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - - var owner models.User - found, err := h.db.Where("username = ?", ownerName).Get(&owner) - if err != nil || !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - - var repo models.Repository - found, err = h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo) - if err != nil || !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - return repo.ID, true + return resolveRepoID(h.db, w, r) } func (h *PRHandler) lookupPR(w http.ResponseWriter, r *http.Request) (*models.PullRequest, bool) { diff --git a/internal/api/handlers/repo_lookup.go b/internal/api/handlers/repo_lookup.go new file mode 100644 index 0000000..918ef4f --- /dev/null +++ b/internal/api/handlers/repo_lookup.go @@ -0,0 +1,55 @@ +package handlers + +// repo_lookup.go — shared helper used by all handlers that resolve +// {owner}/{repo} URL params to a repository row. +// +// The owner segment can be either a user username (user-owned repo) or a +// workspace handle (workspace-owned repo). This tries user-namespace first, +// then workspace-namespace, so the lookup is always unambiguous. + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "xorm.io/xorm" + + "github.com/forgeo/forgebucket/internal/models" +) + +// resolveRepoID resolves /{owner}/{repo} to a repository ID. +// It tries user namespace first, then workspace namespace. +// Returns (repoID, true) on success or writes a 404 and returns (0, false). +func resolveRepoID(db *xorm.Engine, w http.ResponseWriter, r *http.Request) (int64, bool) { + repo, ok := resolveRepo(db, w, r) + if !ok { + return 0, false + } + return repo.ID, true +} + +// resolveRepo is the full repo lookup returning the Repository struct. +func resolveRepo(db *xorm.Engine, w http.ResponseWriter, r *http.Request) (*models.Repository, bool) { + ownerName := chi.URLParam(r, "owner") + repoName := chi.URLParam(r, "repo") + + // 1. Try user namespace. + var u models.User + if found, _ := db.Where("username = ?", ownerName).Get(&u); found { + var repo models.Repository + if found2, _ := db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); found2 { + return &repo, true + } + } + + // 2. Try workspace namespace. + var ws models.Workspace + if found, _ := db.Where("handle = ?", ownerName).Get(&ws); found { + var repo models.Repository + if found2, _ := db.Where("workspace_id = ? AND name = ?", ws.ID, repoName).Get(&repo); found2 { + return &repo, true + } + } + + jsonError(w, "repository not found", http.StatusNotFound) + return nil, false +} diff --git a/internal/api/handlers/secret.go b/internal/api/handlers/secret.go index f52b6b7..2706c9f 100644 --- a/internal/api/handlers/secret.go +++ b/internal/api/handlers/secret.go @@ -242,29 +242,7 @@ func ResolveSecretsForRun(db *xorm.Engine, repoID, workspaceID, envID int64, ses // ── Helpers ─────────────────────────────────────────────────────────────────── func (h *SecretHandler) resolveRepoID(w http.ResponseWriter, r *http.Request) (int64, bool) { - owner := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - var u models.User - if found, _ := h.db.Where("username = ?", owner).Get(&u); !found { - // Try workspace - var ws models.Workspace - if found2, _ := h.db.Where("handle = ?", owner).Get(&ws); !found2 { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - var repo models.Repository - if found3, _ := h.db.Where("workspace_id = ? AND name = ?", ws.ID, repoName).Get(&repo); !found3 { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - return repo.ID, true - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return 0, false - } - return repo.ID, true + return resolveRepoID(h.db, w, r) } func (h *SecretHandler) resolveEnvID(w http.ResponseWriter, r *http.Request) (int64, bool) { diff --git a/internal/api/handlers/timeline.go b/internal/api/handlers/timeline.go index 3547408..c79cc96 100644 --- a/internal/api/handlers/timeline.go +++ b/internal/api/handlers/timeline.go @@ -6,7 +6,6 @@ import ( "strconv" "time" - "github.com/go-chi/chi/v5" "xorm.io/xorm" gitdomain "github.com/forgeo/forgebucket/internal/domain/git" @@ -69,23 +68,13 @@ type TimelineEvent struct { // // GET /api/v1/repos/:owner/:repo/timeline?limit=60 func (h *TimelineHandler) GetTimeline(w http.ResponseWriter, r *http.Request) { - owner := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - limit := 60 if l, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && l > 0 && l <= 200 { limit = l } - // ── Resolve repo ────────────────────────────────────────────────────────── - var u models.User - if found, _ := h.db.Where("username = ?", owner).Get(&u); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) + repo, ok := resolveRepo(h.db, w, r) + if !ok { return } diff --git a/internal/api/handlers/tokens.go b/internal/api/handlers/tokens.go index 9cfd097..9d16dae 100644 --- a/internal/api/handlers/tokens.go +++ b/internal/api/handlers/tokens.go @@ -30,19 +30,7 @@ type accessTokenResponse struct { } func (h *AccessTokenHandler) lookupRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - var owner models.User - if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, false - } - return &repo, true + return resolveRepo(h.db, w, r) } func (h *AccessTokenHandler) canManage(repo *models.Repository, callerID int64) bool { diff --git a/internal/api/handlers/webhooks.go b/internal/api/handlers/webhooks.go index d4ee223..8814e24 100644 --- a/internal/api/handlers/webhooks.go +++ b/internal/api/handlers/webhooks.go @@ -55,19 +55,7 @@ func toWebhookResp(wh models.Webhook) webhookResponse { } func (h *WebhookHandler) resolveRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") - var owner models.User - if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, false - } - return &repo, true + return resolveRepo(h.db, w, r) } func (h *WebhookHandler) canManage(repo *models.Repository, callerID int64) bool { diff --git a/internal/api/handlers/workflow.go b/internal/api/handlers/workflow.go index 360cb84..500fc48 100644 --- a/internal/api/handlers/workflow.go +++ b/internal/api/handlers/workflow.go @@ -22,19 +22,13 @@ type WorkflowHandler struct{ db *xorm.Engine } func NewWorkflowHandler(db *xorm.Engine) *WorkflowHandler { return &WorkflowHandler{db: db} } func (h *WorkflowHandler) resolveRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, *models.User, bool) { - ownerName := chi.URLParam(r, "owner") - repoName := chi.URLParam(r, "repo") + repo, ok := resolveRepo(h.db, w, r) + if !ok { + return nil, nil, false + } var owner models.User - if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, nil, false - } - var repo models.Repository - if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { - jsonError(w, "repository not found", http.StatusNotFound) - return nil, nil, false - } - return &repo, &owner, true + h.db.ID(repo.OwnerID).Get(&owner) + return repo, &owner, true } func (h *WorkflowHandler) canManage(repo *models.Repository, callerID int64) bool {