diff --git a/main.go b/main.go new file mode 100644 index 0000000..91d78d9 --- /dev/null +++ b/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + // Get database connection string from environment (or empty for defaults) + connString := os.Getenv("DATABASE_URL") + + // Create dashboard instance (handles DB connection internally) + dashboard, err := NewDashboard(connString) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize dashboard: %v\n", err) + os.Exit(1) + } + defer dashboard.db.Close() + + // Start background stats update loop + go dashboard.StartStatsLoop() + + // Get listen address from environment or use default + addr := os.Getenv("DASHBOARD_ADDR") + if addr == "" { + addr = "0.0.0.0:4321" + } + + // Start HTTP server + if err := dashboard.StartServer(addr); err != nil { + fmt.Fprintf(os.Stderr, "Server error: %v\n", err) + os.Exit(1) + } +} diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..9b1b5a2 --- /dev/null +++ b/routes.go @@ -0,0 +1,205 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "strings" +) + +func (d *Dashboard) StartServer(addr string) error { + // Determine base URL for OAuth + baseURL := os.Getenv("OAUTH_BASE_URL") + if baseURL == "" { + // Default based on whether we're in production + if strings.Contains(addr, "0.0.0.0") { + baseURL = "https://app.1440.news" + } else { + baseURL = "http://" + addr + } + } + + // Initialize OAuth manager + oauthCfg, err := LoadOAuthConfig(baseURL) + var oauth *OAuthManager + if err != nil { + fmt.Printf("OAuth not configured: %v (dashboard will be unprotected)\n", err) + } else { + oauth, err = NewOAuthManager(*oauthCfg, d.db) + if err != nil { + fmt.Printf("Failed to initialize OAuth: %v (dashboard will be unprotected)\n", err) + oauth = nil + } else { + fmt.Println("OAuth authentication enabled for dashboard") + } + } + + // OAuth endpoints (always public) + if oauth != nil { + http.HandleFunc("/.well-known/oauth-client-metadata", oauth.HandleClientMetadata) + http.HandleFunc("/.well-known/jwks.json", oauth.HandleJWKS) + http.HandleFunc("/auth/login", oauth.HandleLogin) + http.HandleFunc("/auth/callback", oauth.HandleCallback) + http.HandleFunc("/auth/logout", oauth.HandleLogout) + http.HandleFunc("/auth/session", oauth.HandleSessionInfo) + } + + // Helper to wrap handlers with auth if OAuth is enabled + withAuth := func(h http.HandlerFunc) http.HandlerFunc { + if oauth != nil { + return oauth.RequireAuth(h) + } + return h + } + + http.HandleFunc("/dashboard", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleDashboard(w, r) + })) + + // Root handler for 1440.news accounts directory + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + host := r.Host + // Strip port if present + if idx := strings.Index(host, ":"); idx != -1 { + host = host[:idx] + } + + // If this is 1440.news (apex), serve accounts directory + if host == "1440.news" || host == "1440.localhost" { + if r.URL.Path == "/" || r.URL.Path == "" { + d.handleAccountsDirectory(w, r) + return + } + } + + // Otherwise, redirect to dashboard for root path + if r.URL.Path == "/" { + http.Redirect(w, r, "/dashboard", http.StatusFound) + return + } + + // Unknown path + http.NotFound(w, r) + }) + + http.HandleFunc("/api/stats", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIStats(w, r) + })) + http.HandleFunc("/api/allDomains", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIAllDomains(w, r) + })) + http.HandleFunc("/api/domainFeeds", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIDomainFeeds(w, r) + })) + http.HandleFunc("/api/feedInfo", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIFeedInfo(w, r) + })) + http.HandleFunc("/api/feedItems", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIFeedItems(w, r) + })) + http.HandleFunc("/api/search", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPISearch(w, r) + })) + http.HandleFunc("/api/tlds", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPITLDs(w, r) + })) + http.HandleFunc("/api/searchStats", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPISearchStats(w, r) + })) + http.HandleFunc("/api/tldDomains", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPITLDDomains(w, r) + })) + http.HandleFunc("/api/revisitDomain", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIRevisitDomain(w, r) + })) + http.HandleFunc("/api/priorityCrawl", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIPriorityCrawl(w, r) + })) + http.HandleFunc("/api/checkFeed", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPICheckFeed(w, r) + })) + http.HandleFunc("/api/domainsByStatus", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIDomainsByStatus(w, r) + })) + http.HandleFunc("/api/feedsByStatus", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIFeedsByStatus(w, r) + })) + http.HandleFunc("/api/domains", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIDomains(w, r) + })) + http.HandleFunc("/api/feeds", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIFeeds(w, r) + })) + http.HandleFunc("/api/setDomainStatus", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPISetDomainStatus(w, r) + })) + http.HandleFunc("/api/filter", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIFilter(w, r) + })) + http.HandleFunc("/api/enablePublish", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIEnablePublish(w, r) + })) + http.HandleFunc("/api/disablePublish", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIDisablePublish(w, r) + })) + http.HandleFunc("/api/publishEnabled", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIPublishEnabled(w, r) + })) + http.HandleFunc("/api/publishDenied", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIPublishDenied(w, r) + })) + http.HandleFunc("/api/publishCandidates", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIPublishCandidates(w, r) + })) + http.HandleFunc("/api/setPublishStatus", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPISetPublishStatus(w, r) + })) + http.HandleFunc("/api/unpublishedItems", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIUnpublishedItems(w, r) + })) + http.HandleFunc("/api/testPublish", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPITestPublish(w, r) + })) + http.HandleFunc("/api/deriveHandle", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIDeriveHandle(w, r) + })) + http.HandleFunc("/api/publishFeed", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIPublishFeed(w, r) + })) + http.HandleFunc("/api/createAccount", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPICreateAccount(w, r) + })) + http.HandleFunc("/api/publishFeedFull", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIPublishFeedFull(w, r) + })) + http.HandleFunc("/api/updateProfile", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIUpdateProfile(w, r) + })) + http.HandleFunc("/api/languages", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPILanguages(w, r) + })) + http.HandleFunc("/api/denyDomain", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIDenyDomain(w, r) + })) + http.HandleFunc("/api/undenyDomain", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIUndenyDomain(w, r) + })) + http.HandleFunc("/api/dropDomain", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIDropDomain(w, r) + })) + http.HandleFunc("/api/tldStats", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPITLDStats(w, r) + })) + http.HandleFunc("/api/resetAllPublishing", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIResetAllPublishing(w, r) + })) + http.HandleFunc("/api/refreshProfiles", withAuth(func(w http.ResponseWriter, r *http.Request) { + d.handleAPIRefreshProfiles(w, r) + })) + http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) { + http.StripPrefix("/static/", http.FileServer(http.Dir("static"))).ServeHTTP(w, r) + }) + + fmt.Printf("Dashboard running at http://%s\n", addr) + return http.ListenAndServe(addr, nil) +}