Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com
at main 144 lines 4.2 kB view raw
1package main 2 3import ( 4 "html/template" 5 "log" 6 "net/http" 7 "os" 8 "strings" 9 "time" 10 11 "github.com/limeleaf/diffdown/internal/auth" 12 "github.com/limeleaf/diffdown/internal/collaboration" 13 "github.com/limeleaf/diffdown/internal/db" 14 "github.com/limeleaf/diffdown/internal/handler" 15 "github.com/limeleaf/diffdown/internal/middleware" 16) 17 18func main() { 19 // Config 20 port := os.Getenv("PORT") 21 if port == "" { 22 port = "8080" 23 } 24 dbPath := os.Getenv("DB_PATH") 25 if dbPath == "" { 26 dbPath = "./diffdown.db" 27 } 28 sessionSecret := os.Getenv("DIFFDOWN_SESSION_SECRET") 29 if sessionSecret == "" { 30 sessionSecret = "dev-secret-change-in-production" 31 } 32 baseURL := os.Getenv("BASE_URL") 33 if baseURL == "" { 34 baseURL = "http://127.0.0.1:8080" 35 } 36 37 // Init 38 auth.InitStore(sessionSecret) 39 40 database, err := db.Open(dbPath) 41 if err != nil { 42 log.Fatalf("Failed to open database: %v", err) 43 } 44 defer database.Close() 45 46 if err := database.Migrate(); err != nil { 47 log.Fatalf("Failed to run migrations: %v", err) 48 } 49 50 funcMap := template.FuncMap{ 51 "fmtdate": func(s string) string { 52 for _, layout := range []string{time.RFC3339Nano, time.RFC3339} { 53 if t, err := time.Parse(layout, s); err == nil { 54 return t.Format("January 2, 2006 at 15:04") 55 } 56 } 57 return s 58 }, 59 } 60 61 pages := []string{ 62 "about", "atproto_login", "documents", "document_view", "document_edit", 63 "landing", "login", "new_document", "register", 64 } 65 tmpls := make(map[string]*template.Template, len(pages)) 66 for _, page := range pages { 67 name := page + ".html" 68 tmpls[name] = template.Must(template.New(name).Funcs(funcMap).ParseFiles( 69 "templates/base.html", 70 "templates/"+name, 71 )) 72 } 73 74 collabHub := collaboration.NewHub() 75 h := handler.New(database, tmpls, baseURL, collabHub) 76 77 // Routes 78 mux := http.NewServeMux() 79 80 // Static files are handled outside the mux to avoid conflicts with 81 // wildcard patterns. 82 staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("static"))) 83 faviconHandler := http.FileServer(http.Dir("static")) 84 85 // Auth routes 86 87 mux.HandleFunc("POST /auth/logout", h.Logout) 88 mux.HandleFunc("GET /client-metadata.json", h.ClientMetadata) 89 mux.HandleFunc("GET /auth/atproto", h.ATProtoLoginPage) 90 mux.HandleFunc("POST /auth/atproto", h.ATProtoLoginSubmit) 91 mux.HandleFunc("GET /auth/atproto/callback", h.ATProtoCallback) 92 93 // Page routes 94 mux.HandleFunc("GET /about", h.AboutPage) 95 96 // Dashboard 97 mux.HandleFunc("GET /", h.Dashboard) 98 99 // Document routes 100 mux.HandleFunc("GET /docs/new", h.NewDocumentPage) 101 mux.HandleFunc("POST /docs/new", h.NewDocumentSubmit) 102 mux.HandleFunc("GET /docs/{rkey}", h.DocumentView) 103 mux.HandleFunc("GET /docs/{rkey}/edit", h.DocumentEdit) 104 // Collaborator-scoped routes (owner DID in path) 105 mux.HandleFunc("GET /docs/{did}/{rkey}", h.CollaboratorDocumentView) 106 mux.HandleFunc("GET /docs/{did}/{rkey}/edit", h.CollaboratorDocumentEdit) 107 108 // API 109 mux.HandleFunc("POST /api/render", h.APIRender) 110 mux.HandleFunc("POST /api/docs/{rkey}/save", h.APIDocumentSave) 111 mux.HandleFunc("PUT /api/docs/{rkey}/autosave", h.APIDocumentAutoSave) 112 mux.HandleFunc("DELETE /api/docs/{rkey}", h.APIDocumentDelete) 113 mux.HandleFunc("POST /api/docs/{rkey}/invite", h.DocumentInvite) 114 mux.HandleFunc("GET /docs/{rkey}/accept", h.AcceptInvite) 115 mux.HandleFunc("POST /api/docs/{rkey}/comments", h.CommentCreate) 116 mux.HandleFunc("GET /api/docs/{rkey}/comments", h.CommentList) 117 mux.HandleFunc("PATCH /api/docs/{rkey}/comments/{commentId}", h.CommentUpdate) 118 mux.HandleFunc("POST /api/docs/{rkey}/steps", h.SubmitSteps) 119 mux.HandleFunc("GET /api/docs/{rkey}/steps", h.GetSteps) 120 121 // WebSocket 122 mux.HandleFunc("GET /ws/docs/{rkey}", h.CollaboratorWebSocket) 123 124 // Middleware stack 125 stack := middleware.Logger( 126 middleware.InjectUser( 127 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 128 if strings.HasPrefix(r.URL.Path, "/static/") { 129 staticHandler.ServeHTTP(w, r) 130 return 131 } 132 if r.URL.Path == "/favicon.ico" || r.URL.Path == "/favicon.svg" || 133 strings.HasPrefix(r.URL.Path, "/favicon/") { 134 faviconHandler.ServeHTTP(w, r) 135 return 136 } 137 mux.ServeHTTP(w, r) 138 }), 139 ), 140 ) 141 142 log.Printf("Starting server on :%s", port) 143 log.Fatal(http.ListenAndServe(":"+port, stack)) 144}