Skip to content

Commit 0ac237d

Browse files
authored
Merge pull request #1719 from rocket-admin/backend_pg_proxy
Backend pg proxy
2 parents 218fdbc + e464e21 commit 0ac237d

17 files changed

Lines changed: 1035 additions & 35 deletions

backend/src/common/data-injection.tokens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export enum UseCaseType {
119119
SAAS_DELETE_CONNECTION_FOR_HOSTED_DB = 'SAAS_DELETE_CONNECTION_FOR_HOSTED_DB',
120120
SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD = 'SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD',
121121
SAAS_GET_CONNECTIONS_INFO_BY_IDS = 'SAAS_GET_CONNECTIONS_INFO_BY_IDS',
122+
SAAS_GET_HOSTED_CONNECTION_CREDENTIALS = 'SAAS_GET_HOSTED_CONNECTION_CREDENTIALS',
122123

123124
INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',
124125
VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString } from 'class-validator';
3+
4+
export class GetHostedConnectionCredentialsDto {
5+
@ApiProperty()
6+
@IsString()
7+
@IsNotEmpty()
8+
hostedDatabaseId: string;
9+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class HostedConnectionCredentialsRO {
4+
@ApiProperty()
5+
connectionId: string;
6+
7+
@ApiProperty()
8+
host: string;
9+
10+
@ApiProperty()
11+
port: number;
12+
13+
@ApiProperty()
14+
database: string;
15+
16+
@ApiProperty()
17+
username: string;
18+
19+
@ApiProperty()
20+
password: string;
21+
22+
@ApiProperty()
23+
is_frozen: boolean;
24+
}

backend/src/microservices/saas-microservice/saas.controller.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { CreateConnectionForHostedDbDto } from './data-structures/create-connect
2929
import { DeleteConnectionForHostedDbDto } from './data-structures/delete-connection-for-hosted-db.dto.js';
3030
import { FoundConnectionInfoRO } from './data-structures/found-connection-info.ro.js';
3131
import { GetConnectionsInfoByIdsDS } from './data-structures/get-connections-info-by-ids.ds.js';
32+
import { GetHostedConnectionCredentialsDto } from './data-structures/get-hosted-connection-credentials.dto.js';
33+
import { HostedConnectionCredentialsRO } from './data-structures/hosted-connection-credentials.ro.js';
3234
import { RegisterCompanyWebhookDS } from './data-structures/register-company.ds.js';
3335
import { RegisteredCompanyDS } from './data-structures/registered-company.ds.js';
3436
import { SaasRegisterUserWithGithub } from './data-structures/saas-register-user-with-github.js';
@@ -41,6 +43,7 @@ import {
4143
IDeleteConnectionForHostedDb,
4244
IFreezeConnectionsInCompany,
4345
IGetConnectionsInfoByIds,
46+
IGetHostedConnectionCredentials,
4447
IGetUserInfo,
4548
ILoginUserWithGitHub,
4649
ILoginUserWithGoogle,
@@ -100,6 +103,8 @@ export class SaasController {
100103
private readonly updateHostedConnectionPasswordUseCase: IUpdateHostedConnectionPassword,
101104
@Inject(UseCaseType.SAAS_GET_CONNECTIONS_INFO_BY_IDS)
102105
private readonly getConnectionsInfoByIdsUseCase: IGetConnectionsInfoByIds,
106+
@Inject(UseCaseType.SAAS_GET_HOSTED_CONNECTION_CREDENTIALS)
107+
private readonly getHostedConnectionCredentialsUseCase: IGetHostedConnectionCredentials,
103108
) {}
104109

105110
@ApiOperation({ summary: 'Company registered webhook' })
@@ -344,4 +349,17 @@ export class SaasController {
344349
): Promise<Array<FoundConnectionInfoRO>> {
345350
return await this.getConnectionsInfoByIdsUseCase.execute(connectionsData);
346351
}
352+
353+
@ApiOperation({ summary: 'Get decrypted credentials for a hosted connection' })
354+
@ApiBody({ type: GetHostedConnectionCredentialsDto })
355+
@ApiResponse({
356+
status: 200,
357+
type: HostedConnectionCredentialsRO,
358+
})
359+
@Post('/connection/hosted/credentials')
360+
async getHostedConnectionCredentials(
361+
@Body() data: GetHostedConnectionCredentialsDto,
362+
): Promise<HostedConnectionCredentialsRO> {
363+
return await this.getHostedConnectionCredentialsUseCase.execute(data);
364+
}
347365
}

backend/src/microservices/saas-microservice/saas.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { DeleteConnectionForHostedDbUseCase } from './use-cases/delete-connectio
1212
import { FreezeConnectionsInCompanyUseCase } from './use-cases/freeze-connections-in-company.use.case.js';
1313
import { GetConnectionsInfoByIdsUseCase } from './use-cases/get-connections-info-by-ids.use.case.js';
1414
import { GetFullCompanyInfoByUserIdUseCase } from './use-cases/get-full-company-info-by-user-id.use.case.js';
15+
import { GetHostedConnectionCredentialsUseCase } from './use-cases/get-hosted-connection-credentials.use.case.js';
1516
import { GetUserInfoUseCase } from './use-cases/get-user-info.use.case.js';
1617
import { GetUsersCountInCompanyByIdUseCase } from './use-cases/get-users-count-in-company.use.case.js';
1718
import { GetUsersInfosByEmailUseCase } from './use-cases/get-users-infos-by-email.use.case.js';
@@ -105,6 +106,10 @@ import { UpdateHostedConnectionPasswordUseCase } from './use-cases/update-hosted
105106
provide: UseCaseType.SAAS_GET_CONNECTIONS_INFO_BY_IDS,
106107
useClass: GetConnectionsInfoByIdsUseCase,
107108
},
109+
{
110+
provide: UseCaseType.SAAS_GET_HOSTED_CONNECTION_CREDENTIALS,
111+
useClass: GetHostedConnectionCredentialsUseCase,
112+
},
108113
SignInAuditService,
109114
],
110115
controllers: [SaasController],
@@ -131,6 +136,7 @@ export class SaasModule {
131136
{ path: 'saas/connection/hosted', method: RequestMethod.POST },
132137
{ path: 'saas/connection/hosted/delete', method: RequestMethod.POST },
133138
{ path: 'saas/connection/hosted/password', method: RequestMethod.POST },
139+
{ path: 'saas/connection/hosted/credentials', method: RequestMethod.POST },
134140
{ path: 'saas/connections/info', method: RequestMethod.POST },
135141
);
136142
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
2+
import AbstractUseCase from '../../../common/abstract-use.case.js';
3+
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
4+
import { BaseType } from '../../../common/data-injection.tokens.js';
5+
import { Messages } from '../../../exceptions/text/messages.js';
6+
import { GetHostedConnectionCredentialsDto } from '../data-structures/get-hosted-connection-credentials.dto.js';
7+
import { HostedConnectionCredentialsRO } from '../data-structures/hosted-connection-credentials.ro.js';
8+
import { IGetHostedConnectionCredentials } from './saas-use-cases.interface.js';
9+
10+
@Injectable({ scope: Scope.REQUEST })
11+
export class GetHostedConnectionCredentialsUseCase
12+
extends AbstractUseCase<GetHostedConnectionCredentialsDto, HostedConnectionCredentialsRO>
13+
implements IGetHostedConnectionCredentials
14+
{
15+
constructor(
16+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
17+
protected _dbContext: IGlobalDatabaseContext,
18+
) {
19+
super();
20+
}
21+
22+
protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> {
23+
const connection = await this._dbContext.connectionRepository.findOne({
24+
where: { id: inputData.hostedDatabaseId },
25+
});
26+
if (!connection) {
27+
throw new NotFoundException(Messages.CONNECTION_NOT_FOUND);
28+
}
29+
30+
return {
31+
connectionId: connection.id,
32+
host: connection.host,
33+
port: connection.port,
34+
database: connection.database,
35+
username: connection.username,
36+
password: connection.password,
37+
is_frozen: connection.is_frozen,
38+
};
39+
}
40+
}

backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { DeleteConnectionForHostedDbDto } from '../data-structures/delete-connec
1111
import { FoundConnectionInfoRO } from '../data-structures/found-connection-info.ro.js';
1212
import { FreezeConnectionsInCompanyDS } from '../data-structures/freeze-connections-in-company.ds.js';
1313
import { GetConnectionsInfoByIdsDS } from '../data-structures/get-connections-info-by-ids.ds.js';
14+
import { GetHostedConnectionCredentialsDto } from '../data-structures/get-hosted-connection-credentials.dto.js';
15+
import { HostedConnectionCredentialsRO } from '../data-structures/hosted-connection-credentials.ro.js';
1416
import { GetUserInfoByIdDS } from '../data-structures/get-user-info.ds.js';
1517
import { GetUsersInfosByEmailDS } from '../data-structures/get-users-infos-by-email.ds.js';
1618
import { RegisterCompanyWebhookDS } from '../data-structures/register-company.ds.js';
@@ -88,3 +90,7 @@ export interface IUpdateHostedConnectionPassword {
8890
export interface IGetConnectionsInfoByIds {
8991
execute(inputData: GetConnectionsInfoByIdsDS): Promise<Array<FoundConnectionInfoRO>>;
9092
}
93+
94+
export interface IGetHostedConnectionCredentials {
95+
execute(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO>;
96+
}

backend/test/ava-tests/saas-tests/hosted-connection-e2e.test.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi
7979
.send({
8080
companyId: companyId,
8181
userId: userId,
82+
hostedDatabaseId: faker.string.uuid(),
8283
databaseName: 'postgres',
8384
hostname: 'testPg-e2e-testing',
8485
port: 5432,
@@ -89,24 +90,14 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi
8990
.set('Content-Type', 'application/json')
9091
.set('Accept', 'application/json');
9192

92-
t.is(createHostedConnectionResult.status, 201);
93-
9493
const createdConnection = JSON.parse(createHostedConnectionResult.text);
95-
const connectionId = createdConnection.id;
94+
console.log('🚀 ~ createdConnection:', createdConnection);
95+
96+
t.is(createHostedConnectionResult.status, 201);
97+
const connectionId = createdConnection.connectionId;
9698

9799
// Verify connection was created
98100
t.truthy(connectionId);
99-
t.is(createdConnection.type, 'postgres');
100-
t.is(createdConnection.database, 'postgres');
101-
t.is(createdConnection.host, 'testPg-e2e-testing');
102-
t.is(createdConnection.port, 5432);
103-
104-
// Verify admin group was created
105-
t.truthy(createdConnection.groups);
106-
t.is(createdConnection.groups.length, 1);
107-
const adminGroup = createdConnection.groups[0];
108-
t.truthy(adminGroup.id);
109-
t.is(adminGroup.isMain, true);
110101

111102
// Verify connection is accessible via connection groups endpoint
112103
const groupsResponse = await request(app.getHttpServer())
@@ -119,6 +110,7 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi
119110
const groups = JSON.parse(groupsResponse.text);
120111
t.is(groups.length, 1);
121112
t.is(groups[0].accessLevel, AccessLevelEnum.edit);
113+
const adminGroup = groups[0];
122114

123115
// Verify tables endpoint works with this connection
124116
const findTablesResponse = await request(app.getHttpServer())
@@ -132,8 +124,9 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi
132124
t.true(Array.isArray(tables));
133125

134126
// Verify user permissions - user should have full access
127+
const groupId = adminGroup.group.id;
135128
const permissionsResponse = await request(app.getHttpServer())
136-
.get(`/connection/permissions?connectionId=${connectionId}&groupId=${adminGroup.id}`)
129+
.get(`/connection/permissions?connectionId=${connectionId}&groupId=${groupId}`)
137130
.set('Content-Type', 'application/json')
138131
.set('Cookie', token)
139132
.set('Accept', 'application/json');
@@ -177,6 +170,7 @@ test.serial(`${currentTest} should return error when userId does not exist`, asy
177170
.send({
178171
companyId: faker.string.uuid(),
179172
userId: faker.string.uuid(),
173+
hostedDatabaseId: faker.string.uuid(),
180174
databaseName: 'postgres',
181175
hostname: 'testPg-e2e-testing',
182176
port: 5432,
@@ -187,6 +181,8 @@ test.serial(`${currentTest} should return error when userId does not exist`, asy
187181
.set('Content-Type', 'application/json')
188182
.set('Accept', 'application/json');
189183

184+
const responseBody = JSON.parse(result.text);
185+
console.log('🚀 ~ responseBody:', responseBody);
190186
t.is(result.status, 500);
191187
} catch (e) {
192188
console.error('Test error:', e);
@@ -219,6 +215,7 @@ test.serial(`${currentTest} should delete a hosted connection`, async (t) => {
219215
.send({
220216
companyId: companyId,
221217
userId: userId,
218+
hostedDatabaseId: faker.string.uuid(),
222219
databaseName: 'postgres',
223220
hostname: 'testPg-e2e-testing',
224221
port: 5432,
@@ -229,9 +226,10 @@ test.serial(`${currentTest} should delete a hosted connection`, async (t) => {
229226
.set('Content-Type', 'application/json')
230227
.set('Accept', 'application/json');
231228

232-
t.is(createResult.status, 201);
233229
const createdConnection = JSON.parse(createResult.text);
234-
const connectionId = createdConnection.id;
230+
console.log('🚀 ~ createdConnection:', createdConnection);
231+
t.is(createResult.status, 201);
232+
const connectionId = createdConnection.connectionId;
235233

236234
// Verify connection exists
237235
const connectionsBeforeDelete = await request(app.getHttpServer())

0 commit comments

Comments
 (0)