Add OpenAPI docs, frontend, migrations, and API updates

- 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
This commit is contained in:
Michilis
2026-03-02 14:07:55 +00:00
parent 2cb9d72a7f
commit 75105b8b46
8120 changed files with 1486881 additions and 314 deletions

View File

@@ -217,40 +217,76 @@ func hashToken(token string) string {
func userFromCreateRow(u repository.CreateUserRow) models.User {
return models.User{
ID: utils.FromPgUUID(u.ID),
Email: u.Email,
Timezone: u.Timezone,
CreatedAt: utils.FromPgTimestamptz(u.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(u.UpdatedAt),
ID: utils.FromPgUUID(u.ID),
Email: u.Email,
Timezone: u.Timezone,
WeekStartDay: 0,
DateFormat: "MM/dd/yyyy",
TimeFormat: "12h",
DefaultEventDurationMinutes: 60,
DefaultReminderMinutes: 10,
ShowWeekends: true,
WorkingHoursStart: "09:00",
WorkingHoursEnd: "17:00",
NotificationsEmail: true,
CreatedAt: utils.FromPgTimestamptz(u.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(u.UpdatedAt),
}
}
func userFromEmailRow(u repository.GetUserByEmailRow) models.User {
return models.User{
ID: utils.FromPgUUID(u.ID),
Email: u.Email,
Timezone: u.Timezone,
CreatedAt: utils.FromPgTimestamptz(u.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(u.UpdatedAt),
ID: utils.FromPgUUID(u.ID),
Email: u.Email,
Timezone: u.Timezone,
WeekStartDay: int(u.WeekStartDay),
DateFormat: u.DateFormat,
TimeFormat: u.TimeFormat,
DefaultEventDurationMinutes: int(u.DefaultEventDurationMinutes),
DefaultReminderMinutes: int(u.DefaultReminderMinutes),
ShowWeekends: u.ShowWeekends,
WorkingHoursStart: u.WorkingHoursStart,
WorkingHoursEnd: u.WorkingHoursEnd,
NotificationsEmail: u.NotificationsEmail,
CreatedAt: utils.FromPgTimestamptz(u.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(u.UpdatedAt),
}
}
func userFromIDRow(u repository.GetUserByIDRow) models.User {
return models.User{
ID: utils.FromPgUUID(u.ID),
Email: u.Email,
Timezone: u.Timezone,
CreatedAt: utils.FromPgTimestamptz(u.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(u.UpdatedAt),
ID: utils.FromPgUUID(u.ID),
Email: u.Email,
Timezone: u.Timezone,
WeekStartDay: int(u.WeekStartDay),
DateFormat: u.DateFormat,
TimeFormat: u.TimeFormat,
DefaultEventDurationMinutes: int(u.DefaultEventDurationMinutes),
DefaultReminderMinutes: int(u.DefaultReminderMinutes),
ShowWeekends: u.ShowWeekends,
WorkingHoursStart: u.WorkingHoursStart,
WorkingHoursEnd: u.WorkingHoursEnd,
NotificationsEmail: u.NotificationsEmail,
CreatedAt: utils.FromPgTimestamptz(u.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(u.UpdatedAt),
}
}
func userFromUpdateRow(u repository.UpdateUserRow) models.User {
return models.User{
ID: utils.FromPgUUID(u.ID),
Email: u.Email,
Timezone: u.Timezone,
CreatedAt: utils.FromPgTimestamptz(u.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(u.UpdatedAt),
ID: utils.FromPgUUID(u.ID),
Email: u.Email,
Timezone: u.Timezone,
WeekStartDay: int(u.WeekStartDay),
DateFormat: u.DateFormat,
TimeFormat: u.TimeFormat,
DefaultEventDurationMinutes: int(u.DefaultEventDurationMinutes),
DefaultReminderMinutes: int(u.DefaultReminderMinutes),
ShowWeekends: u.ShowWeekends,
WorkingHoursStart: u.WorkingHoursStart,
WorkingHoursEnd: u.WorkingHoursEnd,
NotificationsEmail: u.NotificationsEmail,
CreatedAt: utils.FromPgTimestamptz(u.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(u.UpdatedAt),
}
}

View File

@@ -3,12 +3,15 @@ package service
import (
"context"
"sort"
"strings"
"time"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/repository"
"github.com/calendarapi/internal/utils"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
type AvailabilityService struct {
@@ -84,3 +87,142 @@ func (s *AvailabilityService) GetBusyBlocks(ctx context.Context, userID uuid.UUI
Busy: busy,
}, nil
}
// getBusyBlocksForCalendar
func (s *AvailabilityService) getBusyBlocksForCalendar(ctx context.Context, calID uuid.UUID, rangeStart, rangeEnd time.Time) ([]models.BusyBlock, error) {
pgCalID := utils.ToPgUUID(calID)
events, err := s.queries.ListEventsByCalendarInRange(ctx, repository.ListEventsByCalendarInRangeParams{
CalendarID: pgCalID,
EndTime: utils.ToPgTimestamptz(rangeStart),
StartTime: utils.ToPgTimestamptz(rangeEnd),
})
if err != nil {
return nil, err
}
recurring, err := s.queries.ListRecurringEventsByCalendar(ctx, repository.ListRecurringEventsByCalendarParams{
CalendarID: pgCalID,
StartTime: utils.ToPgTimestamptz(rangeEnd),
})
if err != nil {
return nil, err
}
var busy []models.BusyBlock
for _, ev := range events {
if ev.RecurrenceRule.Valid {
continue
}
busy = append(busy, models.BusyBlock{
Start: utils.FromPgTimestamptz(ev.StartTime),
End: utils.FromPgTimestamptz(ev.EndTime),
EventID: utils.FromPgUUID(ev.ID),
})
}
for _, ev := range recurring {
occs := s.event.expandRecurrence(ev, rangeStart, rangeEnd)
for _, occ := range occs {
if occ.OccurrenceStartTime != nil {
busy = append(busy, models.BusyBlock{
Start: *occ.OccurrenceStartTime,
End: *occ.OccurrenceEndTime,
EventID: occ.ID,
})
}
}
}
return busy, nil
}
// GetBusyBlocksByTokenPublic returns busy blocks for a calendar by its public token. No auth required.
func (s *AvailabilityService) GetBusyBlocksByTokenPublic(ctx context.Context, token string, rangeStart, rangeEnd time.Time) (*models.AvailabilityResponse, error) {
if token == "" {
return nil, models.ErrNotFound
}
cal, err := s.queries.GetCalendarByToken(ctx, pgtype.Text{String: token, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
return nil, models.ErrNotFound
}
return nil, models.ErrInternal
}
calID := utils.FromPgUUID(cal.ID)
busy, err := s.getBusyBlocksForCalendar(ctx, calID, rangeStart, rangeEnd)
if err != nil {
return nil, models.ErrInternal
}
sort.Slice(busy, func(i, j int) bool {
return busy[i].Start.Before(busy[j].Start)
})
if busy == nil {
busy = []models.BusyBlock{}
}
return &models.AvailabilityResponse{
CalendarID: calID,
RangeStart: rangeStart,
RangeEnd: rangeEnd,
Busy: busy,
}, nil
}
// GetBusyBlocksAggregate returns merged busy blocks across multiple calendars by their tokens. No auth required.
func (s *AvailabilityService) GetBusyBlocksAggregate(ctx context.Context, tokens []string, rangeStart, rangeEnd time.Time) (*models.AvailabilityResponse, error) {
if len(tokens) == 0 {
return &models.AvailabilityResponse{
RangeStart: rangeStart,
RangeEnd: rangeEnd,
Busy: []models.BusyBlock{},
}, nil
}
seen := make(map[uuid.UUID]bool)
var allBusy []models.BusyBlock
for _, token := range tokens {
token = strings.TrimSpace(token)
if token == "" {
continue
}
cal, err := s.queries.GetCalendarByToken(ctx, pgtype.Text{String: token, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
continue
}
return nil, models.ErrInternal
}
calID := utils.FromPgUUID(cal.ID)
if seen[calID] {
continue
}
seen[calID] = true
busy, err := s.getBusyBlocksForCalendar(ctx, calID, rangeStart, rangeEnd)
if err != nil {
return nil, models.ErrInternal
}
allBusy = append(allBusy, busy...)
}
sort.Slice(allBusy, func(i, j int) bool {
return allBusy[i].Start.Before(allBusy[j].Start)
})
if allBusy == nil {
allBusy = []models.BusyBlock{}
}
return &models.AvailabilityResponse{
RangeStart: rangeStart,
RangeEnd: rangeEnd,
Busy: allBusy,
}, nil
}

View File

@@ -91,52 +91,64 @@ func (s *BookingService) GetAvailability(ctx context.Context, token string, rang
loc = time.UTC
}
calID := utils.FromPgUUID(bl.CalendarID)
events, err := s.queries.ListEventsByCalendarInRange(ctx, repository.ListEventsByCalendarInRangeParams{
CalendarID: bl.CalendarID,
EndTime: utils.ToPgTimestamptz(rangeStart),
StartTime: utils.ToPgTimestamptz(rangeEnd),
})
// Get owner to fetch all calendars that count for availability
cal, err := s.queries.GetCalendarByID(ctx, bl.CalendarID)
if err != nil {
return nil, models.ErrInternal
}
ownerID := cal.OwnerID
recurring, err := s.queries.ListRecurringEventsByCalendar(ctx, repository.ListRecurringEventsByCalendarParams{
CalendarID: bl.CalendarID,
StartTime: utils.ToPgTimestamptz(rangeEnd),
})
// Include busy blocks from ALL calendars owned by the user that have count_for_availability=true
availCals, err := s.queries.ListCalendarsByOwnerForAvailability(ctx, ownerID)
if err != nil {
return nil, models.ErrInternal
}
var busyBlocks []models.TimeSlot
for _, ev := range events {
if ev.RecurrenceRule.Valid {
buf := time.Duration(bl.BufferMinutes) * time.Minute
for _, ac := range availCals {
events, err := s.queries.ListEventsByCalendarInRange(ctx, repository.ListEventsByCalendarInRangeParams{
CalendarID: ac.ID,
EndTime: utils.ToPgTimestamptz(rangeStart),
StartTime: utils.ToPgTimestamptz(rangeEnd),
})
if err != nil {
continue
}
start := utils.FromPgTimestamptz(ev.StartTime)
end := utils.FromPgTimestamptz(ev.EndTime)
buf := time.Duration(bl.BufferMinutes) * time.Minute
busyBlocks = append(busyBlocks, models.TimeSlot{
Start: start.Add(-buf),
End: end.Add(buf),
})
}
for _, ev := range recurring {
occs := s.event.expandRecurrence(ev, rangeStart, rangeEnd)
for _, occ := range occs {
if occ.OccurrenceStartTime != nil {
buf := time.Duration(bl.BufferMinutes) * time.Minute
busyBlocks = append(busyBlocks, models.TimeSlot{
Start: occ.OccurrenceStartTime.Add(-buf),
End: occ.OccurrenceEndTime.Add(buf),
})
recurring, err := s.queries.ListRecurringEventsByCalendar(ctx, repository.ListRecurringEventsByCalendarParams{
CalendarID: ac.ID,
StartTime: utils.ToPgTimestamptz(rangeEnd),
})
if err != nil {
continue
}
for _, ev := range events {
if ev.RecurrenceRule.Valid {
continue
}
start := utils.FromPgTimestamptz(ev.StartTime)
end := utils.FromPgTimestamptz(ev.EndTime)
busyBlocks = append(busyBlocks, models.TimeSlot{
Start: start.Add(-buf),
End: end.Add(buf),
})
}
for _, ev := range recurring {
occs := s.event.expandRecurrence(ev, rangeStart, rangeEnd)
for _, occ := range occs {
if occ.OccurrenceStartTime != nil {
busyBlocks = append(busyBlocks, models.TimeSlot{
Start: occ.OccurrenceStartTime.Add(-buf),
End: occ.OccurrenceEndTime.Add(buf),
})
}
}
}
}
_ = calID
duration := time.Duration(bl.DurationMinutes) * time.Minute
dayNames := []string{"sun", "mon", "tue", "wed", "thu", "fri", "sat"}

View File

@@ -3,7 +3,9 @@ package service
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/calendarapi/internal/models"
@@ -34,6 +36,15 @@ func generatePublicToken() (string, error) {
return base64.RawURLEncoding.EncodeToString(b), nil
}
func generatePrivateToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
hash := sha256.Sum256(b)
return hex.EncodeToString(hash[:]), nil
}
func (s *CalendarService) icalURL(token string) string {
if token == "" {
return ""
@@ -41,6 +52,13 @@ func (s *CalendarService) icalURL(token string) string {
return fmt.Sprintf("%s/cal/%s/feed.ics", s.baseURL, token)
}
func (s *CalendarService) availabilityURL(token string) string {
if token == "" {
return ""
}
return fmt.Sprintf("%s/availability/%s", s.baseURL, token)
}
func (s *CalendarService) Create(ctx context.Context, userID uuid.UUID, name, color string) (*models.Calendar, error) {
if err := utils.ValidateCalendarName(name); err != nil {
return nil, err
@@ -80,21 +98,41 @@ func (s *CalendarService) Create(ctx context.Context, userID uuid.UUID, name, co
return nil, models.ErrInternal
}
privateToken, err := generatePrivateToken()
if err != nil {
return nil, models.ErrInternal
}
if err := qtx.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
ID: utils.ToPgUUID(calID),
PublicToken: pgtype.Text{String: privateToken, Valid: true},
}); err != nil {
return nil, models.ErrInternal
}
if err := tx.Commit(ctx); err != nil {
return nil, models.ErrInternal
}
s.audit.Log(ctx, "calendar", calID, "CREATE_CALENDAR", userID)
defRem := (*int)(nil)
if cal.DefaultReminderMinutes.Valid {
v := int(cal.DefaultReminderMinutes.Int32)
defRem = &v
}
return &models.Calendar{
ID: utils.FromPgUUID(cal.ID),
Name: cal.Name,
Color: cal.Color,
IsPublic: cal.IsPublic,
ICalURL: s.icalURL(utils.FromPgTextValue(cal.PublicToken)),
Role: "owner",
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
ID: utils.FromPgUUID(cal.ID),
Name: cal.Name,
Color: cal.Color,
IsPublic: cal.IsPublic,
CountForAvailability: cal.CountForAvailability,
DefaultReminderMinutes: defRem,
SortOrder: int(cal.SortOrder),
ICalURL: s.icalURL(privateToken),
AvailabilityURL: s.availabilityURL(privateToken),
Role: "owner",
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
}, nil
}
@@ -106,24 +144,31 @@ func (s *CalendarService) List(ctx context.Context, userID uuid.UUID) ([]models.
calendars := make([]models.Calendar, len(rows))
for i, r := range rows {
defRem := (*int)(nil)
if r.DefaultReminderMinutes.Valid {
v := int(r.DefaultReminderMinutes.Int32)
defRem = &v
}
calendars[i] = models.Calendar{
ID: utils.FromPgUUID(r.ID),
Name: r.Name,
Color: r.Color,
IsPublic: r.IsPublic,
Role: r.Role,
CreatedAt: utils.FromPgTimestamptz(r.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(r.UpdatedAt),
ID: utils.FromPgUUID(r.ID),
Name: r.Name,
Color: r.Color,
IsPublic: r.IsPublic,
CountForAvailability: r.CountForAvailability,
DefaultReminderMinutes: defRem,
SortOrder: int(r.SortOrder),
Role: r.Role,
CreatedAt: utils.FromPgTimestamptz(r.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(r.UpdatedAt),
}
}
// Populate ICalURL for public calendars (requires a separate lookup since ListCalendarsByUser doesn't select public_token)
// Populate ICalURL and AvailabilityURL (requires separate lookup since ListCalendarsByUser doesn't select public_token)
for i := range calendars {
if calendars[i].IsPublic {
cal, err := s.queries.GetCalendarByID(ctx, utils.ToPgUUID(calendars[i].ID))
if err == nil && cal.PublicToken.Valid {
calendars[i].ICalURL = s.icalURL(cal.PublicToken.String)
}
cal, err := s.queries.GetCalendarByID(ctx, utils.ToPgUUID(calendars[i].ID))
if err == nil && cal.PublicToken.Valid {
calendars[i].ICalURL = s.icalURL(cal.PublicToken.String)
calendars[i].AvailabilityURL = s.availabilityURL(cal.PublicToken.String)
}
}
@@ -144,19 +189,40 @@ func (s *CalendarService) Get(ctx context.Context, userID uuid.UUID, calID uuid.
return nil, models.ErrInternal
}
// Backfill private token for existing calendars that don't have one
if !cal.IsPublic && !cal.PublicToken.Valid {
privateToken, err := generatePrivateToken()
if err == nil {
_ = s.queries.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
ID: utils.ToPgUUID(calID),
PublicToken: pgtype.Text{String: privateToken, Valid: true},
})
cal.PublicToken = pgtype.Text{String: privateToken, Valid: true}
}
}
defRem := (*int)(nil)
if cal.DefaultReminderMinutes.Valid {
v := int(cal.DefaultReminderMinutes.Int32)
defRem = &v
}
return &models.Calendar{
ID: utils.FromPgUUID(cal.ID),
Name: cal.Name,
Color: cal.Color,
IsPublic: cal.IsPublic,
ICalURL: s.icalURL(utils.FromPgTextValue(cal.PublicToken)),
Role: role,
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
ID: utils.FromPgUUID(cal.ID),
Name: cal.Name,
Color: cal.Color,
IsPublic: cal.IsPublic,
CountForAvailability: cal.CountForAvailability,
DefaultReminderMinutes: defRem,
SortOrder: int(cal.SortOrder),
ICalURL: s.icalURL(utils.FromPgTextValue(cal.PublicToken)),
AvailabilityURL: s.availabilityURL(utils.FromPgTextValue(cal.PublicToken)),
Role: role,
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
}, nil
}
func (s *CalendarService) Update(ctx context.Context, userID uuid.UUID, calID uuid.UUID, name, color *string, isPublic *bool) (*models.Calendar, error) {
func (s *CalendarService) Update(ctx context.Context, userID uuid.UUID, calID uuid.UUID, name, color *string, isPublic *bool, countForAvailability *bool, defaultReminderMinutes *int, sortOrder *int) (*models.Calendar, error) {
role, err := s.getRole(ctx, calID, userID)
if err != nil {
return nil, err
@@ -184,12 +250,28 @@ func (s *CalendarService) Update(ctx context.Context, userID uuid.UUID, calID uu
if isPublic != nil {
pgPublic = pgtype.Bool{Bool: *isPublic, Valid: true}
}
var pgCountForAvail pgtype.Bool
if countForAvailability != nil {
pgCountForAvail = pgtype.Bool{Bool: *countForAvailability, Valid: true}
}
var pgDefRem pgtype.Int4
if defaultReminderMinutes != nil {
pgDefRem = pgtype.Int4{Int32: int32(*defaultReminderMinutes), Valid: true}
}
var pgSort pgtype.Int4
if sortOrder != nil {
pgSort = pgtype.Int4{Int32: int32(*sortOrder), Valid: true}
}
cal, err := s.queries.UpdateCalendar(ctx, repository.UpdateCalendarParams{
ID: utils.ToPgUUID(calID),
Name: utils.ToPgTextPtr(name),
Color: utils.ToPgTextPtr(color),
IsPublic: pgPublic,
ID: utils.ToPgUUID(calID),
Name: utils.ToPgTextPtr(name),
Color: utils.ToPgTextPtr(color),
IsPublic: pgPublic,
CountForAvailability: pgCountForAvail,
DefaultReminderMinutes: pgDefRem,
SortOrder: pgSort,
})
if err != nil {
if err == pgx.ErrNoRows {
@@ -199,7 +281,8 @@ func (s *CalendarService) Update(ctx context.Context, userID uuid.UUID, calID uu
}
if isPublic != nil {
if *isPublic && !cal.PublicToken.Valid {
if *isPublic {
// Switching to public: use base64url token
token, err := generatePublicToken()
if err != nil {
return nil, models.ErrInternal
@@ -209,26 +292,50 @@ func (s *CalendarService) Update(ctx context.Context, userID uuid.UUID, calID uu
PublicToken: pgtype.Text{String: token, Valid: true},
})
cal.PublicToken = pgtype.Text{String: token, Valid: true}
} else if !*isPublic && cal.PublicToken.Valid {
} else {
// Switching to private: use SHA256 token (replace existing token)
token, err := generatePrivateToken()
if err != nil {
return nil, models.ErrInternal
}
_ = s.queries.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
ID: utils.ToPgUUID(calID),
PublicToken: pgtype.Text{Valid: false},
PublicToken: pgtype.Text{String: token, Valid: true},
})
cal.PublicToken = pgtype.Text{Valid: false}
cal.PublicToken = pgtype.Text{String: token, Valid: true}
}
} else if !cal.IsPublic && !cal.PublicToken.Valid {
// Backfill: private calendar with no token (e.g. from before this feature)
privateToken, err := generatePrivateToken()
if err == nil {
_ = s.queries.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
ID: utils.ToPgUUID(calID),
PublicToken: pgtype.Text{String: privateToken, Valid: true},
})
cal.PublicToken = pgtype.Text{String: privateToken, Valid: true}
}
}
s.audit.Log(ctx, "calendar", calID, "UPDATE_CALENDAR", userID)
defRem := (*int)(nil)
if cal.DefaultReminderMinutes.Valid {
v := int(cal.DefaultReminderMinutes.Int32)
defRem = &v
}
return &models.Calendar{
ID: utils.FromPgUUID(cal.ID),
Name: cal.Name,
Color: cal.Color,
IsPublic: cal.IsPublic,
ICalURL: s.icalURL(utils.FromPgTextValue(cal.PublicToken)),
Role: role,
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
ID: utils.FromPgUUID(cal.ID),
Name: cal.Name,
Color: cal.Color,
IsPublic: cal.IsPublic,
CountForAvailability: cal.CountForAvailability,
DefaultReminderMinutes: defRem,
SortOrder: int(cal.SortOrder),
ICalURL: s.icalURL(utils.FromPgTextValue(cal.PublicToken)),
AvailabilityURL: s.availabilityURL(utils.FromPgTextValue(cal.PublicToken)),
Role: role,
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
}, nil
}

View File

@@ -414,8 +414,8 @@ func (s *EventService) expandRecurrence(ev repository.Event, rangeStart, rangeEn
Title: ev.Title,
Description: utils.FromPgText(ev.Description),
Location: utils.FromPgText(ev.Location),
StartTime: dtStart,
EndTime: dtStart.Add(duration),
StartTime: occStart,
EndTime: occEnd,
Timezone: ev.Timezone,
AllDay: ev.AllDay,
RecurrenceRule: utils.FromPgText(ev.RecurrenceRule),

View File

@@ -33,16 +33,41 @@ func (s *UserService) GetMe(ctx context.Context, userID uuid.UUID) (*models.User
return &user, nil
}
func (s *UserService) Update(ctx context.Context, userID uuid.UUID, timezone *string) (*models.User, error) {
if timezone != nil {
if err := utils.ValidateTimezone(*timezone); err != nil {
type UserUpdateInput struct {
Timezone *string
WeekStartDay *int
DateFormat *string
TimeFormat *string
DefaultEventDurationMinutes *int
DefaultReminderMinutes *int
ShowWeekends *bool
WorkingHoursStart *string
WorkingHoursEnd *string
NotificationsEmail *bool
}
func (s *UserService) Update(ctx context.Context, userID uuid.UUID, in *UserUpdateInput) (*models.User, error) {
if in == nil {
in = &UserUpdateInput{}
}
if in.Timezone != nil {
if err := utils.ValidateTimezone(*in.Timezone); err != nil {
return nil, err
}
}
u, err := s.queries.UpdateUser(ctx, repository.UpdateUserParams{
ID: utils.ToPgUUID(userID),
Timezone: utils.ToPgTextPtr(timezone),
ID: utils.ToPgUUID(userID),
Timezone: utils.ToPgTextPtr(in.Timezone),
WeekStartDay: utils.ToPgInt2Ptr(in.WeekStartDay),
DateFormat: utils.ToPgTextPtr(in.DateFormat),
TimeFormat: utils.ToPgTextPtr(in.TimeFormat),
DefaultEventDurationMinutes: utils.ToPgInt4Ptr(in.DefaultEventDurationMinutes),
DefaultReminderMinutes: utils.ToPgInt4Ptr(in.DefaultReminderMinutes),
ShowWeekends: utils.ToPgBoolPtr(in.ShowWeekends),
WorkingHoursStart: utils.ToPgTextPtr(in.WorkingHoursStart),
WorkingHoursEnd: utils.ToPgTextPtr(in.WorkingHoursEnd),
NotificationsEmail: utils.ToPgBoolPtr(in.NotificationsEmail),
})
if err != nil {
if err == pgx.ErrNoRows {