@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
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { ApiError } from '../../lib/api';
|
|
2
|
+
import type { AstroGlobal } from 'astro';
|
|
3
|
+
import type { Settings } from '../../lib/schema';
|
|
4
|
+
import { I18N, localePath, requestLocale } from '../../lib/i18n';
|
|
5
|
+
import { isExtraLocale } from '../../lib/translations';
|
|
6
|
+
import { siteLang, siteLocale } from '../../lib/dates';
|
|
7
|
+
import { memberError, memberStrings, type MemberStrings } from '../../lib/calendar/member-words';
|
|
2
8
|
|
|
3
9
|
// Shared by the built-in "My events" pages: they're personal, so nothing
|
|
4
10
|
// they answer (redirects included) may be cached or indexed.
|
|
@@ -12,14 +18,31 @@ export function keepPrivate(headers: Headers) {
|
|
|
12
18
|
/** 303 after a POST (the browser GETs the list), 302 otherwise */
|
|
13
19
|
export const redirectTo = (location: string, status = 303) => new Response(null, { status, headers: { Location: location, ...NO_STORE } });
|
|
14
20
|
|
|
15
|
-
/** signed out: sign in, then come back here */
|
|
16
|
-
export const signInRedirect = (url: URL) =>
|
|
21
|
+
/** signed out: sign in, then come back here (the address they used: /es/… is rewritten to these pages) */
|
|
22
|
+
export const signInRedirect = (url: URL, locals?: unknown) => {
|
|
23
|
+
const path = (locals as { acmsPath?: string } | undefined)?.acmsPath ?? url.pathname;
|
|
24
|
+
return redirectTo(`/admin/login?return=${encodeURIComponent(path + url.search)}`, 302);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The page's language (/es/members/events is these pages in Spanish): its
|
|
29
|
+
* words, links that keep the visitor in it, and the locale dates are written
|
|
30
|
+
* in (the site's own locale setting for the main language).
|
|
31
|
+
*/
|
|
32
|
+
export function memberLanguage(Astro: Pick<AstroGlobal, 'url' | 'locals'>, settings: Pick<Settings, 'locale'>): { locale: string; lang: string; w: MemberStrings; here: (path: string) => string; dateLocale: string } {
|
|
33
|
+
const locale = I18N.enabled ? requestLocale(Astro.url, Astro.locals) : '';
|
|
34
|
+
const lang = locale || siteLang(settings);
|
|
35
|
+
return { locale, lang, w: memberStrings(lang), here: (path: string) => localePath(path, locale), dateLocale: isExtraLocale(locale) ? locale : siteLocale(settings) };
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
export const notFound = () => new Response(null, { status: 404, statusText: 'Not found', headers: NO_STORE });
|
|
19
39
|
|
|
20
|
-
/** an error from requests.ts → a message safe to show, and the status to answer with */
|
|
21
|
-
export function failure(err: unknown): { message: string; status: number } {
|
|
22
|
-
if (err instanceof ApiError)
|
|
40
|
+
/** an error from requests.ts (or moves.ts) → a message safe to show, in the page's words when given, and the status to answer with */
|
|
41
|
+
export function failure(err: unknown, w?: MemberStrings, lang?: string): { message: string; status: number; code: string } {
|
|
42
|
+
if (err instanceof ApiError) {
|
|
43
|
+
const code = String((err as { code?: unknown }).code ?? '');
|
|
44
|
+
return { message: w ? memberError(w, err, lang) : err.message, status: err.status, code };
|
|
45
|
+
}
|
|
23
46
|
console.error(err);
|
|
24
|
-
return { message: 'Something went wrong. Please try again.', status: 500 };
|
|
47
|
+
return { message: w?.somethingWrong ?? 'Something went wrong. Please try again.', status: 500, code: '' };
|
|
25
48
|
}
|
|
@@ -3,22 +3,24 @@ import Layout from '../_Layout.astro';
|
|
|
3
3
|
import EventForm from '../_EventForm.astro';
|
|
4
4
|
import { getRepo } from '../../../lib/repo';
|
|
5
5
|
import { currentUser } from '../../../server';
|
|
6
|
-
import {
|
|
6
|
+
import { coded } from '../../../lib/api';
|
|
7
7
|
import { readForm } from '../../../lib/forms/http';
|
|
8
8
|
import { MEMBERS, memberEventsPath, bodyToText, isHost, updateEventRequest, withdrawEventRequest } from '../../../lib/calendar/requests';
|
|
9
9
|
import { eventFormValues, eventRequestInput, memberEventFields, readEventForm } from '../../../lib/calendar/member-form';
|
|
10
10
|
import { mergeEventDraft } from '../../../lib/drafts';
|
|
11
|
-
import { failure, keepPrivate, notFound, redirectTo, signInRedirect } from '../_http';
|
|
11
|
+
import { failure, keepPrivate, memberLanguage, notFound, redirectTo, signInRedirect } from '../_http';
|
|
12
|
+
import { fill } from '../../../lib/live/words';
|
|
12
13
|
|
|
13
14
|
// Edit a request, or suggest changes to a live event the member hosts: the
|
|
14
15
|
// form starts from the event with any changes already waiting merged in.
|
|
15
16
|
// A live event stays as it is until the team approves (updateEventRequest).
|
|
16
17
|
if (!MEMBERS.enabled) return notFound();
|
|
17
18
|
const user = await currentUser(Astro);
|
|
18
|
-
if (!user) return signInRedirect(Astro.url);
|
|
19
|
+
if (!user) return signInRedirect(Astro.url, Astro.locals);
|
|
19
20
|
keepPrivate(Astro.response.headers);
|
|
20
21
|
const repo = await getRepo(Astro.locals);
|
|
21
|
-
const
|
|
22
|
+
const { w, lang, here } = memberLanguage(Astro, await repo.getSettings());
|
|
23
|
+
const home = here(memberEventsPath());
|
|
22
24
|
const hooks = { url: Astro.url, locals: Astro.locals };
|
|
23
25
|
const id = Astro.params.id ?? '';
|
|
24
26
|
const event = await repo.getEvent(id);
|
|
@@ -40,12 +42,12 @@ if (Astro.request.method === 'POST') {
|
|
|
40
42
|
await withdrawEventRequest(repo, user, id, hooks);
|
|
41
43
|
return redirectTo(`${home}?withdrawn=1`);
|
|
42
44
|
}
|
|
43
|
-
if (op !== 'update') throw
|
|
45
|
+
if (op !== 'update') throw coded(400, 'unavailable', 'That action isn’t available here.');
|
|
44
46
|
values = readEventForm(form, fields);
|
|
45
47
|
await updateEventRequest(repo, user, id, eventRequestInput(values), hooks);
|
|
46
48
|
return redirectTo(`${home}?${live ? 'changes' : 'updated'}=1`);
|
|
47
49
|
} catch (err) {
|
|
48
|
-
const f = failure(err);
|
|
50
|
+
const f = failure(err, w, lang);
|
|
49
51
|
error = f.message;
|
|
50
52
|
Astro.response.status = f.status;
|
|
51
53
|
}
|
|
@@ -53,34 +55,33 @@ if (Astro.request.method === 'POST') {
|
|
|
53
55
|
|
|
54
56
|
const r = event.request;
|
|
55
57
|
const canRequest = user.can('events.request');
|
|
56
|
-
const heading = live ?
|
|
57
|
-
const lede = live
|
|
58
|
-
|
|
59
|
-
: r
|
|
60
|
-
:
|
|
61
|
-
: 'It isn’t on the calendar yet. Send it to the team when it’s ready.';
|
|
58
|
+
const heading = live ? w.suggestTitle : w.editTitle;
|
|
59
|
+
const lede = live ? fill(w.suggestLede, { title: event.title })
|
|
60
|
+
: r?.state === 'declined' ? w.declinedLede
|
|
61
|
+
: r ? w.waitingLede
|
|
62
|
+
: w.draftLede;
|
|
62
63
|
const waiting = live && r?.kind === 'changes' && r.state === 'pending';
|
|
63
64
|
const reply = r?.state === 'declined' ? r.reply : '';
|
|
64
65
|
const canWithdraw = canRequest && r && !(r.kind === 'changes' && r.state === 'declined');
|
|
65
66
|
---
|
|
66
67
|
|
|
67
|
-
<Layout title={heading} user={user}>
|
|
68
|
+
<Layout title={heading} user={user} w={w}>
|
|
68
69
|
<div>
|
|
69
|
-
<a class="pc-mem-back" href={home}
|
|
70
|
+
<a class="pc-mem-back" href={home}>{w.back}</a>
|
|
70
71
|
<div class="pc-mem-head"><h1>{heading}</h1></div>
|
|
71
72
|
<p class="pc-mem-lede">{lede}</p>
|
|
72
73
|
</div>
|
|
73
|
-
{waiting && <p class="pc-mem-reply">
|
|
74
|
+
{waiting && <p class="pc-mem-reply">{w.changesWaitingNote}</p>}
|
|
74
75
|
{r?.state === 'declined' && (
|
|
75
|
-
<p class="pc-mem-reply"><strong>{r.kind === 'changes' ?
|
|
76
|
+
<p class="pc-mem-reply"><strong>{r.kind === 'changes' ? w.declinedChanges : w.declinedRequest}</strong>{reply && <> {w.theySaid} {reply}</>}</p>
|
|
76
77
|
)}
|
|
77
78
|
{canRequest
|
|
78
|
-
? <EventForm values={values} calendars={calendars} fields={fields} op="update" submitLabel={live ?
|
|
79
|
-
: <p class="pc-bk-error" role="alert">
|
|
79
|
+
? <EventForm values={values} calendars={calendars} fields={fields} op="update" submitLabel={live ? w.sendChanges : w.submit} error={error} w={w} lang={lang} />
|
|
80
|
+
: <p class="pc-bk-error" role="alert">{w.noRequestRole}</p>}
|
|
80
81
|
{canWithdraw && (
|
|
81
|
-
<form method="post" onsubmit={r!.kind === 'new' ?
|
|
82
|
+
<form method="post" onsubmit={'return confirm(' + JSON.stringify(r!.kind === 'new' ? w.confirmWithdrawRequest : w.confirmWithdrawChanges) + ')'}>
|
|
82
83
|
<input type="hidden" name="op" value="withdraw" />
|
|
83
|
-
<button class="pc-mem-btn is-danger" type="submit">{r!.kind === 'new' ?
|
|
84
|
+
<button class="pc-mem-btn is-danger" type="submit">{r!.kind === 'new' ? w.withdrawRequest : w.withdrawChanges}</button>
|
|
84
85
|
</form>
|
|
85
86
|
)}
|
|
86
87
|
</Layout>
|