@acms-subir/core 0.3.53 → 0.3.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/cli/main.mjs +2 -1
- package/cli/migrations.generated.mjs +76 -6
- package/docs/calendar.md +49 -2
- package/docs/google-calendar.md +302 -0
- package/docs/server.md +68 -1
- package/integration.mjs +15 -5
- package/package.json +1 -1
- package/src/components/LegalSettings.tsx +1 -1
- package/src/lib/api.ts +3 -0
- package/src/lib/backup/content.ts +4 -2
- package/src/lib/calendar/google/client.ts +167 -0
- package/src/lib/calendar/google/connect.ts +161 -0
- package/src/lib/calendar/google/context.ts +50 -0
- package/src/lib/calendar/google/inbound.ts +493 -0
- package/src/lib/calendar/google/links.ts +139 -0
- package/src/lib/calendar/google/map.ts +543 -0
- package/src/lib/calendar/google/match.ts +103 -0
- package/src/lib/calendar/google/migration.ts +71 -0
- package/src/lib/calendar/google/outbound.ts +272 -0
- package/src/lib/calendar/google/service.ts +193 -0
- package/src/lib/calendar/google/store.ts +241 -0
- package/src/lib/calendar/google/types.ts +188 -0
- package/src/lib/calendar/host-requests.ts +247 -0
- package/src/lib/calendar/member-form.ts +17 -11
- package/src/lib/calendar/member-words.ts +473 -0
- package/src/lib/calendar/members-config.ts +2 -2
- package/src/lib/calendar/moves.ts +81 -33
- package/src/lib/calendar/request-migration.ts +17 -0
- package/src/lib/calendar/request-store.ts +104 -0
- package/src/lib/calendar/requests.ts +24 -24
- package/src/lib/calendar/schedule.ts +8 -8
- package/src/lib/display/board.ts +3 -1
- package/src/lib/email/service.ts +22 -2
- package/src/lib/email/templates.ts +20 -2
- package/src/lib/legal/admin.ts +1 -1
- package/src/lib/legal/facts.ts +3 -0
- package/src/lib/legal/text-en.ts +4 -0
- package/src/lib/legal/text-es.ts +4 -0
- package/src/lib/live/crypto.ts +7 -28
- package/src/lib/repo/migrations.ts +8 -0
- package/src/lib/repo/sql.ts +25 -9
- package/src/lib/repo/types.ts +12 -3
- package/src/lib/scheduler.ts +3 -0
- package/src/lib/schema.ts +47 -0
- package/src/lib/sealed.ts +46 -0
- package/src/pages/admin/calendar/events/[id].astro +42 -1
- package/src/pages/admin/calendar/events/index.astro +82 -6
- package/src/pages/admin/calendar/google/match.astro +160 -0
- package/src/pages/admin/calendar/google.astro +107 -0
- package/src/pages/admin/calendar/settings.astro +273 -4
- package/src/pages/admin/index.astro +6 -2
- package/src/pages/admin/integrations.astro +6 -0
- package/src/pages/api/calendar/google/oauth/callback.ts +16 -0
- package/src/pages/members/_EventForm.astro +33 -27
- package/src/pages/members/_Layout.astro +25 -5
- package/src/pages/members/_http.ts +29 -6
- package/src/pages/members/events/[id].astro +21 -20
- package/src/pages/members/events/index.astro +295 -101
- package/src/pages/members/events/new.astro +11 -10
- package/src/server/index.ts +102 -0
- package/src/virtual.d.ts +1 -1
package/src/server/index.ts
CHANGED
|
@@ -205,6 +205,55 @@ export async function moveOccurrenceBack(repo: Repo, who: Who | typeof GOOGLE_SY
|
|
|
205
205
|
return back(internal(repo), who, eventId, occurrenceKey, { url: context.url, locals: context.locals }, opts);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// ---- Google Calendar sync ----------------------------------------------------
|
|
209
|
+
// The two-way sync is set up in the admin (Calendar settings → Synced with
|
|
210
|
+
// Google) and runs from the scheduler. These read what it did, for a site's
|
|
211
|
+
// own pages (a "Synced with Google" badge, a staff report). They don't check
|
|
212
|
+
// permissions and their results name Google calendars: show them to staff.
|
|
213
|
+
|
|
214
|
+
/** The internal tag on appointments booked through a Google booking page (they come in titled "Booked"). Never shown publicly. */
|
|
215
|
+
export const GOOGLE_BOOKING_TAG = '#google-booking';
|
|
216
|
+
|
|
217
|
+
export interface GoogleCopy {
|
|
218
|
+
/** the Google calendar's name and id */
|
|
219
|
+
googleCalendar: string;
|
|
220
|
+
googleCalendarId: string;
|
|
221
|
+
/** the Google event's own page */
|
|
222
|
+
htmlLink: string;
|
|
223
|
+
/** when the pair last synced (ms) */
|
|
224
|
+
syncedAt: number;
|
|
225
|
+
/** created in Google and its description never edited on the site: Google's description is used */
|
|
226
|
+
descriptionFromGoogle: boolean;
|
|
227
|
+
/** a booked appointment from a Google booking page */
|
|
228
|
+
booking: boolean;
|
|
229
|
+
/** the latest field both sides changed (the site kept its value), until someone picks one */
|
|
230
|
+
clash: { field: string; site: string; google: string; at: number } | null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The Google copies of one event (an event in two linked calendars has two). Empty when it isn't synced. */
|
|
234
|
+
export async function googleCopies(repo: Repo, eventId: string): Promise<GoogleCopy[]> {
|
|
235
|
+
const r = internal(repo);
|
|
236
|
+
const items = (await r.gcal.itemsForEvent(eventId)).filter((i) => i.kind !== 'segment' && !i.gone);
|
|
237
|
+
if (!items.length) return [];
|
|
238
|
+
const links = new Map((await r.gcal.listLinks()).map((l) => [l.id, l]));
|
|
239
|
+
return items.map((i) => ({
|
|
240
|
+
googleCalendar: links.get(i.linkId)?.googleName ?? '', googleCalendarId: links.get(i.linkId)?.googleId ?? '', htmlLink: i.htmlLink, syncedAt: i.syncedAt,
|
|
241
|
+
descriptionFromGoogle: i.descGoogle, booking: i.booking, clash: i.conflict && { field: i.conflict.field, site: i.conflict.site, google: i.conflict.google, at: i.conflict.at },
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Recent changes the sync made (newest first, kept 90 days): direction in/out/clash/info, the event, and what changed in words. */
|
|
246
|
+
export async function googleSyncHistory(repo: Repo, opts: { eventId?: string; limit?: number } = {}) {
|
|
247
|
+
return (await internal(repo).gcal.listLog({ eventId: opts.eventId, limit: opts.limit ?? 50 }))
|
|
248
|
+
.map(({ id, at, eventId, direction, action, title, summary, resolved }) => ({ id, at, eventId, direction, action, title, summary, resolved }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Runs the sync once now, as the scheduler does every minute (changes out; each link reads Google every 2 minutes). Returns how many changes it handled. */
|
|
252
|
+
export async function syncGoogleCalendar(repo: Repo, context: Pick<WriteContext, 'locals'>): Promise<number> {
|
|
253
|
+
const { syncGoogle } = await import('../lib/calendar/google/service');
|
|
254
|
+
return syncGoogle(internal(repo), context.locals);
|
|
255
|
+
}
|
|
256
|
+
|
|
208
257
|
// ---- rooms -----------------------------------------------------------------
|
|
209
258
|
// Events hold rooms (CalEvent.roomHold) while they happen. These find where
|
|
210
259
|
// two things want the same room. They don't check permissions, and their
|
|
@@ -300,3 +349,56 @@ export async function declineEventRequest(repo: Repo, who: Who, eventId: string,
|
|
|
300
349
|
export async function setHostedOccurrence(repo: Repo, who: Who, eventId: string, occurrenceKey: string, cancelled: boolean, context: WriteContext): Promise<CalEvent> {
|
|
301
350
|
return (await requests()).setHostedOccurrence(internal(repo), who, eventId, occurrenceKey, cancelled, context);
|
|
302
351
|
}
|
|
352
|
+
|
|
353
|
+
// ---- hosts moving dates ------------------------------------------------------
|
|
354
|
+
// A host moves one date of an event they host (no permission beyond being in
|
|
355
|
+
// hostIds). acms({ members: { hostMoves } }) decides how: 'direct' (default)
|
|
356
|
+
// moves it at once while its rooms are free (409 room-taken otherwise, and
|
|
357
|
+
// the same move can then be sent with requestDateMove); 'approve' turns every
|
|
358
|
+
// host move into a request. Requests are RequestRecord rows (kind 'move')
|
|
359
|
+
// that staff who can publish approve or decline; both sides are emailed.
|
|
360
|
+
|
|
361
|
+
const hostRequests = () => import('../lib/calendar/host-requests');
|
|
362
|
+
export type { HostMoveResult, MoveRequestData } from '../lib/calendar/host-requests';
|
|
363
|
+
export type { RequestRecord } from '../lib/schema';
|
|
364
|
+
|
|
365
|
+
/** A host moves one date: `{ moved }` at once, or `{ request }` when moves need approval. Errors as moveOccurrence's, plus `code` room-not-allowed (a room the event doesn't use and nobody may request). */
|
|
366
|
+
export async function moveHostedDate(repo: Repo, who: Who, eventId: string, occurrenceKey: string, input: import('../lib/calendar/moves').MoveInput, context: WriteContext): Promise<import('../lib/calendar/host-requests').HostMoveResult> {
|
|
367
|
+
return (await hostRequests()).moveHostedDate(internal(repo), who, eventId, occurrenceKey, input, context);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** A host puts a date they moved back where the series has it: at once, or as a request. */
|
|
371
|
+
export async function moveHostedDateBack(repo: Repo, who: Who, eventId: string, occurrenceKey: string, context: WriteContext): Promise<import('../lib/calendar/host-requests').HostMoveResult> {
|
|
372
|
+
return (await hostRequests()).moveHostedDateBack(internal(repo), who, eventId, occurrenceKey, context);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* A host asks staff to move one date (`input`), or to move it back (null),
|
|
377
|
+
* with an optional `message` for staff. Checked like the move, except that a
|
|
378
|
+
* taken room is allowed: staff decide. Asking again about the same date
|
|
379
|
+
* replaces the waiting request. Staff are emailed.
|
|
380
|
+
*/
|
|
381
|
+
export async function requestDateMove(repo: Repo, who: Who, eventId: string, occurrenceKey: string, input: import('../lib/calendar/moves').MoveInput | null, message: string, context: WriteContext): Promise<import('../lib/schema').RequestRecord> {
|
|
382
|
+
return (await hostRequests()).requestDateMove(internal(repo), who, eventId, occurrenceKey, input, message, context);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** A host's own date requests (waiting, approved, declined and withdrawn), newest first. */
|
|
386
|
+
export async function listMyDateRequests(repo: Repo, who: Who, eventId?: string): Promise<import('../lib/schema').RequestRecord[]> {
|
|
387
|
+
if (!who) return [];
|
|
388
|
+
return (await hostRequests()).myDateRequests(internal(repo), who.id, eventId);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** A host withdraws a waiting date request of theirs. */
|
|
392
|
+
export async function withdrawDateRequest(repo: Repo, who: Who, requestId: string): Promise<void> {
|
|
393
|
+
return (await hostRequests()).withdrawDateRequest(internal(repo), who, requestId);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Staff ("Publish content"): approve a host's date move. It runs as them (room clashes come back as warnings) and is recorded as the host's. Pass `seen`, the request's `updatedAt` you showed. */
|
|
397
|
+
export async function approveDateRequest(repo: Repo, who: Who, requestId: string, context: WriteContext, seen?: number): Promise<{ request: import('../lib/schema').RequestRecord; moved: import('../lib/calendar/moves').MoveResult }> {
|
|
398
|
+
return (await hostRequests()).approveDateRequest(internal(repo), who, requestId, context, seen);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Staff: decline a host's date request, with a note the host reads. */
|
|
402
|
+
export async function declineDateRequest(repo: Repo, who: Who, requestId: string, reply: string, context: WriteContext, seen?: number): Promise<import('../lib/schema').RequestRecord> {
|
|
403
|
+
return (await hostRequests()).declineDateRequest(internal(repo), who, requestId, reply, context, seen);
|
|
404
|
+
}
|
package/src/virtual.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ declare module 'virtual:acms-config' {
|
|
|
22
22
|
// resolved by integration.mjs (lib/roles.ts and lib/permissions/catalog.ts read them)
|
|
23
23
|
roles?: { key: string; label: string; summary: string; admin: boolean; home: string }[];
|
|
24
24
|
permissions?: { key: string; label: string; description: string; defaults: string[]; sensitive: boolean }[];
|
|
25
|
-
members?: { enabled: boolean; base: string; notify: string[] };
|
|
25
|
+
members?: { enabled: boolean; base: string; notify: string[]; hostMoves?: 'direct' | 'approve' };
|
|
26
26
|
announcements?: { enabled: boolean; base: string; pages: boolean; topics: Partial<Record<'event' | 'registration' | 'help' | 'info', string>> };
|
|
27
27
|
// set in astro:config:setup (lib/origin-check.ts): off only when the site turned security.checkOrigin off
|
|
28
28
|
originCheck?: { enabled: boolean; signedRoutes: string[] };
|