A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
81
fork

Configure Feed

Select the types of activity you want to include in your feed.

need to add routes

+251
+251
pkg/appview/routes/routes.go
··· 1 + // Package routes provides route registration for the AppView web UI and API endpoints. 2 + package routes 3 + 4 + import ( 5 + "database/sql" 6 + "html/template" 7 + "net/http" 8 + 9 + "atcr.io/pkg/appview/db" 10 + uihandlers "atcr.io/pkg/appview/handlers" 11 + "atcr.io/pkg/appview/holdhealth" 12 + "atcr.io/pkg/appview/middleware" 13 + "atcr.io/pkg/appview/readme" 14 + "atcr.io/pkg/auth/oauth" 15 + "github.com/go-chi/chi/v5" 16 + ) 17 + 18 + // UIDependencies contains all dependencies needed for UI route registration 19 + type UIDependencies struct { 20 + Database *sql.DB 21 + ReadOnlyDB *sql.DB 22 + SessionStore *db.SessionStore 23 + OAuthApp *oauth.App 24 + OAuthStore *db.OAuthStore 25 + Refresher *oauth.Refresher 26 + BaseURL string 27 + DeviceStore *db.DeviceStore 28 + HealthChecker *holdhealth.Checker 29 + ReadmeCache *readme.Cache 30 + Templates *template.Template 31 + } 32 + 33 + // RegisterUIRoutes registers all web UI and API routes on the provided router 34 + func RegisterUIRoutes(router chi.Router, deps UIDependencies) { 35 + // Extract trimmed registry URL for templates 36 + registryURL := trimRegistryURL(deps.BaseURL) 37 + 38 + // OAuth login routes (public) 39 + router.Get("/auth/oauth/login", (&uihandlers.LoginHandler{ 40 + Templates: deps.Templates, 41 + }).ServeHTTP) 42 + 43 + router.Post("/auth/oauth/login", (&uihandlers.LoginSubmitHandler{}).ServeHTTP) 44 + 45 + // Public routes (with optional auth for navbar) 46 + // SECURITY: Public pages use read-only DB 47 + router.Get("/", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 48 + &uihandlers.HomeHandler{ 49 + DB: deps.ReadOnlyDB, 50 + Templates: deps.Templates, 51 + RegistryURL: registryURL, 52 + }, 53 + ).ServeHTTP) 54 + 55 + router.Get("/api/recent-pushes", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 56 + &uihandlers.RecentPushesHandler{ 57 + DB: deps.ReadOnlyDB, 58 + Templates: deps.Templates, 59 + RegistryURL: registryURL, 60 + HealthChecker: deps.HealthChecker, 61 + }, 62 + ).ServeHTTP) 63 + 64 + // SECURITY: Search uses read-only DB to prevent writes and limit access to sensitive tables 65 + router.Get("/search", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 66 + &uihandlers.SearchHandler{ 67 + DB: deps.ReadOnlyDB, 68 + Templates: deps.Templates, 69 + RegistryURL: registryURL, 70 + }, 71 + ).ServeHTTP) 72 + 73 + router.Get("/api/search-results", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 74 + &uihandlers.SearchResultsHandler{ 75 + DB: deps.ReadOnlyDB, 76 + Templates: deps.Templates, 77 + RegistryURL: registryURL, 78 + }, 79 + ).ServeHTTP) 80 + 81 + // Install page (public) 82 + router.Get("/install", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 83 + &uihandlers.InstallHandler{ 84 + Templates: deps.Templates, 85 + RegistryURL: registryURL, 86 + }, 87 + ).ServeHTTP) 88 + 89 + // API route for repository stats (public, read-only) 90 + router.Get("/api/stats/{handle}/{repository}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 91 + &uihandlers.GetStatsHandler{ 92 + DB: deps.ReadOnlyDB, 93 + Directory: deps.OAuthApp.Directory(), 94 + }, 95 + ).ServeHTTP) 96 + 97 + // API routes for stars (require authentication) 98 + router.Post("/api/stars/{handle}/{repository}", middleware.RequireAuth(deps.SessionStore, deps.Database)( 99 + &uihandlers.StarRepositoryHandler{ 100 + DB: deps.Database, // Needs write access 101 + Directory: deps.OAuthApp.Directory(), 102 + Refresher: deps.Refresher, 103 + }, 104 + ).ServeHTTP) 105 + 106 + router.Delete("/api/stars/{handle}/{repository}", middleware.RequireAuth(deps.SessionStore, deps.Database)( 107 + &uihandlers.UnstarRepositoryHandler{ 108 + DB: deps.Database, // Needs write access 109 + Directory: deps.OAuthApp.Directory(), 110 + Refresher: deps.Refresher, 111 + }, 112 + ).ServeHTTP) 113 + 114 + router.Get("/api/stars/{handle}/{repository}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 115 + &uihandlers.CheckStarHandler{ 116 + DB: deps.ReadOnlyDB, // Read-only check 117 + Directory: deps.OAuthApp.Directory(), 118 + Refresher: deps.Refresher, 119 + }, 120 + ).ServeHTTP) 121 + 122 + // Manifest detail API endpoint 123 + router.Get("/api/manifests/{handle}/{repository}/{digest}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 124 + &uihandlers.ManifestDetailHandler{ 125 + DB: deps.ReadOnlyDB, 126 + Directory: deps.OAuthApp.Directory(), 127 + }, 128 + ).ServeHTTP) 129 + 130 + // Manifest health check API endpoint (HTMX polling) 131 + router.Get("/api/manifest-health", (&uihandlers.ManifestHealthHandler{ 132 + HealthChecker: deps.HealthChecker, 133 + }).ServeHTTP) 134 + 135 + router.Get("/u/{handle}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 136 + &uihandlers.UserPageHandler{ 137 + DB: deps.ReadOnlyDB, 138 + Templates: deps.Templates, 139 + RegistryURL: registryURL, 140 + }, 141 + ).ServeHTTP) 142 + 143 + router.Get("/r/{handle}/{repository}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 144 + &uihandlers.RepositoryPageHandler{ 145 + DB: deps.ReadOnlyDB, 146 + Templates: deps.Templates, 147 + RegistryURL: registryURL, 148 + Directory: deps.OAuthApp.Directory(), 149 + Refresher: deps.Refresher, 150 + HealthChecker: deps.HealthChecker, 151 + ReadmeCache: deps.ReadmeCache, 152 + }, 153 + ).ServeHTTP) 154 + 155 + // Authenticated routes 156 + router.Group(func(r chi.Router) { 157 + r.Use(middleware.RequireAuth(deps.SessionStore, deps.Database)) 158 + 159 + r.Get("/settings", (&uihandlers.SettingsHandler{ 160 + Templates: deps.Templates, 161 + Refresher: deps.Refresher, 162 + RegistryURL: registryURL, 163 + }).ServeHTTP) 164 + 165 + r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{ 166 + Refresher: deps.Refresher, 167 + }).ServeHTTP) 168 + 169 + r.Delete("/api/images/{repository}/tags/{tag}", (&uihandlers.DeleteTagHandler{ 170 + DB: deps.Database, 171 + Refresher: deps.Refresher, 172 + }).ServeHTTP) 173 + 174 + r.Delete("/api/images/{repository}/manifests/{digest}", (&uihandlers.DeleteManifestHandler{ 175 + DB: deps.Database, 176 + Refresher: deps.Refresher, 177 + }).ServeHTTP) 178 + 179 + // Device approval page (authenticated) 180 + r.Get("/device", (&uihandlers.DeviceApprovalPageHandler{ 181 + Store: deps.DeviceStore, 182 + SessionStore: deps.SessionStore, 183 + }).ServeHTTP) 184 + 185 + r.Post("/device/approve", (&uihandlers.DeviceApproveHandler{ 186 + Store: deps.DeviceStore, 187 + SessionStore: deps.SessionStore, 188 + }).ServeHTTP) 189 + 190 + // Device management routes 191 + r.Get("/api/devices", (&uihandlers.ListDevicesHandler{ 192 + Store: deps.DeviceStore, 193 + SessionStore: deps.SessionStore, 194 + }).ServeHTTP) 195 + 196 + r.Delete("/api/devices/{id}", (&uihandlers.RevokeDeviceHandler{ 197 + Store: deps.DeviceStore, 198 + SessionStore: deps.SessionStore, 199 + }).ServeHTTP) 200 + }) 201 + 202 + // Logout endpoint (supports both GET and POST) 203 + // Properly revokes OAuth tokens on PDS side before clearing local session 204 + logoutHandler := &uihandlers.LogoutHandler{ 205 + OAuthApp: deps.OAuthApp, 206 + Refresher: deps.Refresher, 207 + SessionStore: deps.SessionStore, 208 + OAuthStore: deps.OAuthStore, 209 + } 210 + router.Get("/auth/logout", logoutHandler.ServeHTTP) 211 + router.Post("/auth/logout", logoutHandler.ServeHTTP) 212 + } 213 + 214 + // CORSMiddleware returns a middleware that sets CORS headers for API endpoints 215 + func CORSMiddleware() func(http.Handler) http.Handler { 216 + return func(next http.Handler) http.Handler { 217 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 218 + // Set CORS headers for all requests 219 + origin := r.Header.Get("Origin") 220 + if origin == "" { 221 + origin = "*" 222 + } 223 + w.Header().Set("Access-Control-Allow-Origin", origin) 224 + w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS") 225 + w.Header().Set("Access-Control-Allow-Headers", "*") 226 + w.Header().Set("Access-Control-Expose-Headers", "*") 227 + w.Header().Set("Access-Control-Max-Age", "300") 228 + 229 + // Handle OPTIONS preflight 230 + if r.Method == "OPTIONS" { 231 + w.WriteHeader(http.StatusOK) 232 + return 233 + } 234 + 235 + next.ServeHTTP(w, r) 236 + }) 237 + } 238 + } 239 + 240 + // trimRegistryURL removes http:// or https:// prefix from a URL 241 + // for use in Docker commands where only the host:port is needed 242 + func trimRegistryURL(url string) string { 243 + // Import strings package inline 244 + if len(url) >= 8 && url[:8] == "https://" { 245 + return url[8:] 246 + } 247 + if len(url) >= 7 && url[:7] == "http://" { 248 + return url[7:] 249 + } 250 + return url 251 + }