Skip to content

Commit 04731c8

Browse files
guguclaude
andauthored
feat: add hosted database password update webhook endpoint (#1690)
* feat: add hosted database password update webhook endpoint Add POST /saas/connection/hosted/password endpoint that rocketadmin-saas calls when a hosted database password is reset, to update the stored connection credentials in the backend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: fix encrypted field lookup in password update and return connectionId from create endpoint The database column is stored encrypted and only decrypted in @afterload, so querying by plaintext value would never match. Now fetches connections by company and filters by decrypted database name in code. Also simplifies create hosted connection response to return just the connectionId. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 333512e commit 04731c8

8 files changed

Lines changed: 133 additions & 35 deletions

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export enum UseCaseType {
116116
SAAS_REGISTER_USER_WITH_SAML = 'SAAS_REGISTER_USER_WITH_SAML',
117117
SAAS_CREATE_CONNECTION_FOR_HOSTED_DB = 'SAAS_CREATE_CONNECTION_FOR_HOSTED_DB',
118118
SAAS_DELETE_CONNECTION_FOR_HOSTED_DB = 'SAAS_DELETE_CONNECTION_FOR_HOSTED_DB',
119+
SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD = 'SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD',
119120

120121
INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',
121122
VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',

backend/src/microservices/saas-microservice/data-structures/common-responce.ds.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ export class SuccessResponse {
44
@ApiProperty()
55
success: boolean;
66
}
7+
8+
export class CreatedConnectionResponse {
9+
@ApiProperty()
10+
connectionId: string;
11+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
3+
4+
export class UpdateHostedConnectionPasswordDto {
5+
@ApiProperty({
6+
description: 'Company ID',
7+
example: '123e4567-e89b-12d3-a456-426614174000',
8+
})
9+
@IsNotEmpty()
10+
@IsString()
11+
@IsUUID()
12+
companyId: string;
13+
14+
@ApiProperty({
15+
description: 'Database name',
16+
example: 'my_database',
17+
})
18+
@IsNotEmpty()
19+
@IsString()
20+
databaseName: string;
21+
22+
@ApiProperty({
23+
description: 'New database password',
24+
example: 'new_secure_password',
25+
})
26+
@IsNotEmpty()
27+
@IsString()
28+
password: string;
29+
}

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,23 @@ import { SkipThrottle } from '@nestjs/throttler';
1616
import { UseCaseType } from '../../common/data-injection.tokens.js';
1717
import { Timeout } from '../../decorators/timeout.decorator.js';
1818
import { CompanyInfoEntity } from '../../entities/company-info/company-info.entity.js';
19+
import { CreatedConnectionDTO } from '../../entities/connection/application/dto/created-connection.dto.js';
1920
import { SaasUsualUserRegisterDS } from '../../entities/user/application/data-structures/usual-register-user.ds.js';
2021
import { FoundUserDto } from '../../entities/user/dto/found-user.dto.js';
2122
import { ExternalRegistrationProviderEnum } from '../../entities/user/enums/external-registration-provider.enum.js';
2223
import { UserEntity } from '../../entities/user/user.entity.js';
2324
import { InTransactionEnum } from '../../enums/in-transaction.enum.js';
2425
import { Messages } from '../../exceptions/text/messages.js';
2526
import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js';
26-
import { SuccessResponse } from './data-structures/common-responce.ds.js';
27+
import { CreatedConnectionResponse, SuccessResponse } from './data-structures/common-responce.ds.js';
28+
import { CreateConnectionForHostedDbDto } from './data-structures/create-connecttion-for-selfhosted-db.dto.js';
29+
import { DeleteConnectionForHostedDbDto } from './data-structures/delete-connection-for-hosted-db.dto.js';
2730
import { RegisterCompanyWebhookDS } from './data-structures/register-company.ds.js';
2831
import { RegisteredCompanyDS } from './data-structures/registered-company.ds.js';
2932
import { SaasRegisterUserWithGithub } from './data-structures/saas-register-user-with-github.js';
3033
import { SaasSAMLUserRegisterDS } from './data-structures/saas-saml-user-register.ds.js';
3134
import { SaasRegisterUserWithGoogleDS } from './data-structures/sass-register-user-with-google.js';
35+
import { UpdateHostedConnectionPasswordDto } from './data-structures/update-hosted-connection-password.dto.js';
3236
import {
3337
ICompanyRegistration,
3438
ICreateConnectionForHostedDb,
@@ -45,10 +49,8 @@ import {
4549
ISaasSAMLRegisterUser,
4650
ISuspendUsers,
4751
ISuspendUsersOverLimit,
52+
IUpdateHostedConnectionPassword,
4853
} from './use-cases/saas-use-cases.interface.js';
49-
import { CreatedConnectionDTO } from '../../entities/connection/application/dto/created-connection.dto.js';
50-
import { CreateConnectionForHostedDbDto } from './data-structures/create-connecttion-for-selfhosted-db.dto.js';
51-
import { DeleteConnectionForHostedDbDto } from './data-structures/delete-connection-for-hosted-db.dto.js';
5254

5355
@UseInterceptors(SentryInterceptor)
5456
@SkipThrottle()
@@ -91,6 +93,8 @@ export class SaasController {
9193
private readonly createConnectionForHostedDbUseCase: ICreateConnectionForHostedDb,
9294
@Inject(UseCaseType.SAAS_DELETE_CONNECTION_FOR_HOSTED_DB)
9395
private readonly deleteConnectionForHostedDbUseCase: IDeleteConnectionForHostedDb,
96+
@Inject(UseCaseType.SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD)
97+
private readonly updateHostedConnectionPasswordUseCase: IUpdateHostedConnectionPassword,
9498
) {}
9599

96100
@ApiOperation({ summary: 'Company registered webhook' })
@@ -288,12 +292,12 @@ export class SaasController {
288292
@ApiBody({ type: CreateConnectionForHostedDbDto })
289293
@ApiResponse({
290294
status: 201,
291-
type: CreatedConnectionDTO,
295+
type: CreatedConnectionResponse,
292296
})
293297
@Post('/connection/hosted')
294298
async createConnectionForHostedDb(
295299
@Body() connectionData: CreateConnectionForHostedDbDto,
296-
): Promise<CreatedConnectionDTO> {
300+
): Promise<CreatedConnectionResponse> {
297301
return await this.createConnectionForHostedDbUseCase.execute(connectionData);
298302
}
299303

@@ -309,4 +313,17 @@ export class SaasController {
309313
): Promise<CreatedConnectionDTO> {
310314
return await this.deleteConnectionForHostedDbUseCase.execute(deleteConnectionData);
311315
}
316+
317+
@ApiOperation({ summary: 'Update password of hosted database connection' })
318+
@ApiBody({ type: UpdateHostedConnectionPasswordDto })
319+
@ApiResponse({
320+
status: 201,
321+
type: SuccessResponse,
322+
})
323+
@Post('/connection/hosted/password')
324+
async updateHostedConnectionPassword(
325+
@Body() updatePasswordData: UpdateHostedConnectionPasswordDto,
326+
): Promise<SuccessResponse> {
327+
return await this.updateHostedConnectionPasswordUseCase.execute(updatePasswordData);
328+
}
312329
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { UserEntity } from '../../entities/user/user.entity.js';
77
import { SignInAuditEntity } from '../../entities/user-sign-in-audit/sign-in-audit.entity.js';
88
import { SignInAuditService } from '../../entities/user-sign-in-audit/sign-in-audit.service.js';
99
import { SaasController } from './saas.controller.js';
10+
import { CreateConnectionForHostedDbUseCase } from './use-cases/create-connection-for-hosted-db.use.case.js';
11+
import { DeleteConnectionForHostedDbUseCase } from './use-cases/delete-connection-for-hosted-db.use.case.js';
1012
import { FreezeConnectionsInCompanyUseCase } from './use-cases/freeze-connections-in-company.use.case.js';
1113
import { GetFullCompanyInfoByUserIdUseCase } from './use-cases/get-full-company-info-by-user-id.use.case.js';
1214
import { GetUserInfoUseCase } from './use-cases/get-user-info.use.case.js';
@@ -20,9 +22,8 @@ import { SaaSRegisterUserWIthSamlUseCase } from './use-cases/register-user-with-
2022
import { SaasUsualRegisterUseCase } from './use-cases/saas-usual-register-user.use.case.js';
2123
import { SuspendUsersUseCase } from './use-cases/suspend-users.use.case.js';
2224
import { SuspendUsersOverLimitUseCase } from './use-cases/suspend-users-over-limit.use.case.js';
23-
import { CreateConnectionForHostedDbUseCase } from './use-cases/create-connection-for-hosted-db.use.case.js';
24-
import { DeleteConnectionForHostedDbUseCase } from './use-cases/delete-connection-for-hosted-db.use.case.js';
2525
import { UnFreezeConnectionsInCompanyUseCase } from './use-cases/unfreeze-connections-in-company-use.case.js';
26+
import { UpdateHostedConnectionPasswordUseCase } from './use-cases/update-hosted-connection-password.use.case.js';
2627

2728
@Module({
2829
imports: [TypeOrmModule.forFeature([SignInAuditEntity, UserEntity])],
@@ -95,6 +96,10 @@ import { UnFreezeConnectionsInCompanyUseCase } from './use-cases/unfreeze-connec
9596
provide: UseCaseType.SAAS_DELETE_CONNECTION_FOR_HOSTED_DB,
9697
useClass: DeleteConnectionForHostedDbUseCase,
9798
},
99+
{
100+
provide: UseCaseType.SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD,
101+
useClass: UpdateHostedConnectionPasswordUseCase,
102+
},
98103
SignInAuditService,
99104
],
100105
controllers: [SaasController],
@@ -120,6 +125,7 @@ export class SaasModule {
120125
{ path: 'saas/user/saml/login', method: RequestMethod.POST },
121126
{ path: 'saas/connection/hosted', method: RequestMethod.POST },
122127
{ path: 'saas/connection/hosted/delete', method: RequestMethod.POST },
128+
{ path: 'saas/connection/hosted/password', method: RequestMethod.POST },
123129
);
124130
}
125131
}

backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,19 @@ import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/en
44
import AbstractUseCase from '../../../common/abstract-use.case.js';
55
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
66
import { BaseType } from '../../../common/data-injection.tokens.js';
7-
import { Messages } from '../../../exceptions/text/messages.js';
8-
import { slackPostMessage } from '../../../helpers/index.js';
9-
import { AccessLevelEnum } from '../../../enums/index.js';
107
import { generateCedarPolicyForGroup } from '../../../entities/cedar-authorization/cedar-policy-generator.js';
11-
import { CreatedConnectionDTO } from '../../../entities/connection/application/dto/created-connection.dto.js';
128
import { ConnectionEntity } from '../../../entities/connection/connection.entity.js';
139
import { readSslCertificate } from '../../../entities/connection/ssl-certificate/read-certificate.js';
14-
import { buildCreatedConnectionDs } from '../../../entities/connection/utils/build-created-connection.ds.js';
10+
import { AccessLevelEnum } from '../../../enums/index.js';
11+
import { Messages } from '../../../exceptions/text/messages.js';
12+
import { slackPostMessage } from '../../../helpers/index.js';
13+
import { CreatedConnectionResponse } from '../data-structures/common-responce.ds.js';
1514
import { CreateConnectionForHostedDbDto } from '../data-structures/create-connecttion-for-selfhosted-db.dto.js';
1615
import { ICreateConnectionForHostedDb } from './saas-use-cases.interface.js';
1716

1817
@Injectable({ scope: Scope.REQUEST })
1918
export class CreateConnectionForHostedDbUseCase
20-
extends AbstractUseCase<CreateConnectionForHostedDbDto, CreatedConnectionDTO>
19+
extends AbstractUseCase<CreateConnectionForHostedDbDto, CreatedConnectionResponse>
2120
implements ICreateConnectionForHostedDb
2221
{
2322
constructor(
@@ -27,17 +26,15 @@ export class CreateConnectionForHostedDbUseCase
2726
super();
2827
}
2928

30-
protected async implementation(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionDTO> {
29+
protected async implementation(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionResponse> {
3130
const { companyId, userId, databaseName, hostname, port, username, password } = inputData;
3231

3332
const connectionAuthor = await this._dbContext.userRepository.findOneUserById(userId);
3433
if (!connectionAuthor) {
3534
throw new InternalServerErrorException(Messages.USER_NOT_FOUND);
3635
}
3736

38-
await slackPostMessage(
39-
Messages.USER_TRY_CREATE_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres),
40-
);
37+
await slackPostMessage(Messages.USER_TRY_CREATE_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres));
4138

4239
const cert = await readSslCertificate();
4340

@@ -85,21 +82,18 @@ export class CreateConnectionForHostedDbUseCase
8582
savedConnection,
8683
connectionAuthor,
8784
);
88-
createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup(
89-
savedConnection.id,
90-
true,
91-
{
92-
connection: { connectionId: savedConnection.id, accessLevel: AccessLevelEnum.edit },
93-
group: { groupId: createdAdminGroup.id, accessLevel: AccessLevelEnum.edit },
94-
tables: [],
95-
},
96-
);
85+
createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup(savedConnection.id, true, {
86+
connection: { connectionId: savedConnection.id, accessLevel: AccessLevelEnum.edit },
87+
group: { groupId: createdAdminGroup.id, accessLevel: AccessLevelEnum.edit },
88+
tables: [],
89+
});
9790
await this._dbContext.groupRepository.saveNewOrUpdatedGroup(createdAdminGroup);
9891
delete createdAdminGroup.connection;
9992
await this._dbContext.userRepository.saveUserEntity(connectionAuthor);
10093
savedConnection.groups = [createdAdminGroup];
10194

102-
const foundCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByCompanyIdWithoutConnections(companyId);
95+
const foundCompany =
96+
await this._dbContext.companyInfoRepository.findCompanyInfoByCompanyIdWithoutConnections(companyId);
10397
if (foundCompany) {
10498
const connectionToUpdate = await this._dbContext.connectionRepository.findOne({
10599
where: { id: savedConnection.id },
@@ -108,11 +102,8 @@ export class CreateConnectionForHostedDbUseCase
108102
await this._dbContext.connectionRepository.saveUpdatedConnection(connectionToUpdate);
109103
}
110104

111-
await slackPostMessage(
112-
Messages.USER_CREATED_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres),
113-
);
105+
await slackPostMessage(Messages.USER_CREATED_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres));
114106

115-
const connectionRO = buildCreatedConnectionDs(savedConnection, null, null);
116-
return connectionRO;
107+
return { connectionId: savedConnection.id };
117108
}
118109
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { SaasUsualUserRegisterDS } from '../../../entities/user/application/data
55
import { FoundUserDto } from '../../../entities/user/dto/found-user.dto.js';
66
import { UserEntity } from '../../../entities/user/user.entity.js';
77
import { InTransactionEnum } from '../../../enums/in-transaction.enum.js';
8-
import { SuccessResponse } from '../data-structures/common-responce.ds.js';
8+
import { CreatedConnectionResponse, SuccessResponse } from '../data-structures/common-responce.ds.js';
99
import { CreateConnectionForHostedDbDto } from '../data-structures/create-connecttion-for-selfhosted-db.dto.js';
1010
import { DeleteConnectionForHostedDbDto } from '../data-structures/delete-connection-for-hosted-db.dto.js';
1111
import { FreezeConnectionsInCompanyDS } from '../data-structures/freeze-connections-in-company.ds.js';
@@ -17,6 +17,7 @@ import { SaasRegisterUserWithGithub } from '../data-structures/saas-register-use
1717
import { SaasSAMLUserRegisterDS } from '../data-structures/saas-saml-user-register.ds.js';
1818
import { SaasRegisterUserWithGoogleDS } from '../data-structures/sass-register-user-with-google.js';
1919
import { SuspendUsersDS } from '../data-structures/suspend-users.ds.js';
20+
import { UpdateHostedConnectionPasswordDto } from '../data-structures/update-hosted-connection-password.dto.js';
2021

2122
export interface ICompanyRegistration {
2223
execute(inputData: RegisterCompanyWebhookDS): Promise<RegisteredCompanyDS>;
@@ -71,9 +72,13 @@ export interface ISaasSAMLRegisterUser {
7172
}
7273

7374
export interface ICreateConnectionForHostedDb {
74-
execute(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionDTO>;
75+
execute(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionResponse>;
7576
}
7677

7778
export interface IDeleteConnectionForHostedDb {
7879
execute(inputData: DeleteConnectionForHostedDbDto): Promise<CreatedConnectionDTO>;
7980
}
81+
82+
export interface IUpdateHostedConnectionPassword {
83+
execute(inputData: UpdateHostedConnectionPasswordDto): Promise<SuccessResponse>;
84+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 { SuccessResponse } from '../data-structures/common-responce.ds.js';
7+
import { UpdateHostedConnectionPasswordDto } from '../data-structures/update-hosted-connection-password.dto.js';
8+
import { IUpdateHostedConnectionPassword } from './saas-use-cases.interface.js';
9+
10+
@Injectable({ scope: Scope.REQUEST })
11+
export class UpdateHostedConnectionPasswordUseCase
12+
extends AbstractUseCase<UpdateHostedConnectionPasswordDto, SuccessResponse>
13+
implements IUpdateHostedConnectionPassword
14+
{
15+
constructor(
16+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
17+
protected _dbContext: IGlobalDatabaseContext,
18+
) {
19+
super();
20+
}
21+
22+
protected async implementation(inputData: UpdateHostedConnectionPasswordDto): Promise<SuccessResponse> {
23+
const { companyId, databaseName, password } = inputData;
24+
25+
const foundCompany =
26+
await this._dbContext.companyInfoRepository.findCompanyInfoByCompanyIdWithoutConnections(companyId);
27+
if (!foundCompany) {
28+
throw new NotFoundException(Messages.COMPANY_NOT_FOUND);
29+
}
30+
31+
const companyConnections = await this._dbContext.connectionRepository.find({
32+
where: { company: { id: companyId } },
33+
});
34+
const connection = companyConnections.find((conn) => conn.database === databaseName);
35+
if (!connection) {
36+
throw new NotFoundException(Messages.CONNECTION_NOT_FOUND);
37+
}
38+
39+
connection.password = password;
40+
await this._dbContext.connectionRepository.saveUpdatedConnection(connection);
41+
42+
return { success: true };
43+
}
44+
}

0 commit comments

Comments
 (0)