ghost 6.40.0 → 6.41.0
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/core/boot.js +8 -8
- package/core/built/admin/assets/{PolarAngleAxis-D7m1zwVk.js → PolarAngleAxis-Bh28wbPV.js} +1 -1
- package/core/built/admin/assets/{_baseAssignValue-BDsvYKNK.js → _baseAssignValue-hdDG700N.js} +1 -1
- package/core/built/admin/assets/{a-large-small-DxRNdz0F.js → a-large-small-BiK5_5LV.js} +1 -1
- package/core/built/admin/assets/{account-migration-SYnE1jBW.js → account-migration-DsSHanIB.js} +1 -1
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{code-editor-view-Bj9rgbLw.mjs → code-editor-view-BEIdFhw3.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-BrZ9YAzd.mjs → index-BEh3nFEy.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-DRndIh9T.mjs → index-BLF2n6YB.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-CIl25Teq.mjs → index-BmPIWs5F.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-CNyNY3Wh.mjs → index-CP9YnNzc.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-SGr5sE2h.mjs → index-Cd2Ns-D2.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-PGoLP2kh.mjs → index-CjqAcKpK.mjs} +5 -5
- package/core/built/admin/assets/admin-x-settings/{index-pCIv_7bB.mjs → index-CnMcD9aj.mjs} +6212 -6158
- package/core/built/admin/assets/admin-x-settings/{index-CPCcgad-.mjs → index-LzsrZC40.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-DPqY4h5h.mjs → index-ZYW0UW7I.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{modals-lo32WZLi.mjs → modals-C48Tq1rW.mjs} +9 -9
- package/core/built/admin/assets/{arrow-right-BixoEua4.js → arrow-right-Bk43nMGd.js} +1 -1
- package/core/built/admin/assets/{at-sign-BsHNViA_.js → at-sign-DWOf36uj.js} +1 -1
- package/core/built/admin/assets/{audience-0RCWjbIK.js → audience-CvP78Gug.js} +1 -1
- package/core/built/admin/assets/{automations-DTUfYvf6.js → automations-BBWZhrha.js} +1 -1
- package/core/built/admin/assets/{automations-DiGkcVt-.js → automations-CmulGWwu.js} +1 -1
- package/core/built/admin/assets/{avatar-flipboard-BgoH-wIq.js → avatar-flipboard-CVf-N6wp.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-C2xwSoS-.js → bluesky-sharing-DVC3I9Um.js} +1 -1
- package/core/built/admin/assets/{chart-X5cLIH1X.js → chart-CUKSNmKY.js} +1 -1
- package/core/built/admin/assets/{checkbox-DMamS4oG.js → checkbox-B_yDSM8z.js} +1 -1
- package/core/built/admin/assets/{chevron-up-CVgBkTk5.js → chevron-up-DuaKtE_M.js} +1 -1
- package/core/built/admin/assets/{chunk.941.f47098cbbce89a049e03.js → chunk.331.2ec2e2702eda02de96cd.js} +3 -3
- package/core/built/admin/assets/{chunk.524.a1d668dcc84709a5d702.js → chunk.524.49cc01002e1a5dba8ef9.js} +4 -4
- package/core/built/admin/assets/{chunk.582.180bf2525cf552f5ac5f.js → chunk.582.4bbc54c5f7d3dfb93288.js} +6 -6
- package/core/built/admin/assets/{circle-alert-Dg3UkvRP.js → circle-alert-bcK_Yqap.js} +1 -1
- package/core/built/admin/assets/{code-editor-view-CAICoZJe.js → code-editor-view-Bl4Jzjhk.js} +1 -1
- package/core/built/admin/assets/{comments-BHru9ZQ8.js → comments-DMea0nul.js} +1 -1
- package/core/built/admin/assets/{content-helpers-BByMp-Y5.js → content-helpers-B2yRzdvJ.js} +1 -1
- package/core/built/admin/assets/{copy-D7jqplrv.js → copy-DVtQi_VJ.js} +1 -1
- package/core/built/admin/assets/{data-list-DUaY9nlT.js → data-list-BF4GeUmt.js} +1 -1
- package/core/built/admin/assets/{deleted-feed-item-jLcSKuul.js → deleted-feed-item-DO2u3qmi.js} +1 -1
- package/core/built/admin/assets/{dropzone-DSPf3N_o.js → dropzone-Cs4DrURS.js} +1 -1
- package/core/built/admin/assets/{edit-profile-BvU2-lW5.js → edit-profile-CnuBNQhO.js} +1 -1
- package/core/built/admin/assets/{editor-D3fcjt_F.js → editor-CTO9FIUW.js} +1 -1
- package/core/built/admin/assets/{empty-indicator-2Pjmce3n.js → empty-indicator-BvyaMcmw.js} +1 -1
- package/core/built/admin/assets/{en-Bu-6PYn5.js → en-CjPXRj8j.js} +1 -1
- package/core/built/admin/assets/{feed-CygLK1Ad.js → feed-D2wk4cCS.js} +1 -1
- package/core/built/admin/assets/{filter-query-core-a0PNU2EO.js → filter-query-core-VEnwTCyO.js} +1 -1
- package/core/built/admin/assets/{filters-jiyRsLMH.js → filters-DtIsuqv5.js} +1 -1
- package/core/built/admin/assets/{gh-chart-CEhBBu3F.js → gh-chart-JxzKqaoO.js} +1 -1
- package/core/built/admin/assets/{ghost-0ab96884815339963364804ab266c99f.js → ghost-30c3dedbe1cca943b92efa7efd2710e4.js} +36 -21
- package/core/built/admin/assets/{growth-Fz3Oatml.js → growth-CPNrBpHK.js} +1 -1
- package/core/built/admin/assets/{hash-CeFrjo3C.js → hash-CJzbTA5k.js} +1 -1
- package/core/built/admin/assets/{inbox-CCirPaG2.js → inbox-CVVAkCmd.js} +1 -1
- package/core/built/admin/assets/{index-Dd2BJRZH.js → index-AkQJUr1z.js} +1 -1
- package/core/built/admin/assets/{index-CYPN5csJ.js → index-BLWOUf31.js} +1 -1
- package/core/built/admin/assets/{index-D7Ui3Fih.js → index-BRBRkRqa.js} +1 -1
- package/core/built/admin/assets/{index-CFEzD2_M.js → index-BkbhBpVC.js} +3 -3
- package/core/built/admin/assets/{index-BYwL7gFN.js → index-BxFFWYdP.js} +1 -1
- package/core/built/admin/assets/{index-D3LfHeBl.js → index-C5_ZKpC1.js} +1 -1
- package/core/built/admin/assets/{index-Bjw2qAph.js → index-Cdbufdfy.js} +1 -1
- package/core/built/admin/assets/{index-BTf6YUBb.js → index-Cq5vsvxE.js} +1 -1
- package/core/built/admin/assets/{index-DPbi4ie_.js → index-CuGoX8Ub.js} +1 -1
- package/core/built/admin/assets/{index-CsaU9dfl.js → index-CuH5raTz.js} +1 -1
- package/core/built/admin/assets/{index-Bf83suMc.js → index-D3A8RTi0.js} +1 -1
- package/core/built/admin/assets/{index-Dskrbhv3.js → index-DIyK69JM.js} +1 -1
- package/core/built/admin/assets/{index-GExKMpy_.js → index-DLFVpwt3.js} +1 -1
- package/core/built/admin/assets/{index-BOMdiHRG.js → index-DQZp_NsL.js} +1 -1
- package/core/built/admin/assets/{index-2qcjFCQs.js → index-DR74ulj2.js} +1 -1
- package/core/built/admin/assets/{index-WKMARia9.js → index-DYE2untj.js} +1 -1
- package/core/built/admin/assets/{index-DNZUJQ8v.js → index-DssW7xhk.js} +1 -1
- package/core/built/admin/assets/{index-Cq4mS7QK.js → index-DvyNqFct.js} +1 -1
- package/core/built/admin/assets/{index-C7xLyJVb.js → index-DxB7NHpG.js} +1 -1
- package/core/built/admin/assets/{index-Bw4DM4Pr.js → index-IDaHqjnu.js} +1 -1
- package/core/built/admin/assets/{index-ClOB5u8L.js → index-TtBMzSeP.js} +1 -1
- package/core/built/admin/assets/{index-takfwHSn.js → index-UMWVIyz7.js} +1 -1
- package/core/built/admin/assets/{koenig-lexical-BJM9wAwQ.js → koenig-lexical-DVAmevhl.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BP-2yicn.js → kpi-card-D3CzvsrI.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DOplXdDT.js → kpi-card-DV-nN7xT.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-Rq5jJW2Z.js → kpi-tabs-B3NkaTwb.js} +1 -1
- package/core/built/admin/assets/{kpis-yHgME_iy.js → kpis-CVwF08QL.js} +1 -1
- package/core/built/admin/assets/{label-fijjy1Mb.js → label-BiPpMny2.js} +1 -1
- package/core/built/admin/assets/{links-s3IeAkbo.js → links-BY0Uo6O5.js} +1 -1
- package/core/built/admin/assets/{list-page-CUcB5ebB.js → list-page-COHNMpOi.js} +1 -1
- package/core/built/admin/assets/{loader-circle-C7mWzo52.js → loader-circle-B1uEfpbZ.js} +1 -1
- package/core/built/admin/assets/{mail-9TFkAaAh.js → mail-ccQhEr7k.js} +1 -1
- package/core/built/admin/assets/{main-layout-XtviVkg1.js → main-layout-BYKdA6Z1.js} +1 -1
- package/core/built/admin/assets/{members-XkHao5Yf.js → members-DkpEnvG5.js} +1 -1
- package/core/built/admin/assets/minus-C68D81nx.js +1 -0
- package/core/built/admin/assets/{modals-B6R7oqvb.js → modals-X6RSWn5Y.js} +3 -3
- package/core/built/admin/assets/{moderation-B2Tixfce.js → moderation-BFrrwfvx.js} +1 -1
- package/core/built/admin/assets/{newsletter-CbESXhxf.js → newsletter-n7kc6cxR.js} +1 -1
- package/core/built/admin/assets/{newsletters-DOIL_cOY.js → newsletters-CSCa3QdE.js} +1 -1
- package/core/built/admin/assets/{note-mUikaB-p.js → note-CMZjeK8e.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-Dqv2nP7b.js → onboarding-route-BaX0a-pu.js} +1 -1
- package/core/built/admin/assets/{overview-yZ-7KJdx.js → overview-CS9dmE_6.js} +1 -1
- package/core/built/admin/assets/{pagemenu-TxGvHwRO.js → pagemenu-O_y8Jm4v.js} +1 -1
- package/core/built/admin/assets/{pencil-CKTgzafp.js → pencil-BcymGPnE.js} +1 -1
- package/core/built/admin/assets/{pin-BU_u-xRX.js → pin-Bl3nSr7I.js} +1 -1
- package/core/built/admin/assets/{post-analytics-CWA8invj.js → post-analytics-DW-TKu-t.js} +1 -1
- package/core/built/admin/assets/{post-analytics-context-BWDhMRq_.js → post-analytics-context-D7PgEMo8.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-Bm0mcxjK.js → post-analytics-header-B8C0vA5Y.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-D8BtRD2u.js → post-share-modal-D0OlASBQ.js} +1 -1
- package/core/built/admin/assets/{posts-u-CAKddc.js → posts-BZaw4-dc.js} +1 -1
- package/core/built/admin/assets/{power-DR9gpiQR.js → power-DWm6pts5.js} +1 -1
- package/core/built/admin/assets/{referrers-DV_Ihg9y.js → referrers-BsZ85Z0a.js} +1 -1
- package/core/built/admin/assets/{repeat-uXmOJ6y8.js → repeat-DQay9e22.js} +1 -1
- package/core/built/admin/assets/{reply-9ZyqaxI-.js → reply-CoNqmExw.js} +1 -1
- package/core/built/admin/assets/{rocket-DMOr1OCt.js → rocket-Dne6P_8f.js} +1 -1
- package/core/built/admin/assets/{select-CPSy44ld.js → select-BfWJkrKc.js} +1 -1
- package/core/built/admin/assets/{settings-krk0432u.js → settings-BoXzKMhL.js} +1 -1
- package/core/built/admin/assets/{settings-66Zv8QvL.js → settings-rRY210wx.js} +25 -25
- package/core/built/admin/assets/{share-modal-BRmGllZq.js → share-modal-CtdLQbnQ.js} +1 -1
- package/core/built/admin/assets/{sort-button-5fyeViQL.js → sort-button-7pVgpa0d.js} +1 -1
- package/core/built/admin/assets/{source-icon-DeeHyZRn.js → source-icon-Dtua85oN.js} +1 -1
- package/core/built/admin/assets/{sprout-956Q4On2.js → sprout-CpwUJAPT.js} +1 -1
- package/core/built/admin/assets/{square-BaCvbKJY.js → square-CDrQjv8R.js} +1 -1
- package/core/built/admin/assets/{stats-BVKNZJO6.js → stats-Yo9Gco0_.js} +1 -1
- package/core/built/admin/assets/{stats-view-gtyaqudG.js → stats-view-DubukX9V.js} +1 -1
- package/core/built/admin/assets/{step-1-09wdjzdX.js → step-1-CH8jWKi7.js} +1 -1
- package/core/built/admin/assets/{step-2-8iaQ0efq.js → step-2-CWy7Qi0Q.js} +1 -1
- package/core/built/admin/assets/{step-3-BuHAZbdD.js → step-3-CNiDFH17.js} +1 -1
- package/core/built/admin/assets/{table-DTofVyjA.js → table-qQi9EXB1.js} +1 -1
- package/core/built/admin/assets/{tabs-CL5eaCGL.js → tabs-M6kFEqPE.js} +1 -1
- package/core/built/admin/assets/{tags-DJrbEw5-.js → tags-74U6lgwY.js} +1 -1
- package/core/built/admin/assets/{tags-U3lciBmd.js → tags-DuMshkTz.js} +1 -1
- package/core/built/admin/assets/{textarea-DdDuNixZ.js → textarea-CGBp9nOo.js} +1 -1
- package/core/built/admin/assets/{tiers-DXKbghdk.js → tiers-D2DYHaeO.js} +1 -1
- package/core/built/admin/assets/{toggle-group-DaZKTYDS.js → toggle-group-GYp_w6Po.js} +1 -1
- package/core/built/admin/assets/{topic-filter-BzqiVOAU.js → topic-filter-N3CzJ01P.js} +1 -1
- package/core/built/admin/assets/{trash-CKK_p9G5.js → trash-CUqAK3SQ.js} +1 -1
- package/core/built/admin/assets/{underline-Bq0P-5Zy.js → underline-CwuJG-Ql.js} +1 -1
- package/core/built/admin/assets/{undo-2-CcBtw-tW.js → undo-2-D6ZRqONE.js} +1 -1
- package/core/built/admin/assets/{upload-BcylHEMw.js → upload-BLiT66bJ.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-Ci0gymWp.js → use-growth-stats-u18So4Gq.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-ByaFwxfZ.js → use-simple-pagination-Dp2sTt2S.js} +1 -1
- package/core/built/admin/assets/{user-round-check-D0I7NO1g.js → user-round-check-BDomHaso.js} +1 -1
- package/core/built/admin/assets/{user-round-x-DeGVH29o.js → user-round-x-Cc0RL-V9.js} +1 -1
- package/core/built/admin/assets/{virtual-list-window-S0xZxQAf.js → virtual-list-window-BnjCoNIr.js} +1 -1
- package/core/built/admin/assets/{wallet-cards-B9TQg4x3.js → wallet-cards-CVZ99Nm3.js} +1 -1
- package/core/built/admin/assets/{web-BRN2mUpo.js → web-B_MLxGxZ.js} +1 -1
- package/core/built/admin/index.html +5 -5
- package/core/server/{services/custom-redirects/file-store.js → adapters/redirects/FileStore.js} +6 -5
- package/core/server/{services/custom-redirects/file-store.ts → adapters/redirects/FileStore.ts} +6 -4
- package/core/server/adapters/redirects/RedirectsStoreBase.d.ts +9 -0
- package/core/server/adapters/redirects/RedirectsStoreBase.js +8 -0
- package/core/server/{services/custom-redirects/gcs-store.js → adapters/redirects/S3RedirectsStore.js} +50 -18
- package/core/server/adapters/redirects/S3RedirectsStore.ts +163 -0
- package/core/server/adapters/scheduling/scheduling-base.js +41 -0
- package/core/server/adapters/scheduling/types.js +2 -0
- package/core/server/adapters/scheduling/types.ts +36 -0
- package/core/server/api/endpoints/authentication.js +26 -5
- package/core/server/api/endpoints/schedules.js +0 -30
- package/core/server/api/endpoints/utils/serializers/output/authentication.js +10 -0
- package/core/server/data/migrations/versions/6.41/2026-05-13-12-00-00-rename-reset-all-passwords-permission.js +21 -0
- package/core/server/data/schema/fixtures/fixtures.json +3 -3
- package/core/server/models/api-key.js +18 -0
- package/core/server/models/user.js +18 -0
- package/core/server/services/adapter-manager/config.js +6 -0
- package/core/server/services/adapter-manager/index.js +1 -0
- package/core/server/services/auth/index.js +4 -0
- package/core/server/services/auth/reset-authentication.js +50 -0
- package/core/server/services/auth/reset-authentication.ts +75 -0
- package/core/server/services/automations/index.js +21 -15
- package/core/server/services/custom-redirects/index.js +2 -4
- package/core/server/services/gifts/gift-bookshelf-repository.js +7 -0
- package/core/server/services/gifts/gift-bookshelf-repository.ts +10 -0
- package/core/server/services/gifts/gift-reminder-scheduler.js +94 -0
- package/core/server/services/gifts/gift-reminder-scheduler.ts +109 -0
- package/core/server/services/gifts/gift-repository.ts +1 -0
- package/core/server/services/gifts/gift-service-wrapper.js +10 -7
- package/core/server/services/gifts/gift-service.js +1 -32
- package/core/server/services/gifts/gift-service.ts +3 -59
- package/core/server/services/internal-keys/index.js +1 -3
- package/core/server/services/internal-keys/index.ts +11 -3
- package/core/server/services/post-scheduling/index.js +16 -28
- package/core/server/services/post-scheduling/index.ts +14 -0
- package/core/server/services/post-scheduling/post-scheduling.js +115 -0
- package/core/server/services/post-scheduling/post-scheduling.ts +138 -0
- package/core/server/services/users.js +29 -16
- package/core/server/web/api/endpoints/admin/middleware.js +8 -4
- package/core/server/web/api/endpoints/admin/routes.js +1 -1
- package/core/shared/config/defaults.json +5 -0
- package/core/shared/labs.js +2 -1
- package/package.json +1 -1
- package/core/built/admin/assets/minus-DPrXh11r.js +0 -1
- package/core/server/services/custom-redirects/gcs-store.ts +0 -117
- package/core/server/services/post-scheduling/post-scheduler-service.js +0 -126
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape of a job the scheduler adapter queues. Time is a unix timestamp;
|
|
3
|
+
* url carries a JWT-signed admin token; extra forwards through to the
|
|
4
|
+
* HTTP callback the adapter fires.
|
|
5
|
+
*/
|
|
6
|
+
export interface SchedulerJob {
|
|
7
|
+
time: number;
|
|
8
|
+
url: string;
|
|
9
|
+
extra: {
|
|
10
|
+
httpMethod: string;
|
|
11
|
+
oldTime?: number | null;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Implemented by scheduler-using subsystems (post-scheduling, automations,
|
|
17
|
+
* gift reminders). The adapter calls `rescheduleAll` on every registered
|
|
18
|
+
* rescheduler when something requires the queue to be rebuilt — currently
|
|
19
|
+
* after an internal API key rotation.
|
|
20
|
+
*/
|
|
21
|
+
export interface Rescheduler {
|
|
22
|
+
rescheduleAll(opts?: {previousKey?: {id: string; secret: string}}): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The contract Ghost expects of any concrete scheduling adapter. Adapters
|
|
27
|
+
* that extend `SchedulingBase` inherit `register` and `rescheduleAll` for
|
|
28
|
+
* free; the three runtime methods (`schedule`, `unschedule`, `run`) are
|
|
29
|
+
* the ones each adapter implementation must provide.
|
|
30
|
+
*/
|
|
31
|
+
export interface SchedulerAdapter {
|
|
32
|
+
run(): void;
|
|
33
|
+
schedule(job: SchedulerJob): void;
|
|
34
|
+
unschedule(job: SchedulerJob, opts?: {bootstrap?: boolean}): void;
|
|
35
|
+
register(rescheduler: Rescheduler): void;
|
|
36
|
+
}
|
|
@@ -12,7 +12,17 @@ const apiMail = require('./index').mail;
|
|
|
12
12
|
const apiSettings = require('./index').settings;
|
|
13
13
|
const UsersService = require('../../services/users');
|
|
14
14
|
const userService = new UsersService({dbBackup, models, auth, apiMail, apiSettings});
|
|
15
|
-
const
|
|
15
|
+
const adapterManager = require('../../services/adapter-manager');
|
|
16
|
+
const schedulerAdapter = adapterManager.getAdapter('scheduling');
|
|
17
|
+
|
|
18
|
+
async function destroyRequestSession(req) {
|
|
19
|
+
if (!req || !req.session) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
req.session.destroy(err => (err ? reject(err) : resolve()));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
16
26
|
|
|
17
27
|
const messages = {
|
|
18
28
|
notTheBlogOwner: 'You are not the site owner.'
|
|
@@ -224,15 +234,26 @@ const controller = {
|
|
|
224
234
|
}
|
|
225
235
|
},
|
|
226
236
|
|
|
227
|
-
|
|
228
|
-
statusCode:
|
|
237
|
+
reset: {
|
|
238
|
+
statusCode: 200,
|
|
229
239
|
headers: {
|
|
230
240
|
cacheInvalidate: false
|
|
231
241
|
},
|
|
232
242
|
permissions: true,
|
|
233
243
|
async query(frame) {
|
|
234
|
-
await
|
|
235
|
-
|
|
244
|
+
const result = await auth.resetAuthentication({
|
|
245
|
+
schedulerAdapter,
|
|
246
|
+
userService,
|
|
247
|
+
options: frame.options
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Express-session would otherwise re-save the current request's
|
|
251
|
+
// session on the response, resurrecting the row deleteAllSessions
|
|
252
|
+
// just wiped. Destroying the request session prevents the re-save
|
|
253
|
+
// and emits a Set-Cookie that expires the cookie on the client.
|
|
254
|
+
await destroyRequestSession(frame.original.session && frame.original.session.req);
|
|
255
|
+
|
|
256
|
+
return result;
|
|
236
257
|
}
|
|
237
258
|
}
|
|
238
259
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const models = require('../../models');
|
|
2
1
|
const postScheduling = require('../../services/posts/post-scheduling');
|
|
3
2
|
|
|
4
3
|
/** @type {import('@tryghost/api-framework').Controller} */
|
|
@@ -52,35 +51,6 @@ const controller = {
|
|
|
52
51
|
response[resourceType] = [scheduledResource];
|
|
53
52
|
return response;
|
|
54
53
|
}
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
getScheduled: {
|
|
58
|
-
// NOTE: this method is for internal use only by DefaultScheduler
|
|
59
|
-
// it is not exposed anywhere!
|
|
60
|
-
headers: {
|
|
61
|
-
cacheInvalidate: false
|
|
62
|
-
},
|
|
63
|
-
permissions: false,
|
|
64
|
-
validation: {
|
|
65
|
-
options: {
|
|
66
|
-
resource: {
|
|
67
|
-
required: true,
|
|
68
|
-
values: ['posts', 'pages']
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
async query(frame) {
|
|
73
|
-
const resourceModel = 'Post';
|
|
74
|
-
const resourceType = (frame.options.resource === 'post') ? 'post' : 'page';
|
|
75
|
-
const cleanOptions = {};
|
|
76
|
-
cleanOptions.filter = `status:scheduled+type:${resourceType}`;
|
|
77
|
-
cleanOptions.columns = ['id', 'published_at', 'created_at', 'type'];
|
|
78
|
-
|
|
79
|
-
const result = await models[resourceModel].findAll(cleanOptions);
|
|
80
|
-
let response = {};
|
|
81
|
-
response[resourceType] = result;
|
|
82
|
-
return response;
|
|
83
|
-
}
|
|
84
54
|
}
|
|
85
55
|
};
|
|
86
56
|
|
|
@@ -71,5 +71,15 @@ module.exports = {
|
|
|
71
71
|
valid: !!data
|
|
72
72
|
}]
|
|
73
73
|
};
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
reset(data, apiConfig, frame) {
|
|
77
|
+
frame.response = {
|
|
78
|
+
security_action: [{
|
|
79
|
+
action: 'reset_authentication',
|
|
80
|
+
api_keys_rotated: data?.apiKeysRotated ?? 0,
|
|
81
|
+
users_locked: data?.usersLocked ?? 0
|
|
82
|
+
}]
|
|
83
|
+
};
|
|
74
84
|
}
|
|
75
85
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
// The `resetAllPasswords` action on the `authentication` object backed the
|
|
4
|
+
// orphaned `/authentication/global_password_reset` endpoint. That endpoint is
|
|
5
|
+
// replaced by `/authentication/reset`, which rotates every credential
|
|
6
|
+
// (api keys, passwords, sessions) in one shot — so we rename the existing
|
|
7
|
+
// permission row to match its new contract rather than introduce a fresh row
|
|
8
|
+
// alongside a stale one.
|
|
9
|
+
|
|
10
|
+
module.exports = createTransactionalMigration(
|
|
11
|
+
async function up(knex) {
|
|
12
|
+
await knex('permissions')
|
|
13
|
+
.where({action_type: 'resetAllPasswords', object_type: 'authentication'})
|
|
14
|
+
.update({name: 'Reset authentication', action_type: 'reset'});
|
|
15
|
+
},
|
|
16
|
+
async function down(knex) {
|
|
17
|
+
await knex('permissions')
|
|
18
|
+
.where({action_type: 'reset', object_type: 'authentication'})
|
|
19
|
+
.update({name: 'Reset all passwords', action_type: 'resetAllPasswords'});
|
|
20
|
+
}
|
|
21
|
+
);
|
|
@@ -630,8 +630,8 @@
|
|
|
630
630
|
"object_type": "offer"
|
|
631
631
|
},
|
|
632
632
|
{
|
|
633
|
-
"name": "Reset
|
|
634
|
-
"action_type": "
|
|
633
|
+
"name": "Reset authentication",
|
|
634
|
+
"action_type": "reset",
|
|
635
635
|
"object_type": "authentication"
|
|
636
636
|
},
|
|
637
637
|
{
|
|
@@ -957,7 +957,7 @@
|
|
|
957
957
|
"snippet": "all",
|
|
958
958
|
"custom_theme_setting": "all",
|
|
959
959
|
"offer": "all",
|
|
960
|
-
"authentication": "
|
|
960
|
+
"authentication": "reset",
|
|
961
961
|
"members_stripe_connect": "auth",
|
|
962
962
|
"newsletter": "all",
|
|
963
963
|
"explore": "read",
|
|
@@ -3,6 +3,7 @@ const security = require('@tryghost/security');
|
|
|
3
3
|
const ghostBookshelf = require('./base');
|
|
4
4
|
const {Role} = require('./role');
|
|
5
5
|
|
|
6
|
+
// secretlint-disable-next-line @secretlint/secretlint-rule-pattern
|
|
6
7
|
const ApiKey = ghostBookshelf.Model.extend({
|
|
7
8
|
tableName: 'api_keys',
|
|
8
9
|
|
|
@@ -61,6 +62,22 @@ const ApiKey = ghostBookshelf.Model.extend({
|
|
|
61
62
|
refreshSecret(data, options) {
|
|
62
63
|
const secret = security.secret.create(data.type);
|
|
63
64
|
return this.edit(Object.assign({}, data, {secret}), options);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Refresh the secret on every API key row, returning the count rotated.
|
|
69
|
+
* Used by the danger-zone reset flow; callers are expected to wrap this
|
|
70
|
+
* in a transaction.
|
|
71
|
+
*
|
|
72
|
+
* @param {Object} options
|
|
73
|
+
* @returns {Promise<{count: number}>}
|
|
74
|
+
*/
|
|
75
|
+
async refreshAllSecrets(options) {
|
|
76
|
+
const apiKeys = await this.findAll(options);
|
|
77
|
+
for (const apiKey of apiKeys.models) {
|
|
78
|
+
await this.refreshSecret(apiKey.toJSON(), Object.assign({}, options, {id: apiKey.id}));
|
|
79
|
+
}
|
|
80
|
+
return {count: apiKeys.length};
|
|
64
81
|
}
|
|
65
82
|
});
|
|
66
83
|
|
|
@@ -69,6 +86,7 @@ const ApiKeys = ghostBookshelf.Collection.extend({
|
|
|
69
86
|
});
|
|
70
87
|
|
|
71
88
|
module.exports = {
|
|
89
|
+
// secretlint-disable-next-line @secretlint/secretlint-rule-pattern
|
|
72
90
|
ApiKey: ghostBookshelf.model('ApiKey', ApiKey),
|
|
73
91
|
ApiKeys: ghostBookshelf.collection('ApiKeys', ApiKeys)
|
|
74
92
|
};
|
|
@@ -174,6 +174,24 @@ User = ghostBookshelf.Model.extend({
|
|
|
174
174
|
return this.get('status') === 'locked';
|
|
175
175
|
},
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Replace this user's password with an opaque random value, and mark
|
|
179
|
+
* them as locked unless they are already inactive (suspended). Suspended
|
|
180
|
+
* users must not be transitioned out of `inactive` — they retain the
|
|
181
|
+
* suspended-signin path — but their password is still rotated so a
|
|
182
|
+
* compromised credential cannot survive a future unsuspend.
|
|
183
|
+
*/
|
|
184
|
+
lock: function lock(options) {
|
|
185
|
+
const update = {
|
|
186
|
+
// secretlint-disable-next-line @secretlint/secretlint-rule-pattern
|
|
187
|
+
password: security.identifier.uid(50)
|
|
188
|
+
};
|
|
189
|
+
if (this.get('status') !== 'inactive') {
|
|
190
|
+
update.status = 'locked';
|
|
191
|
+
}
|
|
192
|
+
return this.save(update, {...options, patch: true});
|
|
193
|
+
},
|
|
194
|
+
|
|
177
195
|
isInactive: function isInactive() {
|
|
178
196
|
return this.get('status') === 'inactive';
|
|
179
197
|
},
|
|
@@ -25,5 +25,11 @@ module.exports = function getAdapterServiceConfig(config) {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// FileStore needs Ghost's resolved content path, which isn't
|
|
29
|
+
// representable as a static value in defaults.json.
|
|
30
|
+
if (adapterServiceConfig.redirects?.FileStore) {
|
|
31
|
+
adapterServiceConfig.redirects.FileStore.basePath ||= config.getContentPath('data');
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
return adapterServiceConfig;
|
|
29
35
|
};
|
|
@@ -16,6 +16,7 @@ adapterManager.registerAdapter('storage', require('ghost-storage-base'));
|
|
|
16
16
|
adapterManager.registerAdapter('scheduling', require('../../adapters/scheduling/scheduling-base'));
|
|
17
17
|
adapterManager.registerAdapter('sso', require('../../adapters/sso/SSOBase'));
|
|
18
18
|
adapterManager.registerAdapter('cache', require('@tryghost/adapter-base-cache'));
|
|
19
|
+
adapterManager.registerAdapter('redirects', require('../../adapters/redirects/RedirectsStoreBase'));
|
|
19
20
|
|
|
20
21
|
module.exports = {
|
|
21
22
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = resetAuthentication;
|
|
7
|
+
const internal_keys_1 = __importDefault(require("../internal-keys"));
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
|
+
const modelsDefault = require('../../models');
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
+
const { deleteAllSessions: deleteAllSessionsDefault } = require('./session');
|
|
12
|
+
/**
|
|
13
|
+
* Rotation, user lock and the audit row commit in a single transaction so
|
|
14
|
+
* app crashes mid-flight can't leave the system half-rotated or lose the
|
|
15
|
+
* audit trail. Session deletion runs immediately after the commit, before
|
|
16
|
+
* the rescheduleAll, so a failure inside the adapter can't leave stale
|
|
17
|
+
* session rows live for an attacker.
|
|
18
|
+
*
|
|
19
|
+
* The schedulerAdapter and userService come from boot's lifecycle, so they
|
|
20
|
+
* are explicit parameters. The auth-domain primitives (models, internalKeys,
|
|
21
|
+
* sessions) default to their module singletons; tests pass overrides.
|
|
22
|
+
*/
|
|
23
|
+
async function resetAuthentication({ schedulerAdapter, userService, options, models = modelsDefault, internalKeys = internal_keys_1.default, deleteAllSessions = deleteAllSessionsDefault }) {
|
|
24
|
+
const previousSchedulerKey = await internalKeys.get('ghost-scheduler');
|
|
25
|
+
const actorId = options?.context?.user ?? null;
|
|
26
|
+
const { apiKeysRotated, usersLocked } = await models.Base.transaction(async (tx) => {
|
|
27
|
+
const txOptions = Object.assign({}, options, { transacting: tx });
|
|
28
|
+
const { count: rotated } = await models.ApiKey.refreshAllSecrets(txOptions);
|
|
29
|
+
const { count: locked } = await userService.lockAll(txOptions);
|
|
30
|
+
if (actorId) {
|
|
31
|
+
await models.Action.add({
|
|
32
|
+
event: 'edited',
|
|
33
|
+
resource_type: 'security_action',
|
|
34
|
+
resource_id: null,
|
|
35
|
+
actor_type: 'user',
|
|
36
|
+
actor_id: actorId,
|
|
37
|
+
context: {
|
|
38
|
+
action_name: 'reset_authentication',
|
|
39
|
+
api_keys_rotated: rotated,
|
|
40
|
+
users_locked: locked
|
|
41
|
+
}
|
|
42
|
+
}, { transacting: tx, autoRefresh: false });
|
|
43
|
+
}
|
|
44
|
+
return { apiKeysRotated: rotated, usersLocked: locked };
|
|
45
|
+
});
|
|
46
|
+
internalKeys.clear();
|
|
47
|
+
await deleteAllSessions();
|
|
48
|
+
await schedulerAdapter.rescheduleAll({ previousKey: previousSchedulerKey });
|
|
49
|
+
return { apiKeysRotated, usersLocked };
|
|
50
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type {Knex} from 'knex';
|
|
3
|
+
import type {InternalApiKey, InternalKeys} from '../internal-keys';
|
|
4
|
+
import internalKeysDefault from '../internal-keys';
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
6
|
+
const modelsDefault = require('../../models');
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
8
|
+
const {deleteAllSessions: deleteAllSessionsDefault} = require('./session');
|
|
9
|
+
|
|
10
|
+
interface ResetAuthenticationArgs {
|
|
11
|
+
schedulerAdapter: {rescheduleAll(opts: {previousKey?: InternalApiKey}): Promise<unknown>};
|
|
12
|
+
userService: {lockAll(options: any): Promise<{count: number}>};
|
|
13
|
+
options: {context?: {user?: string}; [key: string]: unknown};
|
|
14
|
+
models?: any;
|
|
15
|
+
internalKeys?: InternalKeys;
|
|
16
|
+
deleteAllSessions?: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ResetAuthenticationResult {
|
|
20
|
+
apiKeysRotated: number;
|
|
21
|
+
usersLocked: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Rotation, user lock and the audit row commit in a single transaction so
|
|
26
|
+
* app crashes mid-flight can't leave the system half-rotated or lose the
|
|
27
|
+
* audit trail. Session deletion runs immediately after the commit, before
|
|
28
|
+
* the rescheduleAll, so a failure inside the adapter can't leave stale
|
|
29
|
+
* session rows live for an attacker.
|
|
30
|
+
*
|
|
31
|
+
* The schedulerAdapter and userService come from boot's lifecycle, so they
|
|
32
|
+
* are explicit parameters. The auth-domain primitives (models, internalKeys,
|
|
33
|
+
* sessions) default to their module singletons; tests pass overrides.
|
|
34
|
+
*/
|
|
35
|
+
export default async function resetAuthentication({
|
|
36
|
+
schedulerAdapter,
|
|
37
|
+
userService,
|
|
38
|
+
options,
|
|
39
|
+
models = modelsDefault,
|
|
40
|
+
internalKeys = internalKeysDefault,
|
|
41
|
+
deleteAllSessions = deleteAllSessionsDefault
|
|
42
|
+
}: ResetAuthenticationArgs): Promise<ResetAuthenticationResult> {
|
|
43
|
+
const previousSchedulerKey = await internalKeys.get('ghost-scheduler');
|
|
44
|
+
const actorId = options?.context?.user ?? null;
|
|
45
|
+
|
|
46
|
+
const {apiKeysRotated, usersLocked} = await models.Base.transaction(async (tx: Knex.Transaction) => {
|
|
47
|
+
const txOptions = Object.assign({}, options, {transacting: tx});
|
|
48
|
+
|
|
49
|
+
const {count: rotated} = await models.ApiKey.refreshAllSecrets(txOptions);
|
|
50
|
+
const {count: locked} = await userService.lockAll(txOptions);
|
|
51
|
+
|
|
52
|
+
if (actorId) {
|
|
53
|
+
await models.Action.add({
|
|
54
|
+
event: 'edited',
|
|
55
|
+
resource_type: 'security_action',
|
|
56
|
+
resource_id: null,
|
|
57
|
+
actor_type: 'user',
|
|
58
|
+
actor_id: actorId,
|
|
59
|
+
context: {
|
|
60
|
+
action_name: 'reset_authentication',
|
|
61
|
+
api_keys_rotated: rotated,
|
|
62
|
+
users_locked: locked
|
|
63
|
+
}
|
|
64
|
+
}, {transacting: tx, autoRefresh: false});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {apiKeysRotated: rotated, usersLocked: locked};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
internalKeys.clear();
|
|
71
|
+
await deleteAllSessions();
|
|
72
|
+
await schedulerAdapter.rescheduleAll({previousKey: previousSchedulerKey});
|
|
73
|
+
|
|
74
|
+
return {apiKeysRotated, usersLocked};
|
|
75
|
+
}
|
|
@@ -18,10 +18,12 @@ const memberWelcomeEmailService = require('../member-welcome-emails/service');
|
|
|
18
18
|
* httpMethod: string;
|
|
19
19
|
* };
|
|
20
20
|
* }) => void} schedule
|
|
21
|
+
* @prop {(rescheduler: {rescheduleAll: () => unknown}) => void} register
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
class AutomationsService {
|
|
24
25
|
#initialized = false;
|
|
26
|
+
#enqueuePollNow;
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* @param {object} options
|
|
@@ -36,13 +38,9 @@ class AutomationsService {
|
|
|
36
38
|
return;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
domainEvents.dispatch(StartAutomationsPollEvent.create());
|
|
41
|
-
};
|
|
41
|
+
this.#enqueuePollNow = () => domainEvents.dispatch(StartAutomationsPollEvent.create());
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* @param {Readonly<Date>} date
|
|
45
|
-
*/
|
|
43
|
+
/** @param {Readonly<Date>} date */
|
|
46
44
|
const enqueuePollAt = async (date) => {
|
|
47
45
|
try {
|
|
48
46
|
const key = await internalKeys.get('ghost-scheduler');
|
|
@@ -55,18 +53,26 @@ class AutomationsService {
|
|
|
55
53
|
}
|
|
56
54
|
};
|
|
57
55
|
|
|
58
|
-
domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
}));
|
|
56
|
+
domainEvents.subscribe(StartAutomationsPollEvent, oneAtATime(async () => poll({
|
|
57
|
+
memberWelcomeEmailService,
|
|
58
|
+
enqueueAnotherPollNow: this.#enqueuePollNow,
|
|
59
|
+
enqueueAnotherPollAt: enqueuePollAt
|
|
60
|
+
})));
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
schedulerAdapter.register(this);
|
|
67
63
|
|
|
64
|
+
this.#enqueuePollNow();
|
|
68
65
|
this.#initialized = true;
|
|
69
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Re-arm the poll chain. A queued poll signed under the previous scheduler
|
|
70
|
+
* key fails JWT verification when fired; this dispatches a fresh in-process
|
|
71
|
+
* poll that re-schedules the next callback under the current key.
|
|
72
|
+
*/
|
|
73
|
+
rescheduleAll() {
|
|
74
|
+
this.#enqueuePollNow?.();
|
|
75
|
+
}
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
module.exports = AutomationsService;
|
|
78
|
+
module.exports = new AutomationsService();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const config = require('../../../shared/config');
|
|
2
2
|
const urlUtils = require('../../../shared/url-utils');
|
|
3
|
+
const adapterManager = require('../adapter-manager');
|
|
3
4
|
|
|
4
5
|
const DynamicRedirectManager = require('../lib/dynamic-redirect-manager');
|
|
5
|
-
const {FileStore} = require('./file-store');
|
|
6
6
|
const {RedirectsService} = require('./redirects-service');
|
|
7
7
|
const validation = require('./validation');
|
|
8
8
|
|
|
@@ -18,9 +18,7 @@ module.exports = {
|
|
|
18
18
|
init() {
|
|
19
19
|
redirectManager = makeRedirectManager();
|
|
20
20
|
|
|
21
|
-
const store =
|
|
22
|
-
basePath: config.getContentPath('data')
|
|
23
|
-
});
|
|
21
|
+
const store = adapterManager.getAdapter('redirects');
|
|
24
22
|
|
|
25
23
|
redirectsService = new RedirectsService({
|
|
26
24
|
store,
|
|
@@ -83,6 +83,13 @@ class GiftBookshelfRepository {
|
|
|
83
83
|
});
|
|
84
84
|
return collection.models.map(model => this.toGift(model));
|
|
85
85
|
}
|
|
86
|
+
async findUnsentReminders() {
|
|
87
|
+
const now = new Date().toISOString();
|
|
88
|
+
const collection = await this.model.findAll({
|
|
89
|
+
filter: `status:redeemed+consumes_at:>'${now}'+consumes_soon_reminder_sent_at:null`
|
|
90
|
+
});
|
|
91
|
+
return collection.models.map(model => this.toGift(model));
|
|
92
|
+
}
|
|
86
93
|
async create(gift, options = {}) {
|
|
87
94
|
await this.model.add(this.toRow(gift), options);
|
|
88
95
|
}
|
|
@@ -148,6 +148,16 @@ export class GiftBookshelfRepository implements GiftRepository {
|
|
|
148
148
|
return collection.models.map(model => this.toGift(model));
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
async findUnsentReminders(): Promise<Gift[]> {
|
|
152
|
+
const now = new Date().toISOString();
|
|
153
|
+
|
|
154
|
+
const collection = await this.model.findAll({
|
|
155
|
+
filter: `status:redeemed+consumes_at:>'${now}'+consumes_soon_reminder_sent_at:null`
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return collection.models.map(model => this.toGift(model));
|
|
159
|
+
}
|
|
160
|
+
|
|
151
161
|
async create(gift: Gift, options: RepositoryTransactionOptions = {}) {
|
|
152
162
|
await this.model.add(this.toRow(gift), options);
|
|
153
163
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GiftReminderScheduler = void 0;
|
|
7
|
+
const logging_1 = __importDefault(require("@tryghost/logging"));
|
|
8
|
+
const constants_1 = require("./constants");
|
|
9
|
+
// Same-domain (scheduling) primitives, used unconditionally.
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
|
+
const { getSignedAdminToken } = require('../../adapters/scheduling/utils');
|
|
14
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
15
|
+
const GIFT_REMINDER_LEAD_MS = constants_1.GIFT_REMINDER_LEAD_DAYS * MS_PER_DAY;
|
|
16
|
+
class GiftReminderScheduler {
|
|
17
|
+
#apiUrl;
|
|
18
|
+
#adapter;
|
|
19
|
+
#internalKeys;
|
|
20
|
+
#findUnsentReminders;
|
|
21
|
+
constructor({ apiUrl, adapter, internalKeys, findUnsentReminders }) {
|
|
22
|
+
this.#apiUrl = apiUrl;
|
|
23
|
+
this.#adapter = adapter;
|
|
24
|
+
this.#internalKeys = internalKeys;
|
|
25
|
+
this.#findUnsentReminders = findUnsentReminders;
|
|
26
|
+
this.#adapter.register(this);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Queue a single reminder callback for a freshly-redeemed gift. The
|
|
30
|
+
* callback fires at consumesAt - GIFT_REMINDER_LEAD_DAYS. Already-due
|
|
31
|
+
* reminders are skipped — the daily cron picks them up.
|
|
32
|
+
*/
|
|
33
|
+
async scheduleFor(gift) {
|
|
34
|
+
if (!gift.consumesAt) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const time = gift.consumesAt.getTime() - GIFT_REMINDER_LEAD_MS;
|
|
38
|
+
if (time <= Date.now()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const key = await this.#internalKeys.get('ghost-scheduler');
|
|
43
|
+
this.#adapter.schedule(this.#buildJob(time, key));
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
logging_1.default.error({
|
|
47
|
+
event: { name: 'gift_reminder_scheduler.schedule.failed' },
|
|
48
|
+
err,
|
|
49
|
+
giftToken: gift.token
|
|
50
|
+
}, 'Failed to schedule gift reminder');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Re-issue every queued reminder under the current scheduler key. Pass
|
|
55
|
+
* the pre-rotation secret as `previousKey` so each adapter-queued URL
|
|
56
|
+
* can be reconstructed for unschedule before resigning with the new
|
|
57
|
+
* key. Reminders whose fire time has already passed are skipped — the
|
|
58
|
+
* daily cron picks them up.
|
|
59
|
+
*/
|
|
60
|
+
async rescheduleAll({ previousKey } = {}) {
|
|
61
|
+
const currentKey = await this.#internalKeys.get('ghost-scheduler');
|
|
62
|
+
const unscheduleKey = previousKey ?? currentKey;
|
|
63
|
+
const pending = await this.#findUnsentReminders();
|
|
64
|
+
// Same-key rebuild (no previousKey, boot path) → URL signature is
|
|
65
|
+
// identical to the about-to-be-scheduled job. The default adapter
|
|
66
|
+
// implements unschedule via tombstones keyed by URL+time, so a same-URL
|
|
67
|
+
// unschedule poisons the scheduled job. Bootstrap mode skips the
|
|
68
|
+
// tombstone write. Rotation (previousKey provided) → URLs differ, so
|
|
69
|
+
// the tombstone correctly targets the old queued entry.
|
|
70
|
+
const bootstrap = !previousKey;
|
|
71
|
+
for (const gift of pending) {
|
|
72
|
+
if (!gift.consumesAt) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const time = gift.consumesAt.getTime() - GIFT_REMINDER_LEAD_MS;
|
|
76
|
+
if (time <= Date.now()) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
this.#adapter.unschedule(this.#buildJob(time, unscheduleKey), { bootstrap });
|
|
80
|
+
this.#adapter.schedule(this.#buildJob(time, currentKey));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
#buildJob(time, key) {
|
|
84
|
+
const signedAdminToken = getSignedAdminToken({
|
|
85
|
+
publishedAt: new Date(time).toISOString(),
|
|
86
|
+
apiUrl: this.#apiUrl,
|
|
87
|
+
key
|
|
88
|
+
});
|
|
89
|
+
const url = new URL(urlUtils.urlJoin(this.#apiUrl, 'gifts', 'flush_reminders'));
|
|
90
|
+
url.searchParams.set('token', signedAdminToken);
|
|
91
|
+
return { time, url: url.toString(), extra: { httpMethod: 'PUT' } };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.GiftReminderScheduler = GiftReminderScheduler;
|