Security scan updates
@@ -185,8 +185,7 @@ def _csp(config):
|
||||
def create_app(application):
|
||||
@application.after_request
|
||||
def add_security_headers(response):
|
||||
# Add Cross-Origin-Embedder-Policy header
|
||||
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
|
||||
response.headers['Cross-Origin-Embedder-Policy'] = 'credentialless'
|
||||
return response
|
||||
|
||||
@application.context_processor
|
||||
@@ -274,14 +273,11 @@ def create_app(application):
|
||||
content_security_policy=_csp(application.config),
|
||||
content_security_policy_nonce_in=["style-src", "script-src"],
|
||||
permissions_policy={
|
||||
"accelerometer": "()",
|
||||
"ambient-light-sensor": "()",
|
||||
"autoplay": "()",
|
||||
"battery": "()",
|
||||
"accelerometer": "(self \"https://www.youtube-nocookie.com\")",
|
||||
"autoplay": "(self \"https://www.youtube-nocookie.com\")",
|
||||
"camera": "()",
|
||||
"document-domain": "()",
|
||||
"geolocation": "()",
|
||||
"gyroscope": "()",
|
||||
"gyroscope": "(self \"https://www.youtube-nocookie.com\")",
|
||||
"local-fonts": "()",
|
||||
"magnetometer": "()",
|
||||
"microphone": "()",
|
||||
|
||||
@@ -25,30 +25,31 @@
|
||||
this.bindEvents();
|
||||
};
|
||||
ListEntry.optionalAttributes = ['aria-describedby'];
|
||||
ListEntry.prototype.entryTemplate = Hogan.compile(
|
||||
'<div class="list-entry">' +
|
||||
'<label for="{{{id}}}" class="govuk-input--numbered__label">' +
|
||||
'<span class="usa-sr-only">{{listItemName}} number </span>{{number}}.' +
|
||||
'</label>' +
|
||||
'<input' +
|
||||
' name="{{name}}"' +
|
||||
' id="{{id}}"' +
|
||||
' {{#value}}value="{{value}}{{/value}}"' +
|
||||
' {{{sharedAttributes}}}' +
|
||||
'/>' +
|
||||
'{{#button}}' +
|
||||
'<button type="button" class="usa-button input-list__button--remove">' +
|
||||
'Remove<span class="usa-sr-only"> {{listItemName}} number {{number}}</span>' +
|
||||
'</button>' +
|
||||
'{{/button}}' +
|
||||
'</div>'
|
||||
);
|
||||
ListEntry.prototype.addButtonTemplate = Hogan.compile(
|
||||
'<button type="button" class="usa-button input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>'
|
||||
);
|
||||
ListEntry.prototype.renderEntry = function(data) {
|
||||
return `
|
||||
<div class="list-entry">
|
||||
<label for="${data.id}" class="govuk-input--numbered__label">
|
||||
<span class="usa-sr-only">${data.listItemName} number </span>${data.number}.
|
||||
</label>
|
||||
<input
|
||||
name="${data.name}"
|
||||
id="${data.id}"
|
||||
${data.value ? `value="${data.value}"` : ''}
|
||||
${data.sharedAttributes}
|
||||
/>
|
||||
${data.button ? `
|
||||
<button type="button" class="usa-button input-list__button--remove">
|
||||
Remove<span class="usa-sr-only"> ${data.listItemName} number ${data.number}</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
ListEntry.prototype.renderAddButton = function(data) {
|
||||
return `<button type="button" class="usa-button input-list__button--add">Add another ${data.listItemName} (${data.entriesLeft} remaining)</button>`;
|
||||
};
|
||||
ListEntry.prototype.getSharedAttributes = function () {
|
||||
var $inputs = this.$wrapper.find('input'),
|
||||
attributeTemplate = Hogan.compile(' {{name}}="{{value}}"'),
|
||||
generatedAttributes = ['id', 'name', 'value'],
|
||||
attributes = [],
|
||||
attrIdx,
|
||||
@@ -68,7 +69,7 @@
|
||||
while (attrIdx--) {
|
||||
// prevent duplicates
|
||||
if ($.inArray(elmAttrs[attrIdx].name, existingAttributes) === -1) {
|
||||
attrStr += attributeTemplate.render({ 'name': elmAttrs[attrIdx].name, 'value': elmAttrs[attrIdx].value });
|
||||
attrStr += ` ${elmAttrs[attrIdx].name}="${elmAttrs[attrIdx].value}"`;
|
||||
existingAttributes.push(elmAttrs[attrIdx].name);
|
||||
}
|
||||
}
|
||||
@@ -188,10 +189,10 @@
|
||||
if (entryNumber > 1) {
|
||||
dataObj.button = true;
|
||||
}
|
||||
this.$wrapper.append(this.entryTemplate.render(dataObj));
|
||||
this.$wrapper.append(this.renderEntry(dataObj));
|
||||
}.bind(this));
|
||||
if (this.entries.length < this.maxEntries) {
|
||||
this.$wrapper.append(this.addButtonTemplate.render({
|
||||
this.$wrapper.append(this.renderAddButton({
|
||||
'listItemName' : this.listItemName,
|
||||
'entriesLeft' : (this.maxEntries - this.entries.length)
|
||||
}));
|
||||
|
||||
@@ -3,65 +3,70 @@
|
||||
"use strict";
|
||||
|
||||
var Modules = global.GOVUK.Modules;
|
||||
var Hogan = global.Hogan;
|
||||
|
||||
// Object holding all the states for the component's HTML
|
||||
let states = {
|
||||
'initial': Hogan.compile(`
|
||||
{{#showNowAsDefault}}
|
||||
// Template functions for rendering component states
|
||||
let renderStates = {
|
||||
'initial': function(data) {
|
||||
return `
|
||||
${data.showNowAsDefault ? `
|
||||
<div class="radio-select__column margin-y-2">
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
|
||||
<label class="usa-radio__label" for="${data.name}-0">Now</label>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="radio-select__column margin-y-2">
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
|
||||
<label class="usa-radio__label" for="{{name}}-0">Now</label>
|
||||
</div>
|
||||
${data.categories.map(category =>
|
||||
`<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='${category}' />`
|
||||
).join('')}
|
||||
</div>
|
||||
{{/showNowAsDefault}}
|
||||
<div class="radio-select__column margin-y-2">
|
||||
{{#categories}}
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='{{.}}' />
|
||||
{{/categories}}
|
||||
</div>
|
||||
`),
|
||||
'choose': Hogan.compile(`
|
||||
{{#showNowAsDefault}}
|
||||
`;
|
||||
},
|
||||
'choose': function(data) {
|
||||
return `
|
||||
${data.showNowAsDefault ? `
|
||||
<div class="radio-select__column margin-y-2">
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
|
||||
<label class="usa-radio__label" for="${data.name}-0">Now</label>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="radio-select__column margin-y-2">
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
|
||||
<label class="usa-radio__label" for="{{name}}-0">Now</label>
|
||||
</div>
|
||||
${data.choices.map(choice => `
|
||||
<div class="usa-radio js-option">
|
||||
<input class="usa-radio__input" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
|
||||
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
|
||||
</div>
|
||||
{{/showNowAsDefault}}
|
||||
<div class="radio-select__column margin-y-2">
|
||||
{{#choices}}
|
||||
<div class="usa-radio js-option">
|
||||
<input class="usa-radio__input" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
|
||||
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
|
||||
`;
|
||||
},
|
||||
'chosen': function(data) {
|
||||
return `
|
||||
${data.showNowAsDefault ? `
|
||||
<div class="radio-select__column margin-y-2">
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" id="${data.name}-0" name="${data.name}" type="radio" value="">
|
||||
<label class="usa-radio__label" for="${data.name}-0">Now</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/choices}}
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
|
||||
</div>
|
||||
`),
|
||||
'chosen': Hogan.compile(`
|
||||
{{#showNowAsDefault}}
|
||||
` : ''}
|
||||
<div class="radio-select__column margin-y-2">
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" id="{{name}}-0" name="{{name}}" type="radio" value="">
|
||||
<label class="usa-radio__label" for="{{name}}-0">Now</label>
|
||||
</div>
|
||||
${data.choices.map(choice => `
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" checked="checked" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
|
||||
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
{{/showNowAsDefault}}
|
||||
<div class="radio-select__column margin-y-2">
|
||||
{{#choices}}
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" checked="checked" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
|
||||
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
|
||||
</div>
|
||||
{{/choices}}
|
||||
</div>
|
||||
<div class="radio-select__column margin-y-2">
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
|
||||
</div>
|
||||
`)
|
||||
<div class="radio-select__column margin-y-2">
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
let shiftFocus = function(elementToFocus, component) {
|
||||
@@ -80,7 +85,7 @@
|
||||
|
||||
let $component = $(component);
|
||||
let render = (state, data) => {
|
||||
$component.html(states[state].render(data));
|
||||
$component.html(renderStates[state](data));
|
||||
};
|
||||
// store array of all options in component
|
||||
let choices = $('label', $component).toArray().map(function(element) {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<p>Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users
|
||||
can create an account and get started within minutes. We take the security and privacy of messaging data seriously
|
||||
by minimizing data retention and using modern encryption methods.</p>
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fkcy_xbo0Bw?si=qNo94BwL4WCeusy5&start=2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> <h2 class="padding-bottom-3">Product Highlights</h2>
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fkcy_xbo0Bw?si=qNo94BwL4WCeusy5&start=2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen credentialless></iframe> <h2 class="padding-bottom-3">Product Highlights</h2>
|
||||
{% set product_highlights = [
|
||||
{
|
||||
"svg_src": "#send",
|
||||
|
||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 158 KiB |
@@ -44,7 +44,6 @@ const javascripts = () => {
|
||||
.pipe(buffer())
|
||||
.pipe(
|
||||
plugins.addSrc.prepend([
|
||||
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
|
||||
paths.npm + 'jquery/dist/jquery.min.js',
|
||||
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
|
||||
paths.npm + 'textarea-caret/index.js',
|
||||
|
||||
49
package-lock.json
generated
@@ -18,7 +18,6 @@
|
||||
"govuk_frontend_toolkit": "^9.0.1",
|
||||
"govuk-frontend": "2.13.0",
|
||||
"gulp-merge": "^0.1.1",
|
||||
"hogan": "1.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"morphdom": "^2.7.7",
|
||||
"playwright": "^1.54.1",
|
||||
@@ -4404,11 +4403,6 @@
|
||||
"sass-embedded-linux-x64": "^1.89.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -8856,26 +8850,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hogan": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hogan/-/hogan-1.0.2.tgz",
|
||||
"integrity": "sha512-2RV7G4f+Rt9YIYDu01r6pgZvP+XhrXi/JKlXd4b+vRybXk94ui4PQjbh/lFaH8gQtxCygy/WKkqmpm0IyZysJA==",
|
||||
"dependencies": {
|
||||
"hogan.js": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/hogan.js": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz",
|
||||
"integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==",
|
||||
"dependencies": {
|
||||
"mkdirp": "0.3.0",
|
||||
"nopt": "1.0.10"
|
||||
},
|
||||
"bin": {
|
||||
"hulk": "bin/hulk"
|
||||
}
|
||||
},
|
||||
"node_modules/homedir-polyfill": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
|
||||
@@ -11489,15 +11463,6 @@
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
|
||||
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==",
|
||||
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/morphdom": {
|
||||
"version": "2.7.7",
|
||||
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.7.tgz",
|
||||
@@ -11593,20 +11558,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
||||
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"govuk_frontend_toolkit": "^9.0.1",
|
||||
"govuk-frontend": "2.13.0",
|
||||
"gulp-merge": "^0.1.1",
|
||||
"hogan": "1.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"morphdom": "^2.7.7",
|
||||
"playwright": "^1.54.1",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
"""Simple tests to verify ZAP security fixes"""
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -24,11 +23,23 @@ def test_no_duplicate_form_action(client_request, mocker, mock_get_service_and_o
|
||||
assert count == 1
|
||||
|
||||
|
||||
def test_cross_origin_embedder_policy_exists(client_request, mocker, mock_get_service_and_organization_counts):
|
||||
"""Check that Cross-Origin-Embedder-Policy header is present"""
|
||||
def test_cross_origin_embedder_policy_set_to_credentialless(client_request, mocker, mock_get_service_and_organization_counts):
|
||||
"""Check that Cross-Origin-Embedder-Policy is set to 'credentialless' for YouTube compatibility"""
|
||||
mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user")
|
||||
client_request.logout()
|
||||
response = client_request.get_response('.index')
|
||||
|
||||
assert 'Cross-Origin-Embedder-Policy' in response.headers
|
||||
assert response.headers['Cross-Origin-Embedder-Policy'] == 'credentialless'
|
||||
assert response.headers.get('Cross-Origin-Embedder-Policy') == 'credentialless'
|
||||
|
||||
|
||||
def test_permissions_policy_allows_youtube_features(client_request, mocker, mock_get_service_and_organization_counts):
|
||||
"""Check that Permissions-Policy allows necessary features for YouTube embeds"""
|
||||
mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user")
|
||||
client_request.logout()
|
||||
response = client_request.get_response('.index')
|
||||
|
||||
permissions_policy = response.headers.get('Permissions-Policy', '')
|
||||
|
||||
assert 'accelerometer=(self "https://www.youtube-nocookie.com")' in permissions_policy
|
||||
assert 'autoplay=(self "https://www.youtube-nocookie.com")' in permissions_policy
|
||||
assert 'gyroscope=(self "https://www.youtube-nocookie.com")' in permissions_policy
|
||||
|
||||
20
urls.js
@@ -19,17 +19,17 @@ const routes = {
|
||||
authenticated: [
|
||||
{
|
||||
label: 'SMS Template Preview',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/send/a4e530e7-cae2-4e24-a1e9-752141a0b58e/one-off/step-0',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/send/9c2b7a55-8785-4dc6-84d6-eb0e0615590d/one-off/step-0',
|
||||
},
|
||||
// Pages with govuk buttons that need testing
|
||||
{
|
||||
label: 'API Keys',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/api/keys',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/api/keys',
|
||||
},
|
||||
// Pages to test radio buttons before converting govukRadios to USWDS
|
||||
{
|
||||
label: 'API Keys Create',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/api/keys/create',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/api/keys/create',
|
||||
},
|
||||
{
|
||||
label: 'Change User Auth',
|
||||
@@ -41,31 +41,31 @@ const routes = {
|
||||
},
|
||||
{
|
||||
label: 'Service Settings',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings',
|
||||
},
|
||||
{
|
||||
label: 'Service Send Files By Email',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings/send-files-by-email',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings/send-files-by-email',
|
||||
},
|
||||
{
|
||||
label: 'Service SMS Prefix',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings/sms-prefix',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings/sms-prefix',
|
||||
},
|
||||
{
|
||||
label: 'Send One Off Step 2',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/send/a4e530e7-cae2-4e24-a1e9-752141a0b58e/one-off/step-2',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/send/9c2b7a55-8785-4dc6-84d6-eb0e0615590d/one-off/step-2',
|
||||
},
|
||||
{
|
||||
label: 'Choose Template',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/templates',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/templates',
|
||||
},
|
||||
{
|
||||
label: 'Team Members',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/users',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users',
|
||||
},
|
||||
{
|
||||
label: 'Invite User',
|
||||
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/users/invite',
|
||||
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users/invite',
|
||||
},
|
||||
// Platform admin pages with checkboxes
|
||||
{
|
||||
|
||||