mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-10 04:06:06 +00:00
use chi for routes in appview. refactor routes outside of serve.go
This commit is contained in:
+47
-217
@@ -34,7 +34,9 @@ import (
|
||||
"atcr.io/pkg/appview/holdhealth"
|
||||
"atcr.io/pkg/appview/jetstream"
|
||||
"atcr.io/pkg/appview/readme"
|
||||
"github.com/gorilla/mux"
|
||||
"atcr.io/pkg/appview/routes"
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
@@ -165,8 +167,39 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
// Initialize Jetstream workers (background services before HTTP routes)
|
||||
initializeJetstream(uiDatabase, &cfg.Jetstream, defaultHoldDID, testMode)
|
||||
|
||||
// Initialize UI routes with OAuth app, refresher, device store, health checker, and readme cache
|
||||
uiTemplates, uiRouter := initializeUIRoutes(cfg.UI.Enabled, uiDatabase, uiReadOnlyDB, uiSessionStore, oauthApp, oauthStore, refresher, baseURL, deviceStore, healthChecker, readmeCache)
|
||||
// Create main chi router
|
||||
mainRouter := chi.NewRouter()
|
||||
|
||||
// Add core middleware
|
||||
mainRouter.Use(chimiddleware.Logger)
|
||||
mainRouter.Use(chimiddleware.Recoverer)
|
||||
mainRouter.Use(chimiddleware.GetHead) // Automatically handle HEAD requests for GET routes
|
||||
mainRouter.Use(routes.CORSMiddleware())
|
||||
|
||||
// Load templates if UI is enabled
|
||||
var uiTemplates *template.Template
|
||||
if cfg.UI.Enabled {
|
||||
var err error
|
||||
uiTemplates, err = appview.Templates()
|
||||
if err != nil {
|
||||
slog.Warn("Failed to load UI templates", "error", err)
|
||||
} else {
|
||||
// Register UI routes with dependencies
|
||||
routes.RegisterUIRoutes(mainRouter, routes.UIDependencies{
|
||||
Database: uiDatabase,
|
||||
ReadOnlyDB: uiReadOnlyDB,
|
||||
SessionStore: uiSessionStore,
|
||||
OAuthApp: oauthApp,
|
||||
OAuthStore: oauthStore,
|
||||
Refresher: refresher,
|
||||
BaseURL: baseURL,
|
||||
DeviceStore: deviceStore,
|
||||
HealthChecker: healthChecker,
|
||||
ReadmeCache: readmeCache,
|
||||
Templates: uiTemplates,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create OAuth server
|
||||
oauthServer := oauth.NewServer(oauthApp)
|
||||
@@ -307,29 +340,23 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
app := handlers.NewApp(ctx, cfg.Distribution)
|
||||
|
||||
// Create main HTTP mux
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Mount registry at /v2/
|
||||
mux.Handle("/v2/", app)
|
||||
mainRouter.Handle("/v2/*", app)
|
||||
|
||||
// Mount UI routes if enabled
|
||||
if uiSessionStore != nil && uiTemplates != nil && uiRouter != nil {
|
||||
// Mount static files if UI is enabled
|
||||
if uiSessionStore != nil && uiTemplates != nil {
|
||||
// Mount static files
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", appview.StaticHandler()))
|
||||
|
||||
// Mount UI routes directly at root level
|
||||
mux.Handle("/", uiRouter)
|
||||
mainRouter.Handle("/static/*", http.StripPrefix("/static/", appview.StaticHandler()))
|
||||
|
||||
slog.Info("UI enabled", "home", "/", "settings", "/settings")
|
||||
}
|
||||
|
||||
// Mount OAuth endpoints
|
||||
mux.HandleFunc("/auth/oauth/authorize", oauthServer.ServeAuthorize)
|
||||
mux.HandleFunc("/auth/oauth/callback", oauthServer.ServeCallback)
|
||||
mainRouter.Get("/auth/oauth/authorize", oauthServer.ServeAuthorize)
|
||||
mainRouter.Get("/auth/oauth/callback", oauthServer.ServeCallback)
|
||||
|
||||
// OAuth client metadata endpoint
|
||||
mux.HandleFunc("/client-metadata.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
mainRouter.Get("/client-metadata.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
config := oauthApp.GetConfig()
|
||||
metadata := config.ClientMetadata()
|
||||
|
||||
@@ -366,14 +393,14 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
return nil // All errors are non-fatal
|
||||
})
|
||||
|
||||
tokenHandler.RegisterRoutes(mux)
|
||||
mainRouter.Post("/auth/token", tokenHandler.ServeHTTP)
|
||||
|
||||
// Device authorization endpoints (public)
|
||||
mux.Handle("/auth/device/code", &uihandlers.DeviceCodeHandler{
|
||||
mainRouter.Handle("/auth/device/code", &uihandlers.DeviceCodeHandler{
|
||||
Store: deviceStore,
|
||||
AppViewBaseURL: baseURL,
|
||||
})
|
||||
mux.Handle("/auth/device/token", &uihandlers.DeviceTokenHandler{
|
||||
mainRouter.Handle("/auth/device/token", &uihandlers.DeviceTokenHandler{
|
||||
Store: deviceStore,
|
||||
})
|
||||
|
||||
@@ -389,7 +416,7 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
// Create HTTP server
|
||||
server := &http.Server{
|
||||
Addr: cfg.Server.Addr,
|
||||
Handler: mux,
|
||||
Handler: mainRouter,
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
@@ -439,203 +466,6 @@ func createTokenIssuer(cfg *appview.Config) (*token.Issuer, error) {
|
||||
)
|
||||
}
|
||||
|
||||
// initializeUIRoutes initializes the web UI routes
|
||||
// uiEnabled: whether UI is enabled (from Config.UI.Enabled)
|
||||
// database: read-write connection for auth and writes
|
||||
// readOnlyDB: read-only connection for public queries (search, user pages, etc.)
|
||||
// healthChecker: hold endpoint health checker
|
||||
// readmeCache: README cache for repository pages
|
||||
func initializeUIRoutes(uiEnabled bool, database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.SessionStore, oauthApp *oauth.App, oauthStore *db.OAuthStore, refresher *oauth.Refresher, baseURL string, deviceStore *db.DeviceStore, healthChecker *holdhealth.Checker, readmeCache *readme.Cache) (*template.Template, *mux.Router) {
|
||||
// Check if UI is enabled
|
||||
if !uiEnabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Load templates
|
||||
templates, err := appview.Templates()
|
||||
if err != nil {
|
||||
slog.Warn("Failed to load UI templates", "error", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create router
|
||||
router := mux.NewRouter()
|
||||
|
||||
// OAuth login routes (public)
|
||||
router.Handle("/auth/oauth/login", &uihandlers.LoginHandler{
|
||||
Templates: templates,
|
||||
}).Methods("GET")
|
||||
|
||||
router.Handle("/auth/oauth/login", &uihandlers.LoginSubmitHandler{}).Methods("POST")
|
||||
|
||||
// Public routes (with optional auth for navbar)
|
||||
// SECURITY: Public pages use read-only DB
|
||||
router.Handle("/", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.HomeHandler{
|
||||
DB: readOnlyDB,
|
||||
Templates: templates,
|
||||
RegistryURL: uihandlers.TrimRegistryURL(baseURL),
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
router.Handle("/api/recent-pushes", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.RecentPushesHandler{
|
||||
DB: readOnlyDB,
|
||||
Templates: templates,
|
||||
RegistryURL: uihandlers.TrimRegistryURL(baseURL),
|
||||
HealthChecker: healthChecker,
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
// SECURITY: Search uses read-only DB to prevent writes and limit access to sensitive tables
|
||||
router.Handle("/search", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.SearchHandler{
|
||||
DB: readOnlyDB,
|
||||
Templates: templates,
|
||||
RegistryURL: uihandlers.TrimRegistryURL(baseURL),
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
router.Handle("/api/search-results", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.SearchResultsHandler{
|
||||
DB: readOnlyDB,
|
||||
Templates: templates,
|
||||
RegistryURL: uihandlers.TrimRegistryURL(baseURL),
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
// Install page (public)
|
||||
router.Handle("/install", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.InstallHandler{
|
||||
Templates: templates,
|
||||
RegistryURL: uihandlers.TrimRegistryURL(baseURL),
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
// API route for repository stats (public, read-only)
|
||||
router.Handle("/api/stats/{handle}/{repository}", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.GetStatsHandler{
|
||||
DB: readOnlyDB,
|
||||
Directory: oauthApp.Directory(),
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
// API routes for stars (require authentication)
|
||||
router.Handle("/api/stars/{handle}/{repository}", middleware.RequireAuth(sessionStore, database)(
|
||||
&uihandlers.StarRepositoryHandler{
|
||||
DB: database, // Needs write access
|
||||
Directory: oauthApp.Directory(),
|
||||
Refresher: refresher,
|
||||
},
|
||||
)).Methods("POST")
|
||||
|
||||
router.Handle("/api/stars/{handle}/{repository}", middleware.RequireAuth(sessionStore, database)(
|
||||
&uihandlers.UnstarRepositoryHandler{
|
||||
DB: database, // Needs write access
|
||||
Directory: oauthApp.Directory(),
|
||||
Refresher: refresher,
|
||||
},
|
||||
)).Methods("DELETE")
|
||||
|
||||
router.Handle("/api/stars/{handle}/{repository}", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.CheckStarHandler{
|
||||
DB: readOnlyDB, // Read-only check
|
||||
Directory: oauthApp.Directory(),
|
||||
Refresher: refresher,
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
// Manifest detail API endpoint
|
||||
router.Handle("/api/manifests/{handle}/{repository}/{digest}", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.ManifestDetailHandler{
|
||||
DB: readOnlyDB,
|
||||
Directory: oauthApp.Directory(),
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
// Manifest health check API endpoint (HTMX polling)
|
||||
router.Handle("/api/manifest-health", &uihandlers.ManifestHealthHandler{
|
||||
HealthChecker: healthChecker,
|
||||
}).Methods("GET")
|
||||
|
||||
router.Handle("/u/{handle}", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.UserPageHandler{
|
||||
DB: readOnlyDB,
|
||||
Templates: templates,
|
||||
RegistryURL: uihandlers.TrimRegistryURL(baseURL),
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
router.Handle("/r/{handle}/{repository}", middleware.OptionalAuth(sessionStore, database)(
|
||||
&uihandlers.RepositoryPageHandler{
|
||||
DB: readOnlyDB,
|
||||
Templates: templates,
|
||||
RegistryURL: uihandlers.TrimRegistryURL(baseURL),
|
||||
Directory: oauthApp.Directory(),
|
||||
Refresher: refresher,
|
||||
HealthChecker: healthChecker,
|
||||
ReadmeCache: readmeCache,
|
||||
},
|
||||
)).Methods("GET")
|
||||
|
||||
// Authenticated routes
|
||||
authRouter := router.NewRoute().Subrouter()
|
||||
authRouter.Use(middleware.RequireAuth(sessionStore, database))
|
||||
|
||||
authRouter.Handle("/settings", &uihandlers.SettingsHandler{
|
||||
Templates: templates,
|
||||
Refresher: refresher,
|
||||
RegistryURL: uihandlers.TrimRegistryURL(baseURL),
|
||||
}).Methods("GET")
|
||||
|
||||
authRouter.Handle("/api/profile/default-hold", &uihandlers.UpdateDefaultHoldHandler{
|
||||
Refresher: refresher,
|
||||
}).Methods("POST")
|
||||
|
||||
authRouter.Handle("/api/images/{repository}/tags/{tag}", &uihandlers.DeleteTagHandler{
|
||||
DB: database,
|
||||
Refresher: refresher,
|
||||
}).Methods("DELETE")
|
||||
|
||||
authRouter.Handle("/api/images/{repository}/manifests/{digest}", &uihandlers.DeleteManifestHandler{
|
||||
DB: database,
|
||||
Refresher: refresher,
|
||||
}).Methods("DELETE")
|
||||
|
||||
// Device approval page (authenticated)
|
||||
authRouter.Handle("/device", &uihandlers.DeviceApprovalPageHandler{
|
||||
Store: deviceStore,
|
||||
SessionStore: sessionStore,
|
||||
}).Methods("GET")
|
||||
|
||||
authRouter.Handle("/device/approve", &uihandlers.DeviceApproveHandler{
|
||||
Store: deviceStore,
|
||||
SessionStore: sessionStore,
|
||||
}).Methods("POST")
|
||||
|
||||
// Device management routes
|
||||
authRouter.Handle("/api/devices", &uihandlers.ListDevicesHandler{
|
||||
Store: deviceStore,
|
||||
SessionStore: sessionStore,
|
||||
}).Methods("GET")
|
||||
|
||||
authRouter.Handle("/api/devices/{id}", &uihandlers.RevokeDeviceHandler{
|
||||
Store: deviceStore,
|
||||
SessionStore: sessionStore,
|
||||
}).Methods("DELETE")
|
||||
|
||||
// Logout endpoint (supports both GET and POST)
|
||||
// Properly revokes OAuth tokens on PDS side before clearing local session
|
||||
router.Handle("/auth/logout", &uihandlers.LogoutHandler{
|
||||
OAuthApp: oauthApp,
|
||||
Refresher: refresher,
|
||||
SessionStore: sessionStore,
|
||||
OAuthStore: oauthStore,
|
||||
}).Methods("GET", "POST")
|
||||
|
||||
return templates, router
|
||||
}
|
||||
|
||||
// initializeJetstream initializes the Jetstream workers for real-time events and backfill
|
||||
func initializeJetstream(database *sql.DB, jetstreamCfg *appview.JetstreamConfig, defaultHoldDID string, testMode bool) {
|
||||
// Start Jetstream worker
|
||||
|
||||
@@ -10,7 +10,6 @@ require (
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/ipfs/go-block-format v0.2.0
|
||||
github.com/ipfs/go-cid v0.4.1
|
||||
@@ -57,6 +56,7 @@ require (
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
|
||||
+12
-17
@@ -15,7 +15,7 @@ import (
|
||||
"atcr.io/pkg/auth/oauth"
|
||||
"github.com/bluesky-social/indigo/atproto/identity"
|
||||
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// StarRepositoryHandler handles starring a repository
|
||||
@@ -34,9 +34,8 @@ func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Extract parameters
|
||||
vars := mux.Vars(r)
|
||||
handle := vars["handle"]
|
||||
repository := vars["repository"]
|
||||
handle := chi.URLParam(r, "handle")
|
||||
repository := chi.URLParam(r, "repository")
|
||||
|
||||
// Resolve owner's handle to DID
|
||||
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
|
||||
@@ -93,9 +92,8 @@ func (h *UnstarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
// Extract parameters
|
||||
vars := mux.Vars(r)
|
||||
handle := vars["handle"]
|
||||
repository := vars["repository"]
|
||||
handle := chi.URLParam(r, "handle")
|
||||
repository := chi.URLParam(r, "repository")
|
||||
|
||||
// Resolve owner's handle to DID
|
||||
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
|
||||
@@ -155,9 +153,8 @@ func (h *CheckStarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Extract parameters
|
||||
vars := mux.Vars(r)
|
||||
handle := vars["handle"]
|
||||
repository := vars["repository"]
|
||||
handle := chi.URLParam(r, "handle")
|
||||
repository := chi.URLParam(r, "repository")
|
||||
|
||||
// Resolve owner's handle to DID
|
||||
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
|
||||
@@ -200,9 +197,8 @@ type GetStatsHandler struct {
|
||||
|
||||
func (h *GetStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract parameters
|
||||
vars := mux.Vars(r)
|
||||
handle := vars["handle"]
|
||||
repository := vars["repository"]
|
||||
handle := chi.URLParam(r, "handle")
|
||||
repository := chi.URLParam(r, "repository")
|
||||
|
||||
// Resolve owner's handle to DID
|
||||
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
|
||||
@@ -231,10 +227,9 @@ type ManifestDetailHandler struct {
|
||||
|
||||
func (h *ManifestDetailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract parameters
|
||||
vars := mux.Vars(r)
|
||||
handle := vars["handle"]
|
||||
repository := vars["repository"]
|
||||
digest := vars["digest"]
|
||||
handle := chi.URLParam(r, "handle")
|
||||
repository := chi.URLParam(r, "repository")
|
||||
digest := chi.URLParam(r, "digest")
|
||||
|
||||
// Resolve owner's handle to DID
|
||||
ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"atcr.io/pkg/appview/db"
|
||||
)
|
||||
@@ -338,8 +338,7 @@ func (h *RevokeDeviceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Get device ID from URL
|
||||
vars := mux.Vars(r)
|
||||
deviceID := vars["id"]
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
if deviceID == "" {
|
||||
http.Error(w, "device ID required", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"atcr.io/pkg/appview/middleware"
|
||||
"atcr.io/pkg/atproto"
|
||||
"atcr.io/pkg/auth/oauth"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// DeleteTagHandler handles deleting a tag
|
||||
@@ -26,9 +26,8 @@ func (h *DeleteTagHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
repo := vars["repository"]
|
||||
tag := vars["tag"]
|
||||
repo := chi.URLParam(r, "repository")
|
||||
tag := chi.URLParam(r, "tag")
|
||||
|
||||
// Get OAuth session for the authenticated user
|
||||
session, err := h.Refresher.GetSession(r.Context(), user.DID)
|
||||
@@ -74,9 +73,8 @@ func (h *DeleteManifestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
repo := vars["repository"]
|
||||
digest := vars["digest"]
|
||||
repo := chi.URLParam(r, "repository")
|
||||
digest := chi.URLParam(r, "digest")
|
||||
|
||||
// Check if manifest is tagged
|
||||
tagged, err := db.IsManifestTagged(h.DB, user.DID, repo, digest)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"atcr.io/pkg/atproto"
|
||||
"atcr.io/pkg/auth/oauth"
|
||||
"github.com/bluesky-social/indigo/atproto/identity"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// RepositoryPageHandler handles the public repository page
|
||||
@@ -31,9 +31,8 @@ type RepositoryPageHandler struct {
|
||||
}
|
||||
|
||||
func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
handle := vars["handle"]
|
||||
repository := vars["repository"]
|
||||
handle := chi.URLParam(r, "handle")
|
||||
repository := chi.URLParam(r, "repository")
|
||||
|
||||
// Look up user by handle
|
||||
owner, err := db.GetUserByHandle(h.DB, handle)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"atcr.io/pkg/appview/db"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// UserPageHandler handles the public user page showing all images for a user
|
||||
@@ -17,8 +17,7 @@ type UserPageHandler struct {
|
||||
}
|
||||
|
||||
func (h *UserPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
handle := vars["handle"]
|
||||
handle := chi.URLParam(r, "handle")
|
||||
|
||||
// Look up user by handle
|
||||
viewedUser, err := db.GetUserByHandle(h.DB, handle)
|
||||
|
||||
@@ -433,7 +433,7 @@ func ResolveHoldDIDFromURL(holdURL string) string {
|
||||
return "did:web:" + hostname
|
||||
}
|
||||
|
||||
// isDID checks if a string is a DID (starts with "did:")
|
||||
// IsDID checks if a string is a DID (starts with "did:")
|
||||
func IsDID(s string) bool {
|
||||
return len(s) > 4 && s[:4] == "did:"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user