Skip to content

Commit

Permalink
Website: Add admin tool for generating Fleet Premium licenses. (#8478)
Browse files Browse the repository at this point in the history
* create admin/generate-license page

* create generate-license-key action, update routes, policies, importer, regenerate cloud-sdk

* update layouts

* use moment

* Update view-generate-license.js

* Fixing lint errors

* Update generate-license-key.js

* Update redirects in is-super-admin policy

* redirect super admins to the license generator

* Update login form

* requested changes from mike-j-thomas

* Update generate-license.page.js

* Update is-super-admin.js

* Update view-login.js

* Update generate-license-key.js

* Update generate-license-key.js

* use naming convention for js timestamps

* validTo » expiresAt

Co-authored-by: Mike McNeil <[email protected]>
  • Loading branch information
eashaw and mikermcneil authored Dec 5, 2022
1 parent d92d998 commit 48f86b2
Show file tree
Hide file tree
Showing 21 changed files with 352 additions and 15 deletions.
49 changes: 49 additions & 0 deletions website/api/controllers/admin/generate-license-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module.exports = {


friendlyName: 'Generate license key',// FUTURE: Rename this to avoid confusion w/ generators. For example: 'Build license key'


description: 'Generate and return a Fleet Premium license key.',


inputs: {
numberOfHosts: {
type: 'number',
required: true,
},

organization: {
type: 'string',
required: true,
},

expiresAt: {
type: 'number',
required: true,
description: 'A JS timestamp representing when this license will expire.'
}
},


exits: {
success: {
outputFriendlyName: 'License key',
outputType: 'string',
},
},


fn: async function ({numberOfHosts, organization, expiresAt}) {

let licenseKey = await sails.helpers.createLicenseKey.with({
numberOfHosts: numberOfHosts,
organization: organization,
expiresAt: expiresAt
});

return licenseKey;
}


};
35 changes: 35 additions & 0 deletions website/api/controllers/admin/view-generate-license.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module.exports = {


friendlyName: 'View generate license',


description: 'Display "Generate license" page, an admin tool for generating Fleet Premium licenses',


exits: {

success: {
viewTemplatePath: 'pages/admin/generate-license'
},

},


fn: async function () {

// Throw an error if the licenseKeyGeneratorPrivateKey or licenseKeyGeneratorPassphrase are missing.
if(!sails.config.custom.licenseKeyGeneratorPrivateKey) {
throw new Error('Missing config variable: The license key generator private key missing (sails.config.custom.licenseKeyGeneratorPrivateKey)! To use this tool, a license key generator private key is required.');
}

if(!sails.config.custom.licenseKeyGeneratorPassphrase) {
throw new Error('Missing config variable: The license key generator passphrase missing(sails.config.custom.licenseKeyGeneratorPassphrase)! To use this tool, a license key generator passphrase is required.');
}

// Respond with view.
return {};
}


};
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ module.exports = {
let licenseKey = await sails.helpers.createLicenseKey.with({
numberOfHosts: quoteRecord.numberOfHosts,
organization: inputs.organization ? inputs.organization : this.req.me.organization,
validTo: subscription.current_period_end
expiresAt: subscription.current_period_end
});

// Create the subscription record for this order.
Expand Down
5 changes: 4 additions & 1 deletion website/api/controllers/customers/view-new-license.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ module.exports = {
if (!this.req.me) {
throw {redirect: '/customers/register'};
}

// If the user is a super admin, we'll redirect them to the generate-license page.
if(this.req.me.isSuperAdmin) {
throw {redirect: '/admin/generate-license'};
}
// If the user has a license key, we'll redirect them to the customer dashboard.
let userHasExistingSubscription = await Subscription.findOne({user: this.req.me.id});
if (userHasExistingSubscription) {
Expand Down
4 changes: 3 additions & 1 deletion website/api/controllers/entrance/view-login.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ module.exports = {
fn: async function () {

if (this.req.me) {
if(this.req.me.hasBillingCard){
if(this.req.me.isSuperAdmin){
throw {redirect: '/admin/generate-license'};
} else if(this.req.me.hasBillingCard) {
throw {redirect: '/customers/new-license'};
} else {
throw {redirect: '/try-fleet/sandbox'};
Expand Down
4 changes: 2 additions & 2 deletions website/api/helpers/create-license-key.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
required: true,
},

validTo: {
expiresAt: {
type: 'number',
required: true,
description: 'A JS Timestamp representing when this license will expire.'
Expand All @@ -44,7 +44,7 @@ module.exports = {
let token = jwt.sign(
{
iss: 'Fleet Device Management Inc.',
exp: inputs.validTo,
exp: inputs.expiresAt,
sub: inputs.organization,
devices: inputs.numberOfHosts,
note: 'Created with Fleet License key dispenser',
Expand Down
8 changes: 7 additions & 1 deletion website/api/policies/is-super-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ module.exports = async function (req, res, proceed) {
// > For more about where `req.me` comes from, check out this app's
// > custom hook (`api/hooks/custom/index.js`).
if (!req.me) {
return res.unauthorized();
// Rather than use the standard res.unauthorized(), if the request did not come from a logged-in user,
// we'll redirect them to an generic version of the customer login page.
if (req.wantsJSON) {
return res.sendStatus(401);
} else {
return res.redirect('/customers/login?admin');
}
}//•

// Then check that this user is a "super admin".
Expand Down
2 changes: 1 addition & 1 deletion website/assets/js/cloud.setup.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions website/assets/js/pages/admin/generate-license.page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
parasails.registerPage('generate-license', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: {
// Form data
formData: {},
// For tracking client-side validation errors in our form.
// > Has property set to `true` for each invalid property in `formData`.
formErrors: {},
// Form rules
formRules: {
numberOfHosts: {required: true},
organization: {required: true},
expiresAt: {required: true},
},
// Syncing / loading state
syncing: false,
// Server error state
cloudError: '',
generatedLicenseKey: '',
showResult: false,
},

// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
// Get a formatted date string for year from today's date.
let oneYearFromNowDateString = moment(Date.now() + (365*24*60*60*1000)).format('YYYY-MM-DD');
// Set the starting value for the validTo input
this.formData.expiresAt = oneYearFromNowDateString;
},
mounted: async function() {
//…
},

// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
handleSubmittingForm: async function() {
let validToDate = new Date(this.formData.expiresAt);
let validToTimestamp = validToDate.getTime();
this.generatedLicenseKey = await Cloud.generateLicenseKey.with({
numberOfHosts: this.formData.numberOfHosts,
organization: this.formData.organization,
expiresAt: validToTimestamp
});
},

submittedQuoteForm: async function() {
this.syncing = false;
this.showResult = true;
},

clickCopyLicenseKey: function(){
$('[purpose="copied-notification"]').finish();
$('[purpose="copied-notification"]').fadeIn(100).delay(2000).fadeOut(500);
// https://caniuse.com/mdn-api_clipboard_writetext
navigator.clipboard.writeText(this.generatedLicenseKey);
},

clickClearFormFields: async function() {
this.generatedLicenseKey = '';
this.showResult = false;
this.formErrors = {};
this.formData = {};
this.formData.validTo = moment(Date.now() + (365*24*60*60*1000)).format('YYYY-MM-DD');
await this.forceRender();
}
}
});
5 changes: 4 additions & 1 deletion website/assets/js/pages/entrance/login.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ parasails.registerPage('login', {

// Server error state for the form
cloudError: '',
showCustomerLogin: true,
},

// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
//…
if(window.location.search === '?admin') {
this.showCustomerLogin = false;
}
},
mounted: async function() {
//…
Expand Down
1 change: 1 addition & 0 deletions website/assets/styles/importer.less
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
@import 'pages/articles/articles.less';
@import 'pages/reports/state-of-device-management.less';
@import 'pages/osquery-table-details.less';
@import 'pages/admin/generate-license.less';

@import 'pages/transparency.less';
@import 'pages/press-kit.less';
79 changes: 79 additions & 0 deletions website/assets/styles/pages/admin/generate-license.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#generate-license {
padding-top: 80px;

h1 {
font-size: 28px;
line-height: 38px;
}
[purpose='form'] {
max-width: 480px;
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
}
label {
font-weight: 400;
margin-bottom: 4px;
}
.form-control {
height: 40px;
border-radius: 6px;
}
.card {
border-radius: 6px;
}
.card-body {
padding: 2em;
}
}
[purpose='cloud-error'] {
max-width: fit-content;
}
[purpose='submit-button'] {
margin-left: auto;
margin-right: auto;
border-radius: 4px;
padding-top: 10px;
padding-bottom: 10px;
display: flex;
span {
display: inline;
margin-left: auto;
margin-right: auto;
font-size: 16px;
line-height: 20px;
text-align: center;
font-weight: 400;
}

}

[purpose='result'] {
border: 1px solid @bg-lt-gray;
padding: 4px 6px;
border-radius: 4px;
position: relative;
code {
font: @code-font;
color: @core-fleet-black;
}
}
[purpose='copy-button'] {
cursor: pointer;
color: @core-vibrant-blue;
}
[purpose='copied-notification'] {
display: none;
font-weight: 400;
position: absolute;
top: -30px;
right: 0px;
font-size: 16px;
line-height: 20px;
white-space: nowrap;
color: @core-vibrant-blue;
}
}
10 changes: 9 additions & 1 deletion website/assets/styles/pages/entrance/login.less
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@
a {
color: @core-vibrant-blue;
}

[purpose='customer-login-container'] {
max-width: 800px;
}
[purpose='login-container'] {
max-width: 600px;
[purpose='customer-portal-form'] {
max-width: 480px;
}
}
[purpose='customer-portal-form'] {
label {
font-weight: 700;
Expand Down
1 change: 1 addition & 0 deletions website/config/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
module.exports.policies = {

'*': 'is-logged-in',
'admin/*': 'is-super-admin',

// Bypass the `is-logged-in` policy for:
'entrance/*': true,
Expand Down
10 changes: 9 additions & 1 deletion website/config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ module.exports.routes = {
action: 'view-osquery-table-details',
},

'GET /admin/generate-license': {
action: 'admin/view-generate-license',
locals: {
layout: 'layouts/layout-customer'
}
},


// ╦ ╔═╗╔═╗╔═╗╔═╗╦ ╦ ╦═╗╔═╗╔╦╗╦╦═╗╔═╗╔═╗╔╦╗╔═╗
// ║ ║╣ ║ ╦╠═╣║ ╚╦╝ ╠╦╝║╣ ║║║╠╦╝║╣ ║ ║ ╚═╗
Expand Down Expand Up @@ -348,5 +355,6 @@ module.exports.routes = {
'POST /api/v1/entrance/update-password-and-login': { action: 'entrance/update-password-and-login' },
'POST /api/v1/deliver-demo-signup': { action: 'deliver-demo-signup' },
'POST /api/v1/create-or-update-one-newsletter-subscription': { action: 'create-or-update-one-newsletter-subscription' },
'/api/v1/unsubscribe-from-all-newsletters': { action: 'unsubscribe-from-all-newsletters' }
'/api/v1/unsubscribe-from-all-newsletters': { action: 'unsubscribe-from-all-newsletters' },
'POST /api/v1/admin/generate-license-key': { action: 'admin/generate-license-key' },
};
1 change: 1 addition & 0 deletions website/views/layouts/layout-customer.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
<script src="/js/pages/account/account-overview.page.js"></script>
<script src="/js/pages/account/edit-password.page.js"></script>
<script src="/js/pages/account/edit-profile.page.js"></script>
<script src="/js/pages/admin/generate-license.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/contact.page.js"></script>
Expand Down
Loading

0 comments on commit 48f86b2

Please sign in to comment.