@things-factory/oauth2-client 8.0.0-beta.8 → 8.0.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.
@@ -0,0 +1,636 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@operato/help/ox-help-icon.js'
3
+
4
+ import Clipboard from 'clipboard'
5
+ import gql from 'graphql-tag'
6
+ import { css, html } from 'lit'
7
+ import { customElement, property, query, queryAll } from 'lit/decorators.js'
8
+ import { asyncReplace } from 'lit/directives/async-replace.js'
9
+ import { connect } from 'pwa-helpers/connect-mixin.js'
10
+
11
+ import { client } from '@operato/graphql'
12
+ import { notify } from '@operato/layout'
13
+ import { navigate, PageView, store } from '@operato/shell'
14
+ import { sleep } from '@operato/utils'
15
+
16
+ const OAUTH2CLIENT = `
17
+ id
18
+ name
19
+ description
20
+ icon
21
+ grantType
22
+ clientId
23
+ clientSecret
24
+ callbackUrl
25
+ authUrl
26
+ accessTokenUrl
27
+ webhook
28
+ username
29
+ password
30
+ codeChallengeMethod
31
+ codeVerifier
32
+ scopes
33
+ accessToken
34
+ refreshToken
35
+ jwtToken
36
+ expires
37
+ tokenType
38
+ updatedAt
39
+ createdAt
40
+ `
41
+
42
+ @customElement('oauth2-client')
43
+ export class Oauth2Client extends connect(store)(PageView) {
44
+ static styles = [
45
+ css`
46
+ :host {
47
+ display: flex;
48
+ flex-direction: column;
49
+ overflow-y: auto;
50
+ position: relative;
51
+
52
+ background-color: var(--md-sys-color-background);
53
+ padding: var(--spacing-large);
54
+ }
55
+
56
+ h2 {
57
+ margin: var(--title-margin);
58
+ font: var(--title-font);
59
+ color: var(--title-text-color);
60
+ }
61
+
62
+ [page-description] {
63
+ margin: var(--page-description-margin);
64
+ font: var(--page-description-font);
65
+ color: var(--page-description-color);
66
+ }
67
+
68
+ [icon] {
69
+ position: absolute;
70
+ top: 10px;
71
+ right: 10px;
72
+ max-width: 80px;
73
+ }
74
+
75
+ [icon] img {
76
+ max-width: 100%;
77
+ max-height: 100%;
78
+ }
79
+
80
+ [button-primary] {
81
+ background-color: var(--button-primary-background-color);
82
+ border: var(--button-border);
83
+ border-radius: var(--button-border-radius);
84
+ margin: var(--button-margin);
85
+ padding: var(--button-primary-padding);
86
+ color: var(--button-primary-color);
87
+ font: var(--button-primary-font);
88
+ text-transform: var(--button-text-transform);
89
+
90
+ text-decoration: none;
91
+ }
92
+
93
+ [button-primary]:hover {
94
+ background-color: var(--button-primary-active-background-color);
95
+ box-shadow: var(--button-active-box-shadow);
96
+ }
97
+
98
+ [fieldset-container] {
99
+ background-color: var(--md-sys-color-surface);
100
+ margin: var(--spacing-large) 0 var(--spacing-medium) 0;
101
+ padding: var(--spacing-medium);
102
+ border-radius: var(--border-radius);
103
+ box-shadow: var(--box-shadow);
104
+
105
+ label {
106
+ font: var(--label-font);
107
+ color: var(--label-color, var(--md-sys-color-on-surface));
108
+ text-transform: var(--label-text-transform);
109
+ }
110
+
111
+ input,
112
+ select {
113
+ border: var(--border-dim-color);
114
+ border-radius: var(--border-radius);
115
+ margin: var(--input-margin);
116
+ padding: var(--input-padding);
117
+ font: var(--input-font);
118
+
119
+ flex: 1;
120
+ }
121
+
122
+ select:focus,
123
+ input:focus,
124
+ button {
125
+ outline: none;
126
+ }
127
+
128
+ form {
129
+ max-width: var(--content-container-max-width);
130
+ }
131
+ }
132
+
133
+ [fieldset-container] fieldset {
134
+ margin: 0;
135
+ margin-top: -15px;
136
+ }
137
+
138
+ fieldset {
139
+ border-radius: var(--border-radius);
140
+ border: var(--border-dim-color);
141
+ margin: var(--fieldset-margin);
142
+ padding: var(--fieldset-padding);
143
+ }
144
+
145
+ legend {
146
+ padding: var(--legend-padding);
147
+ font-weight: bold;
148
+ color: var(--legend-color);
149
+ }
150
+
151
+ [field-2column] {
152
+ display: grid;
153
+ grid-template-columns: 1fr 1fr;
154
+ grid-gap: 15px;
155
+ }
156
+
157
+ [field] {
158
+ display: flex;
159
+ flex-direction: column;
160
+ position: relative;
161
+ }
162
+
163
+ [grid-span] {
164
+ grid-column: span 2;
165
+ }
166
+
167
+ button {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: var(--spacing-small);
171
+ }
172
+
173
+ button,
174
+ input[type='submit'],
175
+ [button-in-field] {
176
+ background-color: var(--button-background-color);
177
+ border: var(--button-border);
178
+ border-radius: var(--button-border-radius);
179
+ padding: var(--button-padding);
180
+ color: var(--button-color);
181
+ font: var(--button-font);
182
+ text-transform: var(--button-text-transform);
183
+
184
+ margin: var(--spacing-medium) 0 var(--spacing-medium) var(--spacing-medium);
185
+ float: right;
186
+ text-decoration: none;
187
+ }
188
+
189
+ button:hover,
190
+ input[type='submit']:hover {
191
+ border: var(--button-activ-border);
192
+ box-shadow: var(--button-active-box-shadow);
193
+ }
194
+
195
+ [button-in-field] {
196
+ border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;
197
+ position: absolute;
198
+ top: 12px;
199
+ right: 0;
200
+ max-height: 36px;
201
+ }
202
+
203
+ [input-hint] {
204
+ font: var(--input-hint-font);
205
+ color: var(--input-hint-color);
206
+ }
207
+
208
+ @media screen and (max-width: 480px) {
209
+ [field] {
210
+ grid-column: span 2;
211
+ }
212
+ }
213
+ `
214
+ ]
215
+
216
+ @property({ type: Object }) oauth2Client: any
217
+ @property({ type: String }) _grantType?: string
218
+
219
+ @query('form') form!: HTMLFormElement
220
+ @queryAll('[clipboard-copy]') copybuttons
221
+
222
+ private clipboard?: Clipboard
223
+ private _icon?: string
224
+
225
+ get context() {
226
+ return {
227
+ title: {
228
+ icon: 'apps',
229
+ text: this.oauth2Client?.name
230
+ }
231
+ }
232
+ }
233
+
234
+ render() {
235
+ var oauth2Client = this.oauth2Client || {}
236
+
237
+ return html`
238
+ <div>
239
+ <h2><md-icon>apps</md-icon>&nbsp;${oauth2Client.name || ''}</h2>
240
+ <p page-description>${oauth2Client.description || ''}</p>
241
+ </div>
242
+
243
+ <div icon>
244
+ <img src=${oauth2Client.icon}>
245
+ </div>
246
+
247
+ <form>
248
+ <div fieldset-container>
249
+ <fieldset>
250
+ <legend>oauth2 client</legend>
251
+ <div field-2column>
252
+ <div field grid-span>
253
+ <label for='name'>name</label>
254
+ <input type='text' id="name" name="name" .value=${oauth2Client.name || ''}>
255
+ </div>
256
+
257
+ <div field grid-span>
258
+ <label for='description'>description</label>
259
+ <input type='text' id="description" name="description" .value=${oauth2Client.description || ''}>
260
+ </div>
261
+
262
+ <div field grid-span>
263
+ <label for='icon'>icon</label>
264
+ <input type='text' id="icon" name="icon"
265
+ @change=${e => (this._icon = e.target.value)}
266
+ .value=${oauth2Client.icon || ''}
267
+ >
268
+ </div>
269
+ </div>
270
+ </fieldset>
271
+ </div>
272
+
273
+ <div fieldset-container>
274
+ <fieldset>
275
+ <legend>authorization</legend>
276
+ <div field-2column>
277
+ <div field grid-span>
278
+ <label for='grant-type'>grant type
279
+ <ox-help-icon topic='/oauth2-client/grant-type'></ox-help-icon>
280
+ </label>
281
+ <select type='text' id="grant-type" name="grantType" .value=${oauth2Client.grantType || 'code'}
282
+ @change=${e => (this._grantType = e.target.value)}>
283
+ <option></option>
284
+ <option value='code'>Authorization Code Grant</option>
285
+ <option value='jwt'>JWT As Authorization Grant</option>
286
+ <option value='owner'>Resource Owner Password Credentials Grant</option>
287
+ <option value='credentials'>Client Credentials Grant</option>
288
+ </select>
289
+ </div>
290
+
291
+ <div field grid-span>
292
+ <label for='auth-url'>auth url</label>
293
+ <input type='text' id="auth-url" name="authUrl" .value=${oauth2Client.authUrl || ''}>
294
+ <div input-hint>The endpoint for authorization server. This is used to get the authorization code.</div>
295
+ </div>
296
+
297
+ <div field grid-span>
298
+ <label for='access-token-url'>access token url</label>
299
+ <input type='text' id="access-token-url" name="accessTokenUrl" .value=${
300
+ oauth2Client.accessTokenUrl || ''
301
+ }>
302
+ <div input-hint>The endpoint for authentication server. This is used to exchange the authorization code for an access token.</div>
303
+ </div>
304
+
305
+ <div field grid-span>
306
+ <label for='callback-url'>callback url</label>
307
+ <input type='text' id="callback-url" name="callbackUrl" .value=${oauth2Client.callbackUrl || ''}>
308
+ <div input-hint>This is the callback url that you will be redirected to, after your application is authorized.
309
+ This is used to extract the authorization code or access token.
310
+ Normally, this callback url should match the one you use during the application registration process.
311
+ If you leave this field empty, default callback url(${
312
+ location.origin
313
+ }/oauth2-client/callback</label>) will be used.</div>
314
+ </div>
315
+
316
+ <div field grid-span>
317
+ <label for='client-id'>client id</label>
318
+ <input type='text' id="client-id" name="clientId" .value=${oauth2Client.clientId || ''}>
319
+ <div input-hint>The client identifier issued to the client during the application registration process.</div>
320
+ </div>
321
+
322
+ <div field grid-span>
323
+ <label for='client-secret'>client secret</label>
324
+ <input type='text' id="client-secret" name="clientSecret" .value=${oauth2Client.clientSecret || ''}>
325
+ <div input-hint>The client secret issued to the client during the application registration process.</div>
326
+ </div>
327
+
328
+ ${
329
+ this._grantType == 'owner'
330
+ ? html`
331
+ <div field grid-span>
332
+ <label for="username">user name</label>
333
+ <input type="text" id="username" name="username" .value=${oauth2Client.username || ''} />
334
+ </div>
335
+
336
+ <div field grid-span>
337
+ <label for="password">password</label>
338
+ <input type="password" id="password" name="password" .value=${oauth2Client.password || ''} />
339
+ </div>
340
+ `
341
+ : html``
342
+ }
343
+
344
+ ${
345
+ this._grantType == 'jwt'
346
+ ? html`
347
+ <div field grid-span>
348
+ <label for="jwt-token">jwt-token</label>
349
+ <input type="text" id="jwt-token" name="jwtToken" .value=${oauth2Client.jwtToken || ''} />
350
+ <div input-hint>The JWT Bearer Token for JWT As Authorization Grant</div>
351
+ </div>
352
+ `
353
+ : html``
354
+ }
355
+
356
+ <!-- code PKCE grant type not supported yet
357
+ <div field grid-span>
358
+ <label for='code-challenge-method'>code challenge method</label>
359
+ <select type='text' id="code-challenge-method" name="codeChallengeMethod" .value=${
360
+ oauth2Client.codeChallengeMethod
361
+ }>
362
+ <option></option>
363
+ <option>SHA-256</option>
364
+ <option>Plain</option>
365
+ </select>
366
+ <div input-hint>Algorithm used for generating the code challenge</div>
367
+ </div>
368
+
369
+ <div field grid-span>
370
+ <label for='code-verifier'>code verifier
371
+ <ox-help-icon topic='/oauth2-client/code-verifier'></ox-help-icon>
372
+ </label>
373
+ <input type='text' id="code-verifier" name="codeVerifier" .value=${oauth2Client.codeVerifier || ''}>
374
+ <div input-hint>A random, 43-128 character string used to connect the authorization request to the token request.
375
+ Uses the following characters: [A-Z]/[a-z]/[0-9]/"-"/"."/"_"/"~".</div>
376
+ </div>
377
+ -->
378
+
379
+ <div field grid-span>
380
+ <label for='scopes'>scopes</label>
381
+ <input type='text' id="scopes" name="scopes" .value=${oauth2Client.scopes || ''}>
382
+ <div input-hint>The scopes of the access request. It may have multiple space-delimited values.</div>
383
+ </div>
384
+ </div>
385
+ </fieldset>
386
+ </div>
387
+
388
+ <div fieldset-container>
389
+ <fieldset>
390
+ <div field grid-span>
391
+ <label for='token-type'>token type</label>
392
+ <input type='text' id="token-type" .value=${oauth2Client.tokenType || ''} readonly>
393
+ <div input-hint>Added to the authorization header before the access token.</div>
394
+ </div>
395
+
396
+ <legend>access token</legend>
397
+ <div field-2column>
398
+ <div field grid-span>
399
+ <label for="access-token">access token</label>
400
+ <input id="access-token" type="text" .value=${oauth2Client.accessToken || ''} readonly />
401
+ <button button-in-field clipboard-copy @click=${e => e.preventDefault()}>copy</button>
402
+ ${
403
+ oauth2Client.expires
404
+ ? html`<div input-hint>
405
+ expired in ${new Date(Number(oauth2Client.expires))} :
406
+ ${asyncReplace(this.expTimer(oauth2Client.expires))}
407
+ </div>`
408
+ : html``
409
+ }
410
+ </div>
411
+
412
+ <div field grid-span>
413
+ <label for="refresh-token">refresh token</label>
414
+ <input id="refresh-token" type="text" .value=${oauth2Client.refreshToken || ''} readonly />
415
+ <button button-in-field clipboard-copy @click=${e => e.preventDefault()}>copy</button>
416
+ </div>
417
+ </div>
418
+ </fieldset>
419
+ </div>
420
+
421
+ <button @click=${e => this.deleteOauth2Client(e)}>delete this app</button>
422
+ <button @click=${async e => {
423
+ e.preventDefault()
424
+ await this.updateOauth2Client()
425
+ await this.generateOauth2AccessToken()
426
+ }}>get new access token</button>
427
+ <button @click=${async e => {
428
+ e.preventDefault()
429
+ await this.updateOauth2Client()
430
+ await this.refreshOauth2AccessToken()
431
+ }} ?disabled=${!oauth2Client.refreshToken}>refresh access token</button>
432
+ <input type="submit" value="update" @click=${async e => {
433
+ e.preventDefault()
434
+ await this.updateOauth2Client()
435
+ }}>
436
+ </form>
437
+ `
438
+ }
439
+
440
+ updated(changes) {
441
+ if (changes.has('oauth2Client')) {
442
+ this._grantType = this.oauth2Client?.grantType
443
+ }
444
+ }
445
+
446
+ firstUpdated() {
447
+ this.clipboard = new Clipboard(this.copybuttons, {
448
+ target: (trigger => trigger.parentElement.querySelector('input')) as any
449
+ })
450
+ }
451
+
452
+ async pageUpdated(changes, lifecycle, before) {
453
+ if (this.active) {
454
+ await this.fetchOauth2Client()
455
+ }
456
+ }
457
+
458
+ async *expTimer(exp) {
459
+ const DAY = 24 * 60 * 60
460
+ const HOUR = 60 * 60
461
+ const MIN = 60
462
+
463
+ while (this.active) {
464
+ var remain = Math.floor((Number(exp) - Date.now()) / 1000)
465
+ const days = Math.floor(remain / DAY)
466
+ remain -= days * DAY
467
+ const hours = Math.floor(remain / HOUR)
468
+ remain -= hours * HOUR
469
+ const mins = Math.floor(remain / MIN)
470
+ const secs = remain - mins * MIN
471
+
472
+ yield `${days} days ${hours} hours ${mins} mins ${secs} seconds remain`
473
+
474
+ await sleep(1000)
475
+ }
476
+ }
477
+
478
+ async fetchOauth2Client() {
479
+ const response = await client.query({
480
+ query: gql`
481
+ query($id: String!) {
482
+ oauth2Client(id: $id) {
483
+ ${OAUTH2CLIENT}
484
+ }
485
+ }
486
+ `,
487
+ variables: {
488
+ id: this.lifecycle.resourceId
489
+ }
490
+ })
491
+
492
+ this.oauth2Client = response.data.oauth2Client
493
+ }
494
+
495
+ async updateOauth2Client() {
496
+ const formData = new FormData(this.form)
497
+
498
+ const patch = Array.from(formData.entries()).reduce((patch, [key, value]) => {
499
+ patch[key] = value
500
+ return patch
501
+ }, {})
502
+
503
+ const id = this.lifecycle.resourceId
504
+
505
+ const response = await client.mutate({
506
+ mutation: gql`
507
+ mutation($id: String!, $patch: Oauth2ClientPatch!) {
508
+ updateOauth2Client(id: $id, patch: $patch) {
509
+ ${OAUTH2CLIENT}
510
+ }
511
+ }
512
+ `,
513
+ variables: {
514
+ id,
515
+ patch
516
+ }
517
+ })
518
+
519
+ if (response.errors) {
520
+ notify({
521
+ level: 'error',
522
+ message: 'update oauth2 client fail'
523
+ })
524
+ } else {
525
+ this.oauth2Client = response.data.updateOauth2Client
526
+ }
527
+ }
528
+
529
+ async deleteOauth2Client(e) {
530
+ e.preventDefault()
531
+
532
+ const id = this.lifecycle.resourceId
533
+
534
+ const response = await client.mutate({
535
+ mutation: gql`
536
+ mutation ($id: String!) {
537
+ deleteOauth2Client(id: $id)
538
+ }
539
+ `,
540
+ variables: {
541
+ id
542
+ }
543
+ })
544
+
545
+ if (response.errors) {
546
+ notify({
547
+ level: 'error',
548
+ message: 'delete oauth2 client fail'
549
+ })
550
+ } else {
551
+ navigate('oauth2-clients')
552
+ }
553
+ }
554
+
555
+ async generateOauth2AccessToken() {
556
+ const id = this.lifecycle.resourceId
557
+
558
+ if (this.oauth2Client.grantType == 'code') {
559
+ const response = await client.mutate({
560
+ mutation: gql`
561
+ mutation ($id: String!) {
562
+ getOauth2AuthUrl(id: $id)
563
+ }
564
+ `,
565
+ variables: {
566
+ id: this.lifecycle.resourceId
567
+ }
568
+ })
569
+
570
+ if (!response.errors) {
571
+ location.href = response.data.getOauth2AuthUrl
572
+ } else {
573
+ notify({
574
+ level: 'error',
575
+ message: 'getting application access token fail'
576
+ })
577
+ }
578
+ } else {
579
+ const response = await client.mutate({
580
+ mutation: gql`
581
+ mutation($id: String!) {
582
+ getOauth2AccessToken(id: $id) {
583
+ ${OAUTH2CLIENT}
584
+ }
585
+ }
586
+ `,
587
+ variables: {
588
+ id: this.lifecycle.resourceId
589
+ }
590
+ })
591
+
592
+ if (response.errors) {
593
+ notify({
594
+ level: 'error',
595
+ message: 'getting application access token fail'
596
+ })
597
+ } else {
598
+ this.oauth2Client = response.data.getOauth2AccessToken
599
+ notify({
600
+ level: 'info',
601
+ message: 'got application access token successfully'
602
+ })
603
+ }
604
+ }
605
+ }
606
+
607
+ async refreshOauth2AccessToken() {
608
+ const id = this.lifecycle.resourceId
609
+
610
+ const response = await client.mutate({
611
+ mutation: gql`
612
+ mutation($id: String!) {
613
+ refreshOauth2AccessToken(id: $id) {
614
+ ${OAUTH2CLIENT}
615
+ }
616
+ }
617
+ `,
618
+ variables: {
619
+ id: this.lifecycle.resourceId
620
+ }
621
+ })
622
+
623
+ if (response.errors) {
624
+ notify({
625
+ level: 'error',
626
+ message: 'getting application access token fail'
627
+ })
628
+ } else {
629
+ this.oauth2Client = response.data.refreshOauth2AccessToken
630
+ notify({
631
+ level: 'info',
632
+ message: 'got application access token successfully'
633
+ })
634
+ }
635
+ }
636
+ }