package main import ( "html/template" "log" "net/http" "os" "strings" "time" "github.com/limeleaf/diffdown/internal/auth" "github.com/limeleaf/diffdown/internal/collaboration" "github.com/limeleaf/diffdown/internal/db" "github.com/limeleaf/diffdown/internal/handler" "github.com/limeleaf/diffdown/internal/middleware" ) func main() { // Config port := os.Getenv("PORT") if port == "" { port = "8080" } dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "./diffdown.db" } sessionSecret := os.Getenv("DIFFDOWN_SESSION_SECRET") if sessionSecret == "" { sessionSecret = "dev-secret-change-in-production" } baseURL := os.Getenv("BASE_URL") if baseURL == "" { baseURL = "http://127.0.0.1:8080" } // Init auth.InitStore(sessionSecret) database, err := db.Open(dbPath) if err != nil { log.Fatalf("Failed to open database: %v", err) } defer database.Close() if err := database.Migrate(); err != nil { log.Fatalf("Failed to run migrations: %v", err) } funcMap := template.FuncMap{ "fmtdate": func(s string) string { for _, layout := range []string{time.RFC3339Nano, time.RFC3339} { if t, err := time.Parse(layout, s); err == nil { return t.Format("January 2, 2006 at 15:04") } } return s }, } pages := []string{ "about", "atproto_login", "documents", "document_view", "document_edit", "landing", "login", "new_document", "register", } tmpls := make(map[string]*template.Template, len(pages)) for _, page := range pages { name := page + ".html" tmpls[name] = template.Must(template.New(name).Funcs(funcMap).ParseFiles( "templates/base.html", "templates/"+name, )) } collabHub := collaboration.NewHub() h := handler.New(database, tmpls, baseURL, collabHub) // Routes mux := http.NewServeMux() // Static files are handled outside the mux to avoid conflicts with // wildcard patterns. staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("static"))) faviconHandler := http.FileServer(http.Dir("static")) // Auth routes mux.HandleFunc("POST /auth/logout", h.Logout) mux.HandleFunc("GET /client-metadata.json", h.ClientMetadata) mux.HandleFunc("GET /auth/atproto", h.ATProtoLoginPage) mux.HandleFunc("POST /auth/atproto", h.ATProtoLoginSubmit) mux.HandleFunc("GET /auth/atproto/callback", h.ATProtoCallback) // Page routes mux.HandleFunc("GET /about", h.AboutPage) // Dashboard mux.HandleFunc("GET /", h.Dashboard) // Document routes mux.HandleFunc("GET /docs/new", h.NewDocumentPage) mux.HandleFunc("POST /docs/new", h.NewDocumentSubmit) mux.HandleFunc("GET /docs/{rkey}", h.DocumentView) mux.HandleFunc("GET /docs/{rkey}/edit", h.DocumentEdit) // Collaborator-scoped routes (owner DID in path) mux.HandleFunc("GET /docs/{did}/{rkey}", h.CollaboratorDocumentView) mux.HandleFunc("GET /docs/{did}/{rkey}/edit", h.CollaboratorDocumentEdit) // API mux.HandleFunc("POST /api/render", h.APIRender) mux.HandleFunc("POST /api/docs/{rkey}/save", h.APIDocumentSave) mux.HandleFunc("PUT /api/docs/{rkey}/autosave", h.APIDocumentAutoSave) mux.HandleFunc("DELETE /api/docs/{rkey}", h.APIDocumentDelete) mux.HandleFunc("POST /api/docs/{rkey}/invite", h.DocumentInvite) mux.HandleFunc("GET /docs/{rkey}/accept", h.AcceptInvite) mux.HandleFunc("POST /api/docs/{rkey}/comments", h.CommentCreate) mux.HandleFunc("GET /api/docs/{rkey}/comments", h.CommentList) mux.HandleFunc("PATCH /api/docs/{rkey}/comments/{commentId}", h.CommentUpdate) mux.HandleFunc("POST /api/docs/{rkey}/steps", h.SubmitSteps) mux.HandleFunc("GET /api/docs/{rkey}/steps", h.GetSteps) // WebSocket mux.HandleFunc("GET /ws/docs/{rkey}", h.CollaboratorWebSocket) // Middleware stack stack := middleware.Logger( middleware.InjectUser( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/static/") { staticHandler.ServeHTTP(w, r) return } if r.URL.Path == "/favicon.ico" || r.URL.Path == "/favicon.svg" || strings.HasPrefix(r.URL.Path, "/favicon/") { faviconHandler.ServeHTTP(w, r) return } mux.ServeHTTP(w, r) }), ), ) log.Printf("Starting server on :%s", port) log.Fatal(http.ListenAndServe(":"+port, stack)) }