ghost 5.114.1 → 5.115.1
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/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.114.1.tgz → tryghost-adapter-manager-5.115.1.tgz} +0 -0
- package/components/{tryghost-announcement-bar-settings-5.114.1.tgz → tryghost-announcement-bar-settings-5.115.1.tgz} +0 -0
- package/components/{tryghost-api-framework-5.114.1.tgz → tryghost-api-framework-5.115.1.tgz} +0 -0
- package/components/tryghost-constants-5.115.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.114.1.tgz → tryghost-custom-theme-settings-service-5.115.1.tgz} +0 -0
- package/components/{tryghost-data-generator-5.114.1.tgz → tryghost-data-generator-5.115.1.tgz} +0 -0
- package/components/{tryghost-domain-events-5.114.1.tgz → tryghost-domain-events-5.115.1.tgz} +0 -0
- package/components/tryghost-donations-5.115.1.tgz +0 -0
- package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.114.1.tgz → tryghost-email-content-generator-5.115.1.tgz} +0 -0
- package/components/tryghost-email-events-5.115.1.tgz +0 -0
- package/components/tryghost-email-service-5.115.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
- package/components/tryghost-ghost-5.115.1.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.114.1.tgz → tryghost-html-to-plaintext-5.115.1.tgz} +0 -0
- package/components/tryghost-i18n-5.115.1.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
- package/components/{tryghost-job-manager-5.114.1.tgz → tryghost-job-manager-5.115.1.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.114.1.tgz → tryghost-link-redirects-5.115.1.tgz} +0 -0
- package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
- package/components/{tryghost-magic-link-5.114.1.tgz → tryghost-magic-link-5.115.1.tgz} +0 -0
- package/components/{tryghost-mailgun-client-5.114.1.tgz → tryghost-mailgun-client-5.115.1.tgz} +0 -0
- package/components/tryghost-member-attribution-5.115.1.tgz +0 -0
- package/components/{tryghost-member-events-5.114.1.tgz → tryghost-member-events-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-api-5.114.1.tgz → tryghost-members-api-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-csv-5.114.1.tgz → tryghost-members-csv-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-offers-5.114.1.tgz → tryghost-members-offers-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-payments-5.114.1.tgz → tryghost-members-payments-5.115.1.tgz} +0 -0
- package/components/{tryghost-milestones-5.114.1.tgz → tryghost-milestones-5.115.1.tgz} +0 -0
- package/components/tryghost-minifier-5.115.1.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.114.1.tgz → tryghost-mw-error-handler-5.115.1.tgz} +0 -0
- package/components/{tryghost-mw-version-match-5.114.1.tgz → tryghost-mw-version-match-5.115.1.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
- package/components/{tryghost-post-events-5.114.1.tgz → tryghost-post-events-5.115.1.tgz} +0 -0
- package/components/{tryghost-post-revisions-5.114.1.tgz → tryghost-post-revisions-5.115.1.tgz} +0 -0
- package/components/{tryghost-posts-service-5.114.1.tgz → tryghost-posts-service-5.115.1.tgz} +0 -0
- package/components/{tryghost-prometheus-metrics-5.114.1.tgz → tryghost-prometheus-metrics-5.115.1.tgz} +0 -0
- package/components/tryghost-recommendations-5.115.1.tgz +0 -0
- package/components/{tryghost-security-5.114.1.tgz → tryghost-security-5.115.1.tgz} +0 -0
- package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
- package/components/{tryghost-tiers-5.114.1.tgz → tryghost-tiers-5.115.1.tgz} +0 -0
- package/components/{tryghost-webmentions-5.114.1.tgz → tryghost-webmentions-5.115.1.tgz} +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +11 -6
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/core/boot.js +3 -1
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +23497 -23041
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
- package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-15df2af5.mjs} +4 -3
- package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-8ca61d78.mjs} +67 -65
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-d2e6872f.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-8e8821e5.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f5cb3db3.mjs} +3104 -3094
- package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-e8ae4d46.mjs} +3 -3
- package/core/built/admin/assets/{chunk.524.85c5b32bd46b91c147b9.js → chunk.524.2439684964c164c598ab.js} +7 -7
- package/core/built/admin/assets/{chunk.582.449a129a8005f03574bd.js → chunk.582.bf5a2bbb2c4eb69ef1e7.js} +10 -10
- package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +1 -0
- package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +1 -0
- package/core/built/admin/assets/{ghost-c563138cc2c0767bf6eefc9a2587eaa4.js → ghost-df7b9558260aa27d18b195ee895b487d.js} +182 -160
- package/core/built/admin/assets/stats/stats.js +11824 -0
- package/core/built/admin/index.html +4 -4
- package/core/frontend/helpers/ghost_head.js +3 -1
- package/core/frontend/src/cards/css/cta.css +1 -1
- package/core/server/api/endpoints/slugs.js +6 -2
- package/core/server/data/importer/import-manager.js +2 -2
- package/core/server/data/importer/importers/importer-revue.js +128 -0
- package/core/server/data/importer/importers/json-to-html.js +107 -0
- package/core/server/data/migrations/utils/tables.js +2 -4
- package/core/server/data/migrations/versions/5.115/2025-03-24-07-19-27-add-identity-read-permission-to-administrators.js +6 -0
- package/core/server/data/schema/fixtures/fixtures.json +2 -1
- package/core/server/lib/bootstrap-socket.js +87 -0
- package/core/server/lib/package-json/index.js +1 -0
- package/core/server/lib/package-json/package-json.js +160 -0
- package/core/server/lib/package-json/parse.js +57 -0
- package/core/server/models/base/plugins/actions.js +44 -31
- package/core/server/models/base/plugins/generate-slug.js +6 -0
- package/core/server/notify.js +1 -1
- package/core/server/services/activitypub/ActivityPubService.ts +1 -1
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
- package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
- package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
- package/core/server/services/api-version-compatibility/index.js +2 -2
- package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
- package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
- package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
- package/core/server/services/audience-feedback/Feedback.js +35 -0
- package/core/server/services/audience-feedback/index.js +4 -2
- package/core/server/services/auth/session/emails/signin.js +168 -0
- package/core/server/services/auth/session/index.js +2 -2
- package/core/server/services/auth/session/session-from-token.js +69 -0
- package/core/server/services/auth/session/session-service.js +364 -0
- package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
- package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
- package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
- package/core/server/services/explore-ping/ExplorePingService.js +106 -0
- package/core/server/services/explore-ping/index.js +31 -0
- package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
- package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
- package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
- package/core/server/services/invitations/accept.js +5 -2
- package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
- package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
- package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
- package/core/server/services/mail-events/MailEvent.js +20 -0
- package/core/server/services/mail-events/MailEvent.ts +10 -0
- package/core/server/services/mail-events/MailEventRepository.js +2 -0
- package/core/server/services/mail-events/MailEventRepository.ts +5 -0
- package/core/server/services/mail-events/MailEventService.js +124 -0
- package/core/server/services/mail-events/MailEventService.ts +169 -0
- package/core/server/services/mail-events/index.js +1 -1
- package/core/server/services/mail-events/libraries.d.ts +2 -0
- package/core/server/services/members/CaptchaService.js +80 -0
- package/core/server/services/members/api.js +1 -1
- package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
- package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
- package/core/server/services/members/importer/email-template.js +182 -0
- package/core/server/services/members/importer/index.js +30 -0
- package/core/server/services/members/members-ssr.js +333 -0
- package/core/server/services/members/service.js +2 -2
- package/core/server/services/posts/stats/PostStats.js +13 -0
- package/core/server/services/route-settings/SettingsPathManager.js +47 -0
- package/core/server/services/route-settings/index.js +1 -1
- package/core/server/services/stripe/README.md +63 -0
- package/core/server/services/stripe/StripeAPI.js +931 -0
- package/core/server/services/stripe/StripeMigrations.js +613 -0
- package/core/server/services/stripe/StripeService.js +175 -0
- package/core/server/services/stripe/WebhookController.js +100 -0
- package/core/server/services/stripe/WebhookManager.js +175 -0
- package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
- package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
- package/core/server/services/stripe/events/index.js +4 -0
- package/core/server/services/stripe/service.js +1 -1
- package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
- package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
- package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
- package/core/server/services/themes/loader.js +1 -1
- package/core/server/services/themes/to-json.js +1 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/shared/middleware/cache-control.js +51 -0
- package/core/server/web/shared/middleware/index.js +1 -1
- package/core/server/web/well-known.js +1 -1
- package/core/shared/labs.js +3 -1
- package/core/shared/settings-cache/CacheManager.js +64 -6
- package/package.json +103 -134
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +7 -93
- package/components/tryghost-adapter-cache-redis-5.114.1.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.114.1.tgz +0 -0
- package/components/tryghost-audience-feedback-5.114.1.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.114.1.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.114.1.tgz +0 -0
- package/components/tryghost-captcha-service-5.114.1.tgz +0 -0
- package/components/tryghost-constants-5.114.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.114.1.tgz +0 -0
- package/components/tryghost-donations-5.114.1.tgz +0 -0
- package/components/tryghost-email-addresses-5.114.1.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.114.1.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.114.1.tgz +0 -0
- package/components/tryghost-email-events-5.114.1.tgz +0 -0
- package/components/tryghost-email-service-5.114.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.114.1.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.114.1.tgz +0 -0
- package/components/tryghost-extract-api-key-5.114.1.tgz +0 -0
- package/components/tryghost-ghost-5.114.1.tgz +0 -0
- package/components/tryghost-i18n-5.114.1.tgz +0 -0
- package/components/tryghost-identity-token-service-5.114.1.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.114.1.tgz +0 -0
- package/components/tryghost-importer-revue-5.114.1.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.114.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.114.1.tgz +0 -0
- package/components/tryghost-mail-events-5.114.1.tgz +0 -0
- package/components/tryghost-member-attribution-5.114.1.tgz +0 -0
- package/components/tryghost-members-importer-5.114.1.tgz +0 -0
- package/components/tryghost-members-ssr-5.114.1.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.114.1.tgz +0 -0
- package/components/tryghost-minifier-5.114.1.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.114.1.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.114.1.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.114.1.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.114.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.114.1.tgz +0 -0
- package/components/tryghost-package-json-5.114.1.tgz +0 -0
- package/components/tryghost-recommendations-5.114.1.tgz +0 -0
- package/components/tryghost-referrers-5.114.1.tgz +0 -0
- package/components/tryghost-session-service-5.114.1.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.114.1.tgz +0 -0
- package/components/tryghost-slack-notifications-5.114.1.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.114.1.tgz +0 -0
- package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
- package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const urlUtils = require('../../../shared/url-utils');
|
|
2
2
|
const urlService = require('../../services/url');
|
|
3
|
+
|
|
4
|
+
const AudienceFeedbackService = require('./AudienceFeedbackService');
|
|
5
|
+
const AudienceFeedbackController = require('./AudienceFeedbackController');
|
|
6
|
+
const Feedback = require('./Feedback');
|
|
3
7
|
const FeedbackRepository = require('./FeedbackRepository');
|
|
4
8
|
|
|
5
9
|
class AudienceFeedbackServiceWrapper {
|
|
@@ -12,8 +16,6 @@ class AudienceFeedbackServiceWrapper {
|
|
|
12
16
|
// Wire up all the dependencies
|
|
13
17
|
const models = require('../../models');
|
|
14
18
|
|
|
15
|
-
const {AudienceFeedbackService, AudienceFeedbackController, Feedback} = require('@tryghost/audience-feedback');
|
|
16
|
-
|
|
17
19
|
this.repository = new FeedbackRepository({
|
|
18
20
|
Member: models.Member,
|
|
19
21
|
MemberFeedback: models.MemberFeedback,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
module.exports = ({t, siteTitle, email, siteDomain, siteUrl, siteLogo, token, deviceDetails, is2FARequired}) => `
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<meta name="viewport" content="width=device-width">
|
|
6
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
7
|
+
<title>🔑 ${t('Your verification code for {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</title>
|
|
8
|
+
<style>
|
|
9
|
+
/* -------------------------------------
|
|
10
|
+
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
|
11
|
+
------------------------------------- */
|
|
12
|
+
@media only screen and (max-width: 620px) {
|
|
13
|
+
table[class=body] h1 {
|
|
14
|
+
font-size: 28px !important;
|
|
15
|
+
margin-bottom: 10px !important;
|
|
16
|
+
}
|
|
17
|
+
table[class=body] p,
|
|
18
|
+
table[class=body] ul,
|
|
19
|
+
table[class=body] ol,
|
|
20
|
+
table[class=body] td,
|
|
21
|
+
table[class=body] span,
|
|
22
|
+
table[class=body] a {
|
|
23
|
+
font-size: 16px !important;
|
|
24
|
+
}
|
|
25
|
+
table[class=body] .wrapper,
|
|
26
|
+
table[class=body] .article {
|
|
27
|
+
padding: 10px !important;
|
|
28
|
+
}
|
|
29
|
+
table[class=body] .content {
|
|
30
|
+
padding: 0 !important;
|
|
31
|
+
}
|
|
32
|
+
table[class=body] .container {
|
|
33
|
+
padding: 0 !important;
|
|
34
|
+
width: 100% !important;
|
|
35
|
+
}
|
|
36
|
+
table[class=body] .main {
|
|
37
|
+
border-left-width: 0 !important;
|
|
38
|
+
border-radius: 0 !important;
|
|
39
|
+
border-right-width: 0 !important;
|
|
40
|
+
}
|
|
41
|
+
table[class=body] .btn table {
|
|
42
|
+
width: 100% !important;
|
|
43
|
+
}
|
|
44
|
+
table[class=body] .btn a {
|
|
45
|
+
width: 100% !important;
|
|
46
|
+
}
|
|
47
|
+
table[class=body] .img-responsive {
|
|
48
|
+
height: auto !important;
|
|
49
|
+
max-width: 100% !important;
|
|
50
|
+
width: auto !important;
|
|
51
|
+
}
|
|
52
|
+
table[class=body] p[class=small],
|
|
53
|
+
table[class=body] a[class=small] {
|
|
54
|
+
font-size: 11px !important;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/* -------------------------------------
|
|
58
|
+
PRESERVE THESE STYLES IN THE HEAD
|
|
59
|
+
------------------------------------- */
|
|
60
|
+
@media all {
|
|
61
|
+
.ExternalClass {
|
|
62
|
+
width: 100%;
|
|
63
|
+
}
|
|
64
|
+
.ExternalClass,
|
|
65
|
+
.ExternalClass p,
|
|
66
|
+
.ExternalClass span,
|
|
67
|
+
.ExternalClass font,
|
|
68
|
+
.ExternalClass td,
|
|
69
|
+
.ExternalClass div {
|
|
70
|
+
line-height: 100%;
|
|
71
|
+
}
|
|
72
|
+
.recipient-link a {
|
|
73
|
+
color: inherit !important;
|
|
74
|
+
font-family: inherit !important;
|
|
75
|
+
font-size: inherit !important;
|
|
76
|
+
font-weight: inherit !important;
|
|
77
|
+
line-height: inherit !important;
|
|
78
|
+
text-decoration: none !important;
|
|
79
|
+
}
|
|
80
|
+
#MessageViewBody a {
|
|
81
|
+
color: inherit;
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
font-size: inherit;
|
|
84
|
+
font-family: inherit;
|
|
85
|
+
font-weight: inherit;
|
|
86
|
+
line-height: inherit;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
hr {
|
|
90
|
+
border-width: 0;
|
|
91
|
+
height: 0;
|
|
92
|
+
margin-top: 34px;
|
|
93
|
+
margin-bottom: 34px;
|
|
94
|
+
border-bottom-width: 1px;
|
|
95
|
+
border-bottom-color: #EEF5F8;
|
|
96
|
+
}
|
|
97
|
+
a {
|
|
98
|
+
color: #3A464C;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
101
|
+
</head>
|
|
102
|
+
<body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
|
103
|
+
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
|
104
|
+
<tr>
|
|
105
|
+
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;"> </td>
|
|
106
|
+
<td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
|
|
107
|
+
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
|
108
|
+
|
|
109
|
+
<!-- START CENTERED CONTAINER -->
|
|
110
|
+
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">${t('Here\'s your code to login to {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</span>
|
|
111
|
+
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
|
112
|
+
|
|
113
|
+
<!-- START MAIN CONTENT AREA -->
|
|
114
|
+
<tr>
|
|
115
|
+
<td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
|
|
116
|
+
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
|
117
|
+
<tr>
|
|
118
|
+
<td align="center" style="padding-top: 20px; padding-bottom: 12px;"><img src="${siteLogo}" width="60" height="60" style="width: 60px; height: 60px;" /></td>
|
|
119
|
+
</tr>
|
|
120
|
+
<tr>
|
|
121
|
+
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
|
122
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: 600; line-height: 24px; margin: 0; margin-bottom: 15px; margin-top: 50px;">${t('Sign in verification')}</p>
|
|
123
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 24px; margin-bottom: 32px;">${is2FARequired ? '' : t('You just tried to access your account from a new device.')} ${t('For security verification, enter the code below to sign in to {{siteTitle}}:', {siteTitle, interpolation: {escapeValue: false}})}</p>
|
|
124
|
+
</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<tr>
|
|
127
|
+
<td style="padding: 16px; background-color: #F4F5F6; border-radius: 8px; text-align: center; vertical-align: middle;" valign="middle">
|
|
128
|
+
<h2 style="text-align: center; vertical-align: center; letter-spacing: 5px; font-size: 24px; color: #15212A; font-weight: 600; line-height: 24px; margin: 0;">${token}</h2>
|
|
129
|
+
</td>
|
|
130
|
+
</tr>
|
|
131
|
+
<tr>
|
|
132
|
+
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
|
133
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 4px; margin-top: 24px;">${t('Device:')} <strong style="font-weight: 600;">${deviceDetails.device}</strong></p>
|
|
134
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 4px; margin-top: 0px;">${t('Where:')} <strong style="font-weight: 600;">${deviceDetails.location}</strong></p>
|
|
135
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 4px; margin-top: 0px;">${t('When:')} <strong style="font-weight: 600;">${deviceDetails.time}</strong></p>
|
|
136
|
+
</td>
|
|
137
|
+
</tr>
|
|
138
|
+
<tr>
|
|
139
|
+
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
|
140
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 11px; margin-top: 24px;">${t('If you didn\'t try to sign in recently, you can safely ignore this email to deny access.')}</p>
|
|
141
|
+
</td>
|
|
142
|
+
</tr>
|
|
143
|
+
|
|
144
|
+
<!-- START FOOTER -->
|
|
145
|
+
<tr>
|
|
146
|
+
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;">
|
|
147
|
+
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 16px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0;">This message was sent from <a class="small" href="${siteUrl}" style="text-decoration: underline; color: #738A94; font-size: 11px;">${siteDomain}</a> to <a class="small" href="mailto:${email}" style="text-decoration: underline; color: #738A94; font-size: 11px;">${email}</a></p>
|
|
148
|
+
</td>
|
|
149
|
+
</tr>
|
|
150
|
+
|
|
151
|
+
<!-- END FOOTER -->
|
|
152
|
+
</table>
|
|
153
|
+
</td>
|
|
154
|
+
</tr>
|
|
155
|
+
|
|
156
|
+
<!-- END MAIN CONTENT AREA -->
|
|
157
|
+
</table>
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
<!-- END CENTERED CONTAINER -->
|
|
161
|
+
</div>
|
|
162
|
+
</td>
|
|
163
|
+
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;"> </td>
|
|
164
|
+
</tr>
|
|
165
|
+
</table>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const adapterManager = require('../../adapter-manager');
|
|
2
|
-
const createSessionService = require('
|
|
3
|
-
const sessionFromToken = require('
|
|
2
|
+
const createSessionService = require('./session-service');
|
|
3
|
+
const sessionFromToken = require('./session-from-token');
|
|
4
4
|
const createSessionMiddleware = require('./middleware');
|
|
5
5
|
const settingsCache = require('../../../../shared/settings-cache');
|
|
6
6
|
const {GhostMailer} = require('../../mail');
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module.exports = SessionFromToken;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} User
|
|
5
|
+
* @prop {string} id
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('express').Request} Req
|
|
10
|
+
* @typedef {import('express').Response} Res
|
|
11
|
+
* @typedef {import('express').NextFunction} Next
|
|
12
|
+
* @typedef {import('express').RequestHandler} RequestHandler
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns a connect middleware function which exchanges a token for a session
|
|
17
|
+
*
|
|
18
|
+
* @template Token
|
|
19
|
+
* @template Lookup
|
|
20
|
+
*
|
|
21
|
+
* @param { object } deps
|
|
22
|
+
* @param { (req: Req) => Promise<Token> } deps.getTokenFromRequest
|
|
23
|
+
* @param { (token: Token) => Promise<Lookup> } deps.getLookupFromToken
|
|
24
|
+
* @param { (lookup: Lookup) => Promise<User> } deps.findUserByLookup
|
|
25
|
+
* @param { (req: Req, res: Res, user: User) => Promise<void> } deps.createSession
|
|
26
|
+
* @param { boolean } deps.callNextWithError - Whether next should be call with an error or just pass through
|
|
27
|
+
*
|
|
28
|
+
* @returns {RequestHandler}
|
|
29
|
+
*/
|
|
30
|
+
function SessionFromToken({
|
|
31
|
+
getTokenFromRequest,
|
|
32
|
+
getLookupFromToken,
|
|
33
|
+
findUserByLookup,
|
|
34
|
+
createSession,
|
|
35
|
+
callNextWithError
|
|
36
|
+
}) {
|
|
37
|
+
/**
|
|
38
|
+
* @param {Req} req
|
|
39
|
+
* @param {Res} res
|
|
40
|
+
* @param {Next} next
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
async function handler(req, res, next) {
|
|
44
|
+
try {
|
|
45
|
+
const token = await getTokenFromRequest(req);
|
|
46
|
+
if (!token) {
|
|
47
|
+
return next();
|
|
48
|
+
}
|
|
49
|
+
const email = await getLookupFromToken(token);
|
|
50
|
+
if (!email) {
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
const user = await findUserByLookup(email);
|
|
54
|
+
if (!user) {
|
|
55
|
+
return next();
|
|
56
|
+
}
|
|
57
|
+
await createSession(req, res, user);
|
|
58
|
+
next();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (callNextWithError) {
|
|
61
|
+
next(err);
|
|
62
|
+
} else {
|
|
63
|
+
next();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return handler;
|
|
69
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
const {
|
|
2
|
+
BadRequestError
|
|
3
|
+
} = require('@tryghost/errors');
|
|
4
|
+
const errors = require('@tryghost/errors');
|
|
5
|
+
const emailTemplate = require('./emails/signin');
|
|
6
|
+
const UAParser = require('ua-parser-js');
|
|
7
|
+
const got = require('got');
|
|
8
|
+
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]?)$/;
|
|
9
|
+
const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
|
|
10
|
+
|
|
11
|
+
const {totp} = require('otplib');
|
|
12
|
+
totp.options = {
|
|
13
|
+
digits: 6,
|
|
14
|
+
step: 60,
|
|
15
|
+
window: [10, 10]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} User
|
|
20
|
+
* @prop {string} id
|
|
21
|
+
* @prop {(attr: string) => string} get
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} Session
|
|
26
|
+
* @prop {(cb: (err: Error | null) => any) => void} destroy
|
|
27
|
+
* @prop {string} user_id
|
|
28
|
+
* @prop {string} origin
|
|
29
|
+
* @prop {string} user_agent
|
|
30
|
+
* @prop {string} ip
|
|
31
|
+
* @prop {boolean} verified
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {import('express').Request} Req
|
|
36
|
+
* @typedef {import('express').Response} Res
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {object} SessionService
|
|
41
|
+
* @prop {(req: Req, res: Res) => Promise<User | null>} getUserForSession
|
|
42
|
+
* @prop {(req: Req, res: Res) => Promise<void>} removeUserForSession
|
|
43
|
+
* @prop {(req: Req, res: Res, user: User) => Promise<void>} createSessionForUser
|
|
44
|
+
* @prop {(req: Req, res: Res) => Promise<void>} createVerifiedSessionForUser
|
|
45
|
+
* @prop {(req: Req, res: Res) => Promise<void>} verifySession
|
|
46
|
+
* @prop {(req: Req, res: Res) => Promise<void>} sendAuthCodeToUser
|
|
47
|
+
* @prop {(req: Req, res: Res) => Promise<boolean>} verifyAuthCodeForUser
|
|
48
|
+
* @prop {(req: Req, res: Res) => Promise<boolean>} isVerifiedSession
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} deps
|
|
53
|
+
* @param {(req: Req, res: Res) => Promise<Session>} deps.getSession
|
|
54
|
+
* @param {(data: {id: string}) => Promise<User>} deps.findUserById
|
|
55
|
+
* @param {(req: Req) => string} deps.getOriginOfRequest
|
|
56
|
+
* @param {(key: string) => string} deps.getSettingsCache
|
|
57
|
+
* @param {() => string} deps.getBlogLogo
|
|
58
|
+
* @param {import('../../core/core/server/services/mail').GhostMailer} deps.mailer
|
|
59
|
+
* @param {import('../../core/core/shared/labs')} deps.labs
|
|
60
|
+
* @param {import('../../core/core/server/services/i18n').t} deps.t
|
|
61
|
+
* @param {import('../../core/core/shared/url-utils')} deps.urlUtils
|
|
62
|
+
* @returns {SessionService}
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
module.exports = function createSessionService({
|
|
66
|
+
getSession,
|
|
67
|
+
findUserById,
|
|
68
|
+
getOriginOfRequest,
|
|
69
|
+
getSettingsCache,
|
|
70
|
+
getBlogLogo,
|
|
71
|
+
mailer,
|
|
72
|
+
urlUtils,
|
|
73
|
+
labs,
|
|
74
|
+
t
|
|
75
|
+
}) {
|
|
76
|
+
/**
|
|
77
|
+
* cookieCsrfProtection
|
|
78
|
+
*
|
|
79
|
+
* @param {Req} req
|
|
80
|
+
* @param {Session} session
|
|
81
|
+
* @returns {Promise<void>}
|
|
82
|
+
*/
|
|
83
|
+
function cookieCsrfProtection(req, session) {
|
|
84
|
+
// If there is no origin on the session object it means this is a *new*
|
|
85
|
+
// session, that hasn't been initialised yet. So we don't need CSRF protection
|
|
86
|
+
if (!session.origin) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const origin = getOriginOfRequest(req);
|
|
91
|
+
|
|
92
|
+
if (session.origin !== origin) {
|
|
93
|
+
throw new BadRequestError({
|
|
94
|
+
message: `Request made from incorrect origin. Expected '${session.origin}' received '${origin}'.`
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* createSessionForUser
|
|
101
|
+
*
|
|
102
|
+
* @param {Req} req
|
|
103
|
+
* @param {Res} res
|
|
104
|
+
* @param {User} user
|
|
105
|
+
* @returns {Promise<void>}
|
|
106
|
+
*/
|
|
107
|
+
async function createSessionForUser(req, res, user) {
|
|
108
|
+
const session = await getSession(req, res);
|
|
109
|
+
const origin = getOriginOfRequest(req);
|
|
110
|
+
if (!origin) {
|
|
111
|
+
throw new BadRequestError({
|
|
112
|
+
message: 'Could not determine origin of request. Please ensure an Origin or Referrer header is present.'
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
session.user_id = user.id;
|
|
117
|
+
session.origin = origin;
|
|
118
|
+
session.user_agent = req.get('user-agent');
|
|
119
|
+
session.ip = req.ip;
|
|
120
|
+
|
|
121
|
+
if (!labs.isSet('staff2fa')) {
|
|
122
|
+
session.verified = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* createVerifiedSessionForUser
|
|
128
|
+
*
|
|
129
|
+
* @param {Req} req
|
|
130
|
+
* @param {Res} res
|
|
131
|
+
* @param {User} user
|
|
132
|
+
* @returns {Promise<void>}
|
|
133
|
+
*/
|
|
134
|
+
async function createVerifiedSessionForUser(req, res, user) {
|
|
135
|
+
await createSessionForUser(req, res, user);
|
|
136
|
+
await verifySession(req, res);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* generateAuthCodeForUser
|
|
141
|
+
*
|
|
142
|
+
* @param {Req} req
|
|
143
|
+
* @param {Res} res
|
|
144
|
+
* @returns {Promise<string>}
|
|
145
|
+
*/
|
|
146
|
+
async function generateAuthCodeForUser(req, res) {
|
|
147
|
+
const session = await getSession(req, res);
|
|
148
|
+
const secret = getSettingsCache('admin_session_secret') + session.user_id;
|
|
149
|
+
const token = totp.generate(secret);
|
|
150
|
+
return token;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* verifyAuthCodeForUser
|
|
155
|
+
*
|
|
156
|
+
* @param {Req} req
|
|
157
|
+
* @param {Res} res
|
|
158
|
+
* @returns {Promise<boolean>}
|
|
159
|
+
*/
|
|
160
|
+
async function verifyAuthCodeForUser(req, res) {
|
|
161
|
+
const session = await getSession(req, res);
|
|
162
|
+
const secret = getSettingsCache('admin_session_secret') + session.user_id;
|
|
163
|
+
const isValid = totp.check(req.body.token, secret);
|
|
164
|
+
return isValid;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const formatTime = new Intl.DateTimeFormat('en-GB', {
|
|
168
|
+
day: '2-digit',
|
|
169
|
+
month: 'short',
|
|
170
|
+
year: 'numeric',
|
|
171
|
+
hour: '2-digit',
|
|
172
|
+
minute: '2-digit',
|
|
173
|
+
timeZone: 'UTC',
|
|
174
|
+
timeZoneName: 'short'
|
|
175
|
+
}).format;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get a readable location string from an IP address.
|
|
179
|
+
* @param {string} ip - The IP address to look up.
|
|
180
|
+
* @returns {Promise<string>} - A readable location string or 'Unknown'.
|
|
181
|
+
*/
|
|
182
|
+
async function getGeolocationFromIP(ip) {
|
|
183
|
+
if (!ip || (!IPV4_REGEX.test(ip) && !IPV6_REGEX.test(ip))) {
|
|
184
|
+
return 'Unknown';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const gotOpts = {
|
|
188
|
+
timeout: 500
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (process.env.NODE_ENV?.startsWith('test')) {
|
|
192
|
+
gotOpts.retry = 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const geojsUrl = `https://get.geojs.io/v1/ip/geo/${encodeURIComponent(ip)}.json`;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const response = await got(geojsUrl, gotOpts).json();
|
|
199
|
+
|
|
200
|
+
const {city, region, country} = response || {};
|
|
201
|
+
|
|
202
|
+
// Only include non-empty parts in the result
|
|
203
|
+
const locationParts = [city, region, country].filter(Boolean);
|
|
204
|
+
|
|
205
|
+
// If no valid parts, return 'Unknown'
|
|
206
|
+
return locationParts.length > 0 ? locationParts.join(', ').trim() : 'Unknown';
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return 'Unknown';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function getDeviceDetails(userAgent, ip) {
|
|
213
|
+
const parser = new UAParser();
|
|
214
|
+
parser.setUA(userAgent);
|
|
215
|
+
const result = parser.getResult();
|
|
216
|
+
const deviceParts = [
|
|
217
|
+
result.browser?.name || '',
|
|
218
|
+
result.os?.name || ''
|
|
219
|
+
].filter(Boolean);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
device: deviceParts.join(', '),
|
|
223
|
+
location: await getGeolocationFromIP(ip),
|
|
224
|
+
time: formatTime(new Date())
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* sendAuthCodeToUser
|
|
230
|
+
*
|
|
231
|
+
* @param {Req} req
|
|
232
|
+
* @param {Res} res
|
|
233
|
+
* @returns {Promise<void>}
|
|
234
|
+
*/
|
|
235
|
+
async function sendAuthCodeToUser(req, res) {
|
|
236
|
+
const session = await getSession(req, res);
|
|
237
|
+
const token = await generateAuthCodeForUser(req, res);
|
|
238
|
+
|
|
239
|
+
let user;
|
|
240
|
+
try {
|
|
241
|
+
user = await findUserById({id: session.user_id});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
// User session likely doesn't contain a valid user ID
|
|
244
|
+
throw new BadRequestError({
|
|
245
|
+
message: 'Could not fetch user from the session.'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const recipient = user.get('email');
|
|
250
|
+
const siteTitle = getSettingsCache('title');
|
|
251
|
+
const siteLogo = getBlogLogo();
|
|
252
|
+
const siteUrl = urlUtils.urlFor('home', true);
|
|
253
|
+
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
|
254
|
+
const siteDomain = (domain && domain[1]);
|
|
255
|
+
const email = emailTemplate({
|
|
256
|
+
t,
|
|
257
|
+
siteTitle: siteTitle,
|
|
258
|
+
email: recipient,
|
|
259
|
+
siteDomain: siteDomain,
|
|
260
|
+
siteUrl: siteUrl,
|
|
261
|
+
siteLogo: siteLogo,
|
|
262
|
+
token: token,
|
|
263
|
+
deviceDetails: await getDeviceDetails(session.user_agent, session.ip),
|
|
264
|
+
is2FARequired: getSettingsCache('require_email_mfa')
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await mailer.send({
|
|
269
|
+
to: recipient,
|
|
270
|
+
subject: `${token} is your Ghost sign in verification code`,
|
|
271
|
+
html: email
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new errors.EmailError({
|
|
275
|
+
...error,
|
|
276
|
+
message: 'Failed to send email. Please check your site configuration and try again.'
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* verifySession
|
|
283
|
+
*
|
|
284
|
+
* @param {Req} req
|
|
285
|
+
* @param {Res} res
|
|
286
|
+
*/
|
|
287
|
+
async function verifySession(req, res) {
|
|
288
|
+
const session = await getSession(req, res);
|
|
289
|
+
session.verified = true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* isVerifiedSession
|
|
294
|
+
*
|
|
295
|
+
* @param {Req} req
|
|
296
|
+
* @param {Res} res
|
|
297
|
+
*/
|
|
298
|
+
async function isVerifiedSession(req, res) {
|
|
299
|
+
const session = await getSession(req, res);
|
|
300
|
+
return session.verified;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* removeUserForSession
|
|
305
|
+
*
|
|
306
|
+
* @param {Req} req
|
|
307
|
+
* @param {Res} res
|
|
308
|
+
* @returns {Promise<void>}
|
|
309
|
+
*/
|
|
310
|
+
async function removeUserForSession(req, res) {
|
|
311
|
+
const session = await getSession(req, res);
|
|
312
|
+
|
|
313
|
+
const requireMfa = getSettingsCache('require_email_mfa');
|
|
314
|
+
if (requireMfa) {
|
|
315
|
+
session.verified = undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
session.user_id = undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* getUserForSession
|
|
323
|
+
*
|
|
324
|
+
* @param {Req} req
|
|
325
|
+
* @param {Res} res
|
|
326
|
+
* @returns {Promise<User | null>}
|
|
327
|
+
*/
|
|
328
|
+
async function getUserForSession(req, res) {
|
|
329
|
+
// CASE: we don't have a cookie header so allow fallthrough to other
|
|
330
|
+
// auth middleware or final "ensure authenticated" check
|
|
331
|
+
if (!req.headers || !req.headers.cookie) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const session = await getSession(req, res);
|
|
336
|
+
// Enable CSRF bypass (useful for OAuth for example)
|
|
337
|
+
if (!res || !res.locals || !res.locals.bypassCsrfProtection) {
|
|
338
|
+
cookieCsrfProtection(req, session);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!session || !session.user_id) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const user = await findUserById({id: session.user_id});
|
|
347
|
+
return user;
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
getUserForSession,
|
|
355
|
+
createSessionForUser,
|
|
356
|
+
createVerifiedSessionForUser,
|
|
357
|
+
removeUserForSession,
|
|
358
|
+
verifySession,
|
|
359
|
+
isVerifiedSession,
|
|
360
|
+
sendAuthCodeToUser,
|
|
361
|
+
verifyAuthCodeForUser,
|
|
362
|
+
generateAuthCodeForUser
|
|
363
|
+
};
|
|
364
|
+
};
|