mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Website: Add admin page to manage the Fleet Sandbox waitlist (#13111)
Closes: #12954 Changes: - Added an admin page that displays a table containing all of the users that are currently on the Fleet Sandbox waitlist where admins can approve waitlisted users. - Added a new email template that tells users that their Fleet Sandbox instance is ready. - Added a new action: `admin/provision-sandbox-instance-and-deliver-email.js`, an action that provisions a Fleet sandbox instance for a single user and sends them an email telling them that their Fleet Sandbox Instance is ready. - Added a script that provisions a Fleet Sandbox instance for the user who has been on the waitlist the longest and sends them an email telling them that their Sandbox instance is ready.
This commit is contained in:
parent
f7296de183
commit
46802ee56a
20 changed files with 480 additions and 69 deletions
66
website/api/controllers/admin/provision-sandbox-instance-and-deliver-email.js
vendored
Normal file
66
website/api/controllers/admin/provision-sandbox-instance-and-deliver-email.js
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Provision sandbox instance and deliver email',
|
||||
|
||||
|
||||
description: 'Provisions a Fleet sandbox for a user and delivers an email to a user letting them know their Fleet Sandbox instance is ready.',
|
||||
|
||||
|
||||
inputs: {
|
||||
userId: {
|
||||
type: 'number',
|
||||
description: 'The database ID of the user who is currently on the Fleet Sandbox waitlist',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
exits: {
|
||||
success: {
|
||||
description: 'A user was successfully removed from the Fleet Sandbox waitlist.'
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
fn: async function ({userId}) {
|
||||
|
||||
let userToRemoveFromSandboxWaitlist = await User.findOne({id: userId});
|
||||
|
||||
if(!userToRemoveFromSandboxWaitlist.inSandboxWaitlist) {
|
||||
throw new Error(`When attempting to provision a Fleet Sandbox instance for a user (id:${userId}) who is on the waitlist, the user record associated with the provided ID has already been removed from the waitlist.`);
|
||||
}
|
||||
|
||||
let sandboxInstanceDetails = await sails.helpers.fleetSandboxCloudProvisioner.provisionNewFleetSandboxInstance.with({
|
||||
firstName: userToRemoveFromSandboxWaitlist.firstName,
|
||||
lastName: userToRemoveFromSandboxWaitlist.lastName,
|
||||
emailAddress: userToRemoveFromSandboxWaitlist.emailAddress,
|
||||
})
|
||||
.intercept((err)=>{
|
||||
return new Error(`When attempting to provision a new Fleet Sandbox instance for a User (id:${userToRemoveFromSandboxWaitlist.id}), an error occured. Full error: ${err}`);
|
||||
});
|
||||
|
||||
await User.updateOne({id: userId}).set({
|
||||
fleetSandboxURL: sandboxInstanceDetails.fleetSandboxURL,
|
||||
fleetSandboxExpiresAt: sandboxInstanceDetails.fleetSandboxExpiresAt,
|
||||
fleetSandboxDemoKey: sandboxInstanceDetails.fleetSandboxDemoKey,
|
||||
inSandboxWaitlist: false,
|
||||
});
|
||||
|
||||
// Send the user an email to let them know that their Fleet sandbox instance is ready.
|
||||
await sails.helpers.sendTemplateEmail.with({
|
||||
to: userToRemoveFromSandboxWaitlist.emailAddress,
|
||||
from: sails.config.custom.fromEmailAddress,
|
||||
fromName: sails.config.custom.fromName,
|
||||
subject: 'Your Fleet Sandbox instance is ready!',
|
||||
template: 'email-sandbox-ready-approved',
|
||||
templateData: {},
|
||||
});
|
||||
|
||||
// All done.
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -110,6 +110,10 @@ module.exports = {
|
|||
layout = 'layout-email';
|
||||
fakeData = {};
|
||||
break;
|
||||
case 'email-sandbox-ready-approved':
|
||||
layout = 'layout-email';
|
||||
fakeData = {};
|
||||
break;
|
||||
default:
|
||||
layout = 'layout-email-newsletter';
|
||||
fakeData = {
|
||||
|
|
|
|||
31
website/api/controllers/admin/view-sandbox-waitlist.js
vendored
Normal file
31
website/api/controllers/admin/view-sandbox-waitlist.js
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'View sandbox waitlist',
|
||||
|
||||
|
||||
description: 'Display "Sandbox waitlist" page.',
|
||||
|
||||
|
||||
exits: {
|
||||
|
||||
success: {
|
||||
viewTemplatePath: 'pages/admin/sandbox-waitlist'
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
fn: async function () {
|
||||
|
||||
let usersCurrentlyOnWaitlist = await User.find({inSandboxWaitlist: true})
|
||||
.sort('createdAt ASC');
|
||||
|
||||
return {
|
||||
usersWaitingForSandboxInstance: usersCurrentlyOnWaitlist
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
72
website/api/controllers/entrance/signup.js
vendored
72
website/api/controllers/entrance/signup.js
vendored
|
|
@ -79,7 +79,7 @@ the account verification message.)`,
|
|||
'parameters should have been validated/coerced _before_ they were sent.'
|
||||
},
|
||||
|
||||
requestToSandboxTimedOut: {
|
||||
requestToProvisionerTimedOut: {
|
||||
statusCode: 408,
|
||||
description: 'The request to the cloud provisioner exceeded the set timeout.',
|
||||
},
|
||||
|
|
@ -151,63 +151,19 @@ the account verification message.)`,
|
|||
// If the Fleet Sandbox waitlist is not enabled (sails.config.custom.fleetSandboxWaitlistEnabled) We'll provision a Sandbox instance BEFORE creating the new User record.
|
||||
// This way, if this fails, we won't save the new record to the database, and the user will see an error on the signup form asking them to try again.
|
||||
|
||||
const FIVE_DAYS_IN_MS = (5*24*60*60*1000);
|
||||
// Creating an expiration JS timestamp for the Fleet sandbox instance. NOTE: We send this value to the cloud provisioner API as an ISO 8601 string.
|
||||
let fleetSandboxExpiresAt = Date.now() + FIVE_DAYS_IN_MS;
|
||||
|
||||
// Creating a fleetSandboxDemoKey, this will be used for the user's password when we log them into their Sandbox instance.
|
||||
let fleetSandboxDemoKey = await sails.helpers.strings.uuid();
|
||||
|
||||
// Send a POST request to the cloud provisioner API
|
||||
let cloudProvisionerResponseData = await sails.helpers.http.post(
|
||||
'https://sandbox.fleetdm.com/new',
|
||||
{ // Request body
|
||||
'name': firstName + ' ' + lastName,
|
||||
'email': newEmailAddress,
|
||||
'password': fleetSandboxDemoKey, //« this provisioner API was originally designed to accept passwords, but rather than specifying the real plaintext password, since users always access Fleet Sandbox from their fleetdm.com account anyway, this generated demo key is used instead to avoid any confusion
|
||||
'sandbox_expiration': new Date(fleetSandboxExpiresAt).toISOString(), // sending expiration_timestamp as an ISO string.
|
||||
},
|
||||
{ // Request headers
|
||||
'Authorization':sails.config.custom.cloudProvisionerSecret
|
||||
}
|
||||
)
|
||||
.timeout(10000)// FUTURE: set this timeout to be 5000ms
|
||||
.intercept(['requestFailed', 'non200Response'], (err)=>{
|
||||
// If we received a non-200 response from the cloud provisioner API, we'll throw a 500 error.
|
||||
return new Error('When attempting to provision a new user who just signed up ('+emailAddress+'), the cloud provisioner gave a non 200 response. The incomplete user record has not been saved in the database, and the user will be asked to try signing up again. Raw response received from provisioner: '+err.stack);
|
||||
let sandboxInstanceDetails = await sails.helpers.fleetSandboxCloudProvisioner.provisionNewFleetSandboxInstance.with({
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
emailAddress: newEmailAddress,
|
||||
})
|
||||
.intercept({name: 'TimeoutError'},(err)=>{
|
||||
// If the request timed out, log a warning and return a 'requestToSandboxTimedOut' response.
|
||||
sails.log.warn('When attempting to provision a new user who just signed up ('+emailAddress+'), the request to the cloud provisioner took over timed out. The incomplete user record has not been saved in the database, and the user will be asked to try signing up again. Raw error: '+err.stack);
|
||||
return 'requestToSandboxTimedOut';
|
||||
.intercept('requestToProvisionerTimedOut', (err)=>{ // If the request to the Fleet Sandbox provisioner fails, we'll log a warning an return a requestToSandboxTimedOut response. This will tell the frontend to display a message asking the user to retry signing up.
|
||||
sails.log.warn(`When attempting to provision a new Fleet Sandbox instance for a new user signing up (email: ${newEmailAddress}). The Fleet Sandbox provisioner returned a non 200 response. The incomplete user record has not been saved in the database, and the user will be asked to try signing up again. Full error: ${err}`);
|
||||
return 'requestToProvisionerTimedOut';
|
||||
})
|
||||
.intercept((err)=>{ // For any other errors, we'll throw a 500 error.
|
||||
return new Error(`When attempting to provision a new Fleet Sandbox instance for a new user signing up (email: ${newEmailAddress}), an error occured. The incomplete user record has not been saved in the database, and the user will be asked to try signing up again. Full error: ${err}`);
|
||||
});
|
||||
|
||||
if(!cloudProvisionerResponseData.URL) {
|
||||
// If we didn't receive a URL in the response from the cloud provisioner API, we'll throwing an error before we save the new user record and the user will need to try to sign up again.
|
||||
throw new Error(
|
||||
`When provisioning a Fleet Sandbox instance for a new user who just signed up (${emailAddress}), the response data from the cloud provisioner API was malformed. It did not contain a valid Fleet Sandbox instance URL in its expected "URL" property.
|
||||
The incomplete user record has not been saved in the database, and the user will be asked to try signing up again.
|
||||
Here is the malformed response data (parsed response body) from the cloud provisioner API: ${cloudProvisionerResponseData}`
|
||||
);
|
||||
}
|
||||
|
||||
// If "Try Fleet Sandbox" was provided as the signupReason, we'll make sure their Sandbox instance is live before we continue.
|
||||
if(signupReason === 'Try Fleet Sandbox') {
|
||||
// Start polling the /healthz endpoint of the created Fleet Sandbox instance, once it returns a 200 response, we'll continue.
|
||||
await sails.helpers.flow.until( async()=>{
|
||||
let healthCheckResponse = await sails.helpers.http.sendHttpRequest('GET', cloudProvisionerResponseData.URL+'/healthz')
|
||||
.timeout(5000)
|
||||
.tolerate('non200Response')
|
||||
.tolerate('requestFailed')
|
||||
.tolerate({name: 'TimeoutError'});
|
||||
if(healthCheckResponse) {
|
||||
return true;
|
||||
}
|
||||
}, 10000).intercept('tookTooLong', ()=>{
|
||||
return new Error('This newly provisioned Fleet Sandbox instance (for '+emailAddress+') is taking too long to respond with a 2xx status code, even after repeatedly polling the health check endpoint. Note that failed requests and non-2xx responses from the health check endpoint were ignored during polling. Search for a bit of non-dynamic text from this error message in the fleetdm.com source code for more info on exactly how this polling works.');
|
||||
});
|
||||
}
|
||||
|
||||
// Build up data for the new user record and save it to the database.
|
||||
// (Also use `fetch` to retrieve the new ID so that we can use it below.)
|
||||
newUserRecord = await User.create(_.extend({
|
||||
|
|
@ -217,9 +173,9 @@ the account verification message.)`,
|
|||
emailAddress: newEmailAddress,
|
||||
signupReason,
|
||||
password: await sails.helpers.passwords.hashPassword(password),
|
||||
fleetSandboxURL: cloudProvisionerResponseData.URL,
|
||||
fleetSandboxExpiresAt,
|
||||
fleetSandboxDemoKey,
|
||||
fleetSandboxURL: sandboxInstanceDetails.fleetSandboxURL,
|
||||
fleetSandboxExpiresAt: sandboxInstanceDetails.fleetSandboxExpiresAt,
|
||||
fleetSandboxDemoKey: sandboxInstanceDetails.fleetSandboxDemoKey,
|
||||
stripeCustomerId,
|
||||
inSandboxWaitlist: false,
|
||||
tosAcceptedByIp: this.req.ip
|
||||
|
|
|
|||
117
website/api/helpers/fleet-sandbox-cloud-provisioner/provision-new-fleet-sandbox-instance.js
vendored
Normal file
117
website/api/helpers/fleet-sandbox-cloud-provisioner/provision-new-fleet-sandbox-instance.js
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Provision new Fleet Sandbox instance',
|
||||
|
||||
|
||||
description: 'Provisions a new Fleet Sandbox instance and returns the details of the Sandbox instance.',
|
||||
|
||||
|
||||
inputs: {
|
||||
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The first name of the user who is having a Fleet Sandbox instance provisioned for them.',
|
||||
extendedDescription: 'This will be used in the Fleet instance'
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The last name of the user who is having a Fleet Sandbox instance provisioned for them.',
|
||||
extendedDescription: 'This will be used in the Fleet instance'
|
||||
},
|
||||
emailAddress: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The email address of the User record that is having a Fleet sandbox instance provisioned for them.',
|
||||
extendedDescription: 'This will be used in the Fleet instance'
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
|
||||
exits: {
|
||||
|
||||
success: {
|
||||
description: 'All done.',
|
||||
outputFriendlyName: 'Sandbox instance details',
|
||||
outputType: {
|
||||
fleetSandboxDemoKey: 'string',
|
||||
fleetSandboxExpiresAt: 'number',
|
||||
fleetSandboxURL: 'string',
|
||||
},
|
||||
},
|
||||
|
||||
requestToProvisionerTimedOut: {
|
||||
description: 'The request to the Fleet Sandbox provisioner exceeded the set timeout.',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
fn: async function ({firstName, lastName, emailAddress}) {
|
||||
|
||||
const FIVE_DAYS_IN_MS = (5*24*60*60*1000);
|
||||
// Creating an expiration JS timestamp for the Fleet sandbox instance. NOTE: We send this value to the cloud provisioner API as an ISO 8601 string.
|
||||
let fleetSandboxExpiresAt = Date.now() + FIVE_DAYS_IN_MS;
|
||||
|
||||
// Creating a fleetSandboxDemoKey, this will be used for the user's password when we log them into their Sandbox instance.
|
||||
let fleetSandboxDemoKey = await sails.helpers.strings.uuid();
|
||||
|
||||
// Send a POST request to the cloud provisioner API
|
||||
let cloudProvisionerResponseData = await sails.helpers.http.post.with({
|
||||
url: 'https://sandbox.fleetdm.com/new',
|
||||
data: {
|
||||
'name': firstName + ' ' + lastName,
|
||||
'email': emailAddress,
|
||||
'password': fleetSandboxDemoKey, //« this provisioner API was originally designed to accept passwords, but rather than specifying the real plaintext password, since users always access Fleet Sandbox from their fleetdm.com account anyway, this generated demo key is used instead to avoid any confusion
|
||||
'sandbox_expiration': new Date(fleetSandboxExpiresAt).toISOString(), // sending expiration_timestamp as an ISO string.
|
||||
},
|
||||
headers: {
|
||||
'Authorization':sails.config.custom.cloudProvisionerSecret
|
||||
}
|
||||
})
|
||||
.timeout(10000)
|
||||
.intercept(['requestFailed', 'non200Response'], (err)=>{
|
||||
// If we received a non-200 response from the cloud provisioner API, we'll throw a 500 error.
|
||||
return new Error('When attempting to provision a Sandbox instance for a user on the Fleet Sandbox waitlist ('+emailAddress+'), the cloud provisioner gave a non 200 response. Raw response received from provisioner: '+err.stack);
|
||||
})
|
||||
.intercept({name: 'TimeoutError'},()=>{
|
||||
// If the request timed out, log a warning and return a 'requestToSandboxTimedOut' response.
|
||||
return 'requestToProvisionerTimedOut';
|
||||
});
|
||||
|
||||
if(!cloudProvisionerResponseData.URL) {
|
||||
// If we didn't receive a URL in the response from the cloud provisioner API, we'll throw an error before we save the new user record and the user will need to try to sign up again.
|
||||
throw new Error(
|
||||
`The response data from the cloud provisioner API was malformed. It did not contain a valid Fleet Sandbox instance URL in its expected "URL" property.
|
||||
Here is the malformed response data (parsed response body) from the cloud provisioner API: ${cloudProvisionerResponseData}`
|
||||
);
|
||||
}
|
||||
|
||||
// Start polling the /healthz endpoint of the created Fleet Sandbox instance, once it returns a 200 response, we'll continue.
|
||||
await sails.helpers.flow.until( async()=>{
|
||||
let healthCheckResponse = await sails.helpers.http.sendHttpRequest('GET', cloudProvisionerResponseData.URL+'/healthz')
|
||||
.timeout(5000)
|
||||
.tolerate('non200Response')
|
||||
.tolerate('requestFailed')
|
||||
.tolerate({name: 'TimeoutError'});
|
||||
if(healthCheckResponse) {
|
||||
return true;
|
||||
}
|
||||
}, 10000)//∞
|
||||
.intercept('tookTooLong', ()=>{
|
||||
return new Error('This newly provisioned Fleet Sandbox instance (for '+emailAddress+') is taking too long to respond with a 2xx status code, even after repeatedly polling the health check endpoint. Note that failed requests and non-2xx responses from the health check endpoint were ignored during polling. Search for a bit of non-dynamic text from this error message in the fleetdm.com source code for more info on exactly how this polling works.');
|
||||
});
|
||||
|
||||
return {
|
||||
fleetSandboxDemoKey,
|
||||
fleetSandboxExpiresAt,
|
||||
fleetSandboxURL: cloudProvisionerResponseData.URL,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
2
website/assets/js/cloud.setup.js
vendored
2
website/assets/js/cloud.setup.js
vendored
|
|
@ -13,7 +13,7 @@
|
|||
Cloud.setup({
|
||||
|
||||
/* eslint-disable */
|
||||
methods: {"downloadSitemap":{"verb":"GET","url":"/sitemap.xml","args":[]},"downloadRssFeed":{"verb":"GET","url":"/rss/:categoryName","args":["categoryName"]},"receiveUsageAnalytics":{"verb":"POST","url":"/api/v1/webhooks/receive-usage-analytics","args":["anonymousIdentifier","fleetVersion","licenseTier","numHostsEnrolled","numUsers","numTeams","numPolicies","numLabels","softwareInventoryEnabled","vulnDetectionEnabled","systemUsersEnabled","hostStatusWebhookEnabled","numWeeklyActiveUsers","numWeeklyPolicyViolationDaysActual","numWeeklyPolicyViolationDaysPossible","hostsEnrolledByOperatingSystem","hostsEnrolledByOrbitVersion","hostsEnrolledByOsqueryVersion","storedErrors","numHostsNotResponding","organization"]},"receiveFromGithub":{"verb":"GET","url":"/api/v1/webhooks/github","args":["botSignature","action","sender","repository","changes","issue","comment","pull_request","label","release"]},"receiveFromStripe":{"verb":"POST","url":"/api/v1/webhooks/receive-from-stripe","args":["id","type","data","webhookSecret"]},"receiveFromCustomerFleetInstance":{"verb":"POST","url":"/api/v1/webhooks/receive-from-customer-fleet-instance","args":["timestamp","host","webhookSecret"]},"deliverContactFormMessage":{"verb":"POST","url":"/api/v1/deliver-contact-form-message","args":["emailAddress","topic","firstName","lastName","message"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"signup":{"verb":"POST","url":"/api/v1/customers/signup","args":["emailAddress","password","organization","firstName","lastName","signupReason"]},"updateProfile":{"verb":"POST","url":"/api/v1/account/update-profile","args":["firstName","lastName","organization","emailAddress"]},"updatePassword":{"verb":"POST","url":"/api/v1/account/update-password","args":["oldPassword","newPassword"]},"updateBillingCard":{"verb":"POST","url":"/api/v1/account/update-billing-card","args":["stripeToken","billingCardLast4","billingCardBrand","billingCardExpMonth","billingCardExpYear"]},"login":{"verb":"POST","url":"/api/v1/customers/login","args":["emailAddress","password","rememberMe"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"createQuote":{"verb":"POST","url":"/api/v1/customers/create-quote","args":["numberOfHosts"]},"saveBillingInfoAndSubscribe":{"verb":"POST","url":"/api/v1/customers/save-billing-info-and-subscribe","args":["quoteId","organization","firstName","lastName","paymentSource"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deliverDemoSignup":{"verb":"POST","url":"/api/v1/deliver-demo-signup","args":["emailAddress"]},"createOrUpdateOneNewsletterSubscription":{"verb":"POST","url":"/api/v1/create-or-update-one-newsletter-subscription","args":["emailAddress","subscribeTo"]},"unsubscribeFromAllNewsletters":{"verb":"GET","url":"/api/v1/unsubscribe-from-all-newsletters","args":["emailAddress"]},"buildLicenseKey":{"verb":"POST","url":"/api/v1/admin/build-license-key","args":["numberOfHosts","organization","expiresAt","partnerName"]},"createVantaAuthorizationRequest":{"verb":"POST","url":"/api/v1/create-vanta-authorization-request","args":["emailAddress","fleetInstanceUrl","fleetApiKey"]},"deliverMdmBetaSignup":{"verb":"POST","url":"/api/v1/deliver-mdm-beta-signup","args":["emailAddress","fullName","jobTitle","numberOfHosts"]},"deliverAppleCsr":{"verb":"POST","url":"/api/v1/deliver-apple-csr","args":["unsignedCsrData"]},"deliverPremiumUpgradeForm":{"verb":"POST","url":"/api/v1/deliver-premium-upgrade-form","args":["organization","monthsUsingFleetFree","emailAddress","numberOfHosts"]},"deliverLaunchPartySignup":{"verb":"POST","url":"/api/v1/deliver-launch-party-signup","args":["emailAddress","firstName","lastName","jobTitle","phoneNumber"]},"deliverMdmDemoEmail":{"verb":"POST","url":"/api/v1/deliver-mdm-demo-email","args":["emailAddress"]}}
|
||||
methods: {"downloadSitemap":{"verb":"GET","url":"/sitemap.xml","args":[]},"downloadRssFeed":{"verb":"GET","url":"/rss/:categoryName","args":["categoryName"]},"receiveUsageAnalytics":{"verb":"POST","url":"/api/v1/webhooks/receive-usage-analytics","args":["anonymousIdentifier","fleetVersion","licenseTier","numHostsEnrolled","numUsers","numTeams","numPolicies","numLabels","softwareInventoryEnabled","vulnDetectionEnabled","systemUsersEnabled","hostStatusWebhookEnabled","numWeeklyActiveUsers","numWeeklyPolicyViolationDaysActual","numWeeklyPolicyViolationDaysPossible","hostsEnrolledByOperatingSystem","hostsEnrolledByOrbitVersion","hostsEnrolledByOsqueryVersion","storedErrors","numHostsNotResponding","organization"]},"receiveFromGithub":{"verb":"GET","url":"/api/v1/webhooks/github","args":["botSignature","action","sender","repository","changes","issue","comment","pull_request","label","release"]},"receiveFromStripe":{"verb":"POST","url":"/api/v1/webhooks/receive-from-stripe","args":["id","type","data","webhookSecret"]},"receiveFromCustomerFleetInstance":{"verb":"POST","url":"/api/v1/webhooks/receive-from-customer-fleet-instance","args":["timestamp","host","webhookSecret"]},"deliverContactFormMessage":{"verb":"POST","url":"/api/v1/deliver-contact-form-message","args":["emailAddress","topic","firstName","lastName","message"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"signup":{"verb":"POST","url":"/api/v1/customers/signup","args":["emailAddress","password","organization","firstName","lastName","signupReason"]},"updateProfile":{"verb":"POST","url":"/api/v1/account/update-profile","args":["firstName","lastName","organization","emailAddress"]},"updatePassword":{"verb":"POST","url":"/api/v1/account/update-password","args":["oldPassword","newPassword"]},"updateBillingCard":{"verb":"POST","url":"/api/v1/account/update-billing-card","args":["stripeToken","billingCardLast4","billingCardBrand","billingCardExpMonth","billingCardExpYear"]},"login":{"verb":"POST","url":"/api/v1/customers/login","args":["emailAddress","password","rememberMe"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"createQuote":{"verb":"POST","url":"/api/v1/customers/create-quote","args":["numberOfHosts"]},"saveBillingInfoAndSubscribe":{"verb":"POST","url":"/api/v1/customers/save-billing-info-and-subscribe","args":["quoteId","organization","firstName","lastName","paymentSource"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deliverDemoSignup":{"verb":"POST","url":"/api/v1/deliver-demo-signup","args":["emailAddress"]},"createOrUpdateOneNewsletterSubscription":{"verb":"POST","url":"/api/v1/create-or-update-one-newsletter-subscription","args":["emailAddress","subscribeTo"]},"unsubscribeFromAllNewsletters":{"verb":"GET","url":"/api/v1/unsubscribe-from-all-newsletters","args":["emailAddress"]},"buildLicenseKey":{"verb":"POST","url":"/api/v1/admin/build-license-key","args":["numberOfHosts","organization","expiresAt","partnerName"]},"createVantaAuthorizationRequest":{"verb":"POST","url":"/api/v1/create-vanta-authorization-request","args":["emailAddress","fleetInstanceUrl","fleetApiKey"]},"deliverMdmBetaSignup":{"verb":"POST","url":"/api/v1/deliver-mdm-beta-signup","args":["emailAddress","fullName","jobTitle","numberOfHosts"]},"deliverAppleCsr":{"verb":"POST","url":"/api/v1/deliver-apple-csr","args":["unsignedCsrData"]},"deliverPremiumUpgradeForm":{"verb":"POST","url":"/api/v1/deliver-premium-upgrade-form","args":["organization","monthsUsingFleetFree","emailAddress","numberOfHosts"]},"deliverLaunchPartySignup":{"verb":"POST","url":"/api/v1/deliver-launch-party-signup","args":["emailAddress","firstName","lastName","jobTitle","phoneNumber"]},"deliverMdmDemoEmail":{"verb":"POST","url":"/api/v1/deliver-mdm-demo-email","args":["emailAddress"]},"provisionSandboxInstanceAndDeliverEmail":{"verb":"POST","url":"/api/v1/admin/provision-sandbox-instance-and-deliver-email","args":["userId"]}}
|
||||
/* eslint-enable */
|
||||
|
||||
});
|
||||
|
|
|
|||
32
website/assets/js/pages/admin/sandbox-waitlist.page.js
vendored
Normal file
32
website/assets/js/pages/admin/sandbox-waitlist.page.js
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
parasails.registerPage('sandbox-waitlist', {
|
||||
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
|
||||
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
|
||||
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
|
||||
data: {
|
||||
syncing: false,
|
||||
},
|
||||
|
||||
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
|
||||
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
|
||||
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
|
||||
beforeMount: function() {
|
||||
//…
|
||||
},
|
||||
mounted: async function() {
|
||||
//…
|
||||
},
|
||||
|
||||
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
|
||||
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
methods: {
|
||||
clickApproveWaitlistUser: async function(userId) {
|
||||
this.syncing = true;
|
||||
await Cloud.provisionSandboxInstanceAndDeliverEmail.with({userId});
|
||||
this.usersWaitingForSandboxInstance = this.usersWaitingForSandboxInstance.filter((user)=>{
|
||||
return user.id !== userId;
|
||||
});
|
||||
this.syncing = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
1
website/assets/styles/importer.less
vendored
1
website/assets/styles/importer.less
vendored
|
|
@ -76,6 +76,7 @@
|
|||
@import 'pages/vulnerability-management.less';
|
||||
@import 'pages/support.less';
|
||||
@import 'pages/try-fleet/waitlist.less';
|
||||
@import 'pages/admin/sandbox-waitlist.less';
|
||||
|
||||
|
||||
// Imagine = landing pages for Marketing
|
||||
|
|
|
|||
16
website/assets/styles/layout.less
vendored
16
website/assets/styles/layout.less
vendored
|
|
@ -280,6 +280,22 @@ html, body {
|
|||
}
|
||||
}
|
||||
}
|
||||
[purpose='admin-nav'] {
|
||||
justify-content: center;
|
||||
a {
|
||||
margin-left: 30px;
|
||||
margin-right: 30px;
|
||||
color: @core-fleet-black-75;
|
||||
&:hover {
|
||||
color: @core-fleet-black;
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: @core-fleet-black;
|
||||
font-weight: 600;
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// Footer styles
|
||||
[purpose='page-footer'] {
|
||||
|
|
|
|||
64
website/assets/styles/pages/admin/sandbox-waitlist.less
vendored
Normal file
64
website/assets/styles/pages/admin/sandbox-waitlist.less
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#sandbox-waitlist {
|
||||
padding-top: 80px;
|
||||
padding-bottom: 80px;
|
||||
[purpose='approve-button'] {
|
||||
display: flex;
|
||||
padding: 2px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 21px;
|
||||
}
|
||||
[purpose='table-container'] {
|
||||
padding-top: 40px;
|
||||
width: 100%;
|
||||
[purpose='waitlist-table'] {
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
td {
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
thead {
|
||||
font-weight: 600;
|
||||
background-color: @ui-off-white;
|
||||
border-radius: 6px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
outline: 1px solid #E2E4EA;
|
||||
box-shadow: 0 0 0 1px #E2E4EA;
|
||||
tr {
|
||||
td:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
}
|
||||
td:last-child {
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
color: #515774;
|
||||
border-radius: 6px;
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
outline: 1px solid #E2E4EA;
|
||||
box-shadow: 0 0 0 1px #E2E4EA;
|
||||
tr {
|
||||
border-top: 1px solid @core-vibrant-blue-15;
|
||||
}
|
||||
tr:last-child {
|
||||
td:first-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
td:last-child {
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
8
website/config/routes.js
vendored
8
website/config/routes.js
vendored
|
|
@ -222,6 +222,13 @@ module.exports.routes = {
|
|||
},
|
||||
},
|
||||
|
||||
'GET /admin/sandbox-waitlist': {
|
||||
action: 'admin/view-sandbox-waitlist',
|
||||
locals: {
|
||||
layout: 'layouts/layout-customer'
|
||||
},
|
||||
},
|
||||
|
||||
'GET /tables/:tableName': {
|
||||
action: 'view-osquery-table-details',
|
||||
locals: {
|
||||
|
|
@ -547,4 +554,5 @@ module.exports.routes = {
|
|||
'POST /api/v1/deliver-premium-upgrade-form': { action: 'deliver-premium-upgrade-form' },
|
||||
'POST /api/v1/deliver-launch-party-signup': { action: 'imagine/deliver-launch-party-signup' },
|
||||
'POST /api/v1/deliver-mdm-demo-email': { action: 'deliver-mdm-demo-email' },
|
||||
'POST /api/v1/admin/provision-sandbox-instance-and-deliver-email': { action: 'admin/provision-sandbox-instance-and-deliver-email' },
|
||||
};
|
||||
|
|
|
|||
62
website/scripts/provision-sandbox-instance-for-one-user-and-deliver-email.js
vendored
Normal file
62
website/scripts/provision-sandbox-instance-for-one-user-and-deliver-email.js
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Provision Sandbox instance for one user and deliver email.',
|
||||
|
||||
|
||||
description: 'Provisions a new Fleet Sandbox instance for a user on the Fleet Sandbox waitlist, and sends an email to the user.',
|
||||
|
||||
extendedDescription: 'This script will provision a Sandbox instance for the user who has been on the waitlist the longest.',
|
||||
|
||||
|
||||
fn: async function () {
|
||||
|
||||
|
||||
let earliestCreatedUserCurrentlyOnWaitlist = await User.find({inSandboxWaitlist: true})
|
||||
.limit(1)
|
||||
.sort('createdAt ASC');
|
||||
|
||||
// If there are no users on the Fleet sandbox waitlist, end the script.
|
||||
if(earliestCreatedUserCurrentlyOnWaitlist.length === 0){
|
||||
sails.log('There are no users currently waiting on the Fleet Sandbox Waitlist.');
|
||||
return;
|
||||
}
|
||||
|
||||
let userToRemoveFromSandboxWaitlist = earliestCreatedUserCurrentlyOnWaitlist[0];
|
||||
|
||||
let sandboxInstanceDetails = await sails.helpers.fleetSandboxCloudProvisioner.provisionNewFleetSandboxInstance.with({
|
||||
firstName: userToRemoveFromSandboxWaitlist.firstName,
|
||||
lastName: userToRemoveFromSandboxWaitlist.lastName,
|
||||
emailAddress: userToRemoveFromSandboxWaitlist.emailAddress,
|
||||
})
|
||||
.intercept((err)=>{
|
||||
return new Error(`When attempting to provision a new Fleet Sandbox instance for a User (id:${userToRemoveFromSandboxWaitlist.id}), an error occured. Full error: ${err}`);
|
||||
});
|
||||
|
||||
|
||||
await User.updateOne({id: userToRemoveFromSandboxWaitlist.id})
|
||||
.set({
|
||||
fleetSandboxURL: sandboxInstanceDetails.fleetSandboxURL,
|
||||
fleetSandboxExpiresAt: sandboxInstanceDetails.fleetSandboxExpiresAt,
|
||||
fleetSandboxDemoKey: sandboxInstanceDetails.fleetSandboxDemoKey,
|
||||
inSandboxWaitlist: false,
|
||||
});
|
||||
|
||||
|
||||
// Send the user an email to let them know that their Fleet sandbox instance is ready.
|
||||
await sails.helpers.sendTemplateEmail.with({
|
||||
to: userToRemoveFromSandboxWaitlist.emailAddress,
|
||||
from: sails.config.custom.fromEmailAddress,
|
||||
fromName: sails.config.custom.fromName,
|
||||
subject: 'Your Fleet Sandbox instance is ready!',
|
||||
template: 'email-sandbox-ready-approved',
|
||||
templateData: {},
|
||||
});
|
||||
|
||||
sails.log(`Successfully removed a user (id: ${userToRemoveFromSandboxWaitlist.id}) from the Fleet Sandbox waitlist.`);
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
9
website/views/emails/email-sandbox-ready-approved.ejs
vendored
Normal file
9
website/views/emails/email-sandbox-ready-approved.ejs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<% /* Note: This is injected into `views/layouts/layout-email.ejs` */ %>
|
||||
<div style="padding: 24px 0px 0px 0px">
|
||||
<h2 style="font-weight: 600; font-size: 24px; margin-bottom: 32px; margin-top: 0px">Your Fleet Sandbox instance is ready</h2>
|
||||
<p style=" font-size: 16px; line-height: 24px; margin-bottom: 32px; margin-top: 0px">You can now access Fleet.</p>
|
||||
|
||||
<a style="cursor: pointer; font-size: 16px; line-height: 40px; font-weight: 600; margin-bottom: 32px; background: #6A67FE; border-radius: 8px; height: 40px; display: block; color: #FFF; width: 300px; text-decoration: none; text-align: center;" href="https://fleetdm.com/try-fleet/sandbox" target="_blank">Play in the Fleet Sandbox</a>
|
||||
<p style="font-size: 16px; line-height: 24px; margin-bottom: 32px;">Best,<br> The Fleet team</p>
|
||||
|
||||
</div>
|
||||
13
website/views/layouts/layout-customer.ejs
vendored
13
website/views/layouts/layout-customer.ejs
vendored
|
|
@ -141,6 +141,18 @@
|
|||
<a href="/logout" purpose="logout-btn" class="justify-content-end px-3 py-2 <%= _.has(me, 'id') ? 'd-flex' : 'd-none' %>" style="text-decoration: none; line-height: 23px;">Log out</a>
|
||||
</div>
|
||||
</div>
|
||||
<%if(me && me.isSuperAdmin) {%>
|
||||
<div purpose="admin-nav" class="d-flex flex-row align-items-center px-5">
|
||||
<div class=" justif-self-start">
|
||||
<span>Admin pages</span>
|
||||
</div>
|
||||
<div class="d-flex flex-row align-self-end">
|
||||
<a class="px-3 py-2" style="text-decoration: none; line-height: 23px;" href="/admin/generate-license">License generator</a>
|
||||
<a class="px-3 py-2" style="text-decoration: none; line-height: 23px;" href="/admin/email-preview">HTML Email preview tool</a>
|
||||
<a class="px-3 py-2" style="text-decoration: none; line-height: 23px;" href="/admin/sandbox-waitlist">Manage Fleet Sandbox waitlist</a>
|
||||
</div>
|
||||
</div>
|
||||
<%} %>
|
||||
</div>
|
||||
|
||||
<%- body %>
|
||||
|
|
@ -232,6 +244,7 @@
|
|||
<script src="/js/pages/admin/email-preview.page.js"></script>
|
||||
<script src="/js/pages/admin/email-templates.page.js"></script>
|
||||
<script src="/js/pages/admin/generate-license.page.js"></script>
|
||||
<script src="/js/pages/admin/sandbox-waitlist.page.js"></script>
|
||||
<script src="/js/pages/articles/articles.page.js"></script>
|
||||
<script src="/js/pages/articles/basic-article.page.js"></script>
|
||||
<script src="/js/pages/compliance.page.js"></script>
|
||||
|
|
|
|||
20
website/views/layouts/layout-email.ejs
vendored
20
website/views/layouts/layout-email.ejs
vendored
|
|
@ -4,15 +4,17 @@
|
|||
<div style="background: transparent; text-align: left;">
|
||||
<a href="https://fleetdm.com"><img style="display: inline-block; width: 162px; height: 92px; width: auto;" alt="Logo" src="https://fleetdm.com/images/logo-blue-162x92@2x.png"/></a>
|
||||
</div>
|
||||
<%- body %>
|
||||
<hr style="color: #E2E4EA;"/>
|
||||
<div style="display: inline-flex; padding-top: 32px;">
|
||||
<a href="https://fleetdm.com"><img style="height: 20px; width: 20px; margin-right: 24px;" alt="Fleet logo" src="<%= url.resolve(sails.config.custom.baseUrl,'/images/logo-fleet-20x20@2x.png')%>"></a>
|
||||
<a href="https://twitter.com/fleetctl"><img style="height: 20px; width: 24px; margin-right: 24px;" alt="Follow Fleet on Twitter" src="<%= url.resolve(sails.config.custom.baseUrl,'images/logo-twitter-50x44@2x.png')%>"></a>
|
||||
<a href="https://fleetdm.com/slack"><img style="height: 20px; width: 20px;" alt="Join the osquery Slack community" src="<%= url.resolve(sails.config.custom.baseUrl,'images/logo-slack-24x24@2x.png')%>"></a>
|
||||
</div>
|
||||
<div style="text-align: left; padding-top: 15px; font-size: 12px; color: #3E4771;">
|
||||
<p>© 2023 Fleet Device Management Inc.<br> All trademarks, service marks, and company names are the property of their respective owners.</p>
|
||||
<div style="padding: 0px 0px 0px 22px;">
|
||||
<%- body %>
|
||||
<hr style="color: #E2E4EA;"/>
|
||||
<div style="display: inline-flex; padding-top: 32px;">
|
||||
<a href="https://fleetdm.com"><img style="height: 20px; width: 20px; margin-right: 24px;" alt="Fleet logo" src="<%= url.resolve(sails.config.custom.baseUrl,'/images/logo-fleet-20x20@2x.png')%>"></a>
|
||||
<a href="https://twitter.com/fleetctl"><img style="height: 20px; width: 24px; margin-right: 24px;" alt="Follow Fleet on Twitter" src="<%= url.resolve(sails.config.custom.baseUrl,'images/logo-twitter-50x44@2x.png')%>"></a>
|
||||
<a href="https://fleetdm.com/slack"><img style="height: 20px; width: 20px;" alt="Join the osquery Slack community" src="<%= url.resolve(sails.config.custom.baseUrl,'images/logo-slack-24x24@2x.png')%>"></a>
|
||||
</div>
|
||||
<div style="text-align: left; padding-top: 15px; font-size: 12px; color: #3E4771;">
|
||||
<p>© 2023 Fleet Device Management Inc.<br> All trademarks, service marks, and company names are the property of their respective owners.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
1
website/views/layouts/layout-landing.ejs
vendored
1
website/views/layouts/layout-landing.ejs
vendored
|
|
@ -232,6 +232,7 @@
|
|||
<script src="/js/pages/admin/email-preview.page.js"></script>
|
||||
<script src="/js/pages/admin/email-templates.page.js"></script>
|
||||
<script src="/js/pages/admin/generate-license.page.js"></script>
|
||||
<script src="/js/pages/admin/sandbox-waitlist.page.js"></script>
|
||||
<script src="/js/pages/articles/articles.page.js"></script>
|
||||
<script src="/js/pages/articles/basic-article.page.js"></script>
|
||||
<script src="/js/pages/compliance.page.js"></script>
|
||||
|
|
|
|||
1
website/views/layouts/layout-sandbox.ejs
vendored
1
website/views/layouts/layout-sandbox.ejs
vendored
|
|
@ -359,6 +359,7 @@
|
|||
<script src="/js/pages/admin/email-preview.page.js"></script>
|
||||
<script src="/js/pages/admin/email-templates.page.js"></script>
|
||||
<script src="/js/pages/admin/generate-license.page.js"></script>
|
||||
<script src="/js/pages/admin/sandbox-waitlist.page.js"></script>
|
||||
<script src="/js/pages/articles/articles.page.js"></script>
|
||||
<script src="/js/pages/articles/basic-article.page.js"></script>
|
||||
<script src="/js/pages/compliance.page.js"></script>
|
||||
|
|
|
|||
1
website/views/layouts/layout.ejs
vendored
1
website/views/layouts/layout.ejs
vendored
|
|
@ -432,6 +432,7 @@
|
|||
<script src="/js/pages/admin/email-preview.page.js"></script>
|
||||
<script src="/js/pages/admin/email-templates.page.js"></script>
|
||||
<script src="/js/pages/admin/generate-license.page.js"></script>
|
||||
<script src="/js/pages/admin/sandbox-waitlist.page.js"></script>
|
||||
<script src="/js/pages/articles/articles.page.js"></script>
|
||||
<script src="/js/pages/articles/basic-article.page.js"></script>
|
||||
<script src="/js/pages/compliance.page.js"></script>
|
||||
|
|
|
|||
27
website/views/pages/admin/sandbox-waitlist.ejs
vendored
Normal file
27
website/views/pages/admin/sandbox-waitlist.ejs
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<div id="sandbox-waitlist" v-cloak>
|
||||
<div class="container pt-4">
|
||||
<h3>Sandbox waitlist</h3>
|
||||
<div purpose="table-container" v-if="usersWaitingForSandboxInstance.length > 0">
|
||||
<table class="table" purpose="waitlist-table">
|
||||
<thead><tr>
|
||||
<td>Email</td>
|
||||
<td>Date signed up</td>
|
||||
<td></td>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="user in usersWaitingForSandboxInstance">
|
||||
<td>{{user.emailAddress}}</td>
|
||||
<td><js-timestamp format="calendar" :at="user.createdAt"></js-timestamp></td>
|
||||
<td><ajax-button purpose="approve-button" :syncing="syncing" class="btn btn-sm btn-success ml-auto" @click="clickApproveWaitlistUser(user.id)">Approve</ajax-button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div purpose="table-container" v-else>
|
||||
<h4> There are no users currently on the Fleet Sandbox waitlist.</h4>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>
|
||||
2
website/views/pages/try-fleet/register.ejs
vendored
2
website/views/pages/try-fleet/register.ejs
vendored
|
|
@ -46,7 +46,7 @@
|
|||
<a class="mx-auto font-weight-bold d-flex align-items-center py-3" href="/try-fleet/login">Sign in with existing account <img alt="A blue arrow pointing right" style="height: 10px; margin-left: 6px;" src="/images/arrow-right-blue-18x10@2x.png"></a>
|
||||
<ajax-button style="height: 53px;" purpose="submit-button" spinner="true" type="submit" :syncing="syncing" class="btn btn-block btn-lg btn-info">Try again</ajax-button>
|
||||
</div>
|
||||
<cloud-error purpose="cloud-error" v-else-if="cloudError === 'requestToSandboxTimedOut'">
|
||||
<cloud-error purpose="cloud-error" v-else-if="cloudError === 'requestToProvisionerTimedOut'">
|
||||
<p purpose="error-message">Fleet Sandbox is experiencing unusually high activity. Please refresh the page in 13 seconds and try signing up again.</p>
|
||||
</cloud-error>
|
||||
<cloud-error purpose="cloud-error" v-else-if="cloudError"></cloud-error>
|
||||
|
|
|
|||
Loading…
Reference in a new issue