@boopkit/cap-oauth-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/capability.json +120 -0
  2. package/dist/authorization-code-service.d.ts +105 -0
  3. package/dist/authorization-code-service.d.ts.map +1 -0
  4. package/dist/authorization-code-service.js +142 -0
  5. package/dist/authorization-code-service.js.map +1 -0
  6. package/dist/bearer.d.ts +146 -0
  7. package/dist/bearer.d.ts.map +1 -0
  8. package/dist/bearer.js +150 -0
  9. package/dist/bearer.js.map +1 -0
  10. package/dist/body-field.d.ts +24 -0
  11. package/dist/body-field.d.ts.map +1 -0
  12. package/dist/body-field.js +43 -0
  13. package/dist/body-field.js.map +1 -0
  14. package/dist/client-registry-service.d.ts +120 -0
  15. package/dist/client-registry-service.d.ts.map +1 -0
  16. package/dist/client-registry-service.js +450 -0
  17. package/dist/client-registry-service.js.map +1 -0
  18. package/dist/config.d.ts +82 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +157 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/connections-controller.d.ts +106 -0
  23. package/dist/connections-controller.d.ts.map +1 -0
  24. package/dist/connections-controller.js +161 -0
  25. package/dist/connections-controller.js.map +1 -0
  26. package/dist/consent-controller.d.ts +197 -0
  27. package/dist/consent-controller.d.ts.map +1 -0
  28. package/dist/consent-controller.js +532 -0
  29. package/dist/consent-controller.js.map +1 -0
  30. package/dist/discovery-controller.d.ts +16 -0
  31. package/dist/discovery-controller.d.ts.map +1 -0
  32. package/dist/discovery-controller.js +52 -0
  33. package/dist/discovery-controller.js.map +1 -0
  34. package/dist/discovery.d.ts +95 -0
  35. package/dist/discovery.d.ts.map +1 -0
  36. package/dist/discovery.js +101 -0
  37. package/dist/discovery.js.map +1 -0
  38. package/dist/errors.d.ts +26 -0
  39. package/dist/errors.d.ts.map +1 -0
  40. package/dist/errors.js +41 -0
  41. package/dist/errors.js.map +1 -0
  42. package/dist/express.d.ts +66 -0
  43. package/dist/express.d.ts.map +1 -0
  44. package/dist/express.js +3 -0
  45. package/dist/express.js.map +1 -0
  46. package/dist/grant-service.d.ts +102 -0
  47. package/dist/grant-service.d.ts.map +1 -0
  48. package/dist/grant-service.js +137 -0
  49. package/dist/grant-service.js.map +1 -0
  50. package/dist/index.d.ts +67 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +148 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/metadata-document.d.ts +60 -0
  55. package/dist/metadata-document.d.ts.map +1 -0
  56. package/dist/metadata-document.js +148 -0
  57. package/dist/metadata-document.js.map +1 -0
  58. package/dist/oauth-error.d.ts +45 -0
  59. package/dist/oauth-error.d.ts.map +1 -0
  60. package/dist/oauth-error.js +51 -0
  61. package/dist/oauth-error.js.map +1 -0
  62. package/dist/pkce.d.ts +29 -0
  63. package/dist/pkce.d.ts.map +1 -0
  64. package/dist/pkce.js +65 -0
  65. package/dist/pkce.js.map +1 -0
  66. package/dist/property-array.d.ts +60 -0
  67. package/dist/property-array.d.ts.map +1 -0
  68. package/dist/property-array.js +31 -0
  69. package/dist/property-array.js.map +1 -0
  70. package/dist/public-id.d.ts +35 -0
  71. package/dist/public-id.d.ts.map +1 -0
  72. package/dist/public-id.js +76 -0
  73. package/dist/public-id.js.map +1 -0
  74. package/dist/rate-limit.d.ts +56 -0
  75. package/dist/rate-limit.d.ts.map +1 -0
  76. package/dist/rate-limit.js +55 -0
  77. package/dist/rate-limit.js.map +1 -0
  78. package/dist/redirect-uri.d.ts +89 -0
  79. package/dist/redirect-uri.d.ts.map +1 -0
  80. package/dist/redirect-uri.js +249 -0
  81. package/dist/redirect-uri.js.map +1 -0
  82. package/dist/registration-controller.d.ts +62 -0
  83. package/dist/registration-controller.d.ts.map +1 -0
  84. package/dist/registration-controller.js +79 -0
  85. package/dist/registration-controller.js.map +1 -0
  86. package/dist/request-client.d.ts +19 -0
  87. package/dist/request-client.d.ts.map +1 -0
  88. package/dist/request-client.js +32 -0
  89. package/dist/request-client.js.map +1 -0
  90. package/dist/revocation-controller.d.ts +28 -0
  91. package/dist/revocation-controller.d.ts.map +1 -0
  92. package/dist/revocation-controller.js +176 -0
  93. package/dist/revocation-controller.js.map +1 -0
  94. package/dist/revocation-service.d.ts +113 -0
  95. package/dist/revocation-service.d.ts.map +1 -0
  96. package/dist/revocation-service.js +121 -0
  97. package/dist/revocation-service.js.map +1 -0
  98. package/dist/token-controller.d.ts +79 -0
  99. package/dist/token-controller.d.ts.map +1 -0
  100. package/dist/token-controller.js +207 -0
  101. package/dist/token-controller.js.map +1 -0
  102. package/dist/token-service.d.ts +175 -0
  103. package/dist/token-service.d.ts.map +1 -0
  104. package/dist/token-service.js +251 -0
  105. package/dist/token-service.js.map +1 -0
  106. package/dist/types.d.ts +130 -0
  107. package/dist/types.d.ts.map +1 -0
  108. package/dist/types.js +13 -0
  109. package/dist/types.js.map +1 -0
  110. package/dist/user-store.d.ts +110 -0
  111. package/dist/user-store.d.ts.map +1 -0
  112. package/dist/user-store.js +40 -0
  113. package/dist/user-store.js.map +1 -0
  114. package/package.json +37 -0
  115. package/scaffold/prisma/schema/oauth-clients.prisma +80 -0
  116. package/scaffold/reference/oauth-server.ref.md +478 -0
  117. package/scaffold/src/middleware/oauth-server.ts +227 -0
  118. package/scaffold/src/scripts/seed-oauth-clients.ts +72 -0
  119. package/scaffold/src/services/oauth-server.service.ts +101 -0
  120. package/scaffold/styles/oauth-server.css +345 -0
  121. package/scaffold/views/oauth-connected-clients.hbs +98 -0
  122. package/scaffold/views/oauth-consent-error.hbs +22 -0
  123. package/scaffold/views/oauth-consent.hbs +138 -0
  124. package/scaffold/views/oauth-disconnect-confirm.hbs +57 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * @boopkit/cap-oauth-server — the consent screen's styles.
3
+ *
4
+ * Deliberately the same register as cap-users' `auth.css`: one centred card on
5
+ * a plain page, the same radii, the same brand tokens, the same weight of type.
6
+ * That is the point rather than laziness — this page belongs to the application
7
+ * it is rendered in, and a user meets it in the middle of a flow they started
8
+ * somewhere else on the same site. A page that looked like a protocol console
9
+ * would read as a different site at the exact moment they are deciding whether
10
+ * to trust one.
11
+ *
12
+ * THE ONE RULE THAT IS NOT TASTE. `.consent-icon` fixes the icon box's size and
13
+ * `.consent-icon-image` fills it with `object-fit: cover`. Under open dynamic
14
+ * registration `logo_uri` is supplied by an unauthenticated stranger, and a
15
+ * 4000px image, a 1px image or a tall thin one must all produce the same
16
+ * layout. The image never gets a say in how much room it has.
17
+ */
18
+
19
+ .consent-page {
20
+ @apply flex flex-1 items-center justify-center px-4 py-12;
21
+ }
22
+
23
+ .consent-card {
24
+ @apply w-full max-w-md rounded-xl bg-white p-8 shadow-md;
25
+ }
26
+
27
+ .consent-card--error {
28
+ @apply text-center;
29
+ }
30
+
31
+ /* === Client identity === */
32
+
33
+ .consent-header {
34
+ @apply mb-6 flex flex-col items-center text-center;
35
+ }
36
+
37
+ /* Fixed. See the header — this is the hostile-input defence, not styling. */
38
+ .consent-icon {
39
+ @apply mb-3 flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden
40
+ rounded-xl border border-gray-200 bg-gray-50;
41
+ }
42
+
43
+ .consent-icon-image {
44
+ @apply h-full w-full object-cover;
45
+ }
46
+
47
+ .consent-icon-monogram {
48
+ @apply text-xl font-semibold text-gray-500;
49
+ }
50
+
51
+ .consent-title {
52
+ @apply text-xl font-semibold text-gray-900;
53
+ }
54
+
55
+ .consent-subtitle {
56
+ @apply mt-1 text-sm text-gray-500;
57
+ }
58
+
59
+ .consent-description {
60
+ @apply mt-2 text-sm text-gray-600;
61
+ }
62
+
63
+ /*
64
+ * Provenance. The modifier classes exist so an application can make an
65
+ * unverified self-registration look different from an operator-configured
66
+ * client without editing the template.
67
+ */
68
+ .consent-provenance {
69
+ @apply mt-3 rounded-md bg-gray-50 px-3 py-2 text-xs text-gray-500;
70
+ }
71
+
72
+ .consent-provenance--dynamic {
73
+ @apply bg-amber-50 text-amber-800;
74
+ }
75
+
76
+ /* === Scopes === */
77
+
78
+ .consent-scopes {
79
+ @apply mt-6;
80
+ }
81
+
82
+ .consent-scopes-title {
83
+ @apply text-xs font-semibold uppercase tracking-wide text-gray-400;
84
+ }
85
+
86
+ .consent-scope-list {
87
+ @apply mt-2 flex flex-col gap-2;
88
+ }
89
+
90
+ .consent-scope {
91
+ @apply rounded-md bg-gray-50 px-3 py-2 text-sm text-gray-700;
92
+ }
93
+
94
+ /*
95
+ * The fallback state: the application published no words for this scope, so the
96
+ * page is showing the raw string. It is marked rather than hidden, because the
97
+ * developer's boot warning is the fix and the user should at least see that
98
+ * this line is machine text.
99
+ */
100
+ .consent-scope--undescribed {
101
+ @apply font-mono text-xs text-gray-500;
102
+ }
103
+
104
+ /* === The coverage statement, before the buttons === */
105
+
106
+ .consent-coverage {
107
+ @apply mt-5 text-xs leading-relaxed text-gray-500;
108
+ }
109
+
110
+ /* === Actions === */
111
+
112
+ .consent-actions {
113
+ @apply mt-6 flex flex-col gap-2;
114
+ }
115
+
116
+ .consent-form {
117
+ @apply flex flex-col gap-3;
118
+ }
119
+
120
+ .consent-form--auth {
121
+ @apply mt-6;
122
+ }
123
+
124
+ .consent-auth-prompt {
125
+ @apply text-sm text-gray-600;
126
+ }
127
+
128
+ .consent-input {
129
+ @apply w-full rounded-lg border border-gray-300 px-3 py-2 text-sm
130
+ placeholder-gray-400 focus:outline-none focus:ring-1;
131
+ &:focus {
132
+ border-color: var(--color-brand-500);
133
+ --tw-ring-color: var(--color-brand-500);
134
+ }
135
+ }
136
+
137
+ .consent-approve {
138
+ @apply w-full rounded-lg px-4 py-2 text-sm font-medium text-white
139
+ focus:outline-none focus:ring-2 focus:ring-offset-2;
140
+ background-color: var(--color-brand-600);
141
+ &:hover {
142
+ background-color: var(--color-brand-700);
143
+ }
144
+ &:focus {
145
+ --tw-ring-color: var(--color-brand-500);
146
+ }
147
+ }
148
+
149
+ .consent-deny {
150
+ @apply w-full rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium
151
+ text-gray-700 transition-colors hover:bg-gray-50;
152
+ }
153
+
154
+ .consent-switch {
155
+ @apply mt-4 w-full text-center text-sm text-gray-500 hover:underline;
156
+ }
157
+
158
+ .consent-providers {
159
+ @apply mt-4;
160
+ }
161
+
162
+ /* === Messages === */
163
+
164
+ .consent-messages {
165
+ @apply mb-4;
166
+ }
167
+
168
+ .consent-error {
169
+ @apply rounded-md bg-red-50 px-3 py-2 text-sm text-red-700;
170
+ }
171
+
172
+ .consent-notice {
173
+ @apply rounded-md bg-green-50 px-3 py-2 text-sm text-green-800;
174
+ }
175
+
176
+ .consent-links {
177
+ @apply mt-6 text-center text-xs text-gray-400;
178
+ }
179
+
180
+ .consent-links a {
181
+ @apply hover:underline;
182
+ }
183
+
184
+ /**
185
+ * === The connected-clients screen, and the disconnect confirmation ===
186
+ *
187
+ * A list page rather than a card page, so it takes the wider `max-w-2xl` that a
188
+ * settings surface wants — but the same radii, the same grays and the same
189
+ * brand tokens as the consent card, because it is the other half of one story
190
+ * and a user arrives here having seen that card.
191
+ *
192
+ * The confirmation page reuses `.consent-card` outright and adds only what is
193
+ * different about it: a confirm button in red rather than in the brand colour.
194
+ * That is the one thing that must not be "tidied" into `.consent-approve` — the
195
+ * brand button is what a person taps to proceed with something they want, and
196
+ * this page is asking them to break something. Cancel keeps `.consent-deny`, so
197
+ * the two do not carry equal weight; it sits directly under the confirm, full
198
+ * width, because a person who arrived here by mistake should not have to aim.
199
+ */
200
+
201
+ .connections-page {
202
+ @apply mx-auto w-full max-w-2xl px-4 py-10;
203
+ }
204
+
205
+ .connections-header {
206
+ @apply mb-6;
207
+ }
208
+
209
+ .connections-title {
210
+ @apply text-2xl font-semibold text-gray-900;
211
+ }
212
+
213
+ .connections-subtitle {
214
+ @apply mt-1 text-sm text-gray-500;
215
+ }
216
+
217
+ .connections-notice {
218
+ @apply mb-6 rounded-md bg-green-50 px-4 py-3 text-sm text-green-800;
219
+ }
220
+
221
+ /* The coverage statement. Above the list, because it governs every row. */
222
+ .connections-coverage {
223
+ @apply mb-6 rounded-md bg-gray-50 px-4 py-3 text-xs leading-relaxed text-gray-500;
224
+ }
225
+
226
+ .connections-section {
227
+ @apply mb-10;
228
+ }
229
+
230
+ /*
231
+ * The separation that makes "separate sections" true to look at rather than
232
+ * only true in the markup. A rule and a gap, not a heavier container: the
233
+ * previously-connected list is meant to recede.
234
+ */
235
+ .connections-section--previous {
236
+ @apply border-t border-gray-200 pt-8;
237
+ }
238
+
239
+ .connections-section-title {
240
+ @apply text-xs font-semibold uppercase tracking-wide text-gray-400;
241
+ }
242
+
243
+ .connections-section-note {
244
+ @apply mt-1 text-xs text-gray-500;
245
+ }
246
+
247
+ .connections-empty {
248
+ @apply rounded-xl border border-dashed border-gray-200 px-6 py-10 text-center text-sm text-gray-500;
249
+ }
250
+
251
+ .connection-list {
252
+ @apply flex flex-col gap-3;
253
+ }
254
+
255
+ .connection-list--previous {
256
+ @apply mt-3;
257
+ }
258
+
259
+ .connection-item {
260
+ @apply flex items-start gap-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm;
261
+ }
262
+
263
+ /*
264
+ * Quieter, and deliberately so. A disconnected client is history rather than
265
+ * inventory: it stays legible, it does not compete with a live connection.
266
+ */
267
+ .connection-item--previous {
268
+ @apply border-gray-100 bg-gray-50 p-4 shadow-none;
269
+ }
270
+
271
+ /* Fixed, for the same reason `.consent-icon` is. The image never gets a say. */
272
+ .connection-icon {
273
+ @apply flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden
274
+ rounded-lg border border-gray-200 bg-gray-50;
275
+ }
276
+
277
+ .connection-icon-image {
278
+ @apply h-full w-full object-cover;
279
+ }
280
+
281
+ .connection-icon-monogram {
282
+ @apply text-sm font-semibold text-gray-500;
283
+ }
284
+
285
+ .connection-body {
286
+ @apply min-w-0 flex-1;
287
+ }
288
+
289
+ .connection-name {
290
+ @apply text-base font-semibold text-gray-900;
291
+ }
292
+
293
+ .connection-item--previous .connection-name {
294
+ @apply text-sm font-medium text-gray-600;
295
+ }
296
+
297
+ .connection-meta {
298
+ @apply mt-0.5 text-xs text-gray-500;
299
+ }
300
+
301
+ .connection-provenance {
302
+ @apply mt-2 text-xs text-gray-500;
303
+ }
304
+
305
+ .connection-provenance--dynamic {
306
+ @apply text-amber-700;
307
+ }
308
+
309
+ .connection-scopes-title {
310
+ @apply mt-3 text-xs font-semibold uppercase tracking-wide text-gray-400;
311
+ }
312
+
313
+ .connection-scope-list {
314
+ @apply mt-1 flex flex-col gap-1;
315
+ }
316
+
317
+ .connection-scope {
318
+ @apply text-sm text-gray-700;
319
+ }
320
+
321
+ .connection-scope--undescribed {
322
+ @apply font-mono text-xs text-gray-500;
323
+ }
324
+
325
+ .connection-disconnect {
326
+ @apply shrink-0 rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium
327
+ text-gray-700 transition-colors hover:border-red-300 hover:bg-red-50 hover:text-red-700;
328
+ }
329
+
330
+ /* === The confirmation page === */
331
+
332
+ .disconnect-consequence {
333
+ @apply mt-2 text-sm leading-relaxed text-gray-600;
334
+ }
335
+
336
+ .disconnect-confirm {
337
+ @apply w-full rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white
338
+ transition-colors hover:bg-red-700 focus:outline-none focus:ring-2
339
+ focus:ring-red-500 focus:ring-offset-2;
340
+ }
341
+
342
+ /* A link styled as the reassuring button. `.consent-deny` carries the rest. */
343
+ .disconnect-cancel {
344
+ @apply block text-center no-underline;
345
+ }
@@ -0,0 +1,98 @@
1
+ {{!--
2
+ @boopkit/cap-oauth-server — the connected-clients screen.
3
+
4
+ Three things on this page are load-bearing rather than decorative, and a
5
+ redesign has to keep all three:
6
+
7
+ 1. The COVERAGE STATEMENT. Grants are keyed on the user, so a connected
8
+ application reaches every account that person reaches. A list of client
9
+ names with no such sentence invites the natural wrong guess — that a
10
+ connector reaches only the account they were looking at when they
11
+ approved it.
12
+ 2. The PREVIOUSLY CONNECTED section. Revocation is soft and the audit trail
13
+ is the reason it is soft; a screen that showed only live grants would hide
14
+ the ended rows from the only person who wants them.
15
+ 3. Disconnect is a LINK to a confirmation page, never a form button here.
16
+ One-way doors get their own page. Nothing on this page performs a write.
17
+ --}}
18
+ <div class="connections-page">
19
+ <div class="connections-header">
20
+ <h1 class="connections-title">Connected applications</h1>
21
+ <p class="connections-subtitle">
22
+ Applications you have given access to your account.
23
+ </p>
24
+ </div>
25
+
26
+ {{#if disconnected}}
27
+ <div class="connections-notice">
28
+ {{disconnected}} has been disconnected. It can no longer act on your behalf.
29
+ </div>
30
+ {{/if}}
31
+
32
+ <p class="connections-coverage">{{coverage}}</p>
33
+
34
+ <section class="connections-section">
35
+ {{#if hasConnected}}
36
+ <ul class="connection-list">
37
+ {{#each connected}}
38
+ <li class="connection-item">
39
+ <div class="connection-icon">
40
+ {{#if logoUri}}
41
+ <img class="connection-icon-image" src="{{logoUri}}" alt="" referrerpolicy="no-referrer" />
42
+ {{else}}
43
+ <span class="connection-icon-monogram">{{monogram}}</span>
44
+ {{/if}}
45
+ </div>
46
+
47
+ <div class="connection-body">
48
+ <h2 class="connection-name">{{name}}</h2>
49
+ <p class="connection-meta">Connected on {{connectedOn}}</p>
50
+ <p class="connection-provenance connection-provenance--{{mode}}">{{provenance}}</p>
51
+
52
+ <h3 class="connection-scopes-title">It can</h3>
53
+ <ul class="connection-scope-list">
54
+ {{#each scopes}}
55
+ <li class="connection-scope{{#unless described}} connection-scope--undescribed{{/unless}}">
56
+ {{description}}
57
+ </li>
58
+ {{else}}
59
+ <li class="connection-scope">Sign you in and confirm who you are.</li>
60
+ {{/each}}
61
+ </ul>
62
+ </div>
63
+
64
+ {{!-- A link, not a form. The write lives on the confirmation page. --}}
65
+ <a class="connection-disconnect" href="{{disconnectPath}}">Disconnect</a>
66
+ </li>
67
+ {{/each}}
68
+ </ul>
69
+ {{else}}
70
+ <p class="connections-empty">
71
+ You have not connected any applications yet. When you do, they will appear here
72
+ and you can disconnect them at any time.
73
+ </p>
74
+ {{/if}}
75
+ </section>
76
+
77
+ {{#if hasPrevious}}
78
+ <section class="connections-section connections-section--previous">
79
+ <h2 class="connections-section-title">Previously connected</h2>
80
+ <p class="connections-section-note">
81
+ These applications no longer have access. They are kept here as a record of what
82
+ was once connected.
83
+ </p>
84
+ <ul class="connection-list connection-list--previous">
85
+ {{#each previous}}
86
+ <li class="connection-item connection-item--previous">
87
+ <div class="connection-body">
88
+ <h3 class="connection-name">{{name}}</h3>
89
+ <p class="connection-meta">
90
+ Connected on {{connectedOn}} · Disconnected on {{disconnectedOn}}
91
+ </p>
92
+ </div>
93
+ </li>
94
+ {{/each}}
95
+ </ul>
96
+ </section>
97
+ {{/if}}
98
+ </div>
@@ -0,0 +1,22 @@
1
+ {{!--
2
+ @boopkit/cap-oauth-server — the authorization request we could not answer.
3
+
4
+ Reached only when the request named no usable client or an unregistered
5
+ redirect URI, which is the one case RFC 6749 forbids answering with a
6
+ redirect: sending an error to an unverified redirect URI is the open redirect.
7
+ So the person is told here instead, on our own page.
8
+
9
+ The message names what was wrong with the request and nothing about the
10
+ registry — a page that distinguished "no such client" from "that client is
11
+ suspended" would be an enumeration oracle for whoever is probing.
12
+ --}}
13
+ <div class="consent-page">
14
+ <section class="consent-card consent-card--error">
15
+ <h1 class="consent-title">This connection request could not be completed</h1>
16
+ <p class="consent-error">{{errorMessage}}</p>
17
+ <p class="consent-description">
18
+ Nothing has been shared and no access has been granted. Go back to the application
19
+ that sent you here and try connecting again.
20
+ </p>
21
+ </section>
22
+ </div>
@@ -0,0 +1,138 @@
1
+ {{!--
2
+ @boopkit/cap-oauth-server — the consent screen.
3
+
4
+ One page, three variants. The card at the top is identical in all three; what
5
+ changes below it is whether the user is approving, signing in, or creating an
6
+ account. Everything the authorization request carried travels in hidden fields
7
+ inside each form, so the request never has to round-trip through a `?next=`
8
+ parameter — see the controller's header for why that matters.
9
+
10
+ Three things on this page are load-bearing rather than decorative, and a
11
+ redesign has to keep all three:
12
+
13
+ 1. The icon sits in a FIXED box. Its size comes from `.consent-icon` and
14
+ never from the image, because under open registration the image is
15
+ supplied by an unauthenticated stranger.
16
+ 2. The provenance line says where this client's claims came from. Without it
17
+ a self-registered client is displayed exactly like an operator-configured
18
+ one, which makes the page a phishing surface.
19
+ 3. The coverage statement sits BEFORE the buttons. Grants are keyed on the
20
+ user, so this reaches every account they reach — a person must not
21
+ discover that afterwards.
22
+ --}}
23
+ <div class="consent-page">
24
+ <section class="consent-card">
25
+
26
+ <header class="consent-header">
27
+ <div class="consent-icon">
28
+ {{#if client.logoUri}}
29
+ <img class="consent-icon-image" src="{{client.logoUri}}" alt="" referrerpolicy="no-referrer" />
30
+ {{else}}
31
+ <span class="consent-icon-monogram">{{client.monogram}}</span>
32
+ {{/if}}
33
+ </div>
34
+ <h1 class="consent-title">{{client.name}}</h1>
35
+ <p class="consent-subtitle">wants to connect to your account</p>
36
+ </header>
37
+
38
+ {{#if errorMessage}}
39
+ <div class="consent-messages">
40
+ <div class="consent-error">{{errorMessage}}</div>
41
+ </div>
42
+ {{/if}}
43
+
44
+ {{#if justSignedIn}}
45
+ <div class="consent-messages">
46
+ <div class="consent-notice">You are signed in. Approve the request below to continue.</div>
47
+ </div>
48
+ {{/if}}
49
+
50
+ {{#if client.description}}
51
+ <p class="consent-description">{{client.description}}</p>
52
+ {{/if}}
53
+
54
+ <p class="consent-provenance consent-provenance--{{client.mode}}">{{client.provenance}}</p>
55
+
56
+ <div class="consent-scopes">
57
+ <h2 class="consent-scopes-title">It is asking to</h2>
58
+ <ul class="consent-scope-list">
59
+ {{#each scopes}}
60
+ <li class="consent-scope{{#unless described}} consent-scope--undescribed{{/unless}}">
61
+ {{description}}
62
+ </li>
63
+ {{else}}
64
+ <li class="consent-scope">Sign you in and confirm who you are.</li>
65
+ {{/each}}
66
+ </ul>
67
+ </div>
68
+
69
+ <p class="consent-coverage">
70
+ This applies to your account here, including any organisation or team you belong to
71
+ now or join later. You can disconnect it at any time.
72
+ </p>
73
+
74
+ {{#if isAuthenticated}}
75
+ <div class="consent-actions">
76
+ <form class="consent-form consent-form--approve" method="POST" action="{{action}}">
77
+ {{#each hiddenFields}}<input type="hidden" name="{{name}}" value="{{value}}" />{{/each}}
78
+ <input type="hidden" name="action" value="approve" />
79
+ <button class="consent-approve" type="submit">Approve</button>
80
+ </form>
81
+ {{!-- Cancel is a protocol response, not a navigation: it posts back so
82
+ the waiting client is told, rather than left to time out. --}}
83
+ <form class="consent-form consent-form--deny" method="POST" action="{{action}}">
84
+ {{#each hiddenFields}}<input type="hidden" name="{{name}}" value="{{value}}" />{{/each}}
85
+ <input type="hidden" name="action" value="deny" />
86
+ <button class="consent-deny" type="submit">Cancel</button>
87
+ </form>
88
+ </div>
89
+ {{/if}}
90
+
91
+ {{#if isLogin}}
92
+ <form class="consent-form consent-form--auth" method="POST" action="{{action}}">
93
+ {{#each hiddenFields}}<input type="hidden" name="{{name}}" value="{{value}}" />{{/each}}
94
+ <input type="hidden" name="action" value="login" />
95
+ <p class="consent-auth-prompt">Sign in to continue.</p>
96
+ <input class="consent-input" type="email" name="email" placeholder="Email" autofocus value="{{email}}" required />
97
+ <input class="consent-input" type="password" name="password" placeholder="Password" required />
98
+ <button class="consent-approve" type="submit">Sign in</button>
99
+ </form>
100
+
101
+ {{#if oauthProviders}}
102
+ <div class="consent-providers">{{> oauth-providers}}</div>
103
+ {{/if}}
104
+
105
+ <form class="consent-form consent-form--switch" method="POST" action="{{action}}">
106
+ {{#each hiddenFields}}<input type="hidden" name="{{name}}" value="{{value}}" />{{/each}}
107
+ <input type="hidden" name="action" value="showSignup" />
108
+ <button class="consent-switch" type="submit">Don't have an account? Sign up</button>
109
+ </form>
110
+ {{/if}}
111
+
112
+ {{#if isSignup}}
113
+ <form class="consent-form consent-form--auth" method="POST" action="{{action}}">
114
+ {{#each hiddenFields}}<input type="hidden" name="{{name}}" value="{{value}}" />{{/each}}
115
+ <input type="hidden" name="action" value="signup" />
116
+ <p class="consent-auth-prompt">Create an account to continue.</p>
117
+ <input class="consent-input" type="email" name="email" placeholder="Email" autofocus value="{{email}}" required />
118
+ <input class="consent-input" type="password" name="password" placeholder="Password" required />
119
+ <input class="consent-input" type="password" name="passwordConfirmation" placeholder="Confirm password" required />
120
+ <button class="consent-approve" type="submit">Create account</button>
121
+ </form>
122
+
123
+ <form class="consent-form consent-form--switch" method="POST" action="{{action}}">
124
+ {{#each hiddenFields}}<input type="hidden" name="{{name}}" value="{{value}}" />{{/each}}
125
+ <input type="hidden" name="action" value="showLogin" />
126
+ <button class="consent-switch" type="submit">Already have an account? Sign in</button>
127
+ </form>
128
+ {{/if}}
129
+
130
+ {{#if client.policyUri}}
131
+ <p class="consent-links">
132
+ <a href="{{client.policyUri}}" rel="noopener noreferrer nofollow" target="_blank">Privacy policy</a>
133
+ {{#if client.tosUri}} · <a href="{{client.tosUri}}" rel="noopener noreferrer nofollow" target="_blank">Terms</a>{{/if}}
134
+ </p>
135
+ {{/if}}
136
+
137
+ </section>
138
+ </div>
@@ -0,0 +1,57 @@
1
+ {{!--
2
+ @boopkit/cap-oauth-server — the disconnect confirmation page.
3
+
4
+ A dedicated page rather than an inline button on the list row, and rather than
5
+ a native confirm(). Disconnecting is not undoable, and the consequence is not
6
+ visible from a row in a list: whatever that application was doing stops
7
+ mid-way, and getting it back means walking the whole authorization flow again.
8
+ The page's job is to name the client and that consequence before the commit.
9
+
10
+ Nothing here is destructive until the form is submitted. Reaching this page
11
+ changes nothing.
12
+ --}}
13
+ <div class="consent-page">
14
+ <section class="consent-card">
15
+
16
+ <header class="consent-header">
17
+ <div class="consent-icon">
18
+ {{#if connection.logoUri}}
19
+ <img class="consent-icon-image" src="{{connection.logoUri}}" alt="" referrerpolicy="no-referrer" />
20
+ {{else}}
21
+ <span class="consent-icon-monogram">{{connection.monogram}}</span>
22
+ {{/if}}
23
+ </div>
24
+ <h1 class="consent-title">Disconnect {{connection.name}}?</h1>
25
+ <p class="consent-subtitle">It will lose access to your account immediately.</p>
26
+ </header>
27
+
28
+ <p class="disconnect-consequence">
29
+ {{connection.name}} stops being able to act on your behalf on its very next request,
30
+ including anything it is part-way through. To use it again you will have to connect
31
+ it and approve it a second time.
32
+ </p>
33
+
34
+ <div class="consent-scopes">
35
+ <h2 class="consent-scopes-title">What it can do today</h2>
36
+ <ul class="consent-scope-list">
37
+ {{#each connection.scopes}}
38
+ <li class="consent-scope{{#unless described}} consent-scope--undescribed{{/unless}}">
39
+ {{description}}
40
+ </li>
41
+ {{else}}
42
+ <li class="consent-scope">Sign you in and confirm who you are.</li>
43
+ {{/each}}
44
+ </ul>
45
+ </div>
46
+
47
+ <p class="consent-coverage">{{coverage}}</p>
48
+
49
+ <div class="consent-actions">
50
+ <form class="consent-form" method="POST" action="{{action}}">
51
+ <button class="disconnect-confirm" type="submit">Disconnect</button>
52
+ </form>
53
+ <a class="consent-deny disconnect-cancel" href="{{cancelPath}}">Keep it connected</a>
54
+ </div>
55
+
56
+ </section>
57
+ </div>