159 lines
5.3 KiB
Go
159 lines
5.3 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"embed"
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
|
|
"github.com/tim/cairnquire/apps/server/internal/auth"
|
|
"github.com/tim/cairnquire/apps/server/internal/config"
|
|
"github.com/tim/cairnquire/apps/server/internal/docs"
|
|
"github.com/tim/cairnquire/apps/server/internal/realtime"
|
|
"github.com/tim/cairnquire/apps/server/internal/store"
|
|
"github.com/tim/cairnquire/apps/server/internal/sync"
|
|
)
|
|
|
|
//go:embed templates/*.gohtml static/*
|
|
var assets embed.FS
|
|
|
|
type Dependencies struct {
|
|
Config config.Config
|
|
Logger *slog.Logger
|
|
Documents *docs.Service
|
|
Repository *docs.Repository
|
|
ContentStore *store.ContentStore
|
|
Hub *realtime.Hub
|
|
SyncService *sync.Service
|
|
SyncRepo *sync.Repository
|
|
Auth *auth.Service
|
|
}
|
|
|
|
type Server struct {
|
|
config config.Config
|
|
logger *slog.Logger
|
|
documents *docs.Service
|
|
repository *docs.Repository
|
|
contentStore *store.ContentStore
|
|
hub *realtime.Hub
|
|
syncService *sync.Service
|
|
syncRepo *sync.Repository
|
|
auth *auth.Service
|
|
authLimiter *rateLimiter
|
|
templates *template.Template
|
|
webEnabled bool
|
|
}
|
|
|
|
func New(deps Dependencies) (http.Handler, error) {
|
|
templates, err := template.New("").Funcs(template.FuncMap{
|
|
"trimMd": func(path string) string {
|
|
return strings.TrimSuffix(path, ".md")
|
|
},
|
|
"sub": func(a, b int) int {
|
|
return a - b
|
|
},
|
|
}).ParseFS(assets, "templates/*.gohtml")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
server := &Server{
|
|
config: deps.Config,
|
|
logger: deps.Logger,
|
|
documents: deps.Documents,
|
|
repository: deps.Repository,
|
|
contentStore: deps.ContentStore,
|
|
hub: deps.Hub,
|
|
syncService: deps.SyncService,
|
|
syncRepo: deps.SyncRepo,
|
|
auth: deps.Auth,
|
|
authLimiter: newRateLimiter(5, time.Minute),
|
|
templates: templates,
|
|
}
|
|
|
|
if _, err := os.Stat(deps.Config.Web.DistDir); err == nil {
|
|
server.webEnabled = true
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(middleware.RequestID)
|
|
router.Use(middleware.RealIP)
|
|
router.Use(middleware.Recoverer)
|
|
router.Use(server.timeoutExceptWebSocket(30 * time.Second))
|
|
router.Use(server.requestLogger)
|
|
router.Use(server.securityHeaders)
|
|
router.Use(server.authMiddleware)
|
|
|
|
router.Get("/", server.handleIndex)
|
|
router.Get("/login", server.handleLoginPage)
|
|
router.Get("/account", server.handleAccountPage)
|
|
router.Get("/device/verify", server.handleDeviceVerifyPage)
|
|
router.Get("/health", server.handleHealth)
|
|
router.Get("/sw.js", server.handleServiceWorker)
|
|
router.Get("/ws", server.handleWebSocket)
|
|
router.Get("/api/auth/me", server.handleAuthMe)
|
|
router.Post("/api/auth/register/password", server.handlePasswordRegister)
|
|
router.Post("/api/auth/login/password", server.handlePasswordLogin)
|
|
router.Post("/api/auth/logout", server.handleLogout)
|
|
router.Post("/api/auth/password/change", server.handleChangePassword)
|
|
router.Delete("/api/auth/account", server.handleDeleteAccount)
|
|
router.Post("/api/auth/passkeys/register/begin", server.handlePasskeyRegisterBegin)
|
|
router.Post("/api/auth/passkeys/register/finish", server.handlePasskeyRegisterFinish)
|
|
router.Post("/api/auth/passkeys/login/begin", server.handlePasskeyLoginBegin)
|
|
router.Post("/api/auth/passkeys/login/finish", server.handlePasskeyLoginFinish)
|
|
router.Get("/api/tokens", server.handleListAPITokens)
|
|
router.Post("/api/tokens", server.handleCreateAPIToken)
|
|
router.Delete("/api/tokens/{id}", server.handleRevokeAPIToken)
|
|
router.Post("/api/device/start", server.handleDeviceStart)
|
|
router.Post("/api/device/approve", server.handleDeviceApprove)
|
|
router.Post("/api/device/token", server.handleDeviceToken)
|
|
router.Get("/api/device/verify", server.handleDeviceVerify)
|
|
router.Post("/api/admin/login", server.handleAdminLogin)
|
|
router.Get("/api/admin/users", server.handleAdminUsers)
|
|
router.Post("/api/admin/users", server.handleAdminCreateUser)
|
|
router.Patch("/api/admin/users/{id}/role", server.handleAdminUpdateUserRole)
|
|
router.Get("/api/admin/auth-users", server.handleAdminAuthUsers)
|
|
router.Get("/api/admin/workspace", server.handleAdminWorkspace)
|
|
router.Post("/api/admin/workspace/sync", server.handleAdminWorkspaceSync)
|
|
router.Get("/docs", server.handleDocsIndex)
|
|
router.Get("/docs/*", server.handleDocument)
|
|
router.Get("/api/documents", server.handleDocuments)
|
|
router.Post("/api/documents/*", server.handleDocumentSave)
|
|
router.Get("/api/search", server.handleSearch)
|
|
router.Post("/api/uploads", server.handleUpload)
|
|
router.Get("/attachments/{hash}", server.handleAttachment)
|
|
|
|
// Sync protocol endpoints
|
|
router.Post("/api/sync/init", server.handleSyncInit)
|
|
router.Post("/api/sync/delta", server.handleSyncDelta)
|
|
router.Post("/api/sync/resolve", server.handleSyncResolve)
|
|
router.Get("/api/content/{hash}", server.handleContentFetch)
|
|
|
|
router.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(mustSub("static"))))
|
|
|
|
if server.webEnabled {
|
|
router.Get("/app", server.handleAppRedirect)
|
|
router.Handle("/app/*", http.StripPrefix("/app/", server.appFileServer()))
|
|
}
|
|
|
|
return router, nil
|
|
}
|
|
|
|
func (s *Server) handleAppRedirect(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/app/", http.StatusMovedPermanently)
|
|
}
|
|
|
|
func mustSub(path string) http.FileSystem {
|
|
sub, err := fsSub(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return http.FS(sub)
|
|
}
|