ghost 5.116.2 → 5.118.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.
Files changed (131) hide show
  1. package/components/{tryghost-api-framework-5.116.2.tgz → tryghost-api-framework-5.118.0.tgz} +0 -0
  2. package/components/tryghost-constants-5.118.0.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.118.0.tgz +0 -0
  4. package/components/tryghost-custom-theme-settings-service-5.118.0.tgz +0 -0
  5. package/components/tryghost-domain-events-5.118.0.tgz +0 -0
  6. package/components/tryghost-donations-5.118.0.tgz +0 -0
  7. package/components/tryghost-email-addresses-5.118.0.tgz +0 -0
  8. package/components/{tryghost-email-service-5.116.2.tgz → tryghost-email-service-5.118.0.tgz} +0 -0
  9. package/components/tryghost-email-suppression-list-5.118.0.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.118.0.tgz +0 -0
  11. package/components/tryghost-i18n-5.118.0.tgz +0 -0
  12. package/components/{tryghost-job-manager-5.116.2.tgz → tryghost-job-manager-5.118.0.tgz} +0 -0
  13. package/components/tryghost-link-replacer-5.118.0.tgz +0 -0
  14. package/components/{tryghost-magic-link-5.116.2.tgz → tryghost-magic-link-5.118.0.tgz} +0 -0
  15. package/components/{tryghost-member-attribution-5.116.2.tgz → tryghost-member-attribution-5.118.0.tgz} +0 -0
  16. package/components/tryghost-member-events-5.118.0.tgz +0 -0
  17. package/components/{tryghost-members-csv-5.116.2.tgz → tryghost-members-csv-5.118.0.tgz} +0 -0
  18. package/components/{tryghost-members-offers-5.116.2.tgz → tryghost-members-offers-5.118.0.tgz} +0 -0
  19. package/components/tryghost-mw-error-handler-5.118.0.tgz +0 -0
  20. package/components/tryghost-mw-vhost-5.118.0.tgz +0 -0
  21. package/components/{tryghost-post-events-5.116.2.tgz → tryghost-post-events-5.118.0.tgz} +0 -0
  22. package/components/tryghost-post-revisions-5.118.0.tgz +0 -0
  23. package/components/tryghost-posts-service-5.118.0.tgz +0 -0
  24. package/components/tryghost-prometheus-metrics-5.118.0.tgz +0 -0
  25. package/components/tryghost-security-5.118.0.tgz +0 -0
  26. package/components/tryghost-tiers-5.118.0.tgz +0 -0
  27. package/components/tryghost-webmentions-5.118.0.tgz +0 -0
  28. package/content/themes/casper/LICENSE +1 -1
  29. package/content/themes/casper/README.md +1 -1
  30. package/content/themes/casper/assets/built/screen.css +1 -1
  31. package/content/themes/casper/assets/built/screen.css.map +1 -1
  32. package/content/themes/casper/assets/css/screen.css +1 -1
  33. package/content/themes/casper/author.hbs +23 -2
  34. package/content/themes/casper/package.json +2 -2
  35. package/content/themes/casper/partials/icons/bluesky.hbs +3 -0
  36. package/content/themes/casper/partials/icons/instagram.hbs +5 -0
  37. package/content/themes/casper/partials/icons/linkedin.hbs +3 -0
  38. package/content/themes/casper/partials/icons/mastodon.hbs +3 -0
  39. package/content/themes/casper/partials/icons/threads.hbs +3 -0
  40. package/content/themes/casper/partials/icons/tiktok.hbs +3 -0
  41. package/content/themes/casper/partials/icons/twitter.hbs +3 -1
  42. package/content/themes/casper/partials/icons/youtube.hbs +3 -0
  43. package/content/themes/source/LICENSE +1 -1
  44. package/content/themes/source/README.md +1 -1
  45. package/content/themes/source/assets/built/screen.css +1 -1
  46. package/content/themes/source/assets/built/screen.css.map +1 -1
  47. package/content/themes/source/assets/css/screen.css +7 -12
  48. package/content/themes/source/author.hbs +24 -3
  49. package/content/themes/source/package.json +2 -2
  50. package/content/themes/source/partials/feature-image.hbs +2 -2
  51. package/content/themes/source/partials/icons/bluesky.hbs +3 -0
  52. package/content/themes/source/partials/icons/instagram.hbs +5 -0
  53. package/content/themes/source/partials/icons/linkedin.hbs +3 -0
  54. package/content/themes/source/partials/icons/mastodon.hbs +3 -0
  55. package/content/themes/source/partials/icons/threads.hbs +3 -0
  56. package/content/themes/source/partials/icons/tiktok.hbs +3 -0
  57. package/content/themes/source/partials/icons/youtube.hbs +3 -0
  58. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +31793 -26588
  59. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-550846e0.mjs → CodeEditorView-1143c509.mjs} +2 -2
  60. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  61. package/core/built/admin/assets/admin-x-settings/{index-f3cb3f4d.mjs → index-19ebc8ad.mjs} +2 -2
  62. package/core/built/admin/assets/admin-x-settings/{index-4ce2fcd1.mjs → index-ac104f42.mjs} +2635 -2607
  63. package/core/built/admin/assets/admin-x-settings/{modals-6bc20529.mjs → modals-994901ee.mjs} +6680 -6165
  64. package/core/built/admin/assets/{chunk.524.578de86e5014b911b05a.js → chunk.524.5710919eb507b9a81166.js} +8 -8
  65. package/core/built/admin/assets/{chunk.582.21bf3e37b5d84ac4b58a.js → chunk.582.c8cb99b85cfa13fc7df1.js} +10 -10
  66. package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js → chunk.713.48f120c377bcaffdfddf.js} +6 -9
  67. package/core/built/admin/assets/{ghost-868c537d5c02ca65323d0122596a67ec.js → ghost-cd90a28b214ee800a007bb62cd45e6e6.js} +780 -775
  68. package/core/built/admin/assets/posts/posts.js +11561 -11302
  69. package/core/built/admin/assets/stats/stats.js +76076 -59355
  70. package/core/built/admin/index.html +4 -4
  71. package/core/frontend/helpers/social_url.js +31 -0
  72. package/core/server/api/endpoints/users.js +7 -0
  73. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  74. package/core/server/data/migrations/versions/5.117/2025-04-14-02-36-30-add-additional-social-accounts-columns-to-user-table.js +38 -0
  75. package/core/server/data/schema/schema.js +7 -0
  76. package/core/server/services/auth/session/index.js +5 -2
  77. package/core/server/services/auth/session/middleware.js +2 -1
  78. package/core/server/services/auth/session/session-service.js +7 -6
  79. package/core/server/services/members/api.js +2 -2
  80. package/core/server/services/members/members-api/controllers/MemberController.js +214 -0
  81. package/core/server/services/members/members-api/controllers/RouterController.js +667 -0
  82. package/core/server/services/members/members-api/controllers/WellKnownController.js +46 -0
  83. package/core/server/services/members/members-api/members-api.js +404 -0
  84. package/core/server/services/members/members-api/repositories/EventRepository.js +984 -0
  85. package/core/server/services/members/members-api/repositories/MemberRepository.js +1739 -0
  86. package/core/server/services/members/members-api/repositories/ProductRepository.js +662 -0
  87. package/core/server/services/members/members-api/services/GeolocationService.js +23 -0
  88. package/core/server/services/members/members-api/services/MemberBREADService.js +444 -0
  89. package/core/server/services/members/members-api/services/PaymentsService.js +522 -0
  90. package/core/server/services/members/members-api/services/TokenService.js +54 -0
  91. package/core/server/services/milestones/BookshelfMilestoneRepository.js +8 -9
  92. package/core/server/services/milestones/InMemoryMilestoneRepository.js +119 -0
  93. package/core/server/services/milestones/Milestone.js +231 -0
  94. package/core/server/services/milestones/MilestoneCreatedEvent.js +22 -0
  95. package/core/server/services/milestones/MilestonesService.js +327 -0
  96. package/core/server/services/milestones/service.js +2 -2
  97. package/core/server/services/newsletters/index.js +1 -1
  98. package/core/server/services/public-config/config.js +2 -1
  99. package/core/server/services/settings/settings-service.js +1 -1
  100. package/core/server/services/slack-notifications/SlackNotifications.js +1 -1
  101. package/core/server/services/slack-notifications/SlackNotificationsService.js +2 -2
  102. package/core/server/services/staff/StaffService.js +1 -1
  103. package/core/shared/config/defaults.json +3 -0
  104. package/core/shared/config/env/config.testing-mysql.json +3 -0
  105. package/core/shared/config/env/config.testing.json +3 -0
  106. package/core/shared/labs.js +2 -2
  107. package/package.json +63 -63
  108. package/tsconfig.tsbuildinfo +1 -1
  109. package/yarn.lock +306 -70
  110. package/components/tryghost-constants-5.116.2.tgz +0 -0
  111. package/components/tryghost-custom-fonts-5.116.2.tgz +0 -0
  112. package/components/tryghost-custom-theme-settings-service-5.116.2.tgz +0 -0
  113. package/components/tryghost-domain-events-5.116.2.tgz +0 -0
  114. package/components/tryghost-donations-5.116.2.tgz +0 -0
  115. package/components/tryghost-email-addresses-5.116.2.tgz +0 -0
  116. package/components/tryghost-email-suppression-list-5.116.2.tgz +0 -0
  117. package/components/tryghost-html-to-plaintext-5.116.2.tgz +0 -0
  118. package/components/tryghost-i18n-5.116.2.tgz +0 -0
  119. package/components/tryghost-link-replacer-5.116.2.tgz +0 -0
  120. package/components/tryghost-member-events-5.116.2.tgz +0 -0
  121. package/components/tryghost-members-api-5.116.2.tgz +0 -0
  122. package/components/tryghost-milestones-5.116.2.tgz +0 -0
  123. package/components/tryghost-mw-error-handler-5.116.2.tgz +0 -0
  124. package/components/tryghost-mw-vhost-5.116.2.tgz +0 -0
  125. package/components/tryghost-post-revisions-5.116.2.tgz +0 -0
  126. package/components/tryghost-posts-service-5.116.2.tgz +0 -0
  127. package/components/tryghost-prometheus-metrics-5.116.2.tgz +0 -0
  128. package/components/tryghost-security-5.116.2.tgz +0 -0
  129. package/components/tryghost-tiers-5.116.2.tgz +0 -0
  130. package/components/tryghost-webmentions-5.116.2.tgz +0 -0
  131. /package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js.LICENSE.txt → chunk.713.48f120c377bcaffdfddf.js.LICENSE.txt} +0 -0
@@ -0,0 +1,662 @@
1
+ const {UpdateCollisionError, NotFoundError, MethodNotAllowedError, ValidationError, BadRequestError} = require('@tryghost/errors');
2
+ const tpl = require('@tryghost/tpl');
3
+
4
+ const messages = {
5
+ priceMustBeInteger: 'Tier prices must be an integer.',
6
+ priceIsNegative: 'Tier prices must not be negative',
7
+ maxPriceExceeded: 'Tier prices may not exceed 999999.99'
8
+ };
9
+
10
+ /**
11
+ * @typedef {object} ProductModel
12
+ */
13
+
14
+ /**
15
+ * @typedef {object} StripePriceInput
16
+ * @param {string} nickname
17
+ * @param {string} currency
18
+ * @param {number} amount
19
+ * @param {'recurring'|'one-time'} type
20
+ * @param {string | null} interval
21
+ * @param {string?} stripe_product_id
22
+ * @param {string?} stripe_price_id
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} BenefitInput
27
+ * @param {string} name
28
+ */
29
+
30
+ function validatePrice(price) {
31
+ if (!Number.isInteger(price.amount)) {
32
+ throw new ValidationError({
33
+ message: tpl(messages.priceMustBeInteger)
34
+ });
35
+ }
36
+
37
+ if (price.amount < 0) {
38
+ throw new ValidationError({
39
+ message: tpl(messages.priceIsNegative)
40
+ });
41
+ }
42
+
43
+ if (price.amount > 9999999999) {
44
+ throw new ValidationError({
45
+ message: tpl(messages.maxPriceExceeded)
46
+ });
47
+ }
48
+ }
49
+
50
+ class ProductRepository {
51
+ /**
52
+ * @param {object} deps
53
+ * @param {any} deps.Product
54
+ * @param {any} deps.Settings
55
+ * @param {any} deps.StripeProduct
56
+ * @param {any} deps.StripePrice
57
+ * @param {import('@tryghost/members-api/lib/services/stripe-api')} deps.stripeAPIService
58
+ */
59
+ constructor({
60
+ Product,
61
+ Settings,
62
+ StripeProduct,
63
+ StripePrice,
64
+ stripeAPIService
65
+ }) {
66
+ this._Product = Product;
67
+ this._Settings = Settings;
68
+ this._StripeProduct = StripeProduct;
69
+ this._StripePrice = StripePrice;
70
+ this._stripeAPIService = stripeAPIService;
71
+ }
72
+
73
+ /**
74
+ * Retrieves a Product by either stripe_product_id, stripe_price_id, id or slug
75
+ *
76
+ * @param {{stripe_product_id: string} | {stripe_price_id: string} | {id: string} | {slug: string}} data
77
+ * @param {object} options
78
+ *
79
+ * @returns {Promise<ProductModel>}
80
+ */
81
+ async get(data, options = {}) {
82
+ if (!options.transacting) {
83
+ return this._Product.transaction((transacting) => {
84
+ return this.get(data, {
85
+ ...options,
86
+ transacting
87
+ });
88
+ });
89
+ }
90
+ if ('stripe_product_id' in data) {
91
+ const stripeProduct = await this._StripeProduct.findOne({
92
+ stripe_product_id: data.stripe_product_id
93
+ }, options);
94
+
95
+ if (!stripeProduct) {
96
+ return null;
97
+ }
98
+
99
+ return await stripeProduct.related('product').fetch(options);
100
+ }
101
+
102
+ if ('stripe_price_id' in data) {
103
+ const stripePrice = await this._StripePrice.findOne({
104
+ stripe_price_id: data.stripe_price_id
105
+ }, options);
106
+
107
+ if (!stripePrice) {
108
+ return null;
109
+ }
110
+
111
+ const stripeProduct = await stripePrice.related('stripeProduct').fetch(options);
112
+
113
+ if (!stripeProduct) {
114
+ return null;
115
+ }
116
+
117
+ return await stripeProduct.related('product').fetch(options);
118
+ }
119
+
120
+ if ('id' in data) {
121
+ return await this._Product.findOne({id: data.id}, options);
122
+ }
123
+
124
+ if ('slug' in data) {
125
+ return await this._Product.findOne({slug: data.slug}, options);
126
+ }
127
+
128
+ throw new NotFoundError({message: 'Missing id, slug, stripe_product_id or stripe_price_id from data'});
129
+ }
130
+
131
+ /**
132
+ * Fetches the default product
133
+ * @param {Object} options
134
+ * @returns {Promise<ProductModel>}
135
+ */
136
+ async getDefaultProduct(options = {}) {
137
+ const defaultProductPage = await this.list({
138
+ filter: 'type:paid+active:true',
139
+ limit: 1,
140
+ ...options
141
+ });
142
+
143
+ return defaultProductPage.data[0];
144
+ }
145
+
146
+ /**
147
+ * Creates a product from a name
148
+ *
149
+ * @param {object} data
150
+ * @param {string} data.name
151
+ * @param {string} data.description
152
+ * @param {'public'|'none'} data.visibility
153
+ * @param {string} data.welcome_page_url
154
+ * @param {BenefitInput[]} data.benefits
155
+ * @param {StripePriceInput[]} data.stripe_prices
156
+ * @param {StripePriceInput|null} data.monthly_price
157
+ * @param {StripePriceInput|null} data.yearly_price
158
+ * @param {string} data.product_id
159
+ * @param {string} data.stripe_product_id
160
+ * @param {number} data.trial_days
161
+ *
162
+ * @param {object} options
163
+ *
164
+ * @returns {Promise<ProductModel>}
165
+ **/
166
+ async create(data, options = {}) {
167
+ if (!this._stripeAPIService.configured && (data.stripe_prices || data.monthly_price || data.yearly_price)) {
168
+ throw new UpdateCollisionError({
169
+ message: 'The requested functionality requires Stripe to be configured. See https://ghost.org/integrations/stripe/',
170
+ code: 'STRIPE_NOT_CONFIGURED'
171
+ });
172
+ }
173
+
174
+ if (!options.transacting) {
175
+ return this._Product.transaction((transacting) => {
176
+ return this.create(data, {
177
+ ...options,
178
+ transacting
179
+ });
180
+ });
181
+ }
182
+
183
+ if (data.monthly_price) {
184
+ validatePrice(data.monthly_price);
185
+ }
186
+
187
+ if (data.yearly_price) {
188
+ validatePrice(data.monthly_price);
189
+ }
190
+
191
+ if (data.yearly_price && data.monthly_price && data.yearly_price.currency !== data.monthly_price.currency) {
192
+ throw new BadRequestError({
193
+ message: 'The monthly and yearly price must use the same currency'
194
+ });
195
+ }
196
+
197
+ if (data.stripe_prices) {
198
+ data.stripe_prices.forEach(validatePrice);
199
+ }
200
+
201
+ const productData = {
202
+ type: 'paid',
203
+ active: true,
204
+ visibility: data.visibility,
205
+ name: data.name,
206
+ description: data.description,
207
+ benefits: data.benefits,
208
+ welcome_page_url: data.welcome_page_url
209
+ };
210
+
211
+ if (data.monthly_price) {
212
+ productData.monthly_price = data.monthly_price.amount;
213
+ productData.currency = data.monthly_price.currency;
214
+ }
215
+
216
+ if (data.yearly_price) {
217
+ productData.yearly_price = data.yearly_price.amount;
218
+ productData.currency = data.yearly_price.currency;
219
+ }
220
+
221
+ if (Reflect.has(data, 'trial_days')) {
222
+ productData.trial_days = data.trial_days;
223
+ }
224
+
225
+ const product = await this._Product.add(productData, options);
226
+
227
+ if (this._stripeAPIService.configured) {
228
+ const stripeProduct = await this._stripeAPIService.createProduct({
229
+ name: productData.name
230
+ });
231
+
232
+ await this._StripeProduct.add({
233
+ product_id: product.id,
234
+ stripe_product_id: stripeProduct.id
235
+ }, options);
236
+
237
+ if (data.monthly_price || data.yearly_price) {
238
+ if (data.monthly_price) {
239
+ const price = await this._stripeAPIService.createPrice({
240
+ product: stripeProduct.id,
241
+ active: true,
242
+ nickname: `Monthly`,
243
+ currency: data.monthly_price.currency,
244
+ amount: data.monthly_price.amount,
245
+ type: 'recurring',
246
+ interval: 'month'
247
+ });
248
+
249
+ const stripePrice = await this._StripePrice.add({
250
+ stripe_price_id: price.id,
251
+ stripe_product_id: stripeProduct.id,
252
+ active: true,
253
+ nickname: price.nickname,
254
+ currency: price.currency,
255
+ amount: price.unit_amount,
256
+ type: 'recurring',
257
+ interval: 'month'
258
+ }, options);
259
+
260
+ await this._Product.edit({monthly_price_id: stripePrice.id}, {id: product.id, transacting: options.transacting});
261
+ }
262
+
263
+ if (data.yearly_price) {
264
+ const price = await this._stripeAPIService.createPrice({
265
+ product: stripeProduct.id,
266
+ active: true,
267
+ nickname: `Yearly`,
268
+ currency: data.yearly_price.currency,
269
+ amount: data.yearly_price.amount,
270
+ type: 'recurring',
271
+ interval: 'year'
272
+ });
273
+
274
+ const stripePrice = await this._StripePrice.add({
275
+ stripe_price_id: price.id,
276
+ stripe_product_id: stripeProduct.id,
277
+ active: true,
278
+ nickname: price.nickname,
279
+ currency: price.currency,
280
+ amount: price.unit_amount,
281
+ type: 'recurring',
282
+ interval: 'year'
283
+ }, options);
284
+
285
+ await this._Product.edit({yearly_price_id: stripePrice.id}, {id: product.id, transacting: options.transacting});
286
+ }
287
+ } else if (data.stripe_prices) {
288
+ for (const newPrice of data.stripe_prices) {
289
+ const price = await this._stripeAPIService.createPrice({
290
+ product: stripeProduct.id,
291
+ active: true,
292
+ nickname: newPrice.nickname,
293
+ currency: newPrice.currency,
294
+ amount: newPrice.amount,
295
+ type: newPrice.type,
296
+ interval: newPrice.interval
297
+ });
298
+
299
+ await this._StripePrice.add({
300
+ stripe_price_id: price.id,
301
+ stripe_product_id: stripeProduct.id,
302
+ active: true,
303
+ nickname: newPrice.nickname,
304
+ currency: newPrice.currency,
305
+ amount: newPrice.amount,
306
+ type: newPrice.type,
307
+ interval: newPrice.interval
308
+ }, options);
309
+ }
310
+ }
311
+
312
+ await product.related('stripePrices').fetch(options);
313
+ await product.related('monthlyPrice').fetch(options);
314
+ await product.related('yearlyPrice').fetch(options);
315
+ }
316
+
317
+ return product;
318
+ }
319
+
320
+ /**
321
+ * Updates a product by id
322
+ *
323
+ * @param {object} data
324
+ * @param {string} data.id
325
+ * @param {string} data.name
326
+ * @param {string} data.description
327
+ * @param {number} data.trial_days
328
+ * @param {'public'|'none'} data.visibility
329
+ * @param {string} data.welcome_page_url
330
+ * @param {BenefitInput[]} data.benefits
331
+ *
332
+ * @param {StripePriceInput[]} [data.stripe_prices]
333
+ * @param {StripePriceInput|null} data.monthly_price
334
+ * @param {StripePriceInput|null} data.yearly_price
335
+ *
336
+ * @param {object} options
337
+ *
338
+ * @returns {Promise<ProductModel>}
339
+ **/
340
+ async update(data, options = {}) {
341
+ if (!this._stripeAPIService.configured && (data.stripe_prices || data.monthly_price || data.yearly_price)) {
342
+ throw new UpdateCollisionError({
343
+ message: 'The requested functionality requires Stripe to be configured. See https://ghost.org/integrations/stripe/',
344
+ code: 'STRIPE_NOT_CONFIGURED'
345
+ });
346
+ }
347
+
348
+ if (!options.transacting) {
349
+ return this._Product.transaction((transacting) => {
350
+ return this.update(data, {
351
+ ...options,
352
+ transacting
353
+ });
354
+ });
355
+ }
356
+
357
+ if (data.monthly_price) {
358
+ validatePrice(data.monthly_price);
359
+ }
360
+
361
+ if (data.yearly_price) {
362
+ validatePrice(data.monthly_price);
363
+ }
364
+
365
+ if (data.stripe_prices) {
366
+ data.stripe_prices.forEach(validatePrice);
367
+ }
368
+
369
+ if (data.yearly_price && data.monthly_price && data.yearly_price.currency !== data.monthly_price.currency) {
370
+ throw new BadRequestError({
371
+ message: 'The monthly and yearly price must use the same currency'
372
+ });
373
+ }
374
+
375
+ const productId = data.id || options.id;
376
+
377
+ const existingProduct = await this._Product.findOne({id: productId}, options);
378
+
379
+ let productData = {
380
+ name: data.name,
381
+ visibility: data.visibility,
382
+ description: data.description,
383
+ benefits: data.benefits,
384
+ welcome_page_url: data.welcome_page_url
385
+ };
386
+
387
+ if (data.monthly_price) {
388
+ productData.monthly_price = data.monthly_price.amount;
389
+ productData.currency = data.monthly_price.currency;
390
+ }
391
+
392
+ if (data.yearly_price) {
393
+ productData.yearly_price = data.yearly_price.amount;
394
+ productData.currency = data.yearly_price.currency;
395
+ }
396
+
397
+ if (Reflect.has(data, 'active')) {
398
+ productData.active = data.active;
399
+ }
400
+
401
+ if (Reflect.has(data, 'trial_days')) {
402
+ productData.trial_days = data.trial_days;
403
+ }
404
+
405
+ if (existingProduct.get('type') === 'free') {
406
+ delete productData.name;
407
+ delete productData.active;
408
+ delete productData.trial_days;
409
+ }
410
+
411
+ if (existingProduct.get('active') === true && productData.active === false) {
412
+ const portalProductsSetting = await this._Settings.findOne({
413
+ key: 'portal_products'
414
+ }, options);
415
+
416
+ let portalProducts;
417
+ try {
418
+ portalProducts = JSON.parse(portalProductsSetting.get('value'));
419
+ } catch (err) {
420
+ portalProducts = [];
421
+ }
422
+
423
+ const updatedProducts = portalProducts.filter(product => product !== productId);
424
+
425
+ await this._Settings.edit({
426
+ key: 'portal_products',
427
+ value: JSON.stringify(updatedProducts)
428
+ }, {
429
+ ...options,
430
+ id: portalProductsSetting.get('id')
431
+ });
432
+ }
433
+
434
+ let product = await this._Product.edit(productData, {
435
+ ...options,
436
+ id: productId
437
+ });
438
+
439
+ if (this._stripeAPIService.configured && product.get('type') !== 'free') {
440
+ await product.related('stripeProducts').fetch(options);
441
+
442
+ if (!product.related('stripeProducts').first()) {
443
+ const stripeProduct = await this._stripeAPIService.createProduct({
444
+ name: product.get('name')
445
+ });
446
+
447
+ await this._StripeProduct.add({
448
+ product_id: product.id,
449
+ stripe_product_id: stripeProduct.id
450
+ }, options);
451
+
452
+ await product.related('stripeProducts').fetch(options);
453
+ } else {
454
+ if (product.attributes.name !== product._previousAttributes.name) {
455
+ const stripeProduct = product.related('stripeProducts').first();
456
+ await this._stripeAPIService.updateProduct(stripeProduct.get('stripe_product_id'), {
457
+ name: product.get('name')
458
+ });
459
+ }
460
+ }
461
+
462
+ const defaultStripeProduct = product.related('stripeProducts').first();
463
+
464
+ if (data.monthly_price || data.yearly_price) {
465
+ if (data.monthly_price) {
466
+ const existingPrice = await this._StripePrice.findOne({
467
+ stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
468
+ amount: data.monthly_price.amount,
469
+ currency: data.monthly_price.currency,
470
+ type: 'recurring',
471
+ interval: 'month',
472
+ active: true
473
+ }, options);
474
+ let priceModel;
475
+ if (existingPrice) {
476
+ priceModel = existingPrice;
477
+
478
+ await this._stripeAPIService.updatePrice(priceModel.get('stripe_price_id'), {
479
+ active: true
480
+ });
481
+
482
+ await this._StripePrice.edit({
483
+ active: true
484
+ }, {...options, id: priceModel.id});
485
+ } else {
486
+ const price = await this._stripeAPIService.createPrice({
487
+ product: defaultStripeProduct.get('stripe_product_id'),
488
+ active: true,
489
+ nickname: `Monthly`,
490
+ currency: data.monthly_price.currency,
491
+ amount: data.monthly_price.amount,
492
+ type: 'recurring',
493
+ interval: 'month'
494
+ });
495
+
496
+ const stripePrice = await this._StripePrice.add({
497
+ stripe_price_id: price.id,
498
+ stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
499
+ active: true,
500
+ nickname: price.nickname,
501
+ currency: price.currency,
502
+ amount: price.unit_amount,
503
+ type: 'recurring',
504
+ interval: 'month'
505
+ }, options);
506
+
507
+ priceModel = stripePrice;
508
+ }
509
+
510
+ product = await this._Product.edit({monthly_price_id: priceModel.id}, {...options, id: product.id});
511
+ }
512
+
513
+ if (data.yearly_price) {
514
+ const existingPrice = await this._StripePrice.findOne({
515
+ stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
516
+ amount: data.yearly_price.amount,
517
+ currency: data.yearly_price.currency,
518
+ type: 'recurring',
519
+ interval: 'year',
520
+ active: true
521
+ }, options);
522
+ let priceModel;
523
+
524
+ if (existingPrice) {
525
+ priceModel = existingPrice;
526
+
527
+ await this._stripeAPIService.updatePrice(priceModel.get('stripe_price_id'), {
528
+ active: true
529
+ });
530
+
531
+ await this._StripePrice.edit({
532
+ active: true
533
+ }, {...options, id: priceModel.id});
534
+ } else {
535
+ const price = await this._stripeAPIService.createPrice({
536
+ product: defaultStripeProduct.get('stripe_product_id'),
537
+ active: true,
538
+ nickname: `Yearly`,
539
+ currency: data.yearly_price.currency,
540
+ amount: data.yearly_price.amount,
541
+ type: 'recurring',
542
+ interval: 'year'
543
+ });
544
+
545
+ const stripePrice = await this._StripePrice.add({
546
+ stripe_price_id: price.id,
547
+ stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
548
+ active: true,
549
+ nickname: price.nickname,
550
+ currency: price.currency,
551
+ amount: price.unit_amount,
552
+ type: 'recurring',
553
+ interval: 'year'
554
+ }, options);
555
+
556
+ priceModel = stripePrice;
557
+ }
558
+
559
+ product = await this._Product.edit({yearly_price_id: priceModel.id}, {...options, id: product.id});
560
+ }
561
+ } else if (data.stripe_prices) {
562
+ const newPrices = data.stripe_prices.filter(price => !price.stripe_price_id);
563
+ const existingPrices = data.stripe_prices.filter((price) => {
564
+ return !!price.stripe_price_id && !!price.stripe_product_id;
565
+ });
566
+
567
+ for (const existingPrice of existingPrices) {
568
+ const existingProductId = existingPrice.stripe_product_id;
569
+ let stripeProduct = await this._StripeProduct.findOne({stripe_product_id: existingProductId}, options);
570
+ if (!stripeProduct) {
571
+ stripeProduct = await this._StripeProduct.add({
572
+ product_id: product.id,
573
+ stripe_product_id: existingProductId
574
+ }, options);
575
+ }
576
+ const stripePrice = await this._StripePrice.findOne({stripe_price_id: existingPrice.stripe_price_id}, options);
577
+
578
+ if (!stripePrice) {
579
+ await this._StripePrice.add({
580
+ stripe_price_id: existingPrice.stripe_price_id,
581
+ stripe_product_id: stripeProduct.get('stripe_product_id'),
582
+ active: existingPrice.active,
583
+ nickname: existingPrice.nickname,
584
+ description: existingPrice.description,
585
+ currency: existingPrice.currency,
586
+ amount: existingPrice.amount,
587
+ type: existingPrice.type,
588
+ interval: existingPrice.interval
589
+ }, options);
590
+ } else {
591
+ const updated = await this._StripePrice.edit({
592
+ nickname: existingPrice.nickname,
593
+ description: existingPrice.description,
594
+ active: existingPrice.active
595
+ }, {
596
+ ...options,
597
+ id: stripePrice.id
598
+ });
599
+
600
+ await this._stripeAPIService.updatePrice(updated.get('stripe_price_id'), {
601
+ nickname: updated.get('nickname'),
602
+ active: updated.get('active')
603
+ });
604
+ }
605
+ }
606
+
607
+ for (const newPrice of newPrices) {
608
+ const newProductId = newPrice.stripe_product_id;
609
+ const stripeProduct = newProductId ?
610
+ await this._StripeProduct.findOne({stripe_product_id: newProductId}, options) : defaultStripeProduct;
611
+
612
+ const price = await this._stripeAPIService.createPrice({
613
+ product: stripeProduct.get('stripe_product_id'),
614
+ active: true,
615
+ nickname: newPrice.nickname,
616
+ currency: newPrice.currency,
617
+ amount: newPrice.amount,
618
+ type: newPrice.type,
619
+ interval: newPrice.interval
620
+ });
621
+
622
+ await this._StripePrice.add({
623
+ stripe_price_id: price.id,
624
+ stripe_product_id: stripeProduct.get('stripe_product_id'),
625
+ active: price.active,
626
+ nickname: price.nickname,
627
+ description: newPrice.description,
628
+ currency: price.currency,
629
+ amount: price.unit_amount,
630
+ type: price.type,
631
+ interval: price.recurring && price.recurring.interval || null
632
+ }, options);
633
+ }
634
+ }
635
+
636
+ await product.related('stripeProducts').fetch(options);
637
+ await product.related('stripePrices').fetch(options);
638
+ await product.related('monthlyPrice').fetch(options);
639
+ await product.related('yearlyPrice').fetch(options);
640
+ await product.related('benefits').fetch(options);
641
+ }
642
+
643
+ return product;
644
+ }
645
+
646
+ /**
647
+ * Returns a paginated list of Products
648
+ *
649
+ * @params {object} options
650
+ *
651
+ * @returns {Promise<{data: ProductModel[], meta: object}>}
652
+ **/
653
+ async list(options = {}) {
654
+ return this._Product.findPage(options);
655
+ }
656
+
657
+ async destroy() {
658
+ throw new MethodNotAllowedError({message: 'Cannot destroy products, yet...'});
659
+ }
660
+ }
661
+
662
+ module.exports = ProductRepository;
@@ -0,0 +1,23 @@
1
+ const got = require('got');
2
+ const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
3
+ const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
4
+
5
+ module.exports = class GeolocationService {
6
+ async getGeolocationFromIP(ipAddress) {
7
+ if (!ipAddress || (!IPV4_REGEX.test(ipAddress) && !IPV6_REGEX.test(ipAddress))) {
8
+ return;
9
+ }
10
+
11
+ const gotOpts = {
12
+ timeout: 500
13
+ };
14
+
15
+ if (process.env.NODE_ENV?.startsWith('test')) {
16
+ gotOpts.retry = 0;
17
+ }
18
+
19
+ const geojsUrl = `https://get.geojs.io/v1/ip/geo/${encodeURIComponent(ipAddress)}.json`;
20
+ const response = await got(geojsUrl, gotOpts).json();
21
+ return response;
22
+ }
23
+ };