update depedencies, changes to support updated authentication-module
[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 function renderProfileLI(profile) {
14 return `\t<li><a class="uri" name="${profile}">${profile}</a></li>`;
15 }
16
17
18 function renderProfileScopeIndicator(profile, scope, selected) {
19 const checked = selected ? ' checked' : '';
20 return `\t\t<td>
21 \t\t\t<input type="checkbox" id="${profile}-${scope}" name="scopes-${profile}" value="${scope}"${checked}>
22 \t\t</td>`;
23 }
24
25 function renderScopeRow(scope, details, profiles) {
26 return `\t<tr class="scope">
27 ${(profiles || []).map((profile) => renderProfileScopeIndicator(profile, scope, details.profiles.includes(profile))).join('\n')}
28 \t\t<th scope="row"><label>${scope}<label></th>
29 \t\t<td class="description">${details.description}</td>
30 \t\t<td>${details.application}</td>
31 \t\t<td class="scope-actions">` +
32 (details.isManuallyAdded ? `
33 \t\t\t<button name="action" value="delete-scope-${encodeURIComponent(scope)}">Delete</button>
34 ` : '') + `
35 \t\t</td>
36 \t</tr>`;
37 }
38
39
40 function renderProfileHeader(profile) {
41 return `<th class="vertical uri">
42 \t\t${profile}
43 </th>`;
44 }
45
46
47 function scopeIndexTable(scopeIndex, profiles) {
48 return `<table>
49 <thead>
50 \t<tr>
51 ${(profiles || []).map((profile) => renderProfileHeader(profile)).join('\n')}
52 \t\t<th>Scope</th>
53 \t\t<th>Description</th>
54 \t\t<th>Application</th>
55 \t\t<th class="scope-actions"></th>
56 \t</tr>
57 </thead>
58 <tbody>
59 ${Object.entries(scopeIndex).sort(th.scopeCompare).map(([scope, details]) => renderScopeRow(scope, details, profiles)).join('\n')}
60 </tbody>
61 </table>`;
62 }
63
64 function _tokenType(token) {
65 if (token.resource) {
66 return 'ticket-token';
67 }
68 if (!token.isToken) {
69 return 'profile';
70 }
71 return 'token';
72 }
73
74 function renderTokenRow(token) {
75 const createdTitle = token.refreshed ? 'Refreshed At' : 'Created At';
76 const createdDate = token.refreshed ? token.refreshed : token.created;
77 return `\t\t<tr>
78 <td>${_tokenType(token)}</td>
79 \t\t\t<td class="uri">${token.clientId}</td>
80 \t\t\t<td class="uri">${token.profile}</td>
81 <td class="scope">${(token.scopes || []).join(', ')}</td>
82 \t\t\t<td class="code">${token.codeId}</td>
83 \t\t\t<td>${th.timeElement(createdDate, { title: createdTitle })}</td>
84 \t\t\t<td>${th.timeElement(token.expires, { title: 'Expires At' })}</td>
85 \t\t\t<td>${token.isRevoked}</td>
86 <td>${token.resource ? token.resource : ''}</td>
87 \t\t\t<td>` + (
88 token.isRevoked ? '' : `
89 \t\t\t\t<button name="action" value="revoke-${token.codeId}">Revoke</button>`) + `
90 \t\t\t</td>
91 \t\t</tr>`;
92 }
93
94 function noTokensRows() {
95 return [`\t\t<tr>
96 \t\t\t<td colspan="100%" class="centered">(No active or recent tokens.)</td>
97 \t\t</tr>`];
98 }
99
100 function tokenTable(tokens) {
101 const tokenRows = tokens?.length ? tokens.map((token) => renderTokenRow(token)) : noTokensRows();
102 return `<table>
103 \t<thead>
104 \t\t<tr>
105 <th>Type</th>
106 \t\t\t<th>Client Identifier / Ticket Subject</th>
107 \t\t\t<th>Profile</th>
108 <th>Scopes</th>
109 \t\t\t<th>Code</th>
110 \t\t\t<th>Created or Refreshed</th>
111 \t\t\t<th>Expires</th>
112 \t\t\t<th>Revoked</th>
113 <th>Resource</th>
114 \t\t\t<th></th>
115 \t\t</tr>
116 \t</thead>
117 \t<tbody>
118 ${tokenRows.join('\n')}
119 \t</tbody>
120 </table>`;
121 }
122
123 function mainContent(ctx) {
124 return `<section>
125 \t<h2>Profiles</h2>
126 \t<ul>
127 \t${(ctx.profilesScopes?.profiles || []).map((p) => renderProfileLI(p)).join('\n')}
128 \t</ul>
129 \t<form action="" method="POST">
130 \t\t<fieldset>
131 \t\t\t<legend>Add New Profile</legend>
132 \t\t\t<div>
133 \t\t\t\tThe profile identity URIs associated with this account.
134 \t\t\t\tEach must indicate this service as the authorization endpoint.
135 \t\t\t</div>
136 \t\t\t<br>
137 \t\t\t<label for="profile">Profile URL:</label>
138 \t\t\t<input type="url" id="profile" name="profile" size="96">
139 \t\t\t<button name="action" value="new-profile">Add Profile</button>
140 \t\t</fieldset>
141 \t</form>
142 </section>
143 <section>
144 \t<h2>Scopes</h2>
145 \t<form action="" method="POST">
146 \t\t<details>
147 \t\t<summary>
148 \t\tScopes Associated with Profiles for Convenience
149 \t\t</summary>
150 \t\t\t<fieldset>
151 \t\t\t\t<legend>Manage Additional Profile Scope Availability</legend>
152 \t\t\t\t<div>
153 \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.
154 \t\t\t\t\tSelecting one for a profile makes it conveniently available for quick inclusion when authorizing a client request.
155 \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.
156 \t\t\t\t</div>
157 \t\t\t\t<br>
158 \t\t${scopeIndexTable(ctx.profilesScopes.scopeIndex, ctx.profilesScopes.profiles)}
159 \t\t\t\t<button name="action" value="save-scopes">Save</button>
160 \t\t\t</fieldset>
161 \t\t</form>
162 \t\t<br>
163 \t\t<form action="" method="POST">
164 \t\t\t<fieldset>
165 \t\t\t\t<legend>Add New Scope</legend>
166 \t\t\t\t<label for="scope">Scope:</label>
167 \t\t\t\t<input type="text" id="scope" name="scope">
168 \t\t\t\t<label for="description">Description:</label>
169 \t\t\t\t<input type="text" id="description" name="description">
170 \t\t\t\t<label for="application">Application:</label>
171 \t\t\t\t<input type="text" id="application" name="application">
172 \t\t\t\t<button name="action" value="new-scope">Add Scope</button>
173 \t\t\t</fieldset>
174 \t\t</details>
175 \t</form>
176 </section>
177 <section>
178 \t<h2>Tokens</h2>
179 \t<form action="" method="POST">
180 ${tokenTable(ctx.tokens)}
181 \t</form>
182 </section>`;
183 }
184
185
186 /**
187 *
188 * @param {Object} ctx
189 * @param {Object} ctx.profilesScopes.scopeIndex
190 * @param {String[]} ctx.profilesScopes.profiles
191 * @param {Object[]} ctx.tokens
192 * @param {Object} options
193 * @param {Object} options.manager
194 * @param {String} options.manager.pageTitle
195 * @param {String} options.manager.logoUrl
196 * @param {String[]} options.manager.footerEntries
197 * @returns {String}
198 */
199 module.exports = (ctx, options) => {
200 const pagePathLevel = 1;
201 const htmlOptions = {
202 pageIdentifier: 'admin',
203 pageTitle: options.manager.pageTitle + ' - Admin',
204 logoUrl: options.manager.logoUrl,
205 footerEntries: options.manager.footerEntries,
206 };
207 th.navLinks(pagePathLevel, ctx, htmlOptions);
208 sessionNavLinks(1, ctx, htmlOptions);
209 const content = [
210 mainContent(ctx),
211 ];
212 return th.htmlPage(1, ctx, htmlOptions, content);
213 };