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))
}