Skip to content

Commit 8adbac7

Browse files
ZhaoJiSenw940853815fit2botgithub-actions[bot]ibuler
authored
Pr@dev@merge osm dev (#1535)
* feat: configurable url prefix (#1509) (#1512) * feat: configurable url prefix * perf: Update Dockerfile with new base image tag --------- Co-authored-by: fit2bot <68588906+fit2bot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * perf: update build base image * perf: update base image * feat:support proxy site prefix (#1516) Co-authored-by: Crane.z <1481445951@qq.com> * fix: share session page not load favicon * style:make web terminal logo smaller * feat: Added a feature for important message notifications * fix: fullscreen issue * fix: modify remember password through config * fix:Modify the split-screen logic for asset connection * fix:Make the guide function work properly * chore: update github action * deps: update some dep pkgs * fix: client detection * perf: update support ssh key (#1528) * feat: add support for SSH key authentication in connect dialog * perf: update support ssh key * perf: Update Dockerfile with new base image tag --------- Co-authored-by: wan92hen <wangzhen@fit2cloud.com> Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix: version display * fix: strip build suffix and trailing content from version display --------- Co-authored-by: wrd <w940853815@gmail.com> Co-authored-by: fit2bot <68588906+fit2bot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: Crane.z <1481445951@qq.com> Co-authored-by: Bai <baijiangjie@gmail.com> Co-authored-by: wan92hen <wangzhen@fit2cloud.com>
1 parent 4585cb3 commit 8adbac7

5 files changed

Lines changed: 235 additions & 30 deletions

File tree

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

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,29 +50,93 @@
5050

5151
<nz-form-item
5252
*ngIf="accountSelected && !accountSelected?.has_secret && accountSelected.username !== '@ANON'"
53-
class="form-field password"
53+
class="form-field credential"
5454
>
55-
<nz-form-label class="zone-label" nzFor='password' nzNoColon>{{ "Password" | translate }}</nz-form-label>
55+
<nz-form-label class="zone-label" nzNoColon>{{ "Credential" | translate }}</nz-form-label>
5656
<nz-form-control class="input-field">
57-
<nz-input-group [nzSuffix]="suffixTemplate">
58-
<input
59-
#password
60-
[(ngModel)]="manualAuthInfo.secret"
61-
[autocomplete]="hidePassword ? 'new-password' : 'off'"
62-
[autofocus]='manualAuthInfo.username? true : null'
63-
[type]="hidePassword ? 'password' : 'text'"
64-
name="password"
57+
<div *ngIf="protocol.name === 'ssh'" class="credential-toolbar">
58+
<nz-radio-group
59+
[(ngModel)]="secretType"
60+
(ngModelChange)="onSecretTypeChange($event)"
61+
class="secret-type-radio"
62+
name="secretType"
63+
>
64+
<label nz-radio nzValue="password">
65+
<i class="fa fa-lock"></i> {{ 'Password' | translate }}
66+
</label>
67+
<label nz-radio nzValue="ssh_key">
68+
<i class="fa fa-key"></i> {{ 'SSH key' | translate }}
69+
</label>
70+
</nz-radio-group>
71+
<label
72+
*ngIf="!rememberAuthDisabled"
73+
[(ngModel)]="manualAuthInfo.rememberAuth"
74+
class="remember-auth"
75+
nz-checkbox
76+
>
77+
{{ "Remember password" | translate }}
78+
</label>
79+
</div>
80+
81+
<ng-container *ngIf="secretType === 'password' || protocol.name !== 'ssh'">
82+
<div class="credential-row">
83+
<nz-input-group class="credential-input" [nzSuffix]="suffixTemplate">
84+
<input
85+
#password
86+
[(ngModel)]="passwordSecret"
87+
(ngModelChange)="onPasswordSecretChange($event)"
88+
[autocomplete]="hidePassword ? 'new-password' : 'off'"
89+
[autofocus]='manualAuthInfo.username ? true : null'
90+
[type]="hidePassword ? 'password' : 'text'"
91+
name="password"
92+
nz-input
93+
>
94+
<ng-template #suffixTemplate>
95+
<i (click)="hidePassword = !hidePassword" [nzType]="hidePassword ? 'eye-invisible' : 'eye'" nz-icon></i>
96+
</ng-template>
97+
</nz-input-group>
98+
<label
99+
*ngIf="!rememberAuthDisabled && protocol.name !== 'ssh'"
100+
[(ngModel)]="manualAuthInfo.rememberAuth"
101+
class="remember-auth"
102+
nz-checkbox
103+
>
104+
{{ "Remember password" | translate }}
105+
</label>
106+
</div>
107+
</ng-container>
108+
109+
<div *ngIf="secretType === 'ssh_key' && protocol.name === 'ssh'" class="ssh-key-wrap">
110+
<textarea
111+
class="ssh-key-input"
112+
[(ngModel)]="sshKeySecret"
113+
(ngModelChange)="onSshKeySecretChange($event)"
114+
[autofocus]='manualAuthInfo.username ? true : null'
115+
[placeholder]="'Paste private key here' | translate"
116+
name="sshKey"
65117
nz-input
118+
rows="3"
119+
></textarea>
120+
<button
121+
class="ssh-key-file-btn"
122+
nz-button
123+
nzSize="small"
124+
type="button"
125+
nz-tooltip
126+
[nzTooltipTitle]="'Load file' | translate"
127+
(click)="selectSshKeyFile()"
66128
>
67-
<ng-template #suffixTemplate>
68-
<i (click)="hidePassword = !hidePassword" [nzType]="hidePassword ? 'eye-invisible' : 'eye'" nz-icon></i>
69-
</ng-template>
70-
</nz-input-group>
71-
<label *ngIf="!rememberAuthDisabled" [(ngModel)]="manualAuthInfo.rememberAuth" class="remember-auth" nz-checkbox>
72-
{{ "Remember password" | translate }}
73-
</label>
129+
<span nz-icon nzType="upload" nzTheme="outline"></span>
130+
</button>
131+
<input
132+
#sshKeyFileInput
133+
type="file"
134+
accept=".key,.pem,.txt"
135+
(change)="onSshKeyFileSelected($event)"
136+
class="ssh-key-file-input"
137+
/>
138+
</div>
74139
</nz-form-control>
75-
76140
</nz-form-item>
77141

78142

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

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,81 @@
99
margin-bottom: 20px;
1010
}
1111

12-
.zone-label {
13-
}
14-
1512
.account-info {
1613
border: solid #cbc9c9 1px;
1714
padding: 10px 10px 0 10px;
1815
}
1916

20-
.password {
21-
::ng-deep .ant-input-affix-wrapper {
22-
width: 70%;
17+
.credential {
18+
.credential-input {
19+
flex: 1;
20+
min-width: 0;
21+
}
22+
23+
::ng-deep .credential-input.ant-input-affix-wrapper,
24+
::ng-deep .credential-row .ant-input-affix-wrapper {
25+
width: 100%;
2326
}
2427

2528
.remember-auth {
26-
width: 30%;
27-
// display: inline-block;
29+
flex-shrink: 0;
30+
margin: 0;
2831
font-size: 12px;
29-
vertical-align: bottom;
30-
// text-align: right;
31-
padding-left: 20px;
32+
white-space: nowrap;
33+
}
34+
}
35+
36+
.credential-toolbar {
37+
display: flex;
38+
align-items: center;
39+
justify-content: space-between;
40+
gap: 12px;
41+
margin-bottom: 8px;
42+
}
43+
44+
.credential-row {
45+
display: flex;
46+
align-items: center;
47+
gap: 12px;
48+
}
49+
50+
.secret-type-radio {
51+
margin-bottom: 0;
52+
53+
label[nz-radio] i {
54+
margin-right: 4px;
55+
}
56+
}
57+
58+
.ssh-key-wrap {
59+
position: relative;
60+
61+
.ssh-key-input {
62+
resize: vertical;
63+
width: 100%;
64+
padding-bottom: 36px;
3265
}
66+
67+
.ssh-key-file-btn {
68+
position: absolute;
69+
right: 6px;
70+
bottom: 6px;
71+
z-index: 1;
72+
margin: 0;
73+
width: 24px;
74+
min-width: 24px;
75+
height: 24px;
76+
padding: 0;
77+
line-height: 24px;
78+
79+
.anticon {
80+
font-size: 12px;
81+
}
82+
}
83+
}
84+
85+
.ssh-key-file-input {
86+
display: none;
3387
}
3488

3589
.input-field {

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
3434
@Output() manualUsernameChanged: EventEmitter<string> = new EventEmitter<string>();
3535
@ViewChild('username', { static: false }) usernameRef: ElementRef;
3636
@ViewChild('password', { static: false }) passwordRef: ElementRef;
37+
@ViewChild('sshKeyFileInput', { static: false }) sshKeyFileInputRef: ElementRef<HTMLInputElement>;
3738

3839
public hidePassword = true;
3940
public rememberAuthDisabled = null;
41+
public passwordSecret = '';
42+
public sshKeySecret = '';
4043
usernameControl = new FormControl();
4144
localAuthItems: AuthInfo[];
4245
filteredOptions: AuthInfo[];
@@ -111,6 +114,14 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
111114
return this.accountSelected.username === '@INPUT' || this.accountSelected.username === '@USER';
112115
}
113116

117+
get secretType(): 'password' | 'ssh_key' {
118+
return this.manualAuthInfo?.['input_secret_type'] || 'password';
119+
}
120+
121+
set secretType(value: 'password' | 'ssh_key') {
122+
this.manualAuthInfo['input_secret_type'] = value;
123+
}
124+
114125
public compareFn = (f1: Account, f2: Account) => {
115126
if (!f1 || !f2) return false;
116127
return f1.alias === f2.alias && f1.id === f2.id;
@@ -144,6 +155,7 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
144155
this.accountSelectedChange.emit(this.accountSelected);
145156
this.onAccountChanged();
146157
}
158+
this.syncSecretsFromManualAuth();
147159
}
148160

149161
ngOnDestroy() {
@@ -309,6 +321,10 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
309321
this.manualAuthInfo.username = this.accountSelected.username;
310322
}
311323
this.manualAuthInfo.secret = '';
324+
this.passwordSecret = '';
325+
this.sshKeySecret = '';
326+
// 切换账号时先回到默认类型,再由本地缓存 merge 覆盖(避免沿用上一账号的 ssh_key)
327+
this.manualAuthInfo['input_secret_type'] = 'password';
312328
this.localAuthItems = this._appSvc.getAccountLocalAuth(this.asset.id);
313329
if (this.manualAuthInfo.username) {
314330
this.localAuthItems = this.localAuthItems.filter(
@@ -318,6 +334,8 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
318334
if (this.localAuthItems && this.localAuthItems.length > 0) {
319335
this.manualAuthInfo = Object.assign(this.manualAuthInfo, this.localAuthItems[0]);
320336
}
337+
this.syncSecretsFromManualAuth();
338+
this.applyCurrentSecretToManualAuth();
321339
this.setUsernamePlaceholder();
322340
setTimeout(() => {
323341
if (this.manualAuthInfo.username && this.passwordRef) {
@@ -330,6 +348,67 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
330348
this._cdRef.detectChanges();
331349
}
332350

351+
onSecretTypeChange(_type: 'password' | 'ssh_key') {
352+
this.applyCurrentSecretToManualAuth();
353+
}
354+
355+
onPasswordSecretChange(secret: string) {
356+
this.passwordSecret = secret;
357+
if (this.secretType === 'password') {
358+
this.manualAuthInfo.secret = secret;
359+
}
360+
}
361+
362+
onSshKeySecretChange(secret: string) {
363+
this.sshKeySecret = secret;
364+
if (this.secretType === 'ssh_key') {
365+
this.manualAuthInfo.secret = secret;
366+
}
367+
}
368+
369+
selectSshKeyFile() {
370+
this.sshKeyFileInputRef?.nativeElement?.click();
371+
}
372+
373+
onSshKeyFileSelected(event: Event) {
374+
const target = event.target as HTMLInputElement;
375+
const file = target?.files?.[0];
376+
if (!file) {
377+
return;
378+
}
379+
const reader = new FileReader();
380+
reader.onload = () => {
381+
const content = typeof reader.result === 'string' ? reader.result : '';
382+
this.sshKeySecret = content;
383+
if (this.secretType === 'ssh_key') {
384+
this.manualAuthInfo.secret = content;
385+
}
386+
this._cdRef.detectChanges();
387+
};
388+
reader.onerror = () => {
389+
console.error('Load SSH key file failed');
390+
};
391+
reader.readAsText(file);
392+
target.value = '';
393+
}
394+
395+
private syncSecretsFromManualAuth() {
396+
const secret = this.manualAuthInfo?.secret || '';
397+
const secretType = this.manualAuthInfo?.['input_secret_type'] || 'password';
398+
if (secretType === 'ssh_key') {
399+
this.sshKeySecret = secret;
400+
this.passwordSecret = '';
401+
} else {
402+
this.passwordSecret = secret;
403+
this.sshKeySecret = '';
404+
}
405+
}
406+
407+
private applyCurrentSecretToManualAuth() {
408+
this.manualAuthInfo.secret =
409+
this.secretType === 'ssh_key' ? this.sshKeySecret : this.passwordSecret;
410+
}
411+
333412
onFocus() {
334413
if (!this.accountManualAuthInit) {
335414
this.usernameControl.setValue('');
@@ -342,6 +421,8 @@ export class ElementSelectAccountComponent implements OnInit, OnDestroy {
342421
this.filteredOptions = this.localAuthItems.filter(authInfo => {
343422
if (authInfo.username.toLowerCase() === filterValue) {
344423
this.manualAuthInfo = Object.assign(this.manualAuthInfo, authInfo);
424+
this.syncSecretsFromManualAuth();
425+
this.applyCurrentSecretToManualAuth();
345426
}
346427
return authInfo.username.toLowerCase().includes(filterValue);
347428
});

src/app/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ export class AuthInfo {
402402
username: string;
403403
secret: string;
404404
rememberAuth: boolean;
405+
input_secret_type?: 'password' | 'ssh_key';
405406
}
406407

407408
export class ConnectOption {

src/app/services/http.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,13 +369,18 @@ export class HttpService {
369369
const isVirtual = account.username.startsWith('@');
370370
const username = isVirtual ? manualAuthInfo.username : account.username;
371371
const secret = encryptPassword(manualAuthInfo.secret);
372-
const connectOption = connectData.connectOption;
372+
const connectOption = { ...(connectData.connectOption || {}) };
373+
// 始终以当前表单为准,避免 connectOption 里残留上一次的 input_secret_type
374+
const inputSecretType =
375+
(manualAuthInfo && manualAuthInfo['input_secret_type']) || 'password';
376+
373377
const data = {
374378
asset: asset.id,
375379
account: account.alias, // 主要是有特殊账号,匿名、虚拟
376380
protocol: protocol.name,
377381
input_username: username,
378382
input_secret: secret,
383+
input_secret_type: inputSecretType,
379384
connect_method: connectMethod.value,
380385
connect_options: connectOption
381386
};

0 commit comments

Comments
 (0)