Skip to content
Open
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Server Configuration
PORT=3000

# Environment (development or production)
# In production: database migrations are used instead of schema synchronization
# In development: schema is auto-synchronized for faster iteration
# NODE_ENV=production

# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-in-production

Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "typeorm-ts-node-commonjs",
"migration:generate": "npx typeorm-ts-node-commonjs migration:generate -d src/data-source.ts",
"migration:run": "npx typeorm-ts-node-commonjs migration:run -d src/data-source.ts",
"migration:revert": "npx typeorm-ts-node-commonjs migration:revert -d src/data-source.ts",
"migration:show": "npx typeorm-ts-node-commonjs migration:show -d src/data-source.ts",
"migration:create": "npx typeorm-ts-node-commonjs migration:create"
},
"dependencies": {
"@nestjs-modules/mailer": "^2.0.2",
Expand Down
58 changes: 6 additions & 52 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,15 @@ import { OidcModule } from './modules/oidc/oidc.module';
import { SysinfoModule } from './modules/sysinfo/sysinfo.module';
import { DashboardModule } from './modules/dashboard/dashboard.module';
import { DatabaseModule } from './database/database.module';
import { Sysinfo, Peer } from './common/entities';
import { ConnectionAudit } from './modules/audit/entities/connection-audit.entity';
import { FileAudit } from './modules/audit/entities/file-audit.entity';
import { AlarmAudit } from './modules/audit/entities/alarm-audit.entity';
import { AddressBook } from './modules/address-book/entities/address-book.entity';
import { AddressBookPeer } from './modules/address-book/entities/address-book-peer.entity';
import { AddressBookTag } from './modules/address-book/entities/address-book-tag.entity';
import { AddressBookPeerTag } from './modules/address-book/entities/address-book-peer-tag.entity';
import { AddressBookRule } from './modules/address-book/entities/address-book-rule.entity';
import { User } from './modules/user/entities/user.entity';
import { UserToken } from './modules/user/entities/user-token.entity';
import { OidcProvider } from './modules/oidc/entities/oidc-provider.entity';
import { OidcAuthState } from './modules/oidc/entities/oidc-auth-state.entity';
import { DeviceGroup } from './modules/device-group/entities/device-group.entity';
import { DeviceGroupUserPermission } from './modules/device-group/entities/device-group-user-permission.entity';
import { UserUserPermission } from './modules/device-group/entities/user-user-permission.entity';
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
import { LoginSession } from './modules/auth/entities/login-session.entity';
import { PasskeyCredential } from './modules/auth/entities/passkey-credential.entity';
import { SystemSetting } from './modules/settings/entities/system-setting.entity';
import { ActiveConnection } from './modules/heartbeat/entities/active-connection.entity';
import { SettingsModule } from './modules/settings/settings.module';
import { LdapModule } from './modules/ldap/ldap.module';
import { StrategyModule } from './modules/strategy/strategy.module';
import { Strategy } from './modules/strategy/entities/strategy.entity';
import { UpdateCheckModule } from './modules/update-check/update-check.module';
import { NexusModule } from './modules/nexus/nexus.module';
import { NexusToken } from './modules/nexus/entities/nexus-token.entity';
import { NexusBuild } from './modules/nexus/entities/nexus-build.entity';
import { UserGroupModule } from './modules/user-group/user-group.module';
import { UserGroup } from './modules/user-group/entities/user-group.entity';
import { ALL_ENTITIES } from './entities';
import { InitialSchema1785305180672 } from './migrations/1785305180672-InitialSchema';

/**
* 应用根模块
Expand Down Expand Up @@ -82,34 +60,10 @@ import { UserGroup } from './modules/user-group/entities/user-group.entity';
TypeOrmModule.forRoot({
type: 'sqlite',
database: process.env.DB_PATH || 'rustdesk-console.db',
entities: [
Sysinfo,
Peer,
ConnectionAudit,
FileAudit,
AlarmAudit,
AddressBook,
AddressBookPeer,
AddressBookTag,
AddressBookPeerTag,
AddressBookRule,
User,
UserToken,
OidcProvider,
OidcAuthState,
DeviceGroup,
DeviceGroupUserPermission,
UserUserPermission,
LoginSession,
PasskeyCredential,
SystemSetting,
ActiveConnection,
Strategy,
NexusToken,
NexusBuild,
UserGroup,
],
synchronize: true,
entities: ALL_ENTITIES,
synchronize: process.env.NODE_ENV !== 'production',
migrationsRun: false,
migrations: [InitialSchema1785305180672],
logging: false,
}),
DatabaseModule,
Expand Down
20 changes: 20 additions & 0 deletions src/data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'reflect-metadata';
import 'dotenv/config';
import { DataSource } from 'typeorm';
import { ALL_ENTITIES } from './entities';
import { InitialSchema1785305180672 } from './migrations/1785305180672-InitialSchema';

/**
* TypeORM CLI 专用数据源配置
* 用于 migration:generate / migration:run / migration:revert 等 CLI 命令
*
* 注意:此文件独立于 NestJS 运行,通过 ts-node 加载
* 实体列表与 app.module.ts 共享 src/entities/index.ts
*/
export default new DataSource({
type: 'sqlite',
database: process.env.DB_PATH || 'rustdesk-console.db',
entities: ALL_ENTITIES,
migrations: [InitialSchema1785305180672],
synchronize: false,
});
4 changes: 4 additions & 0 deletions src/database/database-init.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { User, UserStatus } from '../modules/user/entities/user.entity';
import { OidcProvider } from '../modules/oidc/entities/oidc-provider.entity';
import { OidcAuthState } from '../modules/oidc/entities/oidc-auth-state.entity';
import { UserGroupService } from '../modules/user-group/user-group.service';
import { MigrationService } from './migration.service';

@Injectable()
/**
Expand All @@ -26,10 +27,13 @@ export class DatabaseInitService implements OnModuleInit {
private oidcProviderRepository: Repository<OidcProvider>,
@InjectRepository(OidcAuthState)
private oidcAuthStateRepository: Repository<OidcAuthState>,
private readonly migrationService: MigrationService,
private readonly userGroupService: UserGroupService,
) {}

async onModuleInit() {
// 在种子数据初始化之前运行数据库迁移(生产环境)
await this.migrationService.runMigrationsIfNeeded();
const defaultGroup = await this.userGroupService.initializeStorage();
await this.createDefaultAdmin(defaultGroup.guid);
await this.createDefaultOidcProviders();
Expand Down
8 changes: 5 additions & 3 deletions src/database/database.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { OidcProvider } from '../modules/oidc/entities/oidc-provider.entity';
import { OidcAuthState } from '../modules/oidc/entities/oidc-auth-state.entity';
import { SystemSetting } from '../modules/settings/entities/system-setting.entity';
import { DatabaseInitService } from './database-init.service';
import { MigrationService } from './migration.service';
import { UserGroupModule } from '../modules/user-group/user-group.module';

@Global()
Expand All @@ -14,7 +15,8 @@ import { UserGroupModule } from '../modules/user-group/user-group.module';
* 负责数据库连接和初始化配置
*
* 提供服务:
* - DatabaseInitService
* - MigrationService - 生产环境数据库迁移执行
* - DatabaseInitService - 种子数据初始化
*/
@Module({
imports: [
Expand All @@ -27,7 +29,7 @@ import { UserGroupModule } from '../modules/user-group/user-group.module';
]),
UserGroupModule,
],
providers: [DatabaseInitService],
exports: [DatabaseInitService],
providers: [MigrationService, DatabaseInitService],
exports: [MigrationService, DatabaseInitService],
})
export class DatabaseModule {}
113 changes: 113 additions & 0 deletions src/database/migration.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

/**
* MigrationService
* 在生产环境中负责数据库迁移的执行
*
* 职责:
* 1. 检测已有数据库(从 synchronize 迁移过来的),标记初始迁移为已执行
* 2. 执行所有待处理的数据库迁移
*
* 设计说明:
* 此服务不使用 OnModuleInit,而是由 DatabaseInitService 在种子数据初始化前显式调用,
* 以确保迁移在种子数据操作之前完成。
* 不能使用 migrationsRun: true,因为 TypeORM 在 DataSource 初始化阶段执行迁移,
* 早于任何 NestJS 的 OnModuleInit 钩子,无法在迁移执行前标记已有数据库的初始迁移。
*/
@Injectable()
export class MigrationService {
private readonly logger = new Logger(MigrationService.name);

constructor(
@InjectDataSource()
private dataSource: DataSource,
) {}

/**
* 在生产环境中运行数据库迁移(包括已有数据库的初始迁移标记)
* 应在 DatabaseInitService 的种子数据操作之前调用
*/
async runMigrationsIfNeeded(): Promise<void> {
if (process.env.NODE_ENV !== 'production') return;

await this.markInitialMigrationAsRunIfNeeded();
await this.runPendingMigrations();
}

/**
* 执行所有待处理的数据库迁移
*/
private async runPendingMigrations(): Promise<void> {
try {
const hasPending = await this.dataSource.showMigrations();
if (hasPending) {
this.logger.log('Running pending database migrations...');
await this.dataSource.runMigrations({ transaction: 'all' });
this.logger.log('Migrations completed successfully');
} else {
this.logger.log('No pending migrations');
}
} catch (error) {
this.logger.error(
`Failed to run migrations: ${error instanceof Error ? error.message : String(error)}`,
);
throw error;
}
}

/**
* 标记初始迁移为已执行(针对从 synchronize 迁移过来的已有数据库)
*
* 场景:生产环境中已有通过 synchronize: true 创建的数据库,
* 启用迁移后 TypeORM 会认为初始迁移未执行而尝试重新建表。
* 此方法检测已有数据库并标记初始迁移为已执行,避免重复建表。
*/
private async markInitialMigrationAsRunIfNeeded(): Promise<void> {
const initialMigrationName = 'InitialSchema1785305180672';

try {
// 检查 migrations 表是否存在
const hasMigrationsTable = await this.dataSource.query(

Check failure on line 72 in src/database/migration.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
"SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'",
);

if (hasMigrationsTable.length === 0) {

Check failure on line 76 in src/database/migration.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .length on an `any` value
// migrations 表不存在,说明是全新数据库,TypeORM 会正常执行迁移
return;
}

// 检查初始迁移是否已记录
const existingRecord = await this.dataSource.query(

Check failure on line 82 in src/database/migration.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
'SELECT * FROM migrations WHERE name = ?',
[initialMigrationName],
);

if (existingRecord.length > 0) {

Check failure on line 87 in src/database/migration.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .length on an `any` value
// 初始迁移已记录,无需处理
return;
}

// migrations 表存在但初始迁移未记录 → 检查核心表是否存在
const hasUsersTable = await this.dataSource.query(

Check failure on line 93 in src/database/migration.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
"SELECT name FROM sqlite_master WHERE type='table' AND name='users'",
);

if (hasUsersTable.length > 0) {

Check failure on line 97 in src/database/migration.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .length on an `any` value
// 核心表存在,说明是从 synchronize 迁移过来的已有数据库
await this.dataSource.query(
'INSERT INTO migrations (timestamp, name) VALUES (?, ?)',
[1785305180672, initialMigrationName],
);
this.logger.log(
'Marked initial migration as already executed for existing database',
);
}
} catch (error) {
this.logger.warn(
`Failed to check migration status: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
60 changes: 60 additions & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* 集中实体列表
* 统一管理所有 TypeORM 实体,供 app.module.ts 和 data-source.ts 共享
* 避免两处维护不同的实体列表导致不同步
*/
import { Sysinfo } from '../common/entities/sysinfo.entity';
import { Peer } from '../common/entities/peer.entity';
import { ConnectionAudit } from '../modules/audit/entities/connection-audit.entity';
import { FileAudit } from '../modules/audit/entities/file-audit.entity';
import { AlarmAudit } from '../modules/audit/entities/alarm-audit.entity';
import { AddressBook } from '../modules/address-book/entities/address-book.entity';
import { AddressBookPeer } from '../modules/address-book/entities/address-book-peer.entity';
import { AddressBookTag } from '../modules/address-book/entities/address-book-tag.entity';
import { AddressBookPeerTag } from '../modules/address-book/entities/address-book-peer-tag.entity';
import { AddressBookRule } from '../modules/address-book/entities/address-book-rule.entity';
import { User } from '../modules/user/entities/user.entity';
import { UserToken } from '../modules/user/entities/user-token.entity';
import { Invitation } from '../modules/user/entities/invitation.entity';
import { OidcProvider } from '../modules/oidc/entities/oidc-provider.entity';
import { OidcAuthState } from '../modules/oidc/entities/oidc-auth-state.entity';
import { DeviceGroup } from '../modules/device-group/entities/device-group.entity';
import { DeviceGroupUserPermission } from '../modules/device-group/entities/device-group-user-permission.entity';
import { UserUserPermission } from '../modules/device-group/entities/user-user-permission.entity';
import { LoginSession } from '../modules/auth/entities/login-session.entity';
import { PasskeyCredential } from '../modules/auth/entities/passkey-credential.entity';
import { SystemSetting } from '../modules/settings/entities/system-setting.entity';
import { ActiveConnection } from '../modules/heartbeat/entities/active-connection.entity';
import { Strategy } from '../modules/strategy/entities/strategy.entity';
import { NexusToken } from '../modules/nexus/entities/nexus-token.entity';
import { NexusBuild } from '../modules/nexus/entities/nexus-build.entity';
import { UserGroup } from '../modules/user-group/entities/user-group.entity';

export const ALL_ENTITIES = [
Sysinfo,
Peer,
ConnectionAudit,
FileAudit,
AlarmAudit,
AddressBook,
AddressBookPeer,
AddressBookTag,
AddressBookPeerTag,
AddressBookRule,
User,
UserToken,
Invitation,
OidcProvider,
OidcAuthState,
DeviceGroup,
DeviceGroupUserPermission,
UserUserPermission,
LoginSession,
PasskeyCredential,
SystemSetting,
ActiveConnection,
Strategy,
NexusToken,
NexusBuild,
UserGroup,
];
Loading
Loading