From bb0bdf8da8bc407bda1c34c7080559f83ec455b3 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 14 Apr 2026 09:06:18 +0000 Subject: [PATCH] appview: handlers, state, and templates for more record types w/ repoDID Lewis: May this revision serve well! appview,knotserver: validate git repo ownership according to knot Lewis: May this revision serve well! --- appview/db/notifications.go | 9 ++++++--- appview/issues/issues.go | 6 +++--- appview/labels/labels.go | 2 +- appview/middleware/middleware.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++----- appview/models/repo.go | 5 +++++ appview/pages/repoinfo/repoinfo.go | 4 ++-- appview/pages/templates/goodfirstissues/index.html | 4 ++-- appview/pages/templates/knots/dashboard.html | 2 +- appview/pages/templates/layouts/repobase.html | 2 +- appview/pages/templates/notifications/fragments/item.html | 6 +++--- appview/pages/templates/repo/empty.html | 2 +- appview/pages/templates/repo/fragments/cloneDropdown.html | 8 ++++---- appview/pages/templates/repo/index.html | 1 + appview/pages/templates/repo/pulls/fragments/pullActions.html | 2 +- appview/pages/templates/repo/pulls/fragments/pullHeader.html | 2 +- appview/pages/templates/repo/pulls/pull.html | 2 +- appview/pages/templates/repo/settings/hooks.html | 10 ++++++++++ appview/pages/templates/repo/settings/sites.html | 6 +++--- appview/pages/templates/spindles/dashboard.html | 2 +- appview/pages/templates/timeline/fragments/preview.html | 8 ++++---- appview/pages/templates/timeline/fragments/timeline.html | 8 ++++---- appview/pages/templates/user/fragments/issueEvent.html | 5 +++-- appview/pages/templates/user/fragments/pullEvent.html | 5 +++-- appview/pages/templates/user/fragments/repoCard.html | 4 ++-- appview/pages/templates/user/overview.html | 2 +- appview/pipelines/pipelines.go | 8 ++++---- appview/pulls/compose_helpers_test.go | 2 +- appview/repo/artifact.go | 9 +++++---- appview/repo/feed.go | 6 +++--- appview/repo/index.go | 7 ++++--- appview/repo/opengraph.go | 2 +- appview/repo/repo.go | 39 +++++++++++++++++---------------------- appview/repo/repo_util.go | 2 +- appview/repo/router.go | 1 + appview/repo/settings.go | 23 ++++++++++++----------- appview/repo/tags.go | 4 ++-- appview/repo/webhooks.go | 38 ++++++++++++++++++++++---------------- appview/reporesolver/resolver.go | 16 ++++++++-------- appview/settings/settings.go | 4 ++-- appview/sites/sites.go | 2 +- appview/state/knotstream.go | 43 ++++++++++++++----------------------------- appview/state/profile.go | 6 +++--- appview/state/router.go | 2 +- appview/state/state.go | 101 ++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------- appview/strings/strings.go | 5 +++-- 45 file(s) changed, 258 insertion(s)(+), 225 deletion(s)(-) diff --git a/appview/db/notifications.go b/appview/db/notifications.go --- a/appview/db/notifications.go +++ b/appview/db/notifications.go @@ -135,7 +135,7 @@ query := fmt.Sprintf(` select n.id, n.recipient_did, n.actor_did, n.type, n.entity_type, n.entity_id, n.read, n.created, n.repo_id, n.issue_id, n.pull_id, - r.id as r_id, r.did as r_did, r.name as r_name, r.description as r_description, r.website as r_website, r.topics as r_topics, + r.id as r_id, r.did as r_did, r.rkey as r_rkey, r.name as r_name, r.description as r_description, r.website as r_website, r.topics as r_topics, i.id as i_id, i.did as i_did, i.issue_id as i_issue_id, i.title as i_title, i.open as i_open, p.id as p_id, p.owner_did as p_owner_did, p.pull_id as p_pull_id, p.title as p_title, p.state as p_state from notifications n @@ -164,7 +164,7 @@ var repo models.Repo var issue models.Issue var pull models.Pull var rId, iId, pId sql.NullInt64 - var rDid, rName, rDescription, rWebsite, rTopicStr sql.NullString + var rDid, rRkey, rName, rDescription, rWebsite, rTopicStr sql.NullString var iDid sql.NullString var iIssueId sql.NullInt64 var iTitle sql.NullString @@ -177,7 +177,7 @@ err := rows.Scan( &n.ID, &n.RecipientDid, &n.ActorDid, &typeStr, &n.EntityType, &n.EntityId, &n.Read, &createdStr, &n.RepoId, &n.IssueId, &n.PullId, - &rId, &rDid, &rName, &rDescription, &rWebsite, &rTopicStr, + &rId, &rDid, &rRkey, &rName, &rDescription, &rWebsite, &rTopicStr, &iId, &iDid, &iIssueId, &iTitle, &iOpen, &pId, &pOwnerDid, &pPullId, &pTitle, &pState, ) @@ -198,6 +198,9 @@ if rId.Valid { repo.Id = rId.Int64 if rDid.Valid { repo.Did = rDid.String + } + if rRkey.Valid { + repo.Rkey = rRkey.String } if rName.Valid { repo.Name = rName.String diff --git a/appview/issues/issues.go b/appview/issues/issues.go --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -885,7 +885,7 @@ searchOpts := models.IssueSearchOptions{ Keywords: tf.Keywords, Phrases: tf.Phrases, - RepoAt: f.RepoAt().String(), + RepoDid: f.RepoDid, IsOpen: isOpen, AuthorDid: authorDid, Labels: labels, @@ -945,7 +945,7 @@ } } } else { filters := []orm.Filter{ - orm.FilterEq("repo_at", f.RepoAt()), + orm.FilterEq("repo_did", f.RepoDid), } if isOpen != nil { openInt := 0 @@ -1039,7 +1039,7 @@ body := r.FormValue("body") mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) issue := &models.Issue{ - RepoAt: f.RepoAt(), + RepoDid: syntax.DID(f.RepoDid), Rkey: tid.TID(), Title: r.FormValue("title"), Body: body, diff --git a/appview/labels/labels.go b/appview/labels/labels.go --- a/appview/labels/labels.go +++ b/appview/labels/labels.go @@ -100,7 +100,7 @@ return } // find all the labels that this repo subscribes to - repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt)) + repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_did", repo.RepoDid)) if err != nil { fail("Failed to get labels for this repository.", err) return diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go --- a/appview/middleware/middleware.go +++ b/appview/middleware/middleware.go @@ -2,6 +2,8 @@ package middleware import ( "context" + "database/sql" + "errors" "fmt" "log/slog" "net/http" @@ -234,6 +236,7 @@ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { l := mw.logger.With("middleware", "ResolveRepo") repoName := chi.URLParam(req, "repo") repoName = strings.TrimSuffix(repoName, ".git") + rkey := strings.ToLower(repoName) id, ok := req.Context().Value("resolvedId").(identity.Identity) if !ok { @@ -245,10 +248,53 @@ repo, err := db.GetRepo( mw.db, orm.FilterEq("did", id.DID.String()), - orm.FilterEq("name", repoName), + orm.FilterEq("rkey", rkey), ) if err != nil { - l.Error("failed to resolve repo", "err", err) + if !errors.Is(err, sql.ErrNoRows) { + l.Error("failed to resolve repo", "err", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + hint, hintErr := db.LookupRepoRename(mw.db, id.DID.String(), rkey) + if hintErr != nil && !errors.Is(hintErr, sql.ErrNoRows) { + l.Error("failed to lookup repo rename hint", "err", hintErr) + } + if hint != nil { + parts := strings.SplitN(strings.TrimPrefix(req.URL.Path, "/"), "/", 3) + target := "/" + parts[0] + "/" + hint.Rkey + if len(parts) == 3 { + target += "/" + parts[2] + } + if req.URL.RawQuery != "" { + target += "?" + req.URL.RawQuery + } + http.Redirect(w, req, target, http.StatusMovedPermanently) + return + } + nameRepos, nameErr := db.GetRepos( + mw.db, + orm.FilterEq("did", id.DID.String()), + orm.FilterEq("name", repoName), + ) + if nameErr == nil && len(nameRepos) == 1 && nameRepos[0].RepoDid != "" { + nameRepo := &nameRepos[0] + if _, tidErr := syntax.ParseTID(nameRepo.Rkey); tidErr == nil { + ctx := context.WithValue(req.Context(), "repo", nameRepo) + next.ServeHTTP(w, req.WithContext(ctx)) + return + } + parts := strings.SplitN(strings.TrimPrefix(req.URL.Path, "/"), "/", 3) + target := "/" + nameRepo.RepoDid + if len(parts) == 3 { + target += "/" + parts[2] + } + if req.URL.RawQuery != "" { + target += "?" + req.URL.RawQuery + } + http.Redirect(w, req, target, http.StatusFound) + return + } w.WriteHeader(http.StatusNotFound) mw.pages.ErrorKnot404(w) return @@ -281,7 +327,7 @@ mw.pages.Error404(w) return } - pr, err := db.GetPull(mw.db, orm.FilterEq("repo_at", f.RepoAt()), orm.FilterEq("pull_id", prIdInt)) + pr, err := db.GetPull(mw.db, orm.FilterEq("repo_did", f.RepoDid), orm.FilterEq("pull_id", prIdInt)) if err != nil { l.Error("failed to get pull and comments", "err", err) mw.pages.Error404(w) @@ -324,7 +370,7 @@ mw.pages.Error404(w) return } - issue, err := db.GetIssue(mw.db, f.RepoAt(), issueId) + issue, err := db.GetIssue(mw.db, f.RepoDid, issueId) if err != nil { l.Error("failed to get issues", "err", err) mw.pages.Error404(w) @@ -360,7 +406,7 @@ if r.Header.Get("User-Agent") == "Go-http-client/1.1" { if r.URL.Query().Get("go-get") == "1" { modulePath := userutil.FlattenDid(fullName) if strings.Contains(modulePath, ":") { - modulePath = userutil.FlattenDid(f.Did) + "/" + f.Name + modulePath = userutil.FlattenDid(f.Did) + "/" + f.Rkey } html := fmt.Sprintf( ` diff --git a/appview/models/repo.go b/appview/models/repo.go --- a/appview/models/repo.go +++ b/appview/models/repo.go @@ -136,14 +136,17 @@ if len(name) > 100 { return fmt.Errorf("Repository name must be 100 characters or fewer") } + // check for path traversal attempts if strings.Contains(name, "/") || strings.Contains(name, "\\") { return fmt.Errorf("Repository name contains invalid path characters") } + // check for sequences that could be used for traversal when normalized if strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { return fmt.Errorf("Repository name contains invalid path sequence") } + // then continue with character validation for _, char := range name { if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || @@ -153,6 +156,7 @@ return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") } } + // additional check to prevent multiple sequential dots if strings.Contains(name, "..") { return fmt.Errorf("Repository name cannot contain sequential dots") } @@ -161,6 +165,7 @@ if _, reserved := reservedRepoNames[strings.ToLower(name)]; reserved { return fmt.Errorf("Repository name %q is reserved", name) } + // if all checks pass return nil } diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go --- a/appview/pages/repoinfo/repoinfo.go +++ b/appview/pages/repoinfo/repoinfo.go @@ -20,7 +20,7 @@ } } func (r RepoInfo) FullName() string { - return path.Join(r.owner(), r.Name) + return path.Join(r.owner(), r.Rkey) } func (r RepoInfo) RepoIdentifier() string { @@ -39,7 +39,7 @@ } } func (r RepoInfo) FullNameWithoutAt() string { - return path.Join(r.ownerWithoutAt(), r.Name) + return path.Join(r.ownerWithoutAt(), r.Rkey) } func (r RepoInfo) GetTabs() [][]string { diff --git a/appview/pages/templates/goodfirstissues/index.html b/appview/pages/templates/goodfirstissues/index.html --- a/appview/pages/templates/goodfirstissues/index.html +++ b/appview/pages/templates/goodfirstissues/index.html @@ -46,7 +46,7 @@ {{ else }} {{ i "book-marked" "w-4 h-4 mr-1.5 shrink-0" }} {{ end }} {{ $repoOwner := resolve .Repo.Did }} - {{ $repoOwner }}/{{ .Repo.Name }} + {{ $repoOwner }}/{{ .Repo.Name }} @@ -90,7 +90,7 @@ {{ if gt (len .Issues) 0 }}
{{ range .Issues }} - +
diff --git a/appview/pages/templates/knots/dashboard.html b/appview/pages/templates/knots/dashboard.html --- a/appview/pages/templates/knots/dashboard.html +++ b/appview/pages/templates/knots/dashboard.html @@ -77,7 +77,7 @@ {{ $repos := index $.Repos . }} {{ range $repos }} diff --git a/appview/pages/templates/layouts/repobase.html b/appview/pages/templates/layouts/repobase.html --- a/appview/pages/templates/layouts/repobase.html +++ b/appview/pages/templates/layouts/repobase.html @@ -78,7 +78,7 @@ {{ $sourceOwner := resolve .RepoInfo.Source.Did }}
{{ i "git-fork" "w-3 h-3 shrink-0" }} forked from - + {{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}
diff --git a/appview/pages/templates/notifications/fragments/item.html b/appview/pages/templates/notifications/fragments/item.html --- a/appview/pages/templates/notifications/fragments/item.html +++ b/appview/pages/templates/notifications/fragments/item.html @@ -76,11 +76,11 @@ {{ define "notificationUrl" }} {{ $url := "" }} {{ if eq .Type "repo_starred" }} - {{$url = printf "/%s/%s" (resolve .Repo.Did) .Repo.Name}} + {{$url = printf "/%s/%s" (resolve .Repo.Did) .Repo.Rkey}} {{ else if .Issue }} - {{$url = printf "/%s/%s/issues/%d" (resolve .Repo.Did) .Repo.Name .Issue.IssueId}} + {{$url = printf "/%s/%s/issues/%d" (resolve .Repo.Did) .Repo.Rkey .Issue.IssueId}} {{ else if .Pull }} - {{$url = printf "/%s/%s/pulls/%d" (resolve .Repo.Did) .Repo.Name .Pull.PullId}} + {{$url = printf "/%s/%s/pulls/%d" (resolve .Repo.Did) .Repo.Rkey .Pull.PullId}} {{ else if eq .Type "followed" }} {{$url = printf "/%s" (resolve .ActorDid)}} {{ else }} diff --git a/appview/pages/templates/repo/empty.html b/appview/pages/templates/repo/empty.html --- a/appview/pages/templates/repo/empty.html +++ b/appview/pages/templates/repo/empty.html @@ -35,7 +35,7 @@ {{ $bullet := "mx-2 text-xs bg-gray-200 dark:bg-gray-600 rounded-full size-5 flex items-center justify-center font-mono inline-flex align-middle" }}

1First, generate a new SSH key pair.

2Then add the public key to your account from the keys page in your settings.

-

3Configure your remote to git@{{ $knot | stripPort }}:{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}

+

3Configure your remote to git@{{ $knot | stripPort }}:{{ .RepoInfo.RepoDid }}

4Push!

diff --git a/appview/pages/templates/repo/fragments/cloneDropdown.html b/appview/pages/templates/repo/fragments/cloneDropdown.html --- a/appview/pages/templates/repo/fragments/cloneDropdown.html +++ b/appview/pages/templates/repo/fragments/cloneDropdown.html @@ -38,15 +38,15 @@ {{ template "cloneUrlItem" ( dict "Label" "HTTPS" - "HandleUrl" (printf "https://tangled.org/%s/%s" $repoOwnerHandle .RepoInfo.Name) - "PermaUrl" (printf "https://tangled.org/%s/%s" .RepoInfo.OwnerDid .RepoInfo.Name) + "HandleUrl" (printf "https://tangled.org/%s/%s" $repoOwnerHandle .RepoInfo.Rkey) + "PermaUrl" (printf "https://tangled.org/%s" .RepoInfo.RepoDid) ) }} {{ template "cloneUrlItem" ( dict "Label" "SSH" - "HandleUrl" (printf "git@%s:%s/%s" (stripPort $knot) $repoOwnerHandle .RepoInfo.Name) - "PermaUrl" (printf "git@%s:%s/%s" (stripPort $knot) .RepoInfo.OwnerDid .RepoInfo.Name) + "HandleUrl" (printf "git@%s:%s/%s" (stripPort $knot) $repoOwnerHandle .RepoInfo.Rkey) + "PermaUrl" (printf "git@%s:%s" (stripPort $knot) .RepoInfo.RepoDid) ) }}

diff --git a/appview/pages/templates/repo/index.html b/appview/pages/templates/repo/index.html --- a/appview/pages/templates/repo/index.html +++ b/appview/pages/templates/repo/index.html @@ -361,3 +361,4 @@ {{- if or .HTMLReadme .Readme -}} {{ template "repo/fragments/readme" . }} {{- end -}} {{ end }} + diff --git a/appview/pages/templates/repo/pulls/fragments/pullActions.html b/appview/pages/templates/repo/pulls/fragments/pullActions.html --- a/appview/pages/templates/repo/pulls/fragments/pullActions.html +++ b/appview/pages/templates/repo/pulls/fragments/pullActions.html @@ -34,7 +34,7 @@ comment {{ if .BranchDeleteStatus }}