update dependencies and devDependencies, address lint issues
[squeep-indie-auther] / src / template / admin-html.js
1 'use strict';
2
3 /**
4 * This renders the administrative view for an account,
5 * allowing for adding profile URIs, custom scope bundles,
6 * and management of issued tokens.
7 */
8
9 const th = require('./template-helper');
10 const { sessionNavLinks } = require('@squeep/authentication-module');
11
12
13 /**
14 *
15 * @param {string} profile profile
16 * @returns {string} li
17 */
18 function renderProfileLI(profile) {
19 return `\t<li><a class="uri" id="${profile}">${profile}</a></li>`;
20 }
21
22
23 /**
24 *
25 * @param {string} profile profile
26 * @param {string} scope scope
27 * @param {boolean} selected is selected
28 * @returns {string} td
29 */
30 function renderProfileScopeIndicator(profile, scope, selected) {
31 const checked = selected ? ' checked' : '';
32 return `\t\t<td>
33 \t\t\t<input type="checkbox" id="${profile}-${scope}" name="scopes-${profile}[]" value="${scope}"${checked}>
34 \t\t</td>`;
35 }
36
37 /**
38 *
39 * @param {string} scope scope
40 * @param {object} details details
41 * @param {string[]} profiles profiles
42 * @returns {string} tr
43 */
44 function renderScopeRow(scope, details, profiles) {
45 return `\t<tr class="scope">
46 ${(profiles || []).map((profile) => renderProfileScopeIndicator(profile, scope, details.profiles.includes(profile))).join('\n')}
47 \t\t<th scope="row"><label>${scope}</label></th>
48 \t\t<td class="description">${details.description}</td>
49 \t\t<td>${details.application}</td>
50 \t\t<td class="scope-actions">` +
51 (details.isManuallyAdded ? `
52 \t\t\t<button type="submit" name="action" value="delete-scope-${encodeURIComponent(scope)}">Delete</button>
53 ` : '') + `
54 \t\t</td>
55 \t</tr>`;
56 }
57
58
59 /**
60 *
61 * @param {string} profile profile
62 * @returns {string} th
63 */
64 function renderProfileHeader(profile) {
65 return `<th scope="col" class="vertical uri">
66 \t\t${profile}
67 </th>`;
68 }
69
70
71 /**
72 *
73 * @param {object} scopeIndex scopes
74 * @param {string[]} profiles profiles
75 * @returns {string} table
76 */
77 function scopeIndexTable(scopeIndex, profiles) {
78 return `<table>
79 <thead>
80 \t<tr>
81 ${(profiles || []).map((profile) => renderProfileHeader(profile)).join('\n')}
82 \t\t<th scope="col">Scope</th>
83 \t\t<th scope="col">Description</th>
84 \t\t<th scope="col">Application</th>
85 \t\t<th scope="col" class="scope-actions"></th>
86 \t</tr>
87 </thead>
88 <tbody>
89 ${Object.entries(scopeIndex).sort(th.scopeCompare).map(([scope, details]) => renderScopeRow(scope, details, profiles)).join('\n')}
90 </tbody>
91 </table>`;
92 }
93
94 /**
95 *
96 * @param {object} token token
97 * @returns {string} type
98 */
99 function _tokenType(token) {
100 if (token.resource) {
101 return 'ticket-token';
102 }
103 if (!token.isToken) {
104 return 'profile';
105 }
106 return 'token';
107 }
108
109 /**
110 *
111 * @param {object} token token
112 * @returns {string} tr
113 */
114 function renderTokenRow(token) {
115 const createdTitle = token.refreshed ? 'Refreshed At' : 'Created At';
116 const createdDate = token.refreshed ? token.refreshed : token.created;
117 return `\t\t<tr>
118 <td>${_tokenType(token)}</td>
119 \t\t\t<td class="uri">${token.clientId}</td>
120 \t\t\t<td class="uri">${token.profile}</td>
121 <td class="scope">${(token.scopes || []).join(', ')}</td>
122 \t\t\t<td class="code">${token.codeId}</td>
123 \t\t\t<td>${th.timeElement(createdDate, { title: createdTitle })}</td>
124 \t\t\t<td>${th.timeElement(token.expires, { title: 'Expires At' })}</td>
125 \t\t\t<td>${token.isRevoked}</td>
126 <td>${token.resource ? token.resource : ''}</td>
127 \t\t\t<td>` + (
128 token.isRevoked ? '' : `
129 \t\t\t\t<button type="submit" name="action" value="revoke-${token.codeId}">Revoke</button>`) + `
130 \t\t\t</td>
131 \t\t</tr>`;
132 }
133
134 /**
135 * @returns {string} tr
136 */
137 function noTokensRows() {
138 return [`\t\t<tr>
139 \t\t\t<td colspan="10" class="centered">(No active or recent tokens.)</td>
140 \t\t</tr>`];
141 }
142
143 /**
144 *
145 * @param {object} tokens tokens
146 * @returns {string} table
147 */
148 function tokenTable(tokens) {
149 const tokenRows = tokens?.length ? tokens.map((token) => renderTokenRow(token)) : noTokensRows();
150 const formOpen = tokens?.length ? '<form method="POST">\n' : '';
151 const formClose = tokens?.length ? '\n</form>' : '';
152 return `${formOpen}<table>
153 \t<thead>
154 \t\t<tr>
155 \t\t\t<th scope="col">Type</th>
156 \t\t\t<th scope="col">Client Identifier / Ticket Subject</th>
157 \t\t\t<th scope="col">Profile</th>
158 \t\t\t<th scope="col">Scopes</th>
159 \t\t\t<th scope="col">Code</th>
160 \t\t\t<th scope="col">Created or Refreshed</th>
161 \t\t\t<th scope="col">Expires</th>
162 \t\t\t<th scope="col">Revoked</th>
163 \t\t\t<th scope="col">Resource</th>
164 \t\t\t<th scope="col"></th>
165 \t\t</tr>
166 \t</thead>
167 \t<tbody>
168 ${tokenRows.join('\n')}
169 \t</tbody>
170 </table>${formClose}`;
171 }
172
173 /**
174 *
175 * @param {object} ctx context
176 * @returns {string} section
177 */
178 function mainContent(ctx) {
179 const profileList = (ctx.profilesScopes?.profiles || []).map((p) => renderProfileLI(p)).join('\n');
180 return `<section>
181 \t<h2>Profiles</h2>
182 \t<ul>
183 ${profileList}
184 \t</ul>
185 \t<form method="POST">
186 \t\t<fieldset>
187 \t\t\t<legend>Add New Profile</legend>
188 \t\t\t<div>
189 \t\t\t\tThe profile identity URIs associated with this account.
190 \t\t\t\tEach must indicate this service as the authorization endpoint.
191 \t\t\t</div>
192 \t\t\t<br>
193 \t\t\t<label for="profile">Profile URL:</label>
194 \t\t\t<input type="url" id="profile" name="profile" size="96">
195 \t\t\t<button type="submit" name="action" value="new-profile">Add Profile</button>
196 \t\t</fieldset>
197 \t</form>
198 </section>
199 <section>
200 \t<h2>Scopes</h2>
201 \t\t<details>
202 \t\t\t<summary>
203 \t\t\t\tScopes Associated with Profiles for Convenience
204 \t\t\t</summary>
205 \t\t<form method="POST">
206 \t\t\t<fieldset>
207 \t\t\t\t<legend>Manage Additional Profile Scope Availability</legend>
208 \t\t\t\t<div>
209 \t\t\t\t\tThis table lists pre-defined scopes which you can choose to add to any authorization request, whether the client requested them or not.
210 \t\t\t\t\tSelecting one for a profile makes it conveniently available for quick inclusion when authorizing a client request.
211 \t\t\t\t\tAny scope not in this table or not selected for a profile can always be added in the ad hoc field on the authorization request.
212 \t\t\t\t</div>
213 \t\t\t\t<br>
214 ${scopeIndexTable(ctx.profilesScopes.scopeIndex, ctx.profilesScopes.profiles)}
215 \t\t\t\t<button type="submit" name="action" value="save-scopes">Save</button>
216 \t\t\t</fieldset>
217 \t\t</form>
218 \t\t<br>
219 \t\t<form method="POST">
220 \t\t\t<fieldset>
221 \t\t\t\t<legend>Add New Scope</legend>
222 \t\t\t\t<label for="scope">Scope:</label>
223 \t\t\t\t<input type="text" id="scope" name="scope">
224 \t\t\t\t<label for="description">Description:</label>
225 \t\t\t\t<input type="text" id="description" name="description">
226 \t\t\t\t<label for="application">Application:</label>
227 \t\t\t\t<input type="text" id="application" name="application">
228 \t\t\t\t<button type="submit" name="action" value="new-scope">Add Scope</button>
229 \t\t\t</fieldset>
230 \t\t</form>
231 \t\t</details>
232 </section>
233 <section>
234 \t<h2>Tokens</h2>
235 ${tokenTable(ctx.tokens)}
236 </section>`;
237 }
238
239
240 /**
241 *
242 * @param {object} ctx context
243 * @param {object} ctx.profilesScopes.scopeIndex scopes
244 * @param {string[]} ctx.profilesScopes.profiles profiles
245 * @param {object[]} ctx.tokens tokens
246 * @param {object} options options
247 * @param {object} options.manager manager options
248 * @param {string} options.manager.pageTitle page title
249 * @param {string} options.manager.logoUrl logo url
250 * @param {string[]} options.manager.footerEntries footer entries
251 * @returns {string} page
252 */
253 module.exports = (ctx, options) => {
254 const pagePathLevel = 1;
255 const htmlOptions = {
256 pageIdentifier: 'admin',
257 pageTitle: options.manager.pageTitle + ' - Admin',
258 logoUrl: options.manager.logoUrl,
259 footerEntries: options.manager.footerEntries,
260 };
261 th.navLinks(pagePathLevel, ctx, htmlOptions);
262 sessionNavLinks(1, ctx, htmlOptions);
263 const content = [
264 mainContent(ctx),
265 ];
266 return th.htmlPage(1, ctx, htmlOptions, content);
267 };