Skip to content

Commit 1ee4ef5

Browse files
committed
feat: add self-hosted operations module with initial user creation and configuration checks
1 parent c1ead04 commit 1ee4ef5

15 files changed

Lines changed: 485 additions & 0 deletions

File tree

backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { PersonalTableSettingsModule } from './entities/table-settings/personal-
4747
import { SavedDbQueryModule } from './entities/visualizations/saved-db-query/saved-db-query.module.js';
4848
import { DashboardModule } from './entities/visualizations/dashboard/dashboards.module.js';
4949
import { DashboardWidgetModule } from './entities/visualizations/dashboard-widget/dashboard-widget.module.js';
50+
import { SelfHostedOperationsModule } from './selfhosted-operations/selhosted-operations.module.js';
5051

5152
@Module({
5253
imports: [
@@ -98,6 +99,7 @@ import { DashboardWidgetModule } from './entities/visualizations/dashboard-widge
9899
SavedDbQueryModule,
99100
DashboardModule,
100101
DashboardWidgetModule,
102+
SelfHostedOperationsModule,
101103
],
102104
controllers: [AppController],
103105
providers: [

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,7 @@ export enum UseCaseType {
202202
CREATE_DASHBOARD_WIDGET = 'CREATE_DASHBOARD_WIDGET',
203203
UPDATE_DASHBOARD_WIDGET = 'UPDATE_DASHBOARD_WIDGET',
204204
DELETE_DASHBOARD_WIDGET = 'DELETE_DASHBOARD_WIDGET',
205+
206+
IS_CONFIGURED = 'IS_CONFIGURED',
207+
CREATE_INITIAL_USER = 'CREATE_INITIAL_USER',
205208
}

backend/src/entities/ai/ai-conversation-history/ai-chat-messages/ai-chat-message.entity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class AiChatMessageEntity {
3434
@ManyToOne(
3535
() => UserAiChatEntity,
3636
(ai_chat) => ai_chat.messages,
37+
{ onDelete: 'CASCADE' },
3738
)
3839
@JoinColumn({ name: 'ai_chat_id' })
3940
ai_chat: Relation<UserAiChatEntity>;

backend/src/entities/ai/ai-conversation-history/user-ai-chat/user-ai-chat.entity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class UserAiChatEntity {
2929
@ManyToOne(
3030
() => UserEntity,
3131
(user) => user.ai_chats,
32+
{ onDelete: 'CASCADE' },
3233
)
3334
@JoinColumn({ name: 'user_id' })
3435
user: Relation<UserEntity>;

backend/src/exceptions/text/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,4 +382,6 @@ export const Messages = {
382382
SECRET_DELETED_SUCCESSFULLY: 'Secret deleted successfully',
383383
USER_NOT_FOUND_OR_NOT_IN_COMPANY: 'User not found or not associated with a company',
384384
PERSONAL_TABLE_SETTINGS_NOT_FOUND: 'Personal table settings with this parameters not found',
385+
SELF_HOSTED_ALREADY_CONFIGURED: 'Instance is already configured',
386+
ENDPOINT_NOT_AVAILABLE_IN_THIS_MODE: 'This endpoint is not available in the current mode',
385387
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class AddedCascadeOptionToAiChatEntities1770043047971 implements MigrationInterface {
4+
name = 'AddedCascadeOptionToAiChatEntities1770043047971';
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`ALTER TABLE "ai_chat_message" DROP CONSTRAINT "FK_03bc49058afd5262d6a503bf123"`);
8+
await queryRunner.query(`ALTER TABLE "user_ai_chat" DROP CONSTRAINT "FK_0f95dbd767d42e637345636cb5d"`);
9+
await queryRunner.query(
10+
`ALTER TABLE "ai_chat_message" ADD CONSTRAINT "FK_03bc49058afd5262d6a503bf123" FOREIGN KEY ("ai_chat_id") REFERENCES "user_ai_chat"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
11+
);
12+
await queryRunner.query(
13+
`ALTER TABLE "user_ai_chat" ADD CONSTRAINT "FK_0f95dbd767d42e637345636cb5d" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
14+
);
15+
}
16+
17+
public async down(queryRunner: QueryRunner): Promise<void> {
18+
await queryRunner.query(`ALTER TABLE "user_ai_chat" DROP CONSTRAINT "FK_0f95dbd767d42e637345636cb5d"`);
19+
await queryRunner.query(`ALTER TABLE "ai_chat_message" DROP CONSTRAINT "FK_03bc49058afd5262d6a503bf123"`);
20+
await queryRunner.query(
21+
`ALTER TABLE "user_ai_chat" ADD CONSTRAINT "FK_0f95dbd767d42e637345636cb5d" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
22+
);
23+
await queryRunner.query(
24+
`ALTER TABLE "ai_chat_message" ADD CONSTRAINT "FK_03bc49058afd5262d6a503bf123" FOREIGN KEY ("ai_chat_id") REFERENCES "user_ai_chat"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
25+
);
26+
}
27+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export class CreateInitialUserDs {
2+
email: string;
3+
password: string;
4+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
3+
4+
export class CreateInitialUserDto {
5+
@ApiProperty({ description: 'User email' })
6+
@IsNotEmpty()
7+
@IsString()
8+
@IsEmail()
9+
readonly email: string;
10+
11+
@ApiProperty({ description: 'Admin user password' })
12+
@IsNotEmpty()
13+
@IsString()
14+
@MinLength(8)
15+
@MaxLength(255)
16+
readonly password: string;
17+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class IsConfiguredRo {
4+
@ApiProperty({ example: true, description: 'Indicates whether the self-hosted instance is configured' })
5+
public isConfigured: boolean;
6+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { BadRequestException, Inject, Injectable } 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 { SimpleFoundUserInfoDs } from '../../../entities/user/dto/found-user.dto.js';
6+
import { ICreateInitialUserUseCase } from './selfhosted-use-cases.interfaces.js';
7+
import { CreateInitialUserDs } from '../data-structures/create-initial-user.ds.js';
8+
import { isSaaS } from '../../../helpers/app/is-saas.js';
9+
import { Messages } from '../../../exceptions/text/messages.js';
10+
import { RegisterUserDs } from '../../../entities/user/application/data-structures/register-user-ds.js';
11+
import { UserRoleEnum } from '../../../entities/user/enums/user-role.enum.js';
12+
import { buildRegisteringUser } from '../../../entities/user/utils/build-registering-user.util.js';
13+
import { CompanyInfoEntity } from '../../../entities/company-info/company-info.entity.js';
14+
import { Encryptor } from '../../../helpers/encryption/encryptor.js';
15+
import { buildSimpleUserInfoDs } from '../../../entities/user/utils/build-created-user.ds.js';
16+
17+
@Injectable()
18+
export class CreateInitialUserUseCase
19+
extends AbstractUseCase<CreateInitialUserDs, SimpleFoundUserInfoDs>
20+
implements ICreateInitialUserUseCase
21+
{
22+
constructor(
23+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
24+
protected _dbContext: IGlobalDatabaseContext,
25+
) {
26+
super();
27+
}
28+
29+
protected async implementation(inputData: CreateInitialUserDs): Promise<SimpleFoundUserInfoDs> {
30+
if (isSaaS()) {
31+
throw new BadRequestException(Messages.ENDPOINT_NOT_AVAILABLE_IN_THIS_MODE);
32+
}
33+
34+
const userCount = await this._dbContext.userRepository.count();
35+
if (userCount > 0) {
36+
throw new BadRequestException(Messages.SELF_HOSTED_ALREADY_CONFIGURED);
37+
}
38+
39+
const { email, password } = inputData;
40+
const registerUserData: RegisterUserDs = {
41+
email: email,
42+
password: password,
43+
isActive: true,
44+
gclidValue: null,
45+
name: 'Admin',
46+
role: UserRoleEnum.ADMIN,
47+
};
48+
49+
const savedUser = await this._dbContext.userRepository.saveUserEntity(buildRegisteringUser(registerUserData));
50+
51+
const newCompanyInfo = new CompanyInfoEntity();
52+
newCompanyInfo.id = Encryptor.generateUUID();
53+
const savedCompanyInfo = await this._dbContext.companyInfoRepository.save(newCompanyInfo);
54+
55+
savedUser.company = savedCompanyInfo;
56+
const finalUser = await this._dbContext.userRepository.saveUserEntity(savedUser);
57+
58+
return buildSimpleUserInfoDs(finalUser);
59+
}
60+
}

0 commit comments

Comments
 (0)