X-Git-Url: http://git.squeep.com/?p=squeep-indie-auther;a=blobdiff_plain;f=src%2Ftemplate%2Fauthorization-request-html.js;fp=src%2Ftemplate%2Fauthorization-request-html.js;h=3e312af683721642a18357b1a6e99084aa98daca;hp=0000000000000000000000000000000000000000;hb=b0103b0d496262c438b40bc20304081dbfe41e73;hpb=8ed81748bce7cea7904cac7225b20a60cafdfc16 diff --git a/src/template/authorization-request-html.js b/src/template/authorization-request-html.js new file mode 100644 index 0000000..3e312af --- /dev/null +++ b/src/template/authorization-request-html.js @@ -0,0 +1,376 @@ +'use strict'; + +const th = require('./template-helper'); + + +/** + * @param {Object} hApp + * @param {Object} hApp.properties + * @param {String[]=} hApp.properties.url + * @param {String[]=} hApp.properties.summary + * @param {String[]=} hApp.properties.logo + * @param {String[]=} hApp.properties.name + * @returns {String} + */ +function renderClientIdentifierProperties(hApp) { + const properties = hApp.properties || {}; + const parts = []; + let imgTitle = ''; + const { url, summary, logo, name } = properties; + + parts.push(''); + if (url && url.length) { + parts.push(``); + } + if (summary && summary.length) { + imgTitle = ` title="${summary[0]}"`; + } + if (logo && logo.length) { + let src, alt; + if (typeof logo[0] === 'string') { + src = logo[0]; + alt = 'Client Identifier Logo'; + } else { + ({ value: src, alt } = logo[0]); + } + parts.push(`${alt}`); + } + if (name && name.length) { + parts.push(properties['name'][0]); + } + if (url && url.length) { + parts.push(''); + } + parts.push(''); + return parts.join(''); +} + + +/** + * @param {Object} clientIdentifier + * @param {Object[]} clientIdentifier.items + * @returns {String} + */ +function renderClientIdentifier(clientIdentifier) { + const hAppEntries = clientIdentifier && clientIdentifier.items || []; + return hAppEntries.map(renderClientIdentifierProperties).join(''); +} + + +/** + * @param {String} profile + * @param {Boolean} selected + * @returns {String} + */ +function renderProfileOption(profile, selected) { + return ``; +} + + +/** + * @param {String[]} availableProfiles + * @param {String} hintProfile + * @returns {String} + */ +function renderProfileFieldset(availableProfiles, hintProfile) { + if (!availableProfiles || availableProfiles.length <= 1) { + const profile = availableProfiles && availableProfiles[0] || hintProfile; + return ``; + } + return ` +
+
+ Select Profile +
+ You may choose to identify to this client with a different profile. +
+ + +
`; +} + + +/** + * @param {ScopeDetails} scope + * @param {String} scope.scope + * @param {String} scope.description + * @param {String[]} scope.profiles + * @param {Boolean} checked + * @returns {String} + */ +function renderScopeCheckboxLI(scope, checked) { + let scopeDescription; + if (scope.description) { + scopeDescription = ` + ${scope.description}`; + } else { + scopeDescription = ''; + } + let profileClass; + if (scope.profiles && scope.profiles.length) { + profileClass = ['profile-scope'].concat(scope.profiles.map((profile) => th.escapeCSS(profile))).join(' '); + } else { + profileClass = ''; + } + return ` +
  • + + ${scopeDescription} +
  • `; +} + + +function renderRequestedScopes(requestedScopes) { + if (!requestedScopes || !requestedScopes.length) { + return ''; + } + return ` +
    +
    + Grants Requested By Client +
    + In addition to identifying you by your profile URL, this client has requested the following additional scope thingies. You may disable any of them here. +
    + +
    `; +} + +/** + * @param {ScopeDetails[]} additionalScopes + * @returns {String} + */ +function renderAdditionalScopes(additionalScopes) { + const parts = []; + parts.push(` +
    +
    + Additional Grants`); + if (additionalScopes?.length) { + parts.push(` +
    + Your profile has been configured to offer scopes which were not explicitly requested by the client application. + Select any you would like to include. +
    + +
    `); + } + parts.push(` +
    + You may also specify a space-separated list of any additional ad hoc scopes you would like to associate with this authorization request, which were not explicitly requested by the client application. +
    + + +
    `); + return parts.join(''); +} + + +/** + * + */ +function renderExpiration(requestedScopes) { + const tokenableScopes = requestedScopes.filter((s) => !['profile', 'email'].includes(s)); + if (!tokenableScopes.length) { + return ''; + } + return ` +\t
    +\t
    +\t\tExpiration +\t\t
    +\t\t\tBy default, tokens issued do not automatically expire, but a longevity can be enforced. +\t\t
    +\t\t
    +\t\t
    +\t\t\tSet Expiration +\t\t\t
    +\t\t\t\t${radioButton('expires', 'never', 'Never', true)} +\t\t\t
    +\t\t\t
    +\t\t\t\t${radioButton('expires', '1d', '1 Day')} +\t\t\t
    +\t\t\t
    +\t\t\t\t${radioButton('expires', '1w', '1 Week')} +\t\t\t
    +\t\t\t
    +\t\t\t\t${radioButton('expires', '1m', '1 Month')} +\t\t\t
    +\t\t\t
    +\t\t\t\t${radioButton('expires', 'custom', 'Other:')} +\t\t\t\t +\t\t\t\t +\t\t\t
    +\t\t\t
    +\t\t\t
    +\t\t\t\tTokens with expirations may be allowed to be renewed for a fresh token for an amount of time after they expire. +\t\t\t
    +\t\t\t
    +\t\t\t\t${radioButton('refresh', 'none', 'Not Refreshable', true)} +\t\t\t
    +\t\t\t
    +\t\t\t\t${radioButton('refresh', '1d', '1 Day')} +\t\t\t
    +\t\t\t
    +\t\t\t\t${radioButton('refresh', '1w', '1 Week')} +\t\t\t
    +\t\t\t
    +\t\t\t\t${radioButton('refresh', '1m', '1 Month')} +\t\t\t
    +\t\t\t\t${radioButton('refresh', 'custom', 'Other:')} +\t\t\t\t +\t\t\t\t +\t\t\t
    +\t\t
    +\t
    `; +} + +function radioButton(name, value, label, checked = false, indent = 0) { + const id = `${name}-${value}`; + return th.indented(indent, [ + '
    ', + `\t`, + `\t`, + '
    ', + ]).join(''); +} + +/** + * + * @param {Object} ctx + * @param {Object[]} ctx.notifications + * @param {Object} ctx.session + * @param {String[]=} ctx.session.scope + * @param {URL=} ctx.session.me + * @param {String[]} ctx.session.profiles + * @param {ScopeIndex} ctx.session.scopeIndex + * @param {Object} ctx.session.clientIdentifier + * @param {Object[]} ctx.session.clientIdentifier.items + * @param {Object} ctx.session.clientIdentifier.items.properties + * @param {String[]=} ctx.session.clientIdentifier.items.properties.url + * @param {String[]=} ctx.session.clientIdentifier.items.properties.summary + * @param {String[]=} ctx.session.clientIdentifier.items.properties.logo + * @param {String[]=} ctx.session.clientIdentifier.items.properties.name + * @param {String} ctx.session.clientId + * @param {String} ctx.session.persist + * @param {String} ctx.session.redirectUri + * @param {Object} options + * @returns {String} + */ +function mainContent(ctx, options) { // eslint-disable-line no-unused-vars + const session = ctx.session || {}; + const hintedProfile = (session.me && session.me.href) || (session.profiles && session.profiles.length && session.profiles[0]) || ''; + const scopeIndex = session.scopeIndex || {}; + + // Add requested scopes to index, if not already present, + // and de-associate requested scopes from profiles. + const scopes = session.scope || []; + scopes.forEach((scopeName) => { + if ((scopeName in scopeIndex)) { + scopeIndex[scopeName].profiles = []; // eslint-disable-line security/detect-object-injection + } else { + scopeIndex[scopeName] = { // eslint-disable-line security/detect-object-injection + description: '', + profiles: [], + }; + } + }); + + // Divide scopes between requested and additional from profiles. + const requestedScopes = scopes.map((scope) => ({ + scope, + description: scopeIndex[scope].description, // eslint-disable-line security/detect-object-injection + })); + const additionalScopes = Object.keys(scopeIndex) + .filter((scope) => scopeIndex[scope].profiles.length) // eslint-disable-line security/detect-object-injection + .map((scope) => ({ + scope, + description: scopeIndex[scope].description, // eslint-disable-line security/detect-object-injection + profiles: scopeIndex[scope].profiles, // eslint-disable-line security/detect-object-injection + })); + + return [ + `
    +\tThe application client +\t${renderClientIdentifier(session.clientIdentifier)} +\tat ${session.clientId} would like to identify you as ${hintedProfile}. +
    +
    +\t +\t
    +\t
    +\t\tYou will be redirected to ${session.redirectUri}. +\t
    +
    `, + ]; +} + +/** + * + * @param {Object} ctx + * @param {Object} ctx.session + * @param {String[]=} ctx.session.scope + * @param {URL=} ctx.session.me + * @param {String[]} ctx.session.profiles + * @param {ScopeIndex} ctx.session.scopeIndex + * @param {Object} ctx.session.clientIdentifier + * @param {String} ctx.session.clientId + * @param {String} ctx.session.persist + * @param {String} ctx.session.redirectUri + * @param {Object} options + * @param {Object} options.manager + * @param {String} options.manager.pageTitle + * @param {String} options.manager.footerEntries + * @returns {String} + */ +module.exports = (ctx, options) => { + const htmlOptions = { + pageTitle: `${options.manager.pageTitle} — Authorization Request`, + logoUrl: options.manager.logoUrl, + footerEntries: options.manager.footerEntries, + headElements: [ + ``, + ], + }; + const content = mainContent(ctx, options); + return th.htmlPage(0, ctx, htmlOptions, content); +};