first commit

Made-with: Cursor
This commit is contained in:
Michilis
2026-02-28 02:17:55 +00:00
commit 41f6ae916f
92 changed files with 12332 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
package handlers
import (
"net/http"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
)
type APIKeyHandler struct {
apiKeySvc *service.APIKeyService
}
func NewAPIKeyHandler(apiKeySvc *service.APIKeyService) *APIKeyHandler {
return &APIKeyHandler{apiKeySvc: apiKeySvc}
}
func (h *APIKeyHandler) Create(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
var req struct {
Name string `json:"name"`
Scopes map[string][]string `json:"scopes"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
key, err := h.apiKeySvc.Create(r.Context(), userID, req.Name, req.Scopes)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, key)
}
func (h *APIKeyHandler) List(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
keys, err := h.apiKeySvc.List(r.Context(), userID)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteList(w, keys, models.PageInfo{Limit: utils.DefaultLimit})
}
func (h *APIKeyHandler) Revoke(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
keyID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
if err := h.apiKeySvc.Revoke(r.Context(), userID, keyID); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}

View File

@@ -0,0 +1,106 @@
package handlers
import (
"net/http"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type AttendeeHandler struct {
attendeeSvc *service.AttendeeService
}
func NewAttendeeHandler(attendeeSvc *service.AttendeeService) *AttendeeHandler {
return &AttendeeHandler{attendeeSvc: attendeeSvc}
}
func (h *AttendeeHandler) Add(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
eventID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
var req struct {
Attendees []struct {
UserID *uuid.UUID `json:"user_id"`
Email *string `json:"email"`
} `json:"attendees"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
var addReqs []service.AddAttendeeRequest
for _, a := range req.Attendees {
addReqs = append(addReqs, service.AddAttendeeRequest{
UserID: a.UserID,
Email: a.Email,
})
}
event, err := h.attendeeSvc.AddAttendees(r.Context(), userID, eventID, addReqs)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"event": event})
}
func (h *AttendeeHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
eventID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
attendeeID, err := utils.ValidateUUID(chi.URLParam(r, "attendeeID"))
if err != nil {
utils.WriteError(w, err)
return
}
var req struct {
Status string `json:"status"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
event, err := h.attendeeSvc.UpdateStatus(r.Context(), userID, eventID, attendeeID, req.Status)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"event": event})
}
func (h *AttendeeHandler) Delete(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
eventID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
attendeeID, err := utils.ValidateUUID(chi.URLParam(r, "attendeeID"))
if err != nil {
utils.WriteError(w, err)
return
}
if err := h.attendeeSvc.DeleteAttendee(r.Context(), userID, eventID, attendeeID); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}

View File

@@ -0,0 +1,109 @@
package handlers
import (
"net/http"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
)
type AuthHandler struct {
authSvc *service.AuthService
userSvc *service.UserService
}
func NewAuthHandler(authSvc *service.AuthService, userSvc *service.UserService) *AuthHandler {
return &AuthHandler{authSvc: authSvc, userSvc: userSvc}
}
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
Timezone string `json:"timezone"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
result, err := h.authSvc.Register(r.Context(), req.Email, req.Password, req.Timezone)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, result)
}
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
result, err := h.authSvc.Login(r.Context(), req.Email, req.Password)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, result)
}
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
result, err := h.authSvc.Refresh(r.Context(), req.RefreshToken)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, result)
}
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
if err := h.authSvc.Logout(r.Context(), req.RefreshToken); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserID(r.Context())
if !ok {
utils.WriteError(w, models.ErrAuthRequired)
return
}
user, err := h.userSvc.GetMe(r.Context(), userID)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"user": user})
}

View File

@@ -0,0 +1,60 @@
package handlers
import (
"net/http"
"time"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
)
type AvailabilityHandler struct {
availSvc *service.AvailabilityService
}
func NewAvailabilityHandler(availSvc *service.AvailabilityService) *AvailabilityHandler {
return &AvailabilityHandler{availSvc: availSvc}
}
func (h *AvailabilityHandler) Get(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
q := r.URL.Query()
calIDStr := q.Get("calendar_id")
if calIDStr == "" {
utils.WriteError(w, models.NewValidationError("calendar_id required"))
return
}
calID, err := utils.ValidateUUID(calIDStr)
if err != nil {
utils.WriteError(w, err)
return
}
startStr := q.Get("start")
endStr := q.Get("end")
if startStr == "" || endStr == "" {
utils.WriteError(w, models.NewValidationError("start and end required"))
return
}
start, err := time.Parse(time.RFC3339, startStr)
if err != nil {
utils.WriteError(w, models.NewValidationError("invalid start time"))
return
}
end, err := time.Parse(time.RFC3339, endStr)
if err != nil {
utils.WriteError(w, models.NewValidationError("invalid end time"))
return
}
result, err := h.availSvc.GetBusyBlocks(r.Context(), userID, calID, start, end)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, result)
}

View File

@@ -0,0 +1,98 @@
package handlers
import (
"net/http"
"time"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
)
type BookingHandler struct {
bookingSvc *service.BookingService
}
func NewBookingHandler(bookingSvc *service.BookingService) *BookingHandler {
return &BookingHandler{bookingSvc: bookingSvc}
}
func (h *BookingHandler) CreateLink(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
var req models.BookingConfig
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
link, err := h.bookingSvc.CreateLink(r.Context(), userID, calID, req)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, link)
}
func (h *BookingHandler) GetAvailability(w http.ResponseWriter, r *http.Request) {
token := chi.URLParam(r, "token")
q := r.URL.Query()
startStr := q.Get("start")
endStr := q.Get("end")
if startStr == "" || endStr == "" {
utils.WriteError(w, models.NewValidationError("start and end required"))
return
}
start, err := time.Parse(time.RFC3339, startStr)
if err != nil {
utils.WriteError(w, models.NewValidationError("invalid start time"))
return
}
end, err := time.Parse(time.RFC3339, endStr)
if err != nil {
utils.WriteError(w, models.NewValidationError("invalid end time"))
return
}
result, err := h.bookingSvc.GetAvailability(r.Context(), token, start, end)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, result)
}
func (h *BookingHandler) Reserve(w http.ResponseWriter, r *http.Request) {
token := chi.URLParam(r, "token")
var req struct {
Name string `json:"name"`
Email string `json:"email"`
SlotStart time.Time `json:"slot_start"`
SlotEnd time.Time `json:"slot_end"`
Notes *string `json:"notes"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
event, err := h.bookingSvc.Reserve(r.Context(), token, req.Name, req.Email, req.SlotStart, req.SlotEnd, req.Notes)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"ok": true, "event": event})
}

View File

@@ -0,0 +1,112 @@
package handlers
import (
"net/http"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
)
type CalendarHandler struct {
calSvc *service.CalendarService
}
func NewCalendarHandler(calSvc *service.CalendarService) *CalendarHandler {
return &CalendarHandler{calSvc: calSvc}
}
func (h *CalendarHandler) List(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calendars, err := h.calSvc.List(r.Context(), userID)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteList(w, calendars, models.PageInfo{Limit: utils.DefaultLimit})
}
func (h *CalendarHandler) Create(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
var req struct {
Name string `json:"name"`
Color string `json:"color"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
cal, err := h.calSvc.Create(r.Context(), userID, req.Name, req.Color)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"calendar": cal})
}
func (h *CalendarHandler) Get(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
cal, err := h.calSvc.Get(r.Context(), userID, calID)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"calendar": cal})
}
func (h *CalendarHandler) Update(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
var req struct {
Name *string `json:"name"`
Color *string `json:"color"`
IsPublic *bool `json:"is_public"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
cal, err := h.calSvc.Update(r.Context(), userID, calID, req.Name, req.Color, req.IsPublic)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"calendar": cal})
}
func (h *CalendarHandler) Delete(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
if err := h.calSvc.Delete(r.Context(), userID, calID); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}

View File

@@ -0,0 +1,146 @@
package handlers
import (
"net/http"
"strconv"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
)
type ContactHandler struct {
contactSvc *service.ContactService
}
func NewContactHandler(contactSvc *service.ContactService) *ContactHandler {
return &ContactHandler{contactSvc: contactSvc}
}
func (h *ContactHandler) List(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
q := r.URL.Query()
var search *string
if s := q.Get("search"); s != "" {
search = &s
}
limit := 50
if l := q.Get("limit"); l != "" {
if v, err := strconv.Atoi(l); err == nil {
limit = v
}
}
contacts, cursor, err := h.contactSvc.List(r.Context(), userID, search, limit, q.Get("cursor"))
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteList(w, contacts, models.PageInfo{Limit: int(utils.ClampLimit(limit)), NextCursor: cursor})
}
func (h *ContactHandler) Create(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
var req struct {
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
Email *string `json:"email"`
Phone *string `json:"phone"`
Company *string `json:"company"`
Notes *string `json:"notes"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
contact, err := h.contactSvc.Create(r.Context(), userID, service.CreateContactRequest{
FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email,
Phone: req.Phone,
Company: req.Company,
Notes: req.Notes,
})
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"contact": contact})
}
func (h *ContactHandler) Get(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
contactID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
contact, err := h.contactSvc.Get(r.Context(), userID, contactID)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"contact": contact})
}
func (h *ContactHandler) Update(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
contactID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
var req struct {
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
Email *string `json:"email"`
Phone *string `json:"phone"`
Company *string `json:"company"`
Notes *string `json:"notes"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
contact, err := h.contactSvc.Update(r.Context(), userID, contactID, service.UpdateContactRequest{
FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email,
Phone: req.Phone,
Company: req.Company,
Notes: req.Notes,
})
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"contact": contact})
}
func (h *ContactHandler) Delete(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
contactID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
if err := h.contactSvc.Delete(r.Context(), userID, contactID); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}

View File

@@ -0,0 +1,204 @@
package handlers
import (
"net/http"
"strconv"
"time"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type EventHandler struct {
eventSvc *service.EventService
}
func NewEventHandler(eventSvc *service.EventService) *EventHandler {
return &EventHandler{eventSvc: eventSvc}
}
func (h *EventHandler) List(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
q := r.URL.Query()
startStr := q.Get("start")
endStr := q.Get("end")
if startStr == "" || endStr == "" {
utils.WriteError(w, models.NewValidationError("start and end query params required"))
return
}
start, err := time.Parse(time.RFC3339, startStr)
if err != nil {
utils.WriteError(w, models.NewValidationError("invalid start time"))
return
}
end, err := time.Parse(time.RFC3339, endStr)
if err != nil {
utils.WriteError(w, models.NewValidationError("invalid end time"))
return
}
var calendarID *uuid.UUID
if cid := q.Get("calendar_id"); cid != "" {
id, err := utils.ValidateUUID(cid)
if err != nil {
utils.WriteError(w, err)
return
}
calendarID = &id
}
var search *string
if s := q.Get("search"); s != "" {
search = &s
}
var tag *string
if t := q.Get("tag"); t != "" {
tag = &t
}
limit := 50
if l := q.Get("limit"); l != "" {
if v, err := strconv.Atoi(l); err == nil {
limit = v
}
}
events, cursor, err := h.eventSvc.List(r.Context(), userID, service.ListEventParams{
RangeStart: start,
RangeEnd: end,
CalendarID: calendarID,
Search: search,
Tag: tag,
Limit: limit,
Cursor: q.Get("cursor"),
})
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteList(w, events, models.PageInfo{Limit: int(utils.ClampLimit(limit)), NextCursor: cursor})
}
func (h *EventHandler) Create(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
var req struct {
CalendarID uuid.UUID `json:"calendar_id"`
Title string `json:"title"`
Description *string `json:"description"`
Location *string `json:"location"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Timezone string `json:"timezone"`
AllDay bool `json:"all_day"`
RecurrenceRule *string `json:"recurrence_rule"`
Reminders []int32 `json:"reminders"`
Tags []string `json:"tags"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
event, err := h.eventSvc.Create(r.Context(), userID, service.CreateEventRequest{
CalendarID: req.CalendarID,
Title: req.Title,
Description: req.Description,
Location: req.Location,
StartTime: req.StartTime,
EndTime: req.EndTime,
Timezone: req.Timezone,
AllDay: req.AllDay,
RecurrenceRule: req.RecurrenceRule,
Reminders: req.Reminders,
Tags: req.Tags,
})
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"event": event})
}
func (h *EventHandler) Get(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
eventID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
event, err := h.eventSvc.Get(r.Context(), userID, eventID)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"event": event})
}
func (h *EventHandler) Update(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
eventID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
var req struct {
Title *string `json:"title"`
Description *string `json:"description"`
Location *string `json:"location"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
Timezone *string `json:"timezone"`
AllDay *bool `json:"all_day"`
RecurrenceRule *string `json:"recurrence_rule"`
Tags []string `json:"tags"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
event, err := h.eventSvc.Update(r.Context(), userID, eventID, service.UpdateEventRequest{
Title: req.Title,
Description: req.Description,
Location: req.Location,
StartTime: req.StartTime,
EndTime: req.EndTime,
Timezone: req.Timezone,
AllDay: req.AllDay,
RecurrenceRule: req.RecurrenceRule,
Tags: req.Tags,
})
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"event": event})
}
func (h *EventHandler) Delete(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
eventID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
if err := h.eventSvc.Delete(r.Context(), userID, eventID); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}

View File

@@ -0,0 +1,189 @@
package handlers
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/repository"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type ICSHandler struct {
calSvc *service.CalendarService
eventSvc *service.EventService
queries *repository.Queries
}
func NewICSHandler(calSvc *service.CalendarService, eventSvc *service.EventService, queries *repository.Queries) *ICSHandler {
return &ICSHandler{calSvc: calSvc, eventSvc: eventSvc, queries: queries}
}
func (h *ICSHandler) Export(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
if _, err := h.calSvc.GetRole(r.Context(), calID, userID); err != nil {
utils.WriteError(w, err)
return
}
now := time.Now().UTC()
rangeStart := now.AddDate(-1, 0, 0)
rangeEnd := now.AddDate(1, 0, 0)
events, err := h.queries.ListEventsByCalendarInRange(r.Context(), repository.ListEventsByCalendarInRangeParams{
CalendarID: utils.ToPgUUID(calID),
EndTime: utils.ToPgTimestamptz(rangeStart),
StartTime: utils.ToPgTimestamptz(rangeEnd),
})
if err != nil {
utils.WriteError(w, models.ErrInternal)
return
}
var b strings.Builder
b.WriteString("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//CalendarAPI//EN\r\n")
for _, ev := range events {
b.WriteString("BEGIN:VEVENT\r\n")
b.WriteString(fmt.Sprintf("UID:%s\r\n", utils.FromPgUUID(ev.ID).String()))
b.WriteString(fmt.Sprintf("DTSTART:%s\r\n", utils.FromPgTimestamptz(ev.StartTime).Format("20060102T150405Z")))
b.WriteString(fmt.Sprintf("DTEND:%s\r\n", utils.FromPgTimestamptz(ev.EndTime).Format("20060102T150405Z")))
b.WriteString(fmt.Sprintf("SUMMARY:%s\r\n", ev.Title))
if ev.Description.Valid {
b.WriteString(fmt.Sprintf("DESCRIPTION:%s\r\n", ev.Description.String))
}
if ev.Location.Valid {
b.WriteString(fmt.Sprintf("LOCATION:%s\r\n", ev.Location.String))
}
if ev.RecurrenceRule.Valid {
b.WriteString(fmt.Sprintf("RRULE:%s\r\n", ev.RecurrenceRule.String))
}
b.WriteString("END:VEVENT\r\n")
}
b.WriteString("END:VCALENDAR\r\n")
w.Header().Set("Content-Type", "text/calendar")
w.Header().Set("Content-Disposition", "attachment; filename=calendar.ics")
w.WriteHeader(http.StatusOK)
w.Write([]byte(b.String()))
}
func (h *ICSHandler) Import(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
if err := r.ParseMultipartForm(10 << 20); err != nil {
utils.WriteError(w, models.NewValidationError("invalid multipart form"))
return
}
calIDStr := r.FormValue("calendar_id")
calID, err := utils.ValidateUUID(calIDStr)
if err != nil {
utils.WriteError(w, err)
return
}
role, err := h.calSvc.GetRole(r.Context(), calID, userID)
if err != nil {
utils.WriteError(w, err)
return
}
if role != "owner" && role != "editor" {
utils.WriteError(w, models.ErrForbidden)
return
}
file, _, err := r.FormFile("file")
if err != nil {
utils.WriteError(w, models.NewValidationError("file required"))
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
utils.WriteError(w, models.ErrInternal)
return
}
count := h.parseAndImportICS(r.Context(), string(data), calID, userID)
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"imported": map[string]int{"events": count},
})
}
func (h *ICSHandler) parseAndImportICS(ctx context.Context, data string, calID, userID uuid.UUID) int {
count := 0
lines := strings.Split(data, "\n")
var inEvent bool
var title, description, location, rruleStr string
var dtstart, dtend time.Time
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "BEGIN:VEVENT" {
inEvent = true
title, description, location, rruleStr = "", "", "", ""
dtstart, dtend = time.Time{}, time.Time{}
continue
}
if line == "END:VEVENT" && inEvent {
inEvent = false
if title != "" && !dtstart.IsZero() && !dtend.IsZero() {
_, err := h.queries.CreateEvent(ctx, repository.CreateEventParams{
ID: utils.ToPgUUID(uuid.New()),
CalendarID: utils.ToPgUUID(calID),
Title: title,
Description: utils.ToPgText(description),
Location: utils.ToPgText(location),
StartTime: utils.ToPgTimestamptz(dtstart),
EndTime: utils.ToPgTimestamptz(dtend),
Timezone: "UTC",
RecurrenceRule: utils.ToPgText(rruleStr),
Tags: []string{},
CreatedBy: utils.ToPgUUID(userID),
UpdatedBy: utils.ToPgUUID(userID),
})
if err == nil {
count++
}
}
continue
}
if !inEvent {
continue
}
switch {
case strings.HasPrefix(line, "SUMMARY:"):
title = strings.TrimPrefix(line, "SUMMARY:")
case strings.HasPrefix(line, "DESCRIPTION:"):
description = strings.TrimPrefix(line, "DESCRIPTION:")
case strings.HasPrefix(line, "LOCATION:"):
location = strings.TrimPrefix(line, "LOCATION:")
case strings.HasPrefix(line, "RRULE:"):
rruleStr = strings.TrimPrefix(line, "RRULE:")
case strings.HasPrefix(line, "DTSTART:"):
dtstart, _ = time.Parse("20060102T150405Z", strings.TrimPrefix(line, "DTSTART:"))
case strings.HasPrefix(line, "DTEND:"):
dtend, _ = time.Parse("20060102T150405Z", strings.TrimPrefix(line, "DTEND:"))
}
}
return count
}

View File

@@ -0,0 +1,64 @@
package handlers
import (
"net/http"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
)
type ReminderHandler struct {
reminderSvc *service.ReminderService
}
func NewReminderHandler(reminderSvc *service.ReminderService) *ReminderHandler {
return &ReminderHandler{reminderSvc: reminderSvc}
}
func (h *ReminderHandler) Add(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
eventID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
var req struct {
MinutesBefore []int32 `json:"minutes_before"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
event, err := h.reminderSvc.AddReminders(r.Context(), userID, eventID, req.MinutesBefore)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"event": event})
}
func (h *ReminderHandler) Delete(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
eventID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
reminderID, err := utils.ValidateUUID(chi.URLParam(r, "reminderID"))
if err != nil {
utils.WriteError(w, err)
return
}
if err := h.reminderSvc.DeleteReminder(r.Context(), userID, eventID, reminderID); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}

View File

@@ -0,0 +1,84 @@
package handlers
import (
"net/http"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
"github.com/go-chi/chi/v5"
)
type SharingHandler struct {
calSvc *service.CalendarService
}
func NewSharingHandler(calSvc *service.CalendarService) *SharingHandler {
return &SharingHandler{calSvc: calSvc}
}
func (h *SharingHandler) Share(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
var req struct {
Target struct {
Email string `json:"email"`
} `json:"target"`
Role string `json:"role"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
if err := h.calSvc.Share(r.Context(), userID, calID, req.Target.Email, req.Role); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}
func (h *SharingHandler) ListMembers(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
members, err := h.calSvc.ListMembers(r.Context(), userID, calID)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteList(w, members, models.PageInfo{Limit: utils.DefaultLimit})
}
func (h *SharingHandler) RemoveMember(w http.ResponseWriter, r *http.Request) {
userID, _ := middleware.GetUserID(r.Context())
calID, err := utils.ValidateUUID(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, err)
return
}
targetID, err := utils.ValidateUUID(chi.URLParam(r, "userID"))
if err != nil {
utils.WriteError(w, err)
return
}
if err := h.calSvc.RemoveMember(r.Context(), userID, calID, targetID); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}

View File

@@ -0,0 +1,73 @@
package handlers
import (
"net/http"
"github.com/calendarapi/internal/middleware"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/service"
"github.com/calendarapi/internal/utils"
)
type UserHandler struct {
userSvc *service.UserService
}
func NewUserHandler(userSvc *service.UserService) *UserHandler {
return &UserHandler{userSvc: userSvc}
}
func (h *UserHandler) GetMe(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserID(r.Context())
if !ok {
utils.WriteError(w, models.ErrAuthRequired)
return
}
user, err := h.userSvc.GetMe(r.Context(), userID)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"user": user})
}
func (h *UserHandler) UpdateMe(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserID(r.Context())
if !ok {
utils.WriteError(w, models.ErrAuthRequired)
return
}
var req struct {
Timezone *string `json:"timezone"`
}
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, err)
return
}
user, err := h.userSvc.Update(r.Context(), userID, req.Timezone)
if err != nil {
utils.WriteError(w, err)
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{"user": user})
}
func (h *UserHandler) DeleteMe(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserID(r.Context())
if !ok {
utils.WriteError(w, models.ErrAuthRequired)
return
}
if err := h.userSvc.Delete(r.Context(), userID); err != nil {
utils.WriteError(w, err)
return
}
utils.WriteOK(w)
}