A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
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 + }