- OpenAPI: add missing endpoints (add-from-url, subscriptions, public availability) - OpenAPI: CalendarSubscription schema, Subscriptions tag - Frontend app - Migrations: count_for_availability, subscriptions_sync, user_preferences, calendar_settings - Config, rate limit, auth, calendar, booking, ICS, availability, user service updates Made-with: Cursor
161 lines
5.8 KiB
Go
161 lines
5.8 KiB
Go
package api
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/calendarapi/internal/api/handlers"
|
|
"github.com/calendarapi/internal/api/openapi"
|
|
"github.com/calendarapi/internal/config"
|
|
mw "github.com/calendarapi/internal/middleware"
|
|
"github.com/go-chi/chi/v5"
|
|
chimw "github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/cors"
|
|
)
|
|
|
|
type Handlers struct {
|
|
Auth *handlers.AuthHandler
|
|
User *handlers.UserHandler
|
|
Calendar *handlers.CalendarHandler
|
|
Sharing *handlers.SharingHandler
|
|
Event *handlers.EventHandler
|
|
Reminder *handlers.ReminderHandler
|
|
Attendee *handlers.AttendeeHandler
|
|
Contact *handlers.ContactHandler
|
|
Availability *handlers.AvailabilityHandler
|
|
Booking *handlers.BookingHandler
|
|
APIKey *handlers.APIKeyHandler
|
|
ICS *handlers.ICSHandler
|
|
}
|
|
|
|
func NewRouter(h Handlers, authMW *mw.AuthMiddleware, rateLimiter *mw.RateLimiter, cfg *config.Config) *chi.Mux {
|
|
r := chi.NewRouter()
|
|
|
|
r.Use(chimw.RequestID)
|
|
r.Use(chimw.Logger)
|
|
r.Use(chimw.Recoverer)
|
|
r.Use(chimw.RealIP)
|
|
r.Use(chimw.Timeout(30 * time.Second))
|
|
origins := cfg.CORSOrigins
|
|
if len(origins) == 0 {
|
|
origins = []string{"http://localhost:5173", "http://127.0.0.1:5173"}
|
|
}
|
|
r.Use(cors.Handler(cors.Options{
|
|
AllowedOrigins: origins,
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-API-Key"},
|
|
ExposedHeaders: []string{"Link"},
|
|
AllowCredentials: true,
|
|
MaxAge: 300,
|
|
}))
|
|
r.Use(rateLimiter.Limit)
|
|
|
|
// OpenAPI spec and Swagger UI
|
|
r.Get("/openapi.json", openapi.SpecHandler)
|
|
r.Get("/docs", openapi.DocsHandler)
|
|
|
|
// Public routes (no auth)
|
|
r.Group(func(r chi.Router) {
|
|
r.Post("/auth/register", h.Auth.Register)
|
|
r.Post("/auth/login", h.Auth.Login)
|
|
r.Post("/auth/refresh", h.Auth.Refresh)
|
|
|
|
r.Get("/booking/{token}/availability", h.Booking.GetAvailability)
|
|
r.Post("/booking/{token}/reserve", h.Booking.Reserve)
|
|
|
|
r.Get("/cal/{token}/feed.ics", h.ICS.PublicFeed)
|
|
|
|
// Public availability (no auth) - for external booking tools
|
|
r.Get("/availability/aggregate", h.Availability.GetAggregate)
|
|
r.Get("/availability/{token}", h.Availability.GetByToken)
|
|
})
|
|
|
|
// Authenticated routes
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(authMW.Authenticate)
|
|
|
|
// Auth
|
|
r.Post("/auth/logout", h.Auth.Logout)
|
|
r.Get("/auth/me", h.Auth.Me)
|
|
|
|
// Users
|
|
r.Get("/users/me", h.User.GetMe)
|
|
r.Put("/users/me", h.User.UpdateMe)
|
|
r.Delete("/users/me", h.User.DeleteMe)
|
|
|
|
// API Keys
|
|
r.Post("/api-keys", h.APIKey.Create)
|
|
r.Get("/api-keys", h.APIKey.List)
|
|
r.Delete("/api-keys/{id}", h.APIKey.Revoke)
|
|
|
|
// Calendars
|
|
r.Route("/calendars", func(r chi.Router) {
|
|
r.With(mw.RequireScope("calendars", "read")).Get("/", h.Calendar.List)
|
|
r.With(mw.RequireScope("calendars", "write")).Post("/", h.Calendar.Create)
|
|
r.With(mw.RequireScope("calendars", "write")).Post("/import", h.ICS.Import)
|
|
r.With(mw.RequireScope("calendars", "write")).Post("/import-url", h.ICS.ImportURL)
|
|
r.With(mw.RequireScope("calendars", "write")).Post("/add-from-url", h.ICS.AddFromURL)
|
|
|
|
r.Route("/{id}", func(r chi.Router) {
|
|
r.With(mw.RequireScope("calendars", "read")).Get("/", h.Calendar.Get)
|
|
r.With(mw.RequireScope("calendars", "write")).Put("/", h.Calendar.Update)
|
|
r.With(mw.RequireScope("calendars", "write")).Delete("/", h.Calendar.Delete)
|
|
|
|
// Sharing
|
|
r.With(mw.RequireScope("calendars", "write")).Post("/share", h.Sharing.Share)
|
|
r.With(mw.RequireScope("calendars", "read")).Get("/members", h.Sharing.ListMembers)
|
|
r.With(mw.RequireScope("calendars", "write")).Delete("/members/{userID}", h.Sharing.RemoveMember)
|
|
|
|
// Booking link
|
|
r.With(mw.RequireScope("booking", "write")).Post("/booking-link", h.Booking.CreateLink)
|
|
|
|
// ICS
|
|
r.With(mw.RequireScope("calendars", "read")).Get("/export.ics", h.ICS.Export)
|
|
|
|
// Subscriptions
|
|
r.With(mw.RequireScope("calendars", "read")).Get("/subscriptions", h.ICS.ListSubscriptions)
|
|
r.With(mw.RequireScope("calendars", "write")).Post("/subscriptions", h.ICS.AddSubscription)
|
|
r.With(mw.RequireScope("calendars", "write")).Delete("/subscriptions/{subId}", h.ICS.DeleteSubscription)
|
|
r.With(mw.RequireScope("calendars", "write")).Post("/subscriptions/{subId}/sync", h.ICS.SyncSubscription)
|
|
})
|
|
})
|
|
|
|
// Events
|
|
r.Route("/events", func(r chi.Router) {
|
|
r.With(mw.RequireScope("events", "read")).Get("/", h.Event.List)
|
|
r.With(mw.RequireScope("events", "write")).Post("/", h.Event.Create)
|
|
|
|
r.Route("/{id}", func(r chi.Router) {
|
|
r.With(mw.RequireScope("events", "read")).Get("/", h.Event.Get)
|
|
r.With(mw.RequireScope("events", "write")).Put("/", h.Event.Update)
|
|
r.With(mw.RequireScope("events", "write")).Delete("/", h.Event.Delete)
|
|
|
|
// Reminders
|
|
r.With(mw.RequireScope("events", "write")).Post("/reminders", h.Reminder.Add)
|
|
r.With(mw.RequireScope("events", "write")).Delete("/reminders/{reminderID}", h.Reminder.Delete)
|
|
|
|
// Attendees
|
|
r.With(mw.RequireScope("events", "write")).Post("/attendees", h.Attendee.Add)
|
|
r.With(mw.RequireScope("events", "write")).Put("/attendees/{attendeeID}", h.Attendee.UpdateStatus)
|
|
r.With(mw.RequireScope("events", "write")).Delete("/attendees/{attendeeID}", h.Attendee.Delete)
|
|
})
|
|
})
|
|
|
|
// Contacts
|
|
r.Route("/contacts", func(r chi.Router) {
|
|
r.With(mw.RequireScope("contacts", "read")).Get("/", h.Contact.List)
|
|
r.With(mw.RequireScope("contacts", "write")).Post("/", h.Contact.Create)
|
|
|
|
r.Route("/{id}", func(r chi.Router) {
|
|
r.With(mw.RequireScope("contacts", "read")).Get("/", h.Contact.Get)
|
|
r.With(mw.RequireScope("contacts", "write")).Put("/", h.Contact.Update)
|
|
r.With(mw.RequireScope("contacts", "write")).Delete("/", h.Contact.Delete)
|
|
})
|
|
})
|
|
|
|
// Availability
|
|
r.With(mw.RequireScope("availability", "read")).Get("/availability", h.Availability.Get)
|
|
})
|
|
|
|
return r
|
|
}
|