3 const th
= require('./template-helper');
4 const { sessionNavLinks
} = require('@squeep/authentication-module');
8 * @param {Object} hApp.properties
9 * @param {String[]=} hApp.properties.url
10 * @param {String[]=} hApp.properties.summary
11 * @param {String[]=} hApp.properties.logo
12 * @param {String[]=} hApp.properties.name
15 function renderClientIdentifierProperties(hApp
) {
16 const properties
= hApp
.properties
|| {};
19 const { url
, summary
, logo
, name
} = properties
;
21 parts
.push('<span class="client-identifier">');
23 parts
.push(`<a href="${url[0]}">`);
25 if (summary
?.length
) {
26 imgTitle
= ` title="${summary[0]}"`;
30 if (typeof logo
[0] === 'string') {
32 alt
= 'Client Identifier Logo';
34 ({ value: src
, alt
} = logo
[0]);
36 parts
.push(`<img src="${src}" alt="${alt}"${imgTitle}>`);
39 parts
.push(properties
['name'][0]);
44 parts
.push('</span>');
45 return parts
.join('');
50 * @param {Object} clientIdentifier
51 * @param {Object[]} clientIdentifier.items
54 function renderClientIdentifier(clientIdentifier
) {
55 const hAppEntries
= clientIdentifier
?.items
|| [];
56 return hAppEntries
.map(renderClientIdentifierProperties
).join('');
61 * @param {String} profile
62 * @param {Boolean} selected
65 function renderProfileOption(profile
, selected
) {
66 return `<option value="${profile}"${selected ? ' selected' : ''}>${profile}</option>`;
71 * @param {String[]} availableProfiles
72 * @param {String} hintProfile
75 function renderProfileFieldset(availableProfiles
, hintProfile
) {
76 if (!availableProfiles
|| availableProfiles
.length
<= 1) {
77 const profile
= availableProfiles
?.[0] || hintProfile
;
78 return `<input type="hidden" name="me" value="${profile}">`;
83 <legend>Select Profile</legend>
85 You may choose to identify to this client with a different profile.
87 <label for="me">Choose your identifying profile</label>
88 <select class="uri" name="me" id="me">
89 ${availableProfiles.map((profile) => renderProfileOption(profile, profile === hintProfile)).join('\n')}
96 * @param {ScopeDetails} scope
97 * @param {String} scope.scope
98 * @param {String} scope.description
99 * @param {String[]} scope.profiles
100 * @param {Boolean} checked
103 function renderScopeCheckboxLI(scope
, checked
) {
104 let scopeDescription
;
105 if (scope
.description
) {
107 <span class="description">${scope.description}</span>`;
109 scopeDescription
= '';
112 if (scope
.profiles
?.length
) {
113 profileClass
= ['profile-scope'].concat(scope
.profiles
).join(' ');
118 <li class="${profileClass}">
119 <input type="checkbox" id="scope_${scope.scope}" name="accepted_scopes[]" value="${scope.scope}"${checked ? ' checked' : ''}>
120 <label for="scope_${scope.scope}">${scope.scope}</label>${scopeDescription}
125 function renderRequestedScopes(requestedScopes
) {
126 if (!requestedScopes
?.length
) {
132 <legend>Grants Requested By Client</legend>
134 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.
136 <ul class="scope" id="requested-scope-list">
137 ${requestedScopes.map((scopeDetails) => renderScopeCheckboxLI(scopeDetails, true)).join('\n')}
143 * @param {ScopeDetails[]} additionalScopes
146 function renderAdditionalScopes(additionalScopes
) {
151 <legend>Additional Grants</legend>`);
152 if (additionalScopes
?.length
) {
155 Your profile has been configured to offer scopes which were not explicitly requested by the client application.
156 Select any you would like to include.
158 <ul class="scope" id="additional-scope-list">
159 ${additionalScopes.map((scopeDetails) => renderScopeCheckboxLI(scopeDetails, false)).join('\n')}
165 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.
167 <label for="ad-hoc-scopes">Ad Hoc Scopes</label>
168 <input type="text" id="ad-hoc-scopes" name="ad_hoc_scopes" value="">
170 return parts
.join('');
177 function renderExpiration(requestedScopes
) {
178 const tokenableScopes
= requestedScopes
.filter((s
) => !['profile', 'email'].includes(s
));
179 if (!tokenableScopes
.length
) {
185 \t\t<legend>Expiration</legend>
187 \t\t\tBy default, tokens issued do not automatically expire, but a longevity can be enforced.
191 \t\t\t<summary>Set Expiration</summary>
192 \t\t\t\t${radioButton('expires', 'never', 'Never', true)}
193 \t\t\t\t${radioButton('expires', '1d', '1 Day')}
194 \t\t\t\t${radioButton('expires', '1w', '1 Week')}
195 \t\t\t\t${radioButton('expires', '1m', '1 Month')}
197 \t\t\t\t${radioButton('expires', 'custom', 'Other:')}
198 \t\t\t\t<input type="number" id="expires-seconds" name="expires-seconds">
199 \t\t\t\t<label for="expires-seconds">seconds</label>
203 \t\t\t\tTokens with expirations may be allowed to be renewed for a fresh token for an amount of time after they expire.
205 \t\t\t\t${radioButton('refresh', 'none', 'Not Refreshable', true)}
206 \t\t\t\t${radioButton('refresh', '1d', '1 Day')}
207 \t\t\t\t${radioButton('refresh', '1w', '1 Week')}
208 \t\t\t\t${radioButton('refresh', '1m', '1 Month')}
210 \t\t\t\t${radioButton('refresh', 'custom', 'Other:')}
211 \t\t\t\t<input type="number" id="refresh-seconds" name="refresh-seconds">
212 \t\t\t\t<label for="refresh-seconds">seconds</label>
218 function radioButton(name
, value
, label
, checked
= false, indent
= 0) {
219 const id
= `${name}-${value}`;
220 return th
.indented(indent
, [
222 `\t<input type="radio" name="${name}" id="${id}" value="${value}"${checked ? ' checked' : ''}>`,
223 `\t<label for="${id}">${label}</label>`,
230 * @param {Object} ctx
231 * @param {Object[]} ctx.notifications
232 * @param {Object} ctx.session
233 * @param {String[]=} ctx.session.scope
234 * @param {URL=} ctx.session.me
235 * @param {String[]} ctx.session.profiles
236 * @param {ScopeIndex} ctx.session.scopeIndex
237 * @param {Object} ctx.session.clientIdentifier
238 * @param {Object[]} ctx.session.clientIdentifier.items
239 * @param {Object} ctx.session.clientIdentifier.items.properties
240 * @param {String[]=} ctx.session.clientIdentifier.items.properties.url
241 * @param {String[]=} ctx.session.clientIdentifier.items.properties.summary
242 * @param {String[]=} ctx.session.clientIdentifier.items.properties.logo
243 * @param {String[]=} ctx.session.clientIdentifier.items.properties.name
244 * @param {String} ctx.session.clientId
245 * @param {String} ctx.session.persist
246 * @param {String} ctx.session.redirectUri
247 * @param {Object} options
250 function mainContent(ctx
, options
) { // eslint-disable-line no-unused-vars
251 const session
= ctx
.session
|| {};
252 const hintedProfile
= session
.me
?.href
|| session
.profiles
?.[0] || '';
253 const scopeIndex
= session
.scopeIndex
|| {};
256 * Add requested scopes to index, if not already present,
257 * and de-associate requested scopes from profiles.
259 const scopes
= session
.scope
|| [];
260 scopes
.forEach((scopeName
) => {
261 if ((scopeName
in scopeIndex
)) {
262 scopeIndex
[scopeName
].profiles
= []; // eslint-disable-line security/detect-object-injection
264 scopeIndex
[scopeName
] = { // eslint-disable-line security/detect-object-injection
271 // Divide scopes between requested and additional from profiles.
272 const requestedScopes
= scopes
.map((scope
) => ({
274 description: scopeIndex
[scope
].description
, // eslint-disable-line security/detect-object-injection
276 const additionalScopes
= Object
.keys(scopeIndex
)
277 .filter((scope
) => scopeIndex
[scope
].profiles
.length
) // eslint-disable-line security/detect-object-injection
280 description: scopeIndex
[scope
].description
, // eslint-disable-line security/detect-object-injection
281 profiles: scopeIndex
[scope
].profiles
, // eslint-disable-line security/detect-object-injection
285 `<section class="information">
286 \tThe application client ${renderClientIdentifier(session.clientIdentifier)} at <a class="uri" aria-label="client-identifier" id="${session.clientId}">${session.clientId}</a> would like to identify you as <a class="uri" aria-label="profile"${hintedProfile ? ' id="' + hintedProfile + '"' : ''}>${hintedProfile ? hintedProfile : '(unspecified)'}</a>.
288 <section class="choices">
289 \t<form action="consent" method="POST" class="form-consent">`,
290 renderProfileFieldset(session
.profiles
, hintedProfile
),
291 renderRequestedScopes(requestedScopes
),
292 renderAdditionalScopes(additionalScopes
),
293 renderExpiration(requestedScopes
),
297 \t\t\t<legend>Do you want to allow this?</legend>
298 \t\t\t<button type="submit" class="button-accept" name="accept" value="true">Accept</button>
299 \t\t\t<button type="submit" class="button-decline" name="accept" value="false">Decline</button>
301 \t\t<input type="hidden" name="session" value="${session.persist}">
305 \t\tYou will be redirected to <a class="uri" id="${session.redirectUri ? session.redirectUri : 'unknown-redirect'}">${session.redirectUri}</a>.
313 * @param {Object} ctx
314 * @param {Object} ctx.session
315 * @param {String[]=} ctx.session.scope
316 * @param {URL=} ctx.session.me
317 * @param {String[]} ctx.session.profiles
318 * @param {ScopeIndex} ctx.session.scopeIndex
319 * @param {Object} ctx.session.clientIdentifier
320 * @param {String} ctx.session.clientId
321 * @param {String} ctx.session.persist
322 * @param {String} ctx.session.redirectUri
323 * @param {Object} options
324 * @param {Object} options.manager
325 * @param {String} options.manager.pageTitle
326 * @param {String} options.manager.footerEntries
329 module
.exports
= (ctx
, options
) => {
330 const pagePathLevel
= 0;
331 const htmlOptions
= {
332 pageTitle: `${options.manager.pageTitle} — Authorization Request`,
333 logoUrl: options
.manager
.logoUrl
,
334 footerEntries: options
.manager
.footerEntries
,
337 function queryAll(query, fn) {
338 const nodes = document.querySelectorAll(query);
339 console.log('query ' + query + ' selected ' + nodes.length);
340 return nodes.forEach(fn);
342 function profileSelected(element) {
343 const profileClass = CSS.escape(element.value);
344 // queryAll('.profile-scope input', (n) => n.setAttribute('disabled', ''));
345 queryAll('.profile-scope', (n) => n.classList.add('disabled'));
346 const profileQuery = '.profile-scope.' + profileClass;
347 // queryAll(profileQuery + ' input', (n) => n.removeAttribute('disabled'));
348 queryAll(profileQuery, (n) => n.classList.remove('disabled'));
351 const profileSelect = document.getElementById('me');
352 profileSelect.onchange = () => profileSelected(profileSelect);
353 profileSelected(profileSelect);
355 window.onload = onLoad;
359 th
.navLinks(pagePathLevel
, ctx
, htmlOptions
);
360 sessionNavLinks(pagePathLevel
, ctx
, htmlOptions
);
361 const content
= mainContent(ctx
, options
);
362 return th
.htmlPage(pagePathLevel
, ctx
, htmlOptions
, content
);