mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
Six env flags, two conventions, and both failed silently against the other. Five read `== "true"`, so BOOTNODE_ENABLED=1 left bootnode off and CALENDAR_ENABLED=1 left the calendar off. One read `!= "1"`, so BASE_ENABLE_ADMIN_UI=true served no admin. And ZAP_DISABLED read `!= "true"`, so an operator who wrote ZAP_DISABLED=1 to stop the listener watched it come up anyway. In every case the flag was accepted, ignored, and never mentioned again. This tree already answers "read a boolean from a string" four times, with strconv.ParseBool (tools/filesystem, tools/search, tools/router, core/settings_query). The hand-rolled comparisons were the deviation, so osutils.Bool is that answer given a name and a default, and all six sites read it. Strictly more permissive: every spelling that worked before still works, and the ones that silently did nothing now do what they say. An unparseable value yields the default rather than its opposite — someone who typed something meaningless has not asked for the other thing. Coverage: plugins/zap 5.9% -> 23.5%, and DefaultConfig is pinned including the case that started this (ZAP_DISABLED=1 must disable) and a garbage ZAP_PORT, which must keep 9999 rather than quietly binding somewhere else. Co-authored-by: Hanzo Dev <[email protected]>
92 lines
3.8 KiB
Go
92 lines
3.8 KiB
Go
// Package calendar is the native Base + IAM booking backend that speaks Cal.com's
|
|
// API-v2 shapes, so a public booking page rendered with Cal's <Booker> atom talks
|
|
// straight to Base. It mounts under /v1/calendar and serves the Booker's endpoint
|
|
// subset — public event, available slots, slot holds, booking create/read/cancel,
|
|
// and an anonymous /me — over the scheduling collections defined in migration
|
|
// 1780700000_scheduling.go (eventType, availabilitySchedule, booking). The
|
|
// availability computation, the transactional double-book guard and the rate
|
|
// limiter are the reviewed core, preserved verbatim; this package only reshapes
|
|
// the wire contract from the bespoke /v1/schedule shapes to the Cal atom's.
|
|
package calendar
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/hanzoai/base/tools/osutils"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tools/hook"
|
|
"github.com/hanzoai/base/tools/router"
|
|
)
|
|
|
|
// Config controls the plugin. The booking API is opt-in.
|
|
type Config struct {
|
|
Enabled bool
|
|
}
|
|
|
|
// ConfigFromEnv reads CALENDAR_ENABLED.
|
|
func ConfigFromEnv() Config {
|
|
return Config{Enabled: osutils.Bool("CALENDAR_ENABLED", false)}
|
|
}
|
|
|
|
type plugin struct {
|
|
app core.App
|
|
config Config
|
|
ipLimit *limiter // booking, per client IP
|
|
hostLimit *limiter // booking, per host (owner)
|
|
readIPLimit *limiter // public reads/reserve, per client IP
|
|
readHandleLimit *limiter // public reads, per host handle (hard bound)
|
|
holds *holds // advisory short-TTL slot reservations
|
|
}
|
|
|
|
// MustRegister registers the calendar plugin and panics on error.
|
|
func MustRegister(app core.App, config Config) {
|
|
if err := Register(app, config); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Register mounts the public booking API on the app when enabled.
|
|
func Register(app core.App, config Config) error {
|
|
if !config.Enabled {
|
|
return nil
|
|
}
|
|
p := &plugin{
|
|
app: app,
|
|
config: config,
|
|
ipLimit: newLimiter(time.Minute, 15), // 15 booking attempts/min per client IP
|
|
hostLimit: newLimiter(time.Minute, 60), // 60 booking attempts/min per host
|
|
readIPLimit: newLimiter(time.Minute, 240), // 240 reads/min per client IP (defense-in-depth)
|
|
readHandleLimit: newLimiter(time.Minute, 600), // 600 reads/min per host handle (hard bound)
|
|
holds: newHolds(5 * time.Minute), // advisory slot holds expire after 5 minutes
|
|
}
|
|
app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{
|
|
Id: "calendar",
|
|
Func: func(e *core.ServeEvent) error {
|
|
if err := p.seedHost(); err != nil {
|
|
return err
|
|
}
|
|
p.registerRoutes(e.Router)
|
|
return e.Next()
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// registerRoutes mounts the Cal-shaped booking API under /v1/calendar. Every route
|
|
// is public — a booking page is unauthenticated — so the reads and the
|
|
// availability-gated booking write need no auth; a booking is then managed by its
|
|
// opaque uid. Each route notes the Cal atom hook it serves.
|
|
func (p *plugin) registerRoutes(r *router.Router[*core.RequestEvent]) {
|
|
g := r.Group("/v1/calendar")
|
|
g.GET("/atoms/event-types/{slug}/public", p.handlePublicEvent) // useAtomGetPublicEvent
|
|
g.GET("/slots/available", p.handleAvailableSlots) // useAvailableSlots
|
|
g.POST("/slots/reserve", p.handleReserveSlot) // useReserveSlot
|
|
g.DELETE("/slots/selected-slot", p.handleDeleteSelectedSlot) // useDeleteSelectedSlot
|
|
g.GET("/bookings/{uid}/reschedule", p.handleBookingForReschedule) // useGetBookingForReschedule
|
|
g.POST("/bookings", p.handleCreateBooking) // useCreateBooking
|
|
g.GET("/bookings/{uid}", p.handleGetBooking) // SPA confirmation view
|
|
g.POST("/bookings/{uid}/cancel", p.handleCancelBooking) // SPA cancel action
|
|
g.GET("/me", p.handleMe) // useMe (anonymous, non-leaking)
|
|
}
|