3 const th
= require('./template-helper');
4 const { sessionNavLinks
} = require('@squeep/authentication-module');
7 * @param {object} hApp client identifier h-app
8 * @param {object} hApp.properties properties
9 * @param {string[]=} hApp.properties.url url
10 * @param {string[]=} hApp.properties.summary summary
11 * @param {string[]=} hApp.properties.logo logo
12 * @param {string[]=} hApp.properties.name name
13 * @returns {string} span
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 client identifier
51 * @param {object[]} clientIdentifier.items items
52 * @returns {string} spans
54 function renderClientIdentifier(clientIdentifier
) {
55 const hAppEntries
= clientIdentifier
?.items
|| [];
56 return hAppEntries
.map(renderClientIdentifierProperties
).join('');
61 * @param {string} profile profile
62 * @param {boolean} selected is selected
63 * @returns {string} option
65 function renderProfileOption(profile
, selected
) {
66 return `<option value="${profile}"${selected ? ' selected' : ''}>${profile}</option>`;
71 * @param {string[]} availableProfiles profiles
72 * @param {string} hintProfile profile
73 * @returns {string} fieldset
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 * @typedef {object} ScopeDetails
97 * @property {string} scope scope
98 * @property {string} description description
99 * @property {string[]} profiles profiles
103 * @param {ScopeDetails} scope scope details
104 * @param {boolean} checked is checked
105 * @returns {string} scope li
107 function renderScopeCheckboxLI(scope
, checked
) {
108 let scopeDescription
;
109 if (scope
.description
) {
111 <span class="description">${scope.description}</span>`;
113 scopeDescription
= '';
116 if (scope
.profiles
?.length
) {
117 profileClass
= ['profile-scope'].concat(scope
.profiles
).join(' ');
122 <li class="${profileClass}">
123 <input type="checkbox" id="scope_${scope.scope}" name="accepted_scopes[]" value="${scope.scope}"${checked ? ' checked' : ''}>
124 <label for="scope_${scope.scope}">${scope.scope}</label>${scopeDescription}
131 * @param {ScopeDetails[]=} requestedScopes scope details
132 * @returns {string} fieldset
134 function renderRequestedScopes(requestedScopes
) {
135 if (!requestedScopes
?.length
) {
141 <legend>Grants Requested By Client</legend>
143 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.
145 <ul class="scope" id="requested-scope-list">
146 ${requestedScopes.map((scopeDetails) => renderScopeCheckboxLI(scopeDetails, true)).join('\n')}
152 * @param {ScopeDetails[]} additionalScopes scopes
153 * @returns {string} fieldset
155 function renderAdditionalScopes(additionalScopes
) {
160 <legend>Additional Grants</legend>`);
161 if (additionalScopes
?.length
) {
164 Your profile has been configured to offer scopes which were not explicitly requested by the client application.
165 Select any you would like to include.
167 <ul class="scope" id="additional-scope-list">
168 ${additionalScopes.map((scopeDetails) => renderScopeCheckboxLI(scopeDetails, false)).join('\n')}
174 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.
176 <label for="ad-hoc-scopes">Ad Hoc Scopes</label>
177 <input type="text" id="ad-hoc-scopes" name="ad_hoc_scopes" value="">
179 return parts
.join('');
185 * @param {string[]} requestedScopes scopes
186 * @returns {string} fieldset
188 function renderExpiration(requestedScopes
) {
189 const tokenableScopes
= requestedScopes
.filter((s
) => !['profile', 'email'].includes(s
));
190 if (!tokenableScopes
.length
) {
196 \t\t<legend>Expiration</legend>
198 \t\t\tBy default, tokens issued do not automatically expire, but a longevity can be enforced.
202 \t\t\t<summary>Set Expiration</summary>
203 \t\t\t\t${radioButton('expires', 'never', 'Never', true)}
204 \t\t\t\t${radioButton('expires', '1d', '1 Day')}
205 \t\t\t\t${radioButton('expires', '1w', '1 Week')}
206 \t\t\t\t${radioButton('expires', '1m', '1 Month')}
208 \t\t\t\t${radioButton('expires', 'custom', 'Other:')}
209 \t\t\t\t<input type="number" id="expires-seconds" name="expires-seconds">
210 \t\t\t\t<label for="expires-seconds">seconds</label>
214 \t\t\t\tTokens with expirations may be allowed to be renewed for a fresh token for an amount of time after they expire.
216 \t\t\t\t${radioButton('refresh', 'none', 'Not Refreshable', true)}
217 \t\t\t\t${radioButton('refresh', '1d', '1 Day')}
218 \t\t\t\t${radioButton('refresh', '1w', '1 Week')}
219 \t\t\t\t${radioButton('refresh', '1m', '1 Month')}
221 \t\t\t\t${radioButton('refresh', 'custom', 'Other:')}
222 \t\t\t\t<input type="number" id="refresh-seconds" name="refresh-seconds">
223 \t\t\t\t<label for="refresh-seconds">seconds</label>
231 * @param {string} name name
232 * @param {string} value value
233 * @param {string} label label
234 * @param {boolean} checked is checked
235 * @param {number} indent indent
236 * @returns {string} div
238 function radioButton(name
, value
, label
, checked
= false, indent
= 0) {
239 const id
= `${name}-${value}`;
240 return th
.indented(indent
, [
242 `\t<input type="radio" name="${name}" id="${id}" value="${value}"${checked ? ' checked' : ''}>`,
243 `\t<label for="${id}">${label}</label>`,
249 * @alias {object} ScopeIndex
254 * @param {object} ctx context
255 * @param {object[]} ctx.notifications notifications
256 * @param {object} ctx.session session
257 * @param {string[]=} ctx.session.scope scopes
258 * @param {URL=} ctx.session.me profile
259 * @param {string[]} ctx.session.profiles profiles
260 * @param {ScopeIndex} ctx.session.scopeIndex scopes structure
261 * @param {object} ctx.session.clientIdentifier client identifier
262 * @param {object[]} ctx.session.clientIdentifier.items items
263 * @param {object} ctx.session.clientIdentifier.items.properties properties
264 * @param {string[]=} ctx.session.clientIdentifier.items.properties.url url
265 * @param {string[]=} ctx.session.clientIdentifier.items.properties.summary sumamry
266 * @param {string[]=} ctx.session.clientIdentifier.items.properties.logo logo
267 * @param {string[]=} ctx.session.clientIdentifier.items.properties.name name
268 * @param {string} ctx.session.clientId client id
269 * @param {string} ctx.session.persist persist
270 * @param {string} ctx.session.redirectUri redirect
271 * @param {object} options options
272 * @returns {string} section
274 function mainContent(ctx
, options
) { // eslint-disable-line no-unused-vars
275 const session
= ctx
.session
|| {};
276 const hintedProfile
= session
.me
?.href
|| session
.profiles
?.[0] || '';
277 const scopeIndex
= session
.scopeIndex
|| {};
280 * Add requested scopes to index, if not already present,
281 * and de-associate requested scopes from profiles.
283 const scopes
= session
.scope
|| [];
284 scopes
.forEach((scopeName
) => {
285 if ((scopeName
in scopeIndex
)) {
286 scopeIndex
[scopeName
].profiles
= []; // eslint-disable-line security/detect-object-injection
288 scopeIndex
[scopeName
] = { // eslint-disable-line security/detect-object-injection
295 // Divide scopes between requested and additional from profiles.
296 const requestedScopes
= scopes
.map((scope
) => ({
298 description: scopeIndex
[scope
].description
, // eslint-disable-line security/detect-object-injection
300 const additionalScopes
= Object
.keys(scopeIndex
)
301 .filter((scope
) => scopeIndex
[scope
].profiles
.length
) // eslint-disable-line security/detect-object-injection
304 description: scopeIndex
[scope
].description
, // eslint-disable-line security/detect-object-injection
305 profiles: scopeIndex
[scope
].profiles
, // eslint-disable-line security/detect-object-injection
309 `<section class="information">
310 \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 || '(unspecified)'}</a>.
312 <section class="choices">
313 \t<form action="consent" method="POST" class="form-consent">`,
314 renderProfileFieldset(session
.profiles
, hintedProfile
),
315 renderRequestedScopes(requestedScopes
),
316 renderAdditionalScopes(additionalScopes
),
317 renderExpiration(requestedScopes
),
321 \t\t\t<legend>Do you want to allow this?</legend>
322 \t\t\t<button type="submit" class="button-accept" name="accept" value="true">Accept</button>
323 \t\t\t<button type="submit" class="button-decline" name="accept" value="false">Decline</button>
325 \t\t<input type="hidden" name="session" value="${session.persist}">
329 \t\tYou will be redirected to <a class="uri" id="${session.redirectUri ? session.redirectUri : 'unknown-redirect'}">${session.redirectUri}</a>.
337 * @param {object} ctx context
338 * @param {object} ctx.session session object
339 * @param {string[]=} ctx.session.scope scopes
340 * @param {URL=} ctx.session.me url
341 * @param {string[]} ctx.session.profiles profiles
342 * @param {ScopeIndex} ctx.session.scopeIndex scopes structure
343 * @param {object} ctx.session.clientIdentifier client identifier
344 * @param {string} ctx.session.clientId client id
345 * @param {string} ctx.session.persist persist
346 * @param {string} ctx.session.redirectUri redirect url
347 * @param {object} options options
348 * @param {object} options.manager manager options
349 * @param {string} options.manager.pageTitle page title
350 * @param {string} options.manager.footerEntries footer entries
351 * @returns {string} page
353 module
.exports
= (ctx
, options
) => {
354 const pagePathLevel
= 0;
355 const htmlOptions
= {
356 pageTitle: `${options.manager.pageTitle} — Authorization Request`,
357 logoUrl: options
.manager
.logoUrl
,
358 footerEntries: options
.manager
.footerEntries
,
361 function queryAll(query, fn) {
362 const nodes = document.querySelectorAll(query);
363 console.log('query ' + query + ' selected ' + nodes.length);
364 return nodes.forEach(fn);
366 function profileSelected(element) {
367 const profileClass = CSS.escape(element.value);
368 // queryAll('.profile-scope input', (n) => n.setAttribute('disabled', ''));
369 queryAll('.profile-scope', (n) => n.classList.add('disabled'));
370 const profileQuery = '.profile-scope.' + profileClass;
371 // queryAll(profileQuery + ' input', (n) => n.removeAttribute('disabled'));
372 queryAll(profileQuery, (n) => n.classList.remove('disabled'));
375 const profileSelect = document.getElementById('me');
376 profileSelect.onchange = () => profileSelected(profileSelect);
377 profileSelected(profileSelect);
379 window.onload = onLoad;
383 th
.navLinks(pagePathLevel
, ctx
, htmlOptions
);
384 sessionNavLinks(pagePathLevel
, ctx
, htmlOptions
);
385 const content
= mainContent(ctx
, options
);
386 return th
.htmlPage(pagePathLevel
, ctx
, htmlOptions
, content
);