Skip to content

Selfosted operations module#1553

Merged
Artuomka merged 3 commits into
mainfrom
backend_security_report
Feb 3, 2026
Merged

Selfosted operations module#1553
Artuomka merged 3 commits into
mainfrom
backend_security_report

Conversation

@Artuomka

@Artuomka Artuomka commented Feb 3, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

Copilot AI review requested due to automatic review settings February 3, 2026 09:24
@Artuomka Artuomka enabled auto-merge February 3, 2026 09:26
@Artuomka Artuomka merged commit 2aa16e1 into main Feb 3, 2026
19 checks passed
@Artuomka Artuomka deleted the backend_security_report branch February 3, 2026 09:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a self-hosted operations module to handle initial setup and configuration of self-hosted instances. The module provides endpoints to check if an instance is configured and to create the initial admin user account. The PR also includes database migrations to add CASCADE delete options for better data integrity when companies or AI chat entities are deleted.

Changes:

  • Added a new SelfHostedOperationsModule with dynamic registration that conditionally loads based on SaaS vs self-hosted mode
  • Implemented two endpoints: /selfhosted/is-configured (GET) and /selfhosted/initial-user (POST) for initial instance setup
  • Added database migrations to add CASCADE delete options to connection, AI chat, and AI chat message entities

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
backend/src/selfhosted-operations/selhosted-operations.module.ts Module definition with dynamic registration logic (contains filename typo)
backend/src/selfhosted-operations/selfhosted-operations.controller.ts Controller defining self-hosted setup endpoints
backend/src/selfhosted-operations/application/use-cases/is-configured.use.case.ts Use case to check if instance has been configured
backend/src/selfhosted-operations/application/use-cases/create-initial-user.use.case.ts Use case to create the first admin user
backend/src/selfhosted-operations/application/use-cases/selfhosted-use-cases.interfaces.ts Interface definitions for use cases
backend/src/selfhosted-operations/application/responce-objects/is-configured.ro.ts Response object for configuration status (contains directory name typo)
backend/src/selfhosted-operations/application/dto/create-initial-admin-user.dto.ts DTO with validation for initial user creation
backend/src/selfhosted-operations/application/data-structures/create-initial-user.ds.ts Data structure for user creation
backend/src/migrations/1770045005400-AddCascadeOptionToConnectionEntity.ts Migration to add CASCADE delete to connection entity
backend/src/migrations/1770043047971-AddedCascadeOptionToAiChatEntities.ts Migration to add CASCADE delete to AI chat entities
backend/src/exceptions/text/messages.ts Added error messages for self-hosted operations
backend/src/entities/connection/connection.entity.ts Added CASCADE delete option for company relationship
backend/src/entities/ai/ai-conversation-history/user-ai-chat/user-ai-chat.entity.ts Added CASCADE delete option for user relationship
backend/src/entities/ai/ai-conversation-history/ai-chat-messages/ai-chat-message.entity.ts Added CASCADE delete option for chat relationship
backend/src/common/data-injection.tokens.ts Added dependency injection tokens for new use cases
backend/src/app.module.ts Integrated SelfHostedOperationsModule (contains import typo)
backend/test/ava-tests/non-saas-tests/non-saas-selfhosted-operations-e2e.test.ts E2E tests for self-hosted operations (all tests skipped)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -0,0 +1,45 @@
import { DynamicModule, Module } from '@nestjs/common';

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

The filename "selhosted-operations.module.ts" contains a typo - it should be "selfhosted-operations.module.ts" (missing the 'f'). This inconsistency with the module class name SelfHostedOperationsModule and the directory name selfhosted-operations should be corrected.

Copilot uses AI. Check for mistakes.
import { Body, Controller, Get, HttpStatus, Inject, Post, UseInterceptors } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { SentryInterceptor } from '../interceptors/index.js';
import { IsConfiguredRo } from './application/responce-objects/is-configured.ro.js';

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

The import path contains a typo - "responce-objects" should be "response-objects" (correct spelling). Update this import path once the directory name is corrected to match the codebase convention.

Suggested change
import { IsConfiguredRo } from './application/responce-objects/is-configured.ro.js';
import { IsConfiguredRo } from './application/response-objects/is-configured.ro.js';

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,12 @@
import { InTransactionEnum } from '../../../enums/index.js';
import { SimpleFoundUserInfoDs } from '../../../entities/user/dto/found-user.dto.js';
import { IsConfiguredRo } from '../responce-objects/is-configured.ro.js';

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

The import path contains a typo - "responce-objects" should be "response-objects" (correct spelling). Update this import path once the directory name is corrected to match the codebase convention.

Suggested change
import { IsConfiguredRo } from '../responce-objects/is-configured.ro.js';
import { IsConfiguredRo } from '../response-objects/is-configured.ro.js';

Copilot uses AI. Check for mistakes.
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 { IsConfiguredRo } from '../responce-objects/is-configured.ro.js';

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

The import path contains a typo - "responce-objects" should be "response-objects" (correct spelling). Update this import path once the directory name is corrected to match the codebase convention.

Suggested change
import { IsConfiguredRo } from '../responce-objects/is-configured.ro.js';
import { IsConfiguredRo } from '../response-objects/is-configured.ro.js';

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +248
test.skip(`${currentTest} should return isConfigured false when no users exist`, async (t) => {
const dataSource = app.get<DataSource>(BaseType.DATA_SOURCE);

await clearDatabase(dataSource);

const result = await request(app.getHttpServer())
.get('/selfhosted/is-configured')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 200);
const responseBody = JSON.parse(result.text);
console.log('🚀 ~ responseBody:', responseBody);
t.is(responseBody.isConfigured, false);
t.pass();
});

test.skip(`${currentTest} should return isConfigured true when users exist`, async (t) => {
const dataSource = app.get<DataSource>(BaseType.DATA_SOURCE);
const userRepository = dataSource.getRepository(UserEntity);
const companyRepository = dataSource.getRepository(CompanyInfoEntity);

await clearDatabase(dataSource);

const company = companyRepository.create({
id: faker.string.uuid(),
name: faker.company.name(),
});
await companyRepository.save(company);

const user = userRepository.create({
email: faker.internet.email().toLowerCase(),
password: 'TestPassword123!',
isActive: true,
company: company,
});
await userRepository.save(user);

const result = await request(app.getHttpServer())
.get('/selfhosted/is-configured')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 200);
const responseBody = JSON.parse(result.text);
t.is(responseBody.isConfigured, true);
t.pass();
});

currentTest = 'POST /selfhosted/initial-user';

test.skip(`${currentTest} should create initial user when instance is not configured`, async (t) => {
const dataSource = app.get<DataSource>(BaseType.DATA_SOURCE);
const userRepository = dataSource.getRepository(UserEntity);

await clearDatabase(dataSource);

const email = faker.internet.email().toLowerCase();
const password = 'UserPassword123!';

const result = await request(app.getHttpServer())
.post('/selfhosted/initial-user')
.send({ email, password })
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const responseBody = JSON.parse(result.text);
console.log('🚀 ~ responseBody:', responseBody);

t.is(result.status, 201);
t.is(responseBody.email, email);
t.is(Object.hasOwn(responseBody, 'id'), true);
t.is(responseBody.isActive, true);

const createdUser = await userRepository.findOne({ where: { email } });
t.truthy(createdUser);
t.is(createdUser.email, email);
t.pass();
});

test.skip(`${currentTest} should return error when instance is already configured`, async (t) => {
const dataSource = app.get<DataSource>(BaseType.DATA_SOURCE);
const userRepository = dataSource.getRepository(UserEntity);
const companyRepository = dataSource.getRepository(CompanyInfoEntity);

await clearDatabase(dataSource);

const company = companyRepository.create({
id: faker.string.uuid(),
name: faker.company.name(),
});
await companyRepository.save(company);

const existingUser = userRepository.create({
email: faker.internet.email().toLowerCase(),
password: 'ExistingPassword123!',
isActive: true,
company: company,
});
await userRepository.save(existingUser);

const newEmail = faker.internet.email().toLowerCase();
const newPassword = 'NewUserPassword123!';

const result = await request(app.getHttpServer())
.post('/selfhosted/initial-user')
.send({ email: newEmail, password: newPassword })
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 400);
const responseBody = JSON.parse(result.text);
t.is(responseBody.message, Messages.SELF_HOSTED_ALREADY_CONFIGURED);
t.pass();
});

test.skip(`${currentTest} should return validation error for invalid email`, async (t) => {
const dataSource = app.get<DataSource>(BaseType.DATA_SOURCE);

await clearDatabase(dataSource);

const invalidEmail = 'invalid-email';
const password = 'UserPassword123!';

const result = await request(app.getHttpServer())
.post('/selfhosted/initial-user')
.send({ email: invalidEmail, password })
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 400);
t.pass();
});

test.skip(`${currentTest} should return validation error for short password`, async (t) => {
const dataSource = app.get<DataSource>(BaseType.DATA_SOURCE);

await clearDatabase(dataSource);

const email = faker.internet.email().toLowerCase();
const shortPassword = 'short';

const result = await request(app.getHttpServer())
.post('/selfhosted/initial-user')
.send({ email, password: shortPassword })
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 400);
t.pass();
});

test.skip(`${currentTest} should return validation error when email is missing`, async (t) => {
const dataSource = app.get<DataSource>(BaseType.DATA_SOURCE);

await clearDatabase(dataSource);

const password = 'UserPassword123!';

const result = await request(app.getHttpServer())
.post('/selfhosted/initial-user')
.send({ password })
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 400);
t.pass();
});

test.skip(`${currentTest} should return validation error when password is missing`, async (t) => {
const dataSource = app.get<DataSource>(BaseType.DATA_SOURCE);

await clearDatabase(dataSource);

const email = faker.internet.email().toLowerCase();

const result = await request(app.getHttpServer())
.post('/selfhosted/initial-user')
.send({ email })
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(result.status, 400);
t.pass();
});

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

All tests in this file are skipped using test.skip. If these tests are ready to run, they should be enabled (remove .skip). If they are not yet ready or there's a known issue preventing them from running, consider adding a comment explaining why they're skipped, or move them to a separate WIP file.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +16
@MinLength(8)
@MaxLength(255)
readonly password: string;

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

The password validation for the initial admin user only uses @minlength(8) and @maxlength(255), but doesn't enforce password strength requirements. Other password operations in the codebase (password reset at backend/src/entities/user/dto/password.dto.ts:9-15, password change at backend/src/entities/user/application/data-structures/change-usual-user-password.ds.ts:15-21) use @IsStrongPassword with requirements for minLowercase, minUppercase, minNumbers. Since this creates the first admin account with full system access, consider using the same @IsStrongPassword validation for better security.

Copilot uses AI. Check for mistakes.
Comment thread backend/src/app.module.ts
import { SavedDbQueryModule } from './entities/visualizations/saved-db-query/saved-db-query.module.js';
import { DashboardModule } from './entities/visualizations/dashboard/dashboards.module.js';
import { DashboardWidgetModule } from './entities/visualizations/dashboard-widget/dashboard-widget.module.js';
import { SelfHostedOperationsModule } from './selfhosted-operations/selhosted-operations.module.js';

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

The import path contains a typo - "selhosted-operations.module.js" should be "selfhosted-operations.module.js" (missing the 'f'). This needs to be corrected along with the actual filename.

Suggested change
import { SelfHostedOperationsModule } from './selfhosted-operations/selhosted-operations.module.js';
import { SelfHostedOperationsModule } from './selfhosted-operations/selfhosted-operations.module.js';

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

The directory name "responce-objects" contains a typo - it should be "response-objects" (correct spelling). The codebase consistently uses "response-objects" elsewhere (e.g., backend/src/entities/ai/ai-conversation-history/application/response-objects/user-ai-chat.ro.js, backend/src/entities/table-filters/application/response-objects/created-table-filters.ro.js). This deviation from the established convention needs to be corrected.

Copilot uses AI. Check for mistakes.

t.is(result.status, 200);
const responseBody = JSON.parse(result.text);
console.log('🚀 ~ responseBody:', responseBody);

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

Debug console.log statement should be removed before merging. This appears to be leftover from development/debugging.

Suggested change
console.log('🚀 ~ responseBody:', responseBody);

Copilot uses AI. Check for mistakes.
.set('Accept', 'application/json');

const responseBody = JSON.parse(result.text);
console.log('🚀 ~ responseBody:', responseBody);

Copilot AI Feb 3, 2026

Copy link

Choose a reason for hiding this comment

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

Debug console.log statement should be removed before merging. This appears to be leftover from development/debugging.

Suggested change
console.log('🚀 ~ responseBody:', responseBody);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants