Skip to content

Commit 879c904

Browse files
Bishbash777HeliasSteffen SchmidtXenonR
authored
feat: add ssh tunnel support (#3740)
Co-authored-by: Stefano Borzì <stefanoborzi32@gmail.com> Co-authored-by: Steffen Schmidt <Steffen.Schmidt@bit-in.de> Co-authored-by: Steffen Schmidt <service.github@nocer.net>
1 parent fd39c38 commit 879c904

16 files changed

Lines changed: 410 additions & 14 deletions

apps/keira/src/assets/i18n/en.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@
4949
"DATABASE": "Database",
5050
"USE_SSL": "Use SSL/TLS",
5151
"SSL_TOOLTIP": "Required for servers with require_secure_transport=ON",
52+
"USE_SSH_TUNNEL": "SSH Tunnel",
53+
"SSH_TOOLTIP": "Connect to MySQL through an SSH tunnel",
54+
"SSH_HOST": "SSH Host",
55+
"SSH_PORT": "SSH Port",
56+
"SSH_USER": "SSH User",
57+
"SSH_PASSWORD": "SSH Password / Passphrase",
58+
"SSH_PRIVATE_KEY": "Private Key",
59+
"SSH_KEY_PLACEHOLDER": "Optional - browse to select key file",
5260
"SAVE_PASSWORD": "Save password",
5361
"REMEMBER_ME": "Remember me",
5462
"LOAD_RECENT": "Load recent",
@@ -63,6 +71,8 @@
6371
"UNSAVED_CHANGES": "Unsaved changes",
6472
"DISCONNECT": "Disconnect",
6573
"MODAL_DISCONNECT": "Are you sure you want to disconnect?",
74+
"SSH_TUNNEL_ACTIVE": "SSH Tunnel",
75+
"SSH_TUNNEL_VIA": "Connected via SSH tunnel through {{ host }}",
6676
"CREATURE": {
6777
"TITLE": "Creature",
6878
"SELECT_CREATURE": "Select Creature",

apps/keira/src/assets/i18n/es.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@
4949
"DATABASE": "Base de Datos",
5050
"USE_SSL": "Usar SSL/TLS",
5151
"SSL_TOOLTIP": "Requerido para servidores con require_secure_transport=ON",
52+
"USE_SSH_TUNNEL": "Túnel SSH",
53+
"SSH_TOOLTIP": "Conectar a MySQL a través de un túnel SSH",
54+
"SSH_HOST": "Host SSH",
55+
"SSH_PORT": "Puerto SSH",
56+
"SSH_USER": "Usuario SSH",
57+
"SSH_PASSWORD": "Contraseña SSH / Frase secreta",
58+
"SSH_PRIVATE_KEY": "Clave Privada",
59+
"SSH_KEY_PLACEHOLDER": "Opcional - seleccionar archivo de clave",
5260
"SAVE_PASSWORD": "Guardar Contraseña",
5361
"REMEMBER_ME": "Recordarme",
5462
"LOAD_RECENT": "Cargar Reciente",
@@ -63,6 +71,8 @@
6371
"UNSAVED_CHANGES": "Cambios sin guardar",
6472
"DISCONNECT": "Desconectar",
6573
"MODAL_DISCONNECT": "¿Estás seguro de que deseas desconectarte?",
74+
"SSH_TUNNEL_ACTIVE": "Túnel SSH",
75+
"SSH_TUNNEL_VIA": "Conectado vía túnel SSH a través de {{ host }}",
6676
"CREATURE": {
6777
"TITLE": "Criatura",
6878
"SELECT_CREATURE": "Seleccionar Criatura",

apps/keira/src/assets/i18n/ru.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@
4949
"DATABASE": "База данных",
5050
"USE_SSL": "Использовать SSL/TLS",
5151
"SSL_TOOLTIP": "Требуется для серверов с require_secure_transport=ON",
52+
"USE_SSH_TUNNEL": "SSH туннель",
53+
"SSH_TOOLTIP": "Подключение к MySQL через SSH туннель",
54+
"SSH_HOST": "SSH хост",
55+
"SSH_PORT": "SSH порт",
56+
"SSH_USER": "SSH пользователь",
57+
"SSH_PASSWORD": "SSH пароль / парольная фраза",
58+
"SSH_PRIVATE_KEY": "Приватный ключ",
59+
"SSH_KEY_PLACEHOLDER": "Необязательно - выберите файл ключа",
5260
"SAVE_PASSWORD": "Сохранить пароль",
5361
"REMEMBER_ME": "Запомнить меня",
5462
"LOAD_RECENT": "Последнее",
@@ -63,6 +71,8 @@
6371
"UNSAVED_CHANGES": "Несохраненные изменения",
6472
"DISCONNECT": "Отсоединить",
6573
"MODAL_DISCONNECT": "Вы уверены, что хотите отсоединиться?",
74+
"SSH_TUNNEL_ACTIVE": "SSH Туннель",
75+
"SSH_TUNNEL_VIA": "Подключено через SSH туннель ({{ host }})",
6676
"CREATURE": {
6777
"TITLE": "Существа",
6878
"SELECT_CREATURE": "Выбрать существо",

apps/keira/src/assets/i18n/zh.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@
4949
"DATABASE": "数据库",
5050
"USE_SSL": "使用 SSL/TLS",
5151
"SSL_TOOLTIP": "启用了 require_secure_transport=ON 的服务器需要此选项",
52+
"USE_SSH_TUNNEL": "SSH 隧道",
53+
"SSH_TOOLTIP": "通过 SSH 隧道连接到 MySQL",
54+
"SSH_HOST": "SSH 主机",
55+
"SSH_PORT": "SSH 端口",
56+
"SSH_USER": "SSH 用户",
57+
"SSH_PASSWORD": "SSH 密码 / 密钥口令",
58+
"SSH_PRIVATE_KEY": "私钥",
59+
"SSH_KEY_PLACEHOLDER": "可选 - 浏览选择密钥文件",
5260
"SAVE_PASSWORD": "保存密码",
5361
"REMEMBER_ME": "记住我",
5462
"LOAD_RECENT": "加载最近的",
@@ -63,6 +71,8 @@
6371
"UNSAVED_CHANGES": "未保存的更改",
6472
"DISCONNECT": "断开",
6573
"MODAL_DISCONNECT": "是否确实要断开连接?",
74+
"SSH_TUNNEL_ACTIVE": "SSH 隧道",
75+
"SSH_TUNNEL_VIA": "通过 SSH 隧道连接 ({{ host }})",
6676
"CREATURE": {
6777
"TITLE": "生物",
6878
"SELECT_CREATURE": "选择生物",

electron-builder.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55
"output": "release/"
66
},
77
"files": ["dist/browser/**/*", "main.js", "main.js.map"],
8+
"electronFuses": {
9+
"enableEmbeddedAsarIntegrityValidation": false,
10+
"onlyLoadAppFromAsar": false
11+
},
812
"win": {
913
"icon": "dist/browser",
10-
"target": ["portable"]
14+
"target": ["portable"],
15+
"signAndEditExecutable": false,
16+
"verifyUpdateCodeSignature": false
1117
},
1218
"mac": {
1319
"icon": "dist/browser",

libs/main/connection-window/src/connection-window.component.html

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,63 @@
4646
</label>
4747
</div>
4848
</div>
49+
<div class="form-group">
50+
<div class="form-check">
51+
<input class="form-check-input" type="checkbox" id="ssh-enabled" [formControlName]="'sshEnabled'" />
52+
<label class="form-check-label text-white" for="ssh-enabled">
53+
<i class="fas fa-terminal"></i> {{ 'CONNECTION_WINDOW.USE_SSH_TUNNEL' | translate }}
54+
<i class="fas fa-info-circle ms-1" placement="auto" [tooltip]="'CONNECTION_WINDOW.SSH_TOOLTIP' | translate"></i>
55+
</label>
56+
</div>
57+
</div>
58+
@if (form.get('sshEnabled')?.value) {
59+
<div class="ssh-fields">
60+
<div class="form-group">
61+
<div class="input-group-prepend">
62+
<span class="input-group-text"><i class="fas fa-server"></i>{{ 'CONNECTION_WINDOW.SSH_HOST' | translate }}</span>
63+
</div>
64+
<input [formControlName]="'sshHost'" class="form-control" id="ssh-host" />
65+
</div>
66+
<div class="form-group">
67+
<div class="input-group-prepend">
68+
<span class="input-group-text"><i class="fas fa-network-wired"></i>{{ 'CONNECTION_WINDOW.SSH_PORT' | translate }}</span>
69+
</div>
70+
<input [formControlName]="'sshPort'" type="number" class="form-control" id="ssh-port" />
71+
</div>
72+
<div class="form-group">
73+
<div class="input-group-prepend">
74+
<span class="input-group-text"><i class="fas fa-user"></i>{{ 'CONNECTION_WINDOW.SSH_USER' | translate }}</span>
75+
</div>
76+
<input [formControlName]="'sshUser'" class="form-control" id="ssh-user" />
77+
</div>
78+
<div class="form-group">
79+
<div class="input-group-prepend">
80+
<span class="input-group-text text-wrap"
81+
><i class="fas fa-key"></i>{{ 'CONNECTION_WINDOW.SSH_PASSWORD' | translate }}</span
82+
>
83+
</div>
84+
<input [formControlName]="'sshPassword'" type="password" class="form-control" id="ssh-password" />
85+
</div>
86+
<div class="form-group">
87+
<div class="input-group-prepend">
88+
<span class="input-group-text"><i class="fas fa-file-alt"></i>{{ 'CONNECTION_WINDOW.SSH_PRIVATE_KEY' | translate }}</span>
89+
</div>
90+
<div class="d-flex">
91+
<input
92+
[formControlName]="'sshPrivateKey'"
93+
class="form-control"
94+
id="ssh-private-key"
95+
readonly
96+
[placeholder]="'CONNECTION_WINDOW.SSH_KEY_PLACEHOLDER' | translate"
97+
/>
98+
<label class="btn btn-sm btn-secondary ms-1 mb-0" for="ssh-key-file">
99+
<i class="fas fa-folder-open"></i>
100+
</label>
101+
<input type="file" id="ssh-key-file" class="d-none" (change)="onPrivateKeyFileSelected($event)" />
102+
</div>
103+
</div>
104+
</div>
105+
}
49106
<div class="recent-container">
50107
<div class="row">
51108
<div class="col">

libs/main/connection-window/src/connection-window.component.scss

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ $secondary-color-green: #b7e2ad;
9797

9898
.content {
9999
position: relative;
100-
width: 500px;
100+
width: 600px;
101101
min-height: 55px;
102102
background-color: rgba(0, 0, 0, 0.5);
103103
border-radius: 30px;
@@ -157,7 +157,7 @@ $secondary-color-green: #b7e2ad;
157157
border: 0 !important;
158158

159159
i {
160-
margin-right: 10px;
160+
margin-right: 5px;
161161
margin-bottom: 2px;
162162
}
163163
}
@@ -169,6 +169,20 @@ $secondary-color-green: #b7e2ad;
169169
}
170170
}
171171

172+
.ssh-fields {
173+
border-left: 2px solid rgba(255, 255, 255, 0.3);
174+
padding-left: 10px;
175+
margin-bottom: 4px;
176+
177+
.input-group-prepend {
178+
width: 180px;
179+
180+
.input-group-text {
181+
width: 180px;
182+
}
183+
}
184+
}
185+
172186
.connect-button {
173187
font-weight: bold;
174188
width: 70%;

libs/main/connection-window/src/connection-window.component.spec.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ describe('ConnectionWindowComponent', () => {
143143
password: 'root',
144144
database: 'acore_world',
145145
sslEnabled: true,
146+
sshEnabled: false,
147+
sshHost: '',
148+
sshPort: 22,
149+
sshUser: '',
150+
sshPassword: '',
151+
sshPrivateKey: '',
146152
ssl: { rejectUnauthorized: false },
147153
});
148154
expect(component.error).toBeUndefined();
@@ -165,6 +171,12 @@ describe('ConnectionWindowComponent', () => {
165171
password: 'helias123',
166172
database: 'helias_world',
167173
sslEnabled: false,
174+
sshEnabled: false,
175+
sshHost: '',
176+
sshPort: 22,
177+
sshUser: '',
178+
sshPassword: '',
179+
sshPrivateKey: '',
168180
});
169181

170182
expect(component.error).toBeUndefined();
@@ -188,7 +200,20 @@ describe('ConnectionWindowComponent', () => {
188200
page.clickElement(page.connectBtn);
189201

190202
expect(connectSpy).toHaveBeenCalledTimes(1);
191-
expect(connectSpy).toHaveBeenCalledWith({ host, port, user, password, database, sslEnabled: false });
203+
expect(connectSpy).toHaveBeenCalledWith({
204+
host,
205+
port,
206+
user,
207+
password,
208+
database,
209+
sslEnabled: false,
210+
sshEnabled: false,
211+
sshHost: '',
212+
sshPort: 22,
213+
sshUser: '',
214+
sshPassword: '',
215+
sshPrivateKey: '',
216+
});
192217
expect(component.error).toBeUndefined();
193218
expect(page.errorElement.innerHTML).not.toContain('error-box');
194219
});
@@ -266,6 +291,12 @@ describe('ConnectionWindowComponent', () => {
266291
password,
267292
database: 'helias_world',
268293
sslEnabled: false,
294+
sshEnabled: false,
295+
sshHost: '',
296+
sshPort: 22,
297+
sshUser: '',
298+
sshPassword: '',
299+
sshPrivateKey: '',
269300
});
270301
expect(connectSpy).toHaveBeenCalledTimes(1);
271302
expect(connectSpy).toHaveBeenCalledWith({
@@ -275,6 +306,12 @@ describe('ConnectionWindowComponent', () => {
275306
password,
276307
database: 'helias_world',
277308
sslEnabled: false,
309+
sshEnabled: false,
310+
sshHost: '',
311+
sshPort: 22,
312+
sshUser: '',
313+
sshPassword: '',
314+
sshPrivateKey: '',
278315
});
279316
});
280317

@@ -295,6 +332,12 @@ describe('ConnectionWindowComponent', () => {
295332
password: '',
296333
database: 'helias_world',
297334
sslEnabled: false,
335+
sshEnabled: false,
336+
sshHost: '',
337+
sshPort: 22,
338+
sshUser: '',
339+
sshPassword: '',
340+
sshPrivateKey: '',
298341
});
299342
expect(connectSpy).toHaveBeenCalledTimes(1);
300343
expect(connectSpy).toHaveBeenCalledWith({
@@ -304,6 +347,12 @@ describe('ConnectionWindowComponent', () => {
304347
password: 'helias123',
305348
database: 'helias_world',
306349
sslEnabled: false,
350+
sshEnabled: false,
351+
sshHost: '',
352+
sshPort: 22,
353+
sshUser: '',
354+
sshPassword: '',
355+
sshPrivateKey: '',
307356
});
308357
});
309358
});

libs/main/connection-window/src/connection-window.component.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
1010
import { TooltipModule } from 'ngx-bootstrap/tooltip';
1111

1212
import { QueryErrorComponent } from '@keira/shared/base-editor-components';
13+
import { ElectronService } from '@keira/shared/common-services';
1314
import { KeiraConnectionOptions, MysqlService } from '@keira/shared/db-layer';
1415
import { LoginConfigService } from '@keira/shared/login-config';
1516
import { SwitchLanguageComponent } from '@keira/shared/switch-language';
@@ -34,6 +35,7 @@ export class ConnectionWindowComponent extends SubscriptionHandler implements On
3435
private readonly mysqlService = inject(MysqlService);
3536
private readonly loginConfigService = inject(LoginConfigService);
3637
private readonly changeDetectorRef = inject(ChangeDetectorRef);
38+
private readonly electronService = inject(ElectronService);
3739

3840
private readonly IMAGES_COUNT = 11;
3941
readonly RANDOM_IMAGE = Math.floor(Math.random() * this.IMAGES_COUNT) + 1;
@@ -56,14 +58,20 @@ export class ConnectionWindowComponent extends SubscriptionHandler implements On
5658
password: new FormControl<string>('root') as FormControl<string>,
5759
database: new FormControl<string>('acore_world') as FormControl<string>,
5860
sslEnabled: new FormControl<boolean>(false) as FormControl<boolean>,
61+
sshEnabled: new FormControl<boolean>(false) as FormControl<boolean>,
62+
sshHost: new FormControl<string>('') as FormControl<string>,
63+
sshPort: new FormControl<number>(22) as FormControl<number>,
64+
sshUser: new FormControl<string>('') as FormControl<string>,
65+
sshPassword: new FormControl<string>('') as FormControl<string>,
66+
sshPrivateKey: new FormControl<string>('') as FormControl<string>,
5967
});
6068

6169
this.configs = this.loginConfigService.getConfigs();
6270

6371
if (this.configs?.length > 0) {
6472
// get last saved config
6573
const lastConfig = this.configs[this.configs.length - 1];
66-
this.form.setValue(lastConfig);
74+
this.form.patchValue(lastConfig);
6775

6876
if (!this.form.getRawValue().password) {
6977
this.savePassword = false;
@@ -76,7 +84,7 @@ export class ConnectionWindowComponent extends SubscriptionHandler implements On
7684
}
7785

7886
loadConfig(config: Partial<KeiraConnectionOptions>): void {
79-
this.form.setValue(config);
87+
this.form.patchValue(config);
8088
}
8189

8290
removeAllConfigs(): void {
@@ -85,6 +93,15 @@ export class ConnectionWindowComponent extends SubscriptionHandler implements On
8593
this.form.reset();
8694
}
8795

96+
onPrivateKeyFileSelected(event: Event): void {
97+
const input = event.target as HTMLInputElement;
98+
const file = input.files?.[0];
99+
if (file) {
100+
const keyContent = this.electronService.fs.readFileSync((file as any).path, 'utf8');
101+
this.form.get('sshPrivateKey')?.setValue(keyContent);
102+
}
103+
}
104+
88105
onConnect(): void {
89106
const connectionConfig = this.form.getRawValue();
90107

libs/main/main-window/src/sidebar/sidebar.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
<i class="fa fa-circle connection-status" [class.connected]="mysqlService.getConnectionState() === 'CONNECTED'"></i>
2929
<span>{{ mysqlService.getConnectionState() | translate | titlecase }}</span>
3030
</span>
31+
@if (mysqlService.sshTunnelActive) {
32+
<span class="ssh-tunnel-badge" [title]="'SIDEBAR.SSH_TUNNEL_VIA' | translate: { host: mysqlService.config.sshHost }">
33+
<i class="fas fa-shield-alt"></i> {{ 'SIDEBAR.SSH_TUNNEL_ACTIVE' | translate }}
34+
</span>
35+
}
3136
<span>
3237
<keira-switch-language />
3338
</span>

0 commit comments

Comments
 (0)