Skip to content

Commit 6225136

Browse files
authored
Merge pull request #680 from kubero-dev/feature/add-admin-reset-cli
Feature / add admin reset command
2 parents 3ba4013 + 5f6a959 commit 6225136

File tree

9 files changed

+294
-7
lines changed

9 files changed

+294
-7
lines changed

server/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ $ yarn run start:dev
2121
$ yarn run start:prod
2222
```
2323

24+
## Administration Commands
25+
26+
```bash
27+
# Reset admin account (creates or updates with new password)
28+
$ yarn cli:reset-admin
29+
```
30+
2431
## Run tests
2532

2633
```bash

server/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"dev": "nest start --debug --watch",
1616
"start:prod": "node dist/main",
1717
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18+
"cli": "ts-node -r tsconfig-paths/register src/cli.ts",
19+
"cli:reset-admin": "npm run cli reset-admin",
1820
"test": "jest",
1921
"test:ci": "jest --ci --reporters=default --reporters=jest-junit",
2022
"test:watch": "jest --watch",
@@ -44,6 +46,7 @@
4446
"@nestjs/serve-static": "^5.0.1",
4547
"@nestjs/swagger": "^11.0.3",
4648
"@nestjs/websockets": "^11.0.7",
49+
"nest-commander": "^3.13.0",
4750
"@octokit/core": "^6.1.3",
4851
"@prisma/client": "^6.9.0",
4952
"@types/bcrypt": "^5.0.2",

server/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { DatabaseModule } from './database/database.module';
2424
import { GroupModule } from './groups/groups.module';
2525
import { RolesModule } from './roles/roles.module';
2626
import { TokenModule } from './token/token.module';
27+
import { CliModule } from './cli/cli.module';
2728

2829
@Module({
2930
imports: [
@@ -49,6 +50,7 @@ import { TokenModule } from './token/token.module';
4950
GroupModule,
5051
RolesModule,
5152
TokenModule,
53+
CliModule,
5254
],
5355
controllers: [AppController, TemplatesController],
5456
providers: [AppService, TemplatesService],

server/src/cli.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env node
2+
import { CommandFactory } from 'nest-commander';
3+
import { CliModule } from './cli/cli.module';
4+
5+
async function bootstrap() {
6+
await CommandFactory.run(CliModule, ['warn', 'error', 'log']);
7+
}
8+
9+
bootstrap();

server/src/cli/cli.module.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Module } from '@nestjs/common';
2+
import { ResetAdminCommand } from './commands/reset-admin.command';
3+
import { DatabaseService } from '../database/database.service';
4+
5+
@Module({
6+
providers: [
7+
ResetAdminCommand,
8+
DatabaseService,
9+
],
10+
})
11+
export class CliModule {}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { ResetAdminCommand } from './reset-admin.command';
3+
import { DatabaseService } from '../../database/database.service';
4+
import { Logger } from '@nestjs/common';
5+
6+
describe('ResetAdminCommand', () => {
7+
let command: ResetAdminCommand;
8+
let databaseService: jest.Mocked<DatabaseService>;
9+
let logger: jest.Mocked<Logger>;
10+
11+
beforeEach(async () => {
12+
const module: TestingModule = await Test.createTestingModule({
13+
providers: [
14+
ResetAdminCommand,
15+
{
16+
provide: DatabaseService,
17+
useValue: {
18+
resetAdminUser: jest.fn(),
19+
},
20+
},
21+
],
22+
}).compile();
23+
24+
command = module.get<ResetAdminCommand>(ResetAdminCommand);
25+
databaseService = module.get(DatabaseService);
26+
27+
// Mock the logger
28+
logger = {
29+
log: jest.fn(),
30+
error: jest.fn(),
31+
warn: jest.fn(),
32+
debug: jest.fn(),
33+
verbose: jest.fn(),
34+
} as any;
35+
(command as any).logger = logger;
36+
});
37+
38+
it('should be defined', () => {
39+
expect(command).toBeDefined();
40+
});
41+
42+
describe('run', () => {
43+
let exitSpy: jest.SpyInstance;
44+
45+
beforeEach(() => {
46+
exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {}) as (code?: number) => never);
47+
// We need to call the original implementation of run
48+
jest.spyOn(command, 'run').mockRestore();
49+
});
50+
51+
afterEach(() => {
52+
exitSpy.mockRestore();
53+
});
54+
55+
it('should reset the admin account and exit with code 0', async () => {
56+
await command.run();
57+
expect(logger.log).toHaveBeenCalledWith('Resetting admin account...');
58+
expect(databaseService.resetAdminUser).toHaveBeenCalled();
59+
expect(logger.log).toHaveBeenCalledWith('Admin account has been reset successfully');
60+
expect(exitSpy).toHaveBeenCalledWith(0);
61+
});
62+
63+
it('should log an error and exit with code 1 if resetting fails', async () => {
64+
const error = new Error('Test error');
65+
databaseService.resetAdminUser.mockRejectedValue(error);
66+
await command.run();
67+
expect(logger.log).toHaveBeenCalledWith('Resetting admin account...');
68+
expect(databaseService.resetAdminUser).toHaveBeenCalled();
69+
expect(logger.error).toHaveBeenCalledWith('Failed to reset admin account', error);
70+
expect(exitSpy).toHaveBeenCalledWith(1);
71+
});
72+
});
73+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Command, CommandRunner } from 'nest-commander';
2+
import { Injectable, Logger } from '@nestjs/common';
3+
import { DatabaseService } from '../../database/database.service';
4+
5+
@Command({ name: 'reset-admin', description: 'Reset the admin account with a new random password' })
6+
@Injectable()
7+
export class ResetAdminCommand extends CommandRunner {
8+
private readonly logger = new Logger(ResetAdminCommand.name);
9+
10+
constructor(private readonly databaseService: DatabaseService) {
11+
super();
12+
}
13+
14+
async run(): Promise<void> {
15+
this.logger.log('Resetting admin account...');
16+
17+
try {
18+
await this.databaseService.resetAdminUser();
19+
this.logger.log('Admin account has been reset successfully');
20+
} catch (error) {
21+
this.logger.error('Failed to reset admin account', error);
22+
process.exit(1);
23+
}
24+
25+
process.exit(0);
26+
}
27+
}

server/src/database/database.service.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,78 @@ export class DatabaseService {
167167
}
168168
}
169169

170+
/**
171+
* Resets the admin user account with a new random password.
172+
* If the admin user doesn't exist, it creates one.
173+
* Prints the new username and password to the console.
174+
* @returns {Promise<void>}
175+
*/
176+
async resetAdminUser(): Promise<void> {
177+
const prisma = new PrismaClient();
178+
const adminUser = process.env.KUBERO_ADMIN_USERNAME || 'admin';
179+
const adminEmail = process.env.KUBERO_ADMIN_EMAIL || '[email protected]';
180+
const role = process.env.KUBERO_SYSTEM_USER_ROLE || 'admin';
181+
const userGroups = ['everyone', 'admin'];
182+
183+
try {
184+
// Generate a random password
185+
const plainPassword = crypto
186+
.randomBytes(25)
187+
.toString('base64')
188+
.slice(0, 19);
189+
// Create bcrypt hash
190+
const passwordHash = await bcrypt.hash(plainPassword, 10);
191+
192+
// Check if admin user exists
193+
const existingUser = await prisma.user.findUnique({
194+
where: { id: '2' },
195+
});
196+
197+
if (existingUser) {
198+
// Update existing admin user
199+
await prisma.user.update({
200+
where: { id: '2' },
201+
data: {
202+
password: passwordHash,
203+
updatedAt: new Date(),
204+
},
205+
});
206+
console.log('\n\n\n', 'Admin account has been reset');
207+
} else {
208+
// Create new admin user
209+
await prisma.user.create({
210+
data: {
211+
id: '2',
212+
username: adminUser,
213+
email: adminEmail,
214+
password: passwordHash,
215+
isActive: true,
216+
role: { connect: { name: role } },
217+
userGroups:
218+
userGroups && Array.isArray(userGroups)
219+
? {
220+
connect: userGroups.map((g: any) => ({ name: g })),
221+
}
222+
: undefined,
223+
createdAt: new Date(),
224+
updatedAt: new Date(),
225+
},
226+
});
227+
console.log('\n\n\n', 'New admin account created');
228+
}
229+
230+
console.log(' username: ', adminUser);
231+
console.log(' password: ', plainPassword);
232+
console.log(' email: ', adminEmail, '\n\n\n');
233+
234+
this.logger.log('Admin user reset successfully.');
235+
return;
236+
} catch (error) {
237+
this.logger.error('Failed to reset admin user.', error);
238+
throw error;
239+
}
240+
}
241+
170242
private async migrateLegeacyUsers() {
171243
const prisma = new PrismaClient();
172244

0 commit comments

Comments
 (0)