diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index f689dcf7a8..e8d6221ce5 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -8,6 +8,7 @@ import { ConfirmationQuestion } from 'src/database/commands/questions/confirmati import { UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/upgrade-version-command.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; +import { GenerateApiKeyCommand } from 'src/engine/core-modules/api-key/commands/generate-api-key.command'; import { EventLogCleanupModule } from 'src/engine/core-modules/event-logs/cleanup/event-log-cleanup.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @@ -60,6 +61,7 @@ import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/au ConfirmationQuestion, CronRegisterAllCommand, ListOrphanedWorkspaceEntitiesCommand, + GenerateApiKeyCommand, ], }) export class DatabaseCommandModule {} diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts index 31ef7b227e..cbd3e2c31a 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { ApiKeyResolver } from 'src/engine/core-modules/api-key/api-key.resolver'; +import { GenerateApiKeyCommand } from 'src/engine/core-modules/api-key/commands/generate-api-key.command'; import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service'; import { ApiKeyService } from 'src/engine/core-modules/api-key/services/api-key.service'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; @@ -34,8 +35,18 @@ import { ApiKeyController } from './controllers/api-key.controller'; TokenModule, PermissionsModule, ], - providers: [ApiKeyService, ApiKeyResolver, ApiKeyRoleService], + providers: [ + ApiKeyService, + ApiKeyResolver, + ApiKeyRoleService, + GenerateApiKeyCommand, + ], controllers: [ApiKeyController], - exports: [ApiKeyService, ApiKeyRoleService, TypeOrmModule], + exports: [ + ApiKeyService, + ApiKeyRoleService, + TypeOrmModule, + GenerateApiKeyCommand, + ], }) export class ApiKeyModule {} diff --git a/packages/twenty-server/src/engine/core-modules/api-key/commands/generate-api-key.command.ts b/packages/twenty-server/src/engine/core-modules/api-key/commands/generate-api-key.command.ts new file mode 100644 index 0000000000..f3d649ffa7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/api-key/commands/generate-api-key.command.ts @@ -0,0 +1,149 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { addDays } from 'date-fns'; +import { Command, CommandRunner, Option } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; + +import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; +import { ApiKeyService } from 'src/engine/core-modules/api-key/services/api-key.service'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant'; + +type GenerateApiKeyCommandOptions = { + workspaceId: string; + name: string; + expiresIn?: number; +}; + +const NEVER_EXPIRE_DAYS = 100 * 365; + +@Command({ + name: 'workspace:generate-api-key', + description: 'Generate an API key for a workspace and output the token', +}) +export class GenerateApiKeyCommand extends CommandRunner { + private readonly logger = new Logger(GenerateApiKeyCommand.name); + + constructor( + @InjectRepository(WorkspaceEntity) + private readonly workspaceRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, + private readonly apiKeyService: ApiKeyService, + private readonly twentyConfigService: TwentyConfigService, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id ', + description: 'Workspace ID (required)', + required: true, + }) + parseWorkspaceId(value: string): string { + return value; + } + + @Option({ + flags: '-n, --name ', + description: 'Name of the API key', + defaultValue: 'Developer API Key', + }) + parseName(value: string): string { + return value; + } + + @Option({ + flags: '-e, --expires-in ', + description: + 'Number of days until expiration. Omit for a non-expiring key.', + }) + parseExpiresIn(value: string): number { + const days = parseInt(value, 10); + + if (isNaN(days) || days <= 0) { + throw new Error('--expires-in must be a positive number of days'); + } + + return days; + } + + async run( + _passedParams: string[], + options: GenerateApiKeyCommandOptions, + ): Promise { + const nodeEnv = this.twentyConfigService.get('NODE_ENV'); + + if ( + nodeEnv !== NodeEnvironment.DEVELOPMENT && + nodeEnv !== NodeEnvironment.TEST + ) { + throw new Error( + 'This command is only available in development or test environments', + ); + } + + const expiresAt = addDays( + new Date(), + options.expiresIn ?? NEVER_EXPIRE_DAYS, + ); + + const workspace = await this.workspaceRepository.findOne({ + where: { id: options.workspaceId }, + }); + + if (!isDefined(workspace)) { + this.logger.error(`Workspace ${options.workspaceId} not found.`); + + return; + } + + const adminRole = await this.roleRepository.findOne({ + where: { + workspaceId: workspace.id, + universalIdentifier: STANDARD_ROLE.admin.universalIdentifier, + }, + }); + + if (!isDefined(adminRole)) { + this.logger.error(`No Admin role found for workspace ${workspace.id}.`); + + return; + } + + let apiKey: ApiKeyEntity; + + try { + apiKey = await this.apiKeyService.create({ + name: options.name, + expiresAt, + workspaceId: workspace.id, + roleId: adminRole.id, + }); + } catch (error) { + this.logger.error(`Failed to create API key: ${error}`); + + return; + } + + const tokenResult = await this.apiKeyService.generateApiKeyToken( + workspace.id, + apiKey.id, + expiresAt, + ); + + if (!isDefined(tokenResult)) { + this.logger.error('Failed to generate token.'); + + return; + } + + console.log(`TOKEN:${tokenResult.token}\n`); + } +}