Skip to content

Commit 6477bf3

Browse files
committed
perf: Login asset otp
1 parent 34e37a6 commit 6477bf3

13 files changed

Lines changed: 242 additions & 12 deletions

File tree

src/app/elements/connect/connect-dialog/select-account/select-account.component.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,20 @@
7575

7676
</nz-form-item>
7777

78+
<nz-form-item *ngIf="showOTPCodeInput" class="form-field otp-code">
79+
<nz-form-label class="zone-label" nzFor='otpCode' nzNoColon>{{ "OTP Code" | translate }}</nz-form-label>
80+
<nz-form-control class="input-field">
81+
<input
82+
#otpCode
83+
[(ngModel)]="manualAuthInfo.otp_code"
84+
[placeholder]="'OTP Code' | translate"
85+
autocomplete="one-time-code"
86+
inputmode="numeric"
87+
maxlength="16"
88+
name="otpCode"
89+
nz-input
90+
>
91+
</nz-form-control>
92+
</nz-form-item>
7893

7994

src/app/elements/connect/connect-dialog/select-account/select-account.component.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
}
3333
}
3434

35+
.otp-code {
36+
::ng-deep .ant-input {
37+
width: 70%;
38+
}
39+
}
40+
3541
.input-field {
3642
width: 100%;
3743
}

src/app/elements/connect/connect-dialog/select-account/select-account.component.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
111111
return this.accountSelected.username === '@INPUT' || this.accountSelected.username === '@USER';
112112
}
113113

114+
get showOTPCodeInput() {
115+
return !!this.accountSelected &&
116+
this.accountSelected.username !== '@ANON' &&
117+
!!this.accountSelected.has_otp_secret_key;
118+
}
119+
114120
public compareFn = (f1: Account, f2: Account) => {
115121
if (!f1 || !f2) return false;
116122
return f1.alias === f2.alias && f1.id === f2.id;
@@ -298,6 +304,7 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
298304
if (!this.accountSelected) {
299305
return;
300306
}
307+
this.manualAuthInfo.otp_code = '';
301308
if (this.accountSelected.has_secret) {
302309
return;
303310
}
@@ -317,6 +324,7 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
317324
}
318325
if (this.localAuthItems && this.localAuthItems.length > 0) {
319326
this.manualAuthInfo = Object.assign(this.manualAuthInfo, this.localAuthItems[0]);
327+
this.manualAuthInfo.otp_code = '';
320328
}
321329
this.setUsernamePlaceholder();
322330
setTimeout(() => {
@@ -342,6 +350,7 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
342350
this.filteredOptions = this.localAuthItems.filter(authInfo => {
343351
if (authInfo.username.toLowerCase() === filterValue) {
344352
this.manualAuthInfo = Object.assign(this.manualAuthInfo, authInfo);
353+
this.manualAuthInfo.otp_code = '';
345354
}
346355
return authInfo.username.toLowerCase().includes(filterValue);
347356
});

src/app/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class Account {
3434
name: string;
3535
username: string;
3636
has_secret: boolean;
37+
has_otp_secret_key: boolean;
3738
secret: string;
3839
actions: Array<Action>;
3940
id?: string;
@@ -401,6 +402,7 @@ export class AuthInfo {
401402
alias: string;
402403
username: string;
403404
secret: string;
405+
otp_code: string;
404406
rememberAuth: boolean;
405407
}
406408

src/app/services/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ export class AppService {
348348
for (const auth of auths) {
349349
const newAuth = Object.assign({}, auth);
350350
newAuth.secret = this.decrypt(newAuth.secret);
351+
newAuth.otp_code = '';
351352
newAuths.push(newAuth);
352353
}
353354
return newAuths;
@@ -356,6 +357,7 @@ export class AppService {
356357
setAccountLocalAuth(asset: Asset, account: Account, auth: AuthInfo) {
357358
const assetId = asset.id;
358359
const newAuth = Object.assign({ alias: account.alias, username: account.username }, auth);
360+
newAuth.otp_code = '';
359361

360362
// 如果 auth.alias 是 undefined,保持使用 account.alias
361363
if (auth.alias === undefined && account.alias !== undefined) {

src/app/services/connect-token/acl-dialog/acl-dialog.component.html

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@
2929
<div class="loading-bar-progress"></div>
3030
</div>
3131
</ng-container>
32+
33+
<ng-container *ngIf="content.customContent?.type === 'otp_code'">
34+
<div class="otp-code-box">
35+
<input
36+
#otpCodeInput
37+
[(ngModel)]="otpCode"
38+
(ngModelChange)="onOTPCodeInput()"
39+
[placeholder]="'OTP Code' | translate"
40+
autocomplete="one-time-code"
41+
inputmode="numeric"
42+
maxlength="16"
43+
nz-input
44+
/>
45+
<div *ngIf="otpCodeError" class="otp-code-error">{{ otpCodeError }}</div>
46+
</div>
47+
</ng-container>
3248
</ng-template>
3349
</nz-alert>
3450
</div>
@@ -47,7 +63,12 @@
4763

4864
<div *nzModalFooter class="nz-modal-footer">
4965
<ng-container *ngFor="let action of content.actions">
50-
<button nz-button [nzType]="action.type || 'default'" (click)="action.callback()">
66+
<button
67+
nz-button
68+
[nzLoading]="code === 'otp_code_verify' && action.type === 'primary' && otpCodeSubmitting"
69+
[nzType]="action.type || 'default'"
70+
(click)="action.callback()"
71+
>
5172
{{ action.text | translate }}
5273
</button>
5374
</ng-container>

src/app/services/connect-token/acl-dialog/acl-dialog.component.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
border-radius: 4px;
55
}
66
}
7+
8+
.otp-code-box {
9+
margin-top: 16px;
10+
}
11+
12+
.otp-code-error {
13+
color: #ff4d4f;
14+
font-size: 12px;
15+
margin-top: 8px;
16+
}
717
}
818

919

src/app/services/connect-token/acl-dialog/acl-dialog.component.ts

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {I18nService} from '@app/services/i18n';
33
import {HttpService} from '@app/services/http';
44
import {FaceService} from '@app/services/face';
55
import {HttpErrorResponse} from '@angular/common/http';
6-
import {Component, Inject, OnInit} from '@angular/core';
6+
import {Component, ElementRef, Inject, OnInit, ViewChild} from '@angular/core';
77
import {Asset, ConnectData, ConnectionToken} from '@app/model';
88
import {NzNotificationService} from 'ng-zorro-antd/notification';
99
import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
@@ -32,12 +32,16 @@ interface DialogContent {
3232
styleUrls: ['acl-dialog.component.scss']
3333
})
3434
export class ElementACLDialogComponent implements OnInit {
35+
@ViewChild('otpCodeInput', { static: false }) otpCodeInput: ElementRef;
3536
public asset: Asset;
3637
public connectInfo: ConnectData;
3738
public code: string;
3839
public connectionToken: ConnectionToken = null;
3940
public error: HttpErrorResponse;
4041
public otherError: string;
42+
public otpCode: string = '';
43+
public otpCodeError: string = '';
44+
public otpCodeSubmitting = false;
4145
public ticketAssignees: string = '-';
4246
// Token 的行为,创建或者兑换 Token, create, exchange
4347
public tokenAction: string = 'create';
@@ -82,15 +86,43 @@ export class ElementACLDialogComponent implements OnInit {
8286
}
8387

8488
ngOnInit() {
85-
// 创建 Token 的时候,需要传入 Asset 和 ConnectInfo
86-
this.content = this.getDialogContent(this.data.code);
8789
this.asset = this.data.asset;
8890
this.connectInfo = this.data.connectInfo;
89-
this.code = this.data.code;
91+
this.error = this.data.error;
9092
// 兑换 Token 的时候,需要传入 Token ID
9193
this.tokenID = this.data.tokenID;
9294
// 控制 token 的行为, 创建还是兑换
9395
this.tokenAction = this.data.tokenAction;
96+
this.code = this.resolveDialogCode(this.data.code, this.error);
97+
this.content = this.getDialogContent(this.code);
98+
if (this.code === 'otp_code_verify') {
99+
this.focusOTPCodeInput();
100+
}
101+
}
102+
103+
onOTPCodeInput() {
104+
this.otpCodeError = '';
105+
}
106+
107+
onConfirmOTPCode() {
108+
const otpCode = (this.otpCode || '').trim();
109+
if (!otpCode) {
110+
this.otpCodeError = this._i18n.instant('Please input OTP code');
111+
this.focusOTPCodeInput();
112+
return;
113+
}
114+
this.otpCodeError = '';
115+
this.otpCodeSubmitting = true;
116+
this.requestTokenWithOTPCode(otpCode).subscribe(
117+
(connToken: ConnectionToken) => {
118+
this.otpCodeSubmitting = false;
119+
this.dialogRef.close(connToken);
120+
},
121+
(error: HttpErrorResponse) => {
122+
this.otpCodeSubmitting = false;
123+
this.applyDialogError(error, true);
124+
}
125+
);
94126
}
95127

96128
async onCopySuccess(evt) {
@@ -272,6 +304,91 @@ export class ElementACLDialogComponent implements OnInit {
272304
this.dialogRef.close(null);
273305
}
274306

307+
private requestTokenWithOTPCode(otpCode: string) {
308+
if (this.tokenAction === 'exchange') {
309+
return this._http.exchangeConnectToken(this.tokenID, false, false, undefined, otpCode);
310+
}
311+
if (this.data.connectData && this.data.connectData.direct) {
312+
return this._http.adminConnectToken(
313+
this.asset,
314+
this.data.connectData,
315+
false,
316+
false,
317+
undefined,
318+
otpCode
319+
);
320+
}
321+
return this._http.createConnectToken(this.asset, this.connectInfo, false, false, undefined, otpCode);
322+
}
323+
324+
private resolveDialogCode(code: string, error?: HttpErrorResponse): string {
325+
if (this.hasOTPCodeError(error)) {
326+
return 'otp_code_verify';
327+
}
328+
return code || 'other';
329+
}
330+
331+
private hasOTPCodeError(error?: HttpErrorResponse): boolean {
332+
return !!(error && error.error && typeof error.error === 'object' && error.error.otp_code);
333+
}
334+
335+
private getFieldErrorMessage(error: HttpErrorResponse, field: string): string {
336+
if (!error || !error.error || typeof error.error !== 'object') {
337+
return '';
338+
}
339+
const value = error.error[field];
340+
if (Array.isArray(value)) {
341+
return value.join(' ');
342+
}
343+
return value || '';
344+
}
345+
346+
private getOtherErrorMessage(error: HttpErrorResponse): string {
347+
if (!error) {
348+
return '';
349+
}
350+
let value: any = error.error;
351+
if (!value) {
352+
return error.message;
353+
}
354+
if (typeof value === 'string') {
355+
return value;
356+
}
357+
if (Array.isArray(value)) {
358+
return value.join(' ');
359+
}
360+
if (value.detail) {
361+
return value.detail;
362+
}
363+
return JSON.stringify(value);
364+
}
365+
366+
private applyDialogError(error: HttpErrorResponse, keepOTPCode = false) {
367+
this.error = error;
368+
this.data.error = error;
369+
this.code = this.resolveDialogCode(error?.error?.code, error);
370+
if (this.code === 'otp_code_verify') {
371+
this.otpCodeError = this.getFieldErrorMessage(error, 'otp_code');
372+
if (!keepOTPCode) {
373+
this.otpCode = '';
374+
}
375+
this.content = this.getDialogContent(this.code);
376+
this.focusOTPCodeInput();
377+
return;
378+
}
379+
this.otpCodeError = '';
380+
this.otherError = this.getOtherErrorMessage(error);
381+
this.content = this.getDialogContent(this.code);
382+
}
383+
384+
private focusOTPCodeInput() {
385+
setTimeout(() => {
386+
if (this.otpCodeInput && this.otpCodeInput.nativeElement) {
387+
this.otpCodeInput.nativeElement.focus();
388+
}
389+
}, 0);
390+
}
391+
275392
checkTicket() {
276393
const checkMethod = this.connectionToken.from_ticket_info.check_ticket_api.method.toLowerCase();
277394
const checkURL = this.connectionToken.from_ticket_info.check_ticket_api.url;
@@ -343,6 +460,24 @@ export class ElementACLDialogComponent implements OnInit {
343460
}
344461
]
345462
},
463+
otp_code_verify: {
464+
title: 'OTP verification',
465+
message: 'This account requires OTP code. Please input it to continue.',
466+
customContent: {
467+
type: 'otp_code'
468+
},
469+
actions: [
470+
{
471+
text: 'Cancel',
472+
callback: () => vm.closeDialog()
473+
},
474+
{
475+
text: 'Confirm',
476+
type: 'primary',
477+
callback: () => vm.onConfirmOTPCode()
478+
}
479+
]
480+
},
346481
acl_face_online: {
347482
title: 'Login reminder',
348483
message: 'Face online required',

0 commit comments

Comments
 (0)