319 lines
8.1 KiB
Go
319 lines
8.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
|
|
"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"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type CalendarService struct {
|
|
pool *pgxpool.Pool
|
|
queries *repository.Queries
|
|
audit *AuditService
|
|
}
|
|
|
|
func NewCalendarService(pool *pgxpool.Pool, queries *repository.Queries, audit *AuditService) *CalendarService {
|
|
return &CalendarService{pool: pool, queries: queries, audit: audit}
|
|
}
|
|
|
|
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
|
|
}
|
|
if err := utils.ValidateColor(color); err != nil {
|
|
return nil, err
|
|
}
|
|
if color == "" {
|
|
color = "#3B82F6"
|
|
}
|
|
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return nil, models.ErrInternal
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
qtx := s.queries.WithTx(tx)
|
|
|
|
calID := uuid.New()
|
|
cal, err := qtx.CreateCalendar(ctx, repository.CreateCalendarParams{
|
|
ID: utils.ToPgUUID(calID),
|
|
OwnerID: utils.ToPgUUID(userID),
|
|
Name: name,
|
|
Color: color,
|
|
IsPublic: false,
|
|
})
|
|
if err != nil {
|
|
return nil, models.ErrInternal
|
|
}
|
|
|
|
err = qtx.UpsertCalendarMember(ctx, repository.UpsertCalendarMemberParams{
|
|
CalendarID: utils.ToPgUUID(calID),
|
|
UserID: utils.ToPgUUID(userID),
|
|
Role: "owner",
|
|
})
|
|
if 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)
|
|
|
|
return &models.Calendar{
|
|
ID: utils.FromPgUUID(cal.ID),
|
|
Name: cal.Name,
|
|
Color: cal.Color,
|
|
IsPublic: cal.IsPublic,
|
|
Role: "owner",
|
|
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
|
|
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
|
|
}, nil
|
|
}
|
|
|
|
func (s *CalendarService) List(ctx context.Context, userID uuid.UUID) ([]models.Calendar, error) {
|
|
rows, err := s.queries.ListCalendarsByUser(ctx, utils.ToPgUUID(userID))
|
|
if err != nil {
|
|
return nil, models.ErrInternal
|
|
}
|
|
|
|
calendars := make([]models.Calendar, len(rows))
|
|
for i, r := range rows {
|
|
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),
|
|
}
|
|
}
|
|
return calendars, nil
|
|
}
|
|
|
|
func (s *CalendarService) Get(ctx context.Context, userID uuid.UUID, calID uuid.UUID) (*models.Calendar, error) {
|
|
role, err := s.getRole(ctx, calID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cal, err := s.queries.GetCalendarByID(ctx, utils.ToPgUUID(calID))
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return nil, models.ErrNotFound
|
|
}
|
|
return nil, models.ErrInternal
|
|
}
|
|
|
|
return &models.Calendar{
|
|
ID: utils.FromPgUUID(cal.ID),
|
|
Name: cal.Name,
|
|
Color: cal.Color,
|
|
IsPublic: cal.IsPublic,
|
|
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) {
|
|
role, err := s.getRole(ctx, calID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if role != "owner" && role != "editor" {
|
|
return nil, models.ErrForbidden
|
|
}
|
|
if isPublic != nil && role != "owner" {
|
|
return nil, models.NewForbiddenError("only owner can change is_public")
|
|
}
|
|
|
|
if name != nil {
|
|
if err := utils.ValidateCalendarName(*name); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if color != nil {
|
|
if err := utils.ValidateColor(*color); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var pgPublic pgtype.Bool
|
|
if isPublic != nil {
|
|
pgPublic = pgtype.Bool{Bool: *isPublic, Valid: true}
|
|
}
|
|
|
|
cal, err := s.queries.UpdateCalendar(ctx, repository.UpdateCalendarParams{
|
|
ID: utils.ToPgUUID(calID),
|
|
Name: utils.ToPgTextPtr(name),
|
|
Color: utils.ToPgTextPtr(color),
|
|
IsPublic: pgPublic,
|
|
})
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return nil, models.ErrNotFound
|
|
}
|
|
return nil, models.ErrInternal
|
|
}
|
|
|
|
s.audit.Log(ctx, "calendar", calID, "UPDATE_CALENDAR", userID)
|
|
|
|
return &models.Calendar{
|
|
ID: utils.FromPgUUID(cal.ID),
|
|
Name: cal.Name,
|
|
Color: cal.Color,
|
|
IsPublic: cal.IsPublic,
|
|
Role: role,
|
|
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
|
|
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
|
|
}, nil
|
|
}
|
|
|
|
func (s *CalendarService) Delete(ctx context.Context, userID uuid.UUID, calID uuid.UUID) error {
|
|
role, err := s.getRole(ctx, calID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if role != "owner" {
|
|
return models.ErrForbidden
|
|
}
|
|
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return models.ErrInternal
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
qtx := s.queries.WithTx(tx)
|
|
|
|
pgCalID := utils.ToPgUUID(calID)
|
|
if err := qtx.SoftDeleteEventsByCalendar(ctx, pgCalID); err != nil {
|
|
return models.ErrInternal
|
|
}
|
|
if err := qtx.SoftDeleteCalendar(ctx, pgCalID); err != nil {
|
|
return models.ErrInternal
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return models.ErrInternal
|
|
}
|
|
|
|
s.audit.Log(ctx, "calendar", calID, "DELETE_CALENDAR", userID)
|
|
return nil
|
|
}
|
|
|
|
func (s *CalendarService) Share(ctx context.Context, ownerID uuid.UUID, calID uuid.UUID, targetEmail, role string) error {
|
|
ownerRole, err := s.getRole(ctx, calID, ownerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ownerRole != "owner" {
|
|
return models.ErrForbidden
|
|
}
|
|
if role != "editor" && role != "viewer" {
|
|
return models.NewValidationError("role must be editor or viewer")
|
|
}
|
|
|
|
targetUser, err := s.queries.GetUserByEmail(ctx, utils.NormalizeEmail(targetEmail))
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return models.NewNotFoundError("user not found")
|
|
}
|
|
return models.ErrInternal
|
|
}
|
|
|
|
targetID := utils.FromPgUUID(targetUser.ID)
|
|
if targetID == ownerID {
|
|
return models.NewValidationError("cannot share with yourself")
|
|
}
|
|
|
|
err = s.queries.UpsertCalendarMember(ctx, repository.UpsertCalendarMemberParams{
|
|
CalendarID: utils.ToPgUUID(calID),
|
|
UserID: utils.ToPgUUID(targetID),
|
|
Role: role,
|
|
})
|
|
if err != nil {
|
|
return models.ErrInternal
|
|
}
|
|
|
|
s.audit.Log(ctx, "calendar", calID, "SHARE_CALENDAR", ownerID)
|
|
return nil
|
|
}
|
|
|
|
func (s *CalendarService) ListMembers(ctx context.Context, userID uuid.UUID, calID uuid.UUID) ([]models.CalendarMember, error) {
|
|
if _, err := s.getRole(ctx, calID, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rows, err := s.queries.ListCalendarMembers(ctx, utils.ToPgUUID(calID))
|
|
if err != nil {
|
|
return nil, models.ErrInternal
|
|
}
|
|
|
|
members := make([]models.CalendarMember, len(rows))
|
|
for i, r := range rows {
|
|
members[i] = models.CalendarMember{
|
|
UserID: utils.FromPgUUID(r.UserID),
|
|
Email: r.Email,
|
|
Role: r.Role,
|
|
}
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
func (s *CalendarService) RemoveMember(ctx context.Context, ownerID uuid.UUID, calID uuid.UUID, targetUserID uuid.UUID) error {
|
|
role, err := s.getRole(ctx, calID, ownerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if role != "owner" {
|
|
return models.ErrForbidden
|
|
}
|
|
|
|
targetRole, err := s.getRole(ctx, calID, targetUserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if targetRole == "owner" {
|
|
return models.NewValidationError("cannot remove owner")
|
|
}
|
|
|
|
err = s.queries.DeleteCalendarMember(ctx, repository.DeleteCalendarMemberParams{
|
|
CalendarID: utils.ToPgUUID(calID),
|
|
UserID: utils.ToPgUUID(targetUserID),
|
|
})
|
|
if err != nil {
|
|
return models.ErrInternal
|
|
}
|
|
|
|
s.audit.Log(ctx, "calendar", calID, "REMOVE_MEMBER", ownerID)
|
|
return nil
|
|
}
|
|
|
|
func (s *CalendarService) GetRole(ctx context.Context, calID uuid.UUID, userID uuid.UUID) (string, error) {
|
|
return s.getRole(ctx, calID, userID)
|
|
}
|
|
|
|
func (s *CalendarService) getRole(ctx context.Context, calID uuid.UUID, userID uuid.UUID) (string, error) {
|
|
role, err := s.queries.GetCalendarMemberRole(ctx, repository.GetCalendarMemberRoleParams{
|
|
CalendarID: utils.ToPgUUID(calID),
|
|
UserID: utils.ToPgUUID(userID),
|
|
})
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return "", models.ErrNotFound
|
|
}
|
|
return "", models.ErrInternal
|
|
}
|
|
return role, nil
|
|
}
|