Files
CalendarApi/internal/api/routes.go
Michilis bd24545b7b Fix BASE_URL config loading, add tasks/projects; robust .env path resolution
- Config: try ENV_FILE, .env, ../.env for loading; trim trailing slash from BaseURL
- Log BASE_URL at server startup for verification
- .env.example: document BASE_URL
- Tasks, projects, tags, migrations and related API/handlers

Made-with: Cursor
2026-03-09 18:57:51 +00:00

229 lines
8.9 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
Task *handlers.TaskHandler
Project *handlers.ProjectHandler
Tag *handlers.TagHandler
TaskDependency *handlers.TaskDependencyHandler
TaskReminder *handlers.TaskReminderHandler
TaskWebhook *handlers.TaskWebhookHandler
}
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)
})
})
// Tasks
r.Route("/tasks", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.Task.List)
r.With(mw.RequireScope("tasks", "write")).Post("/", h.Task.Create)
r.Route("/{id}", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.Task.Get)
r.With(mw.RequireScope("tasks", "write")).Put("/", h.Task.Update)
r.With(mw.RequireScope("tasks", "write")).Delete("/", h.Task.Delete)
r.With(mw.RequireScope("tasks", "write")).Post("/complete", h.Task.MarkComplete)
r.With(mw.RequireScope("tasks", "write")).Post("/uncomplete", h.Task.MarkUncomplete)
r.With(mw.RequireScope("tasks", "read")).Get("/subtasks", h.Task.ListSubtasks)
r.Route("/dependencies", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.TaskDependency.ListBlockers)
r.With(mw.RequireScope("tasks", "write")).Post("/", h.TaskDependency.Add)
r.With(mw.RequireScope("tasks", "write")).Delete("/{blocksTaskId}", h.TaskDependency.Remove)
})
r.Route("/reminders", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.TaskReminder.List)
r.With(mw.RequireScope("tasks", "write")).Post("/", h.TaskReminder.Add)
r.With(mw.RequireScope("tasks", "write")).Delete("/{reminderId}", h.TaskReminder.Delete)
})
})
})
// Projects
r.Route("/projects", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.Project.List)
r.With(mw.RequireScope("tasks", "write")).Post("/", h.Project.Create)
r.Route("/{id}", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.Project.Get)
r.With(mw.RequireScope("tasks", "write")).Put("/", h.Project.Update)
r.With(mw.RequireScope("tasks", "write")).Delete("/", h.Project.Delete)
r.With(mw.RequireScope("tasks", "write")).Post("/share", h.Project.Share)
r.With(mw.RequireScope("tasks", "read")).Get("/members", h.Project.ListMembers)
})
})
// Tags
r.Route("/tags", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.Tag.List)
r.With(mw.RequireScope("tasks", "write")).Post("/", h.Tag.Create)
r.Route("/{id}", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.Tag.Get)
r.With(mw.RequireScope("tasks", "write")).Put("/", h.Tag.Update)
r.With(mw.RequireScope("tasks", "write")).Delete("/", h.Tag.Delete)
r.With(mw.RequireScope("tasks", "write")).Post("/attach/{taskId}", h.Tag.AttachToTask)
r.With(mw.RequireScope("tasks", "write")).Delete("/detach/{taskId}", h.Tag.DetachFromTask)
})
})
// Task Webhooks
r.Route("/webhooks", func(r chi.Router) {
r.With(mw.RequireScope("tasks", "read")).Get("/", h.TaskWebhook.List)
r.With(mw.RequireScope("tasks", "write")).Post("/", h.TaskWebhook.Create)
r.With(mw.RequireScope("tasks", "write")).Delete("/{id}", h.TaskWebhook.Delete)
})
// Availability
r.With(mw.RequireScope("availability", "read")).Get("/availability", h.Availability.Get)
})
return r
}