forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview: state/userutil: init new pkg for user handle/did utils

Changed files
+94 -50
appview
pages
templates
state
+14 -1
appview/pages/pages.go
···
"github.com/microcosm-cc/bluemonday"
"github.com/sotangled/tangled/appview/auth"
"github.com/sotangled/tangled/appview/db"
+
"github.com/sotangled/tangled/appview/state/userutil"
"github.com/sotangled/tangled/types"
)
···
return path.Join(r.OwnerWithAt(), r.Name)
}
+
func (r RepoInfo) OwnerWithoutAt() string {
+
if strings.HasPrefix(r.OwnerWithAt(), "@") {
+
return strings.TrimPrefix(r.OwnerWithAt(), "@")
+
} else {
+
return userutil.FlattenDid(r.OwnerDid)
+
}
+
}
+
+
func (r RepoInfo) FullNameWithoutAt() string {
+
return path.Join(r.OwnerWithoutAt(), r.Name)
+
}
+
func (r RepoInfo) GetTabs() [][]string {
tabs := [][]string{
{"overview", "/"},
···
LoggedInUser *auth.User
RepoInfo RepoInfo
types.RepoLogResponse
-
Active string
+
Active string
EmailToDidOrHandle map[string]string
}
+2
appview/pages/templates/layouts/base.html
···
/>
<script src="/static/htmx.min.js"></script>
<link href="/static/tw.css" rel="stylesheet" type="text/css" />
+
<title>{{ block "title" . }}{{ end }} · tangled</title>
+
{{ block "extrameta" . }}{{ end }}
</head>
<body class="bg-slate-100">
<div class="container mx-auto px-1 pt-4 min-h-screen flex flex-col">
+9
appview/pages/templates/layouts/repobase.html
···
{{ define "title" }}{{ .RepoInfo.FullName }}{{ end }}
+
{{ define "extrameta" }}
+
<meta name="vcs:clone" content="https://tangled.sh/{{ .RepoInfo.FullName }}"/>
+
<meta name="forge:summary" content="https://tangled.sh/{{ .RepoInfo.FullName }}">
+
<meta name="forge:dir" content="https://tangled.sh/{{ .RepoInfo.FullName }}/tree/{ref}/{path}">
+
<meta name="forge:file" content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}">
+
<meta name="forge:line" content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}#L{line}">
+
<meta name="go-import" content="tangled.sh/{{ .RepoInfo.FullNameWithoutAt }} git https://tangled.sh/{{ .RepoInfo.FullName }}">
+
{{ end }}
+
{{ define "content" }}
<section id="repo-header" class="mb-4 py-2 px-6">
<p class="text-lg">
+4 -46
appview/state/router.go
···
import (
"net/http"
-
"regexp"
"strings"
"github.com/go-chi/chi/v5"
+
"github.com/sotangled/tangled/appview/state/userutil"
)
func (s *State) Router() http.Handler {
···
// Check if the first path element is a valid handle without '@' or a flattened DID
pathParts := strings.SplitN(pat, "/", 2)
if len(pathParts) > 0 {
-
if isHandleNoAt(pathParts[0]) {
+
if userutil.IsHandleNoAt(pathParts[0]) {
// Redirect to the same path but with '@' prefixed to the handle
redirectPath := "@" + pat
http.Redirect(w, r, "/"+redirectPath, http.StatusFound)
return
-
} else if isFlattenedDid(pathParts[0]) {
+
} else if userutil.IsFlattenedDid(pathParts[0]) {
// Redirect to the unflattened DID version
-
unflattenedDid := unflattenDid(pathParts[0])
+
unflattenedDid := userutil.UnflattenDid(pathParts[0])
var redirectPath string
if len(pathParts) > 1 {
redirectPath = unflattenedDid + "/" + pathParts[1]
···
})
return router
-
}
-
-
func isHandleNoAt(s string) bool {
-
// ref: https://atproto.com/specs/handle
-
re := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
-
return re.MatchString(s)
-
}
-
-
func unflattenDid(s string) string {
-
if !isFlattenedDid(s) {
-
return s
-
}
-
-
parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
-
if len(parts) != 2 {
-
return s
-
}
-
-
return "did:" + parts[0] + ":" + parts[1]
-
}
-
-
// isFlattenedDid checks if the given string is a flattened DID.
-
// A flattened DID is a DID with the :s swapped to -s to satisfy certain
-
// application requirements, such as Go module naming conventions.
-
func isFlattenedDid(s string) bool {
-
// Check if the string starts with "did-"
-
if !strings.HasPrefix(s, "did-") {
-
return false
-
}
-
-
// Split the string to extract method and identifier
-
parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
-
if len(parts) != 2 {
-
return false
-
}
-
-
// Reconstruct as a standard DID format
-
// Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc"
-
reconstructed := "did:" + parts[0] + ":" + parts[1]
-
re := regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
-
-
return re.MatchString(reconstructed)
}
func (s *State) UserRouter() http.Handler {
+3 -3
appview/state/router_test.go appview/state/userutil/userutil_test.go
···
-
package state
+
package userutil
import "testing"
···
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
-
result := unflattenDid(tc.input)
+
result := UnflattenDid(tc.input)
if result != tc.expected {
t.Errorf("unflattenDid(%q) = %q, want %q", tc.input, result, tc.expected)
}
···
func TestIsFlattenedDid(t *testing.T) {
for _, tc := range isFlattenedDidTests {
t.Run(tc.name, func(t *testing.T) {
-
result := isFlattenedDid(tc.input)
+
result := IsFlattenedDid(tc.input)
if result != tc.expected {
t.Errorf("isFlattenedDid(%q) = %v, want %v", tc.input, result, tc.expected)
}
+62
appview/state/userutil/userutil.go
···
+
package userutil
+
+
import (
+
"regexp"
+
"strings"
+
)
+
+
func IsHandleNoAt(s string) bool {
+
// ref: https://atproto.com/specs/handle
+
re := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
+
return re.MatchString(s)
+
}
+
+
func UnflattenDid(s string) string {
+
if !IsFlattenedDid(s) {
+
return s
+
}
+
+
parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
+
if len(parts) != 2 {
+
return s
+
}
+
+
return "did:" + parts[0] + ":" + parts[1]
+
}
+
+
// IsFlattenedDid checks if the given string is a flattened DID.
+
func IsFlattenedDid(s string) bool {
+
// Check if the string starts with "did-"
+
if !strings.HasPrefix(s, "did-") {
+
return false
+
}
+
+
// Split the string to extract method and identifier
+
parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
+
if len(parts) != 2 {
+
return false
+
}
+
+
// Reconstruct as a standard DID format
+
// Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc"
+
reconstructed := "did:" + parts[0] + ":" + parts[1]
+
re := regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
+
+
return re.MatchString(reconstructed)
+
}
+
+
// FlattenDid converts a DID to a flattened format.
+
// A flattened DID is a DID with the :s swapped to -s to satisfy certain
+
// application requirements, such as Go module naming conventions.
+
func FlattenDid(s string) string {
+
if !IsFlattenedDid(s) {
+
return s
+
}
+
+
parts := strings.SplitN(s[4:], ":", 2) // Skip "did:" prefix and split on first ":"
+
if len(parts) != 2 {
+
return s
+
}
+
+
return "did-" + parts[0] + "-" + parts[1]
+
}