Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { BadRequestException, Inject, Injectable, InternalServerErrorException, Scope } from '@nestjs/common';
import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js';
import * as Sentry from '@sentry/node';
import AbstractUseCase from '../../../common/abstract-use.case.js';
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
import { BaseType } from '../../../common/data-injection.tokens.js';
import { Messages } from '../../../exceptions/text/messages.js';
import { Encryptor } from '../../../helpers/encryption/encryptor.js';
import { isConnectionTypeAgent, slackPostMessage } from '../../../helpers/index.js';
import { SharedJobsService } from '../../shared-jobs/shared-jobs.service.js';
import { UserRoleEnum } from '../../user/enums/user-role.enum.js';
import { UserEntity } from '../../user/user.entity.js';
import { CreateConnectionDs } from '../application/data-structures/create-connection.ds.js';
Expand All @@ -27,7 +24,6 @@ export class CreateConnectionUseCase
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
private readonly sharedJobsService: SharedJobsService,
) {
super();
}
Expand All @@ -51,79 +47,58 @@ export class CreateConnectionUseCase
await validateCreateConnectionData(createConnectionData);

createConnectionData = await processAWSConnection(createConnectionData);
let isConnectionTestedSuccessfully: boolean = false;
if (!isConnectionTypeAgent(createConnectionData.connection_parameters.type)) {
const connectionParamsCopy = {
...createConnectionData.connection_parameters,
};
const dao = getDataAccessObject(connectionParamsCopy);
try {
const testResult = await dao.testConnect();
isConnectionTestedSuccessfully = testResult.result;
await dao.testConnect();
} catch (e) {
const text: string = e.message.toLowerCase();
isConnectionTestedSuccessfully = false;
if (text.includes('ssl required') || text.includes('ssl connection required')) {
createConnectionData.connection_parameters.ssl = true;
connectionParamsCopy.ssl = true;
try {
const updatedDao = getDataAccessObject(connectionParamsCopy);
const sslTestResult = await updatedDao.testConnect();
isConnectionTestedSuccessfully = sslTestResult.result;
await updatedDao.testConnect();
} catch (_e) {
isConnectionTestedSuccessfully = false;
createConnectionData.connection_parameters.ssl = false;
connectionParamsCopy.ssl = false;
}
}
}
}
let connectionCopy: ConnectionEntity = null;
try {
const createdConnection: ConnectionEntity = await buildConnectionEntity(createConnectionData, connectionAuthor);
const savedConnection: ConnectionEntity =
await this._dbContext.connectionRepository.saveNewConnection(createdConnection);
const createdConnection: ConnectionEntity = await buildConnectionEntity(createConnectionData, connectionAuthor);
const savedConnection: ConnectionEntity =
await this._dbContext.connectionRepository.saveNewConnection(createdConnection);

connectionCopy = { ...savedConnection } as ConnectionEntity;
if (savedConnection.masterEncryption && masterPwd && !isConnectionTypeAgent(savedConnection.type)) {
connectionCopy = Encryptor.decryptConnectionCredentials(connectionCopy, masterPwd);
}

let token: string;
if (isConnectionTypeAgent(savedConnection.type)) {
token = await this._dbContext.agentRepository.createNewAgentForConnectionAndReturnToken(savedConnection);
}
const createdAdminGroup = await this._dbContext.groupRepository.createdAdminGroupInConnection(
savedConnection,
connectionAuthor,
);
await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup);
delete createdAdminGroup.connection;
await this._dbContext.userRepository.saveUserEntity(connectionAuthor);
createdConnection.groups = [createdAdminGroup];
const foundUserCompany = await this._dbContext.companyInfoRepository.findOneCompanyInfoByUserIdWithConnections(
connectionAuthor.id,
);
if (foundUserCompany) {
const connection = await this._dbContext.connectionRepository.findOne({
where: { id: savedConnection.id },
});
connection.company = foundUserCompany;
await this._dbContext.connectionRepository.saveUpdatedConnection(connection);
}
await slackPostMessage(
Messages.USER_CREATED_CONNECTION(connectionAuthor.email, createConnectionData.connection_parameters.type),
);
const connectionRO = buildCreatedConnectionDs(savedConnection, token, masterPwd);
return connectionRO;
} finally {
if (isConnectionTestedSuccessfully && !isConnectionTypeAgent(connectionCopy.type)) {
// Fire-and-forget: run AI scan in background without blocking response
this.sharedJobsService.scanDatabaseAndCreateSettingsAndWidgetsWithAI(connectionCopy).catch((error) => {
console.error('Background AI scan failed:', error);
Sentry.captureException(error);
});
}
let token: string;
if (isConnectionTypeAgent(savedConnection.type)) {
token = await this._dbContext.agentRepository.createNewAgentForConnectionAndReturnToken(savedConnection);
}
const createdAdminGroup = await this._dbContext.groupRepository.createdAdminGroupInConnection(
savedConnection,
connectionAuthor,
);
await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup);
delete createdAdminGroup.connection;
await this._dbContext.userRepository.saveUserEntity(connectionAuthor);
createdConnection.groups = [createdAdminGroup];
const foundUserCompany = await this._dbContext.companyInfoRepository.findOneCompanyInfoByUserIdWithConnections(
connectionAuthor.id,
);
if (foundUserCompany) {
const connection = await this._dbContext.connectionRepository.findOne({
where: { id: savedConnection.id },
});
connection.company = foundUserCompany;
await this._dbContext.connectionRepository.saveUpdatedConnection(connection);
}
await slackPostMessage(
Messages.USER_CREATED_CONNECTION(connectionAuthor.email, createConnectionData.connection_parameters.type),
);
const connectionRO = buildCreatedConnectionDs(savedConnection, token, masterPwd);
return connectionRO;
}
}
7 changes: 7 additions & 0 deletions frontend/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ const routes: Routes = [
canActivate: [AuthGuard],
title: 'Upgraded successfully | Rocketadmin',
},
{
path: 'auto-configure/:connection-id',
loadComponent: () =>
import('./components/auto-configure/auto-configure.component').then((m) => m.AutoConfigureComponent),
canActivate: [AuthGuard],
title: 'Auto-configure | Rocketadmin',
},
{
path: 'edit-db/:connection-id',
loadComponent: () => import('./components/connect-db/connect-db.component').then((m) => m.ConnectDBComponent),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.auto-configure {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 24px;
height: 100vh;
}

.auto-configure__title {
color: var(--mat-sidenav-content-text-color);
margin: 0;
font-weight: 500;
}

.auto-configure__text {
color: var(--mat-sidenav-content-text-color);
opacity: 0.7;
margin: 0;
max-width: 400px;
text-align: center;
}

.auto-configure__notice {
color: var(--mat-sidenav-content-text-color);
opacity: 0.7;
margin: 0;
text-align: center;
max-width: 400px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@if (loading()) {
<div class="auto-configure">
<mat-spinner diameter="48"></mat-spinner>
<h2 class="auto-configure__title">Auto-configuring your dashboard</h2>
<p class="auto-configure__text">Please wait while we analyze your database and set up the best configuration. This may take a moment.</p>
</div>
} @else if (errorMessage()) {
<div class="auto-configure">
<p class="auto-configure__notice">{{ errorMessage() }}</p>
<a mat-flat-button [routerLink]="'/dashboard/' + connectionId()">Continue to dashboard</a>
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';

import { ApiService } from '../../services/api.service';

@Component({
selector: 'app-auto-configure',
templateUrl: './auto-configure.component.html',
styleUrls: ['./auto-configure.component.css'],
imports: [MatProgressSpinnerModule, MatButtonModule, RouterModule],
})
export class AutoConfigureComponent implements OnInit {
private _route = inject(ActivatedRoute);
private _router = inject(Router);
private _api = inject(ApiService);

protected connectionId = signal<string | null>(null);
protected loading = signal(true);
protected errorMessage = signal<string | null>(null);

ngOnInit(): void {
const connectionId = this._route.snapshot.paramMap.get('connection-id');
if (!connectionId) {
this._router.navigate(['/connections-list']);
return;
}

this.connectionId.set(connectionId);
this._configure(connectionId);
}

private async _configure(connectionId: string): Promise<void> {
const result = await this._api.get<string>(`/ai/v2/setup/${connectionId}`, { responseType: 'text' });

if (result !== null) {
this._router.navigate([`/dashboard/${connectionId}`]);
} else {
this.loading.set(false);
this.errorMessage.set('Auto-configuration could not be completed. You can still configure your tables manually.');
}
Comment on lines +34 to +42

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling logic assumes that a null result always indicates an error, but the API service returns null for any error (including network errors, 401 unauthorized, 404 not found, etc.). This makes it impossible to distinguish between different types of failures or provide more specific error messages to users. Consider checking the specific error type or HTTP status code to provide more contextual error messages, especially for cases like permission denied or connection not found.

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ describe('ConnectDBComponent', () => {
],
providers: [
provideHttpClient(),
provideRouter([{ path: 'dashboard/:id', component: ConnectDBComponent }]),
provideRouter([
{ path: 'dashboard/:id', component: ConnectDBComponent },
{ path: 'auto-configure/:connection-id', component: ConnectDBComponent },
]),
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ConnectDBComponent),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export class ConnectDBComponent implements OnInit {
this.connectionToken = res.token;
this.connectionID = res.id;
} else {
this.router.navigate([`/dashboard/${createdConnectionID}`]);
this.router.navigate([`/auto-configure/${createdConnectionID}`]);
}
this.angulartics2.eventTrack.next({
action: 'Connect DB: connection is added successfully',
Expand Down
Loading