PhoenixKitCalendar.Events (PhoenixKitCalendar v0.1.0)

Copy Markdown View Source

Context for personal calendar events — CRUD with scope-based authorization built into every function.

Authorization model

Every function takes the caller's PhoenixKit.Users.Auth.Scope and authorizes against the TARGET calendar's owner:

Caller vs targetNeeds
Own calendarcalendar
Someone else's, readingcalendar.view_others (or edit_others, which implies view)
Someone else's, writingcalendar.edit_others

All checks go through Scope.can?/2, so they also require the calendar module to be enabled — a stale scope can't keep operating after the module is switched off.

Two invariants hold regardless of the calling UI:

  • owner_uuid is never taken from user-supplied attrs. Creation takes it as an explicit, separately-authorized argument; the schema doesn't cast it; updates can't move an event to another calendar.
  • Mutations on existing events are load-then-authorize: the event's PERSISTED owner decides the required permission, not anything the caller claims.

Summary

Functions

Whether the scope may WRITE (create/update/delete events) on the calendar owned by owner_uuid. Own calendar needs calendar; others' need calendar.edit_others.

Whether the scope may READ the calendar owned by owner_uuid. Own calendar needs calendar; others' need calendar.view_others (or calendar.edit_others, which implies viewing).

Map of owner_uuid => count of events they OWN across all calendars. Requires cross-calendar read access (calendar.view_others / calendar.edit_others).

Creates an event on owner_uuid's calendar.

Deletes an event. Same load-then-authorize rule as update_event/4.

Fetches one event, authorizing READ access against its persisted owner.

Lists EVERYONE's events overlapping the window — the combined "who is busy when" view. Requires cross-calendar read access (calendar.view_others / calendar.edit_others); a caller with only the base key gets {:error, :unauthorized}.

Lists the events on owner_uuid's calendar overlapping the given window (both bounds are dates; the window is [from, until)).

Whether the scope's user currently resolves as a participant of the event (live — see participant_visible_dynamic/1). A pure predicate: callers making ACCESS decisions must combine it with module enablement, as get_event/2 does.

The PubSub topic carrying {:calendar_event_changed, owner_uuid} after every committed create/update/delete. A LiveView showing calendars subscribes and reloads when a change lands for an owner in its current view.

Whether the scope may READ this event: owner-level view access, OR being a live-resolved participant of THIS event (which grants visibility of this event only, never the rest of the owner's calendar). Both require the calendar module to be enabled — module off seals every path.

Updates an event. Authorization runs against the event's PERSISTED owner (load-then-authorize); the changeset cannot move the event to a different calendar because owner_uuid is not castable.

Functions

can_edit?(scope, owner_uuid)

@spec can_edit?(PhoenixKit.Users.Auth.Scope.t() | nil, String.t()) :: boolean()

Whether the scope may WRITE (create/update/delete events) on the calendar owned by owner_uuid. Own calendar needs calendar; others' need calendar.edit_others.

can_view?(scope, owner_uuid)

@spec can_view?(PhoenixKit.Users.Auth.Scope.t() | nil, String.t()) :: boolean()

Whether the scope may READ the calendar owned by owner_uuid. Own calendar needs calendar; others' need calendar.view_others (or calendar.edit_others, which implies viewing).

count_events_by_owner(scope, from \\ nil, until \\ nil, opts \\ [])

@spec count_events_by_owner(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  Date.t() | nil,
  Date.t() | nil,
  keyword()
) ::
  {:ok, %{required(String.t()) => non_neg_integer()}} | {:error, :unauthorized}

Map of owner_uuid => count of events they OWN across all calendars. Requires cross-calendar read access (calendar.view_others / calendar.edit_others).

Pass a from/until date window to count only events overlapping it — that powers the person panel's "empty" badge, which flags a person whose own calendar has no events in the visible range. The count is ownership-scoped by design: the badge answers "is this CALENDAR empty here" (the panel picks calendars to view), not "would soloing them show nothing" — soloing additionally overlays events on OTHER calendars they merely participate in, which belong to those owners' counts.

create_event(scope, owner_uuid, attrs, opts \\ [])

@spec create_event(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  String.t(),
  map(),
  keyword()
) ::
  {:ok, PhoenixKitCalendar.Schemas.Event.t()}
  | {:error, :unauthorized | Ecto.Changeset.t()}

Creates an event on owner_uuid's calendar.

owner_uuid is an explicit argument — never read from attrs — and is authorized before the changeset ever runs. opts may carry :actor_uuid for activity logging (defaults to the scope's user).

delete_event(scope, event, opts \\ [])

@spec delete_event(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKitCalendar.Schemas.Event.t(),
  keyword()
) ::
  {:ok, PhoenixKitCalendar.Schemas.Event.t()}
  | {:error, :unauthorized | :not_found | Ecto.Changeset.t()}

Deletes an event. Same load-then-authorize rule as update_event/4.

get_event(scope, uuid)

@spec get_event(PhoenixKit.Users.Auth.Scope.t() | nil, String.t()) ::
  {:ok, PhoenixKitCalendar.Schemas.Event.t()}
  | {:error, :not_found | :unauthorized}

Fetches one event, authorizing READ access against its persisted owner.

list_all_events(scope, from, until, opts \\ [])

@spec list_all_events(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  Date.t(),
  Date.t(),
  keyword()
) ::
  {:ok, [PhoenixKitCalendar.Schemas.Event.t()]} | {:error, :unauthorized}

Lists EVERYONE's events overlapping the window — the combined "who is busy when" view. Requires cross-calendar read access (calendar.view_others / calendar.edit_others); a caller with only the base key gets {:error, :unauthorized}.

Options

  • :owner_uuids — restrict to these calendars (the person-filter toggles in the Everyone view). Omit or nil for all calendars.

list_events(scope, owner_uuid, from, until, opts \\ [])

@spec list_events(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  String.t(),
  Date.t(),
  Date.t(),
  keyword()
) ::
  {:ok, [PhoenixKitCalendar.Schemas.Event.t()]} | {:error, :unauthorized}

Lists the events on owner_uuid's calendar overlapping the given window (both bounds are dates; the window is [from, until)).

Returns {:ok, events} or {:error, :unauthorized}.

participant?(scope, event)

Whether the scope's user currently resolves as a participant of the event (live — see participant_visible_dynamic/1). A pure predicate: callers making ACCESS decisions must combine it with module enablement, as get_event/2 does.

pubsub_topic()

@spec pubsub_topic() :: String.t()

The PubSub topic carrying {:calendar_event_changed, owner_uuid} after every committed create/update/delete. A LiveView showing calendars subscribes and reloads when a change lands for an owner in its current view.

readable?(scope, event)

Whether the scope may READ this event: owner-level view access, OR being a live-resolved participant of THIS event (which grants visibility of this event only, never the rest of the owner's calendar). Both require the calendar module to be enabled — module off seals every path.

This is the single authorization predicate the participant read paths share with get_event/2, so a participant sees the event AND its participant list under the same rule.

update_event(scope, event, attrs, opts \\ [])

@spec update_event(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKitCalendar.Schemas.Event.t(),
  map(),
  keyword()
) ::
  {:ok, PhoenixKitCalendar.Schemas.Event.t()}
  | {:error, :unauthorized | :not_found | Ecto.Changeset.t()}

Updates an event. Authorization runs against the event's PERSISTED owner (load-then-authorize); the changeset cannot move the event to a different calendar because owner_uuid is not castable.