@friggframework/admin-scripts 2.0.0--canary.517.41839c5.0 → 2.0.0--canary.517.a37d697.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +2 -2
- package/package.json +6 -6
- package/src/application/__tests__/admin-frigg-commands.test.js +18 -18
- package/src/application/__tests__/script-runner.test.js +12 -12
- package/src/application/admin-frigg-commands.js +7 -7
- package/src/application/admin-script-base.js +2 -4
- package/src/application/script-runner.js +4 -4
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
- package/src/infrastructure/__tests__/admin-script-router.test.js +24 -25
- package/src/infrastructure/admin-auth-middleware.js +5 -43
- package/src/infrastructure/admin-script-router.js +14 -16
- package/src/infrastructure/script-executor-handler.js +2 -2
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const { AdminFriggCommands, createAdminFriggCommands } = require('./src/applicat
|
|
|
12
12
|
const { ScriptRunner, createScriptRunner } = require('./src/application/script-runner');
|
|
13
13
|
|
|
14
14
|
// Infrastructure
|
|
15
|
-
const {
|
|
15
|
+
const { validateAdminApiKey } = require('./src/infrastructure/admin-auth-middleware');
|
|
16
16
|
const { router, app, handler: routerHandler } = require('./src/infrastructure/admin-script-router');
|
|
17
17
|
const { handler: executorHandler } = require('./src/infrastructure/script-executor-handler');
|
|
18
18
|
|
|
@@ -45,7 +45,7 @@ module.exports = {
|
|
|
45
45
|
createScriptRunner,
|
|
46
46
|
|
|
47
47
|
// Infrastructure layer
|
|
48
|
-
|
|
48
|
+
validateAdminApiKey,
|
|
49
49
|
router,
|
|
50
50
|
app,
|
|
51
51
|
routerHandler,
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/admin-scripts",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.517.
|
|
4
|
+
"version": "2.0.0--canary.517.a37d697.0",
|
|
5
5
|
"description": "Admin Script Runner for Frigg - Execute maintenance and operational scripts in hosted environments",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@aws-sdk/client-scheduler": "^3.588.0",
|
|
8
|
-
"@friggframework/core": "2.0.0--canary.517.
|
|
8
|
+
"@friggframework/core": "2.0.0--canary.517.a37d697.0",
|
|
9
9
|
"bcryptjs": "^2.4.3",
|
|
10
10
|
"express": "^4.18.2",
|
|
11
11
|
"lodash": "4.17.21",
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"uuid": "^9.0.1"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@friggframework/eslint-config": "2.0.0--canary.517.
|
|
18
|
-
"@friggframework/prettier-config": "2.0.0--canary.517.
|
|
19
|
-
"@friggframework/test": "2.0.0--canary.517.
|
|
17
|
+
"@friggframework/eslint-config": "2.0.0--canary.517.a37d697.0",
|
|
18
|
+
"@friggframework/prettier-config": "2.0.0--canary.517.a37d697.0",
|
|
19
|
+
"@friggframework/test": "2.0.0--canary.517.a37d697.0",
|
|
20
20
|
"chai": "^4.3.6",
|
|
21
21
|
"eslint": "^8.22.0",
|
|
22
22
|
"jest": "^29.7.0",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"maintenance",
|
|
50
50
|
"operations"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "a37d697613667fff814938b1f30ee2d834c4ffcd"
|
|
53
53
|
}
|
|
@@ -5,7 +5,7 @@ jest.mock('@friggframework/core/integrations/repositories/integration-repository
|
|
|
5
5
|
jest.mock('@friggframework/core/user/repositories/user-repository-factory');
|
|
6
6
|
jest.mock('@friggframework/core/modules/repositories/module-repository-factory');
|
|
7
7
|
jest.mock('@friggframework/core/credential/repositories/credential-repository-factory');
|
|
8
|
-
jest.mock('@friggframework/core/admin-scripts/repositories/
|
|
8
|
+
jest.mock('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
|
|
9
9
|
jest.mock('@friggframework/core/queues');
|
|
10
10
|
|
|
11
11
|
describe('AdminFriggCommands', () => {
|
|
@@ -13,7 +13,7 @@ describe('AdminFriggCommands', () => {
|
|
|
13
13
|
let mockUserRepo;
|
|
14
14
|
let mockModuleRepo;
|
|
15
15
|
let mockCredentialRepo;
|
|
16
|
-
let
|
|
16
|
+
let mockAdminProcessRepo;
|
|
17
17
|
let mockQueuerUtil;
|
|
18
18
|
|
|
19
19
|
beforeEach(() => {
|
|
@@ -46,8 +46,8 @@ describe('AdminFriggCommands', () => {
|
|
|
46
46
|
updateCredential: jest.fn(),
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
mockAdminProcessRepo = {
|
|
50
|
+
appendProcessLog: jest.fn().mockResolvedValue(undefined),
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
mockQueuerUtil = {
|
|
@@ -60,14 +60,14 @@ describe('AdminFriggCommands', () => {
|
|
|
60
60
|
const { createUserRepository } = require('@friggframework/core/user/repositories/user-repository-factory');
|
|
61
61
|
const { createModuleRepository } = require('@friggframework/core/modules/repositories/module-repository-factory');
|
|
62
62
|
const { createCredentialRepository } = require('@friggframework/core/credential/repositories/credential-repository-factory');
|
|
63
|
-
const {
|
|
63
|
+
const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
|
|
64
64
|
const { QueuerUtil } = require('@friggframework/core/queues');
|
|
65
65
|
|
|
66
66
|
createIntegrationRepository.mockReturnValue(mockIntegrationRepo);
|
|
67
67
|
createUserRepository.mockReturnValue(mockUserRepo);
|
|
68
68
|
createModuleRepository.mockReturnValue(mockModuleRepo);
|
|
69
69
|
createCredentialRepository.mockReturnValue(mockCredentialRepo);
|
|
70
|
-
|
|
70
|
+
createAdminProcessRepository.mockReturnValue(mockAdminProcessRepo);
|
|
71
71
|
|
|
72
72
|
// Mock QueuerUtil methods
|
|
73
73
|
QueuerUtil.send = mockQueuerUtil.send;
|
|
@@ -158,16 +158,16 @@ describe('AdminFriggCommands', () => {
|
|
|
158
158
|
expect(repo).toBe(mockCredentialRepo);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
it('creates
|
|
161
|
+
it('creates adminProcessRepository on first access', () => {
|
|
162
162
|
const commands = new AdminFriggCommands();
|
|
163
|
-
const {
|
|
163
|
+
const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
|
|
164
164
|
|
|
165
|
-
expect(
|
|
165
|
+
expect(createAdminProcessRepository).not.toHaveBeenCalled();
|
|
166
166
|
|
|
167
|
-
const repo = commands.
|
|
167
|
+
const repo = commands.adminProcessRepository;
|
|
168
168
|
|
|
169
|
-
expect(
|
|
170
|
-
expect(repo).toBe(
|
|
169
|
+
expect(createAdminProcessRepository).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(repo).toBe(mockAdminProcessRepo);
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
|
|
@@ -551,15 +551,15 @@ describe('AdminFriggCommands', () => {
|
|
|
551
551
|
it('log() persists if executionId set', async () => {
|
|
552
552
|
const commands = new AdminFriggCommands({ executionId: 'exec_123' });
|
|
553
553
|
// Force repository creation
|
|
554
|
-
commands.
|
|
554
|
+
commands.adminProcessRepository;
|
|
555
555
|
|
|
556
556
|
commands.log('warn', 'Warning message', { detail: 'xyz' });
|
|
557
557
|
|
|
558
558
|
// Give async operation a chance to execute
|
|
559
559
|
await new Promise(resolve => setImmediate(resolve));
|
|
560
560
|
|
|
561
|
-
expect(
|
|
562
|
-
const callArgs =
|
|
561
|
+
expect(mockAdminProcessRepo.appendProcessLog).toHaveBeenCalled();
|
|
562
|
+
const callArgs = mockAdminProcessRepo.appendProcessLog.mock.calls[0];
|
|
563
563
|
expect(callArgs[0]).toBe('exec_123');
|
|
564
564
|
expect(callArgs[1].level).toBe('warn');
|
|
565
565
|
expect(callArgs[1].message).toBe('Warning message');
|
|
@@ -572,14 +572,14 @@ describe('AdminFriggCommands', () => {
|
|
|
572
572
|
|
|
573
573
|
await new Promise(resolve => setImmediate(resolve));
|
|
574
574
|
|
|
575
|
-
expect(
|
|
575
|
+
expect(mockAdminProcessRepo.appendProcessLog).not.toHaveBeenCalled();
|
|
576
576
|
});
|
|
577
577
|
|
|
578
578
|
it('log() handles persistence failure gracefully', async () => {
|
|
579
579
|
const commands = new AdminFriggCommands({ executionId: 'exec_123' });
|
|
580
580
|
// Force repository creation
|
|
581
|
-
commands.
|
|
582
|
-
|
|
581
|
+
commands.adminProcessRepository;
|
|
582
|
+
mockAdminProcessRepo.appendProcessLog.mockRejectedValue(new Error('DB Error'));
|
|
583
583
|
|
|
584
584
|
// Should not throw
|
|
585
585
|
expect(() => commands.log('error', 'Test error')).not.toThrow();
|
|
@@ -36,9 +36,9 @@ describe('ScriptRunner', () => {
|
|
|
36
36
|
scriptFactory = new ScriptFactory([TestScript]);
|
|
37
37
|
|
|
38
38
|
mockCommands = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
createAdminProcess: jest.fn(),
|
|
40
|
+
updateAdminProcessState: jest.fn(),
|
|
41
|
+
completeAdminProcess: jest.fn(),
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
mockFrigg = {
|
|
@@ -49,11 +49,11 @@ describe('ScriptRunner', () => {
|
|
|
49
49
|
createAdminScriptCommands.mockReturnValue(mockCommands);
|
|
50
50
|
createAdminFriggCommands.mockReturnValue(mockFrigg);
|
|
51
51
|
|
|
52
|
-
mockCommands.
|
|
52
|
+
mockCommands.createAdminProcess.mockResolvedValue({
|
|
53
53
|
id: 'exec-123',
|
|
54
54
|
});
|
|
55
|
-
mockCommands.
|
|
56
|
-
mockCommands.
|
|
55
|
+
mockCommands.updateAdminProcessState.mockResolvedValue({});
|
|
56
|
+
mockCommands.completeAdminProcess.mockResolvedValue({ success: true });
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
afterEach(() => {
|
|
@@ -76,7 +76,7 @@ describe('ScriptRunner', () => {
|
|
|
76
76
|
expect(result.executionId).toBe('exec-123');
|
|
77
77
|
expect(result.metrics.durationMs).toBeGreaterThanOrEqual(0);
|
|
78
78
|
|
|
79
|
-
expect(mockCommands.
|
|
79
|
+
expect(mockCommands.createAdminProcess).toHaveBeenCalledWith({
|
|
80
80
|
scriptName: 'test-script',
|
|
81
81
|
scriptVersion: '1.0.0',
|
|
82
82
|
trigger: 'MANUAL',
|
|
@@ -85,12 +85,12 @@ describe('ScriptRunner', () => {
|
|
|
85
85
|
audit: { apiKeyName: 'test-key' },
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
expect(mockCommands.
|
|
88
|
+
expect(mockCommands.updateAdminProcessState).toHaveBeenCalledWith(
|
|
89
89
|
'exec-123',
|
|
90
90
|
'RUNNING'
|
|
91
91
|
);
|
|
92
92
|
|
|
93
|
-
expect(mockCommands.
|
|
93
|
+
expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
|
|
94
94
|
'exec-123',
|
|
95
95
|
expect.objectContaining({
|
|
96
96
|
status: 'COMPLETED',
|
|
@@ -128,7 +128,7 @@ describe('ScriptRunner', () => {
|
|
|
128
128
|
expect(result.scriptName).toBe('failing-script');
|
|
129
129
|
expect(result.error.message).toBe('Script failed');
|
|
130
130
|
|
|
131
|
-
expect(mockCommands.
|
|
131
|
+
expect(mockCommands.completeAdminProcess).toHaveBeenCalledWith(
|
|
132
132
|
'exec-123',
|
|
133
133
|
expect.objectContaining({
|
|
134
134
|
status: 'FAILED',
|
|
@@ -178,8 +178,8 @@ describe('ScriptRunner', () => {
|
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
expect(result.executionId).toBe('existing-exec-456');
|
|
181
|
-
expect(mockCommands.
|
|
182
|
-
expect(mockCommands.
|
|
181
|
+
expect(mockCommands.createAdminProcess).not.toHaveBeenCalled();
|
|
182
|
+
expect(mockCommands.updateAdminProcessState).toHaveBeenCalledWith(
|
|
183
183
|
'existing-exec-456',
|
|
184
184
|
'RUNNING'
|
|
185
185
|
);
|
|
@@ -25,7 +25,7 @@ class AdminFriggCommands {
|
|
|
25
25
|
this._userRepository = null;
|
|
26
26
|
this._moduleRepository = null;
|
|
27
27
|
this._credentialRepository = null;
|
|
28
|
-
this.
|
|
28
|
+
this._adminProcessRepository = null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// ==================== LAZY-LOADED REPOSITORIES ====================
|
|
@@ -62,12 +62,12 @@ class AdminFriggCommands {
|
|
|
62
62
|
return this._credentialRepository;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
get
|
|
66
|
-
if (!this.
|
|
67
|
-
const {
|
|
68
|
-
this.
|
|
65
|
+
get adminProcessRepository() {
|
|
66
|
+
if (!this._adminProcessRepository) {
|
|
67
|
+
const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
|
|
68
|
+
this._adminProcessRepository = createAdminProcessRepository();
|
|
69
69
|
}
|
|
70
|
-
return this.
|
|
70
|
+
return this._adminProcessRepository;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// ==================== INTEGRATION QUERIES ====================
|
|
@@ -209,7 +209,7 @@ class AdminFriggCommands {
|
|
|
209
209
|
|
|
210
210
|
// Persist to execution record if we have an executionId
|
|
211
211
|
if (this.executionId) {
|
|
212
|
-
this.
|
|
212
|
+
this.adminProcessRepository.appendProcessLog(this.executionId, entry)
|
|
213
213
|
.catch(err => console.error('Failed to persist log:', err));
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { createAdminApiKeyRepository } = require('@friggframework/core/admin-scripts/repositories/admin-api-key-repository-factory');
|
|
1
|
+
const { createAdminProcessRepository } = require('@friggframework/core/admin-scripts/repositories/admin-process-repository-factory');
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Admin Script Base Class
|
|
@@ -87,8 +86,7 @@ class AdminScriptBase {
|
|
|
87
86
|
this.integrationFactory = params.integrationFactory || null;
|
|
88
87
|
|
|
89
88
|
// OPTIONAL: Injected repositories (for testing or custom implementations)
|
|
90
|
-
this.
|
|
91
|
-
this.adminApiKeyRepository = params.adminApiKeyRepository || null;
|
|
89
|
+
this.adminProcessRepository = params.adminProcessRepository || null;
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
/**
|
|
@@ -50,7 +50,7 @@ class ScriptRunner {
|
|
|
50
50
|
|
|
51
51
|
// Create execution record if not provided
|
|
52
52
|
if (!executionId) {
|
|
53
|
-
const execution = await this.commands.
|
|
53
|
+
const execution = await this.commands.createAdminProcess({
|
|
54
54
|
scriptName,
|
|
55
55
|
scriptVersion: definition.version,
|
|
56
56
|
trigger,
|
|
@@ -66,7 +66,7 @@ class ScriptRunner {
|
|
|
66
66
|
try {
|
|
67
67
|
// Update status to RUNNING (skip in dry-run)
|
|
68
68
|
if (!dryRun) {
|
|
69
|
-
await this.commands.
|
|
69
|
+
await this.commands.updateAdminProcessState(executionId, 'RUNNING');
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Create frigg commands for the script
|
|
@@ -99,7 +99,7 @@ class ScriptRunner {
|
|
|
99
99
|
|
|
100
100
|
// Complete execution (skip in dry-run)
|
|
101
101
|
if (!dryRun) {
|
|
102
|
-
await this.commands.
|
|
102
|
+
await this.commands.completeAdminProcess(executionId, {
|
|
103
103
|
status: 'COMPLETED',
|
|
104
104
|
output,
|
|
105
105
|
metrics: {
|
|
@@ -140,7 +140,7 @@ class ScriptRunner {
|
|
|
140
140
|
|
|
141
141
|
// Record failure (skip in dry-run)
|
|
142
142
|
if (!dryRun) {
|
|
143
|
-
await this.commands.
|
|
143
|
+
await this.commands.completeAdminProcess(executionId, {
|
|
144
144
|
status: 'FAILED',
|
|
145
145
|
error: {
|
|
146
146
|
name: error.name,
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { validateAdminApiKey } = require('../admin-auth-middleware');
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
jest.mock('@friggframework/core/application/commands/admin-script-commands', () => ({
|
|
5
|
-
createAdminScriptCommands: jest.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
9
|
-
|
|
10
|
-
describe('adminAuthMiddleware', () => {
|
|
3
|
+
describe('validateAdminApiKey', () => {
|
|
11
4
|
let mockReq;
|
|
12
5
|
let mockRes;
|
|
13
6
|
let mockNext;
|
|
14
|
-
let
|
|
7
|
+
let originalEnv;
|
|
15
8
|
|
|
16
9
|
beforeEach(() => {
|
|
10
|
+
originalEnv = process.env.ADMIN_API_KEY;
|
|
11
|
+
process.env.ADMIN_API_KEY = 'test-admin-key-123';
|
|
12
|
+
|
|
17
13
|
mockReq = {
|
|
18
14
|
headers: {},
|
|
19
|
-
ip: '127.0.0.1',
|
|
20
15
|
};
|
|
21
16
|
|
|
22
17
|
mockRes = {
|
|
@@ -25,124 +20,66 @@ describe('adminAuthMiddleware', () => {
|
|
|
25
20
|
};
|
|
26
21
|
|
|
27
22
|
mockNext = jest.fn();
|
|
28
|
-
|
|
29
|
-
mockCommands = {
|
|
30
|
-
validateAdminApiKey: jest.fn(),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
createAdminScriptCommands.mockReturnValue(mockCommands);
|
|
34
23
|
});
|
|
35
24
|
|
|
36
25
|
afterEach(() => {
|
|
26
|
+
if (originalEnv) {
|
|
27
|
+
process.env.ADMIN_API_KEY = originalEnv;
|
|
28
|
+
} else {
|
|
29
|
+
delete process.env.ADMIN_API_KEY;
|
|
30
|
+
}
|
|
37
31
|
jest.clearAllMocks();
|
|
38
32
|
});
|
|
39
33
|
|
|
40
|
-
describe('
|
|
41
|
-
it('should reject
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
45
|
-
expect(mockRes.json).toHaveBeenCalledWith({
|
|
46
|
-
error: 'Missing or invalid Authorization header',
|
|
47
|
-
code: 'MISSING_AUTH',
|
|
48
|
-
});
|
|
49
|
-
expect(mockNext).not.toHaveBeenCalled();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should reject request with invalid Authorization format', async () => {
|
|
53
|
-
mockReq.headers.authorization = 'InvalidFormat key123';
|
|
34
|
+
describe('Environment configuration', () => {
|
|
35
|
+
it('should reject when ADMIN_API_KEY not configured', () => {
|
|
36
|
+
delete process.env.ADMIN_API_KEY;
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
56
39
|
|
|
57
40
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
58
41
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
59
|
-
error: '
|
|
60
|
-
|
|
42
|
+
error: 'Unauthorized',
|
|
43
|
+
message: 'Admin API key not configured',
|
|
61
44
|
});
|
|
62
45
|
expect(mockNext).not.toHaveBeenCalled();
|
|
63
46
|
});
|
|
64
47
|
});
|
|
65
48
|
|
|
66
|
-
describe('
|
|
67
|
-
it('should reject request
|
|
68
|
-
mockReq
|
|
69
|
-
mockCommands.validateAdminApiKey.mockResolvedValue({
|
|
70
|
-
error: 401,
|
|
71
|
-
reason: 'Invalid API key',
|
|
72
|
-
code: 'INVALID_API_KEY',
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await adminAuthMiddleware(mockReq, mockRes, mockNext);
|
|
49
|
+
describe('Header validation', () => {
|
|
50
|
+
it('should reject request without x-frigg-admin-api-key header', () => {
|
|
51
|
+
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
76
52
|
|
|
77
|
-
expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('invalid-key');
|
|
78
53
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
79
54
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
80
|
-
error: '
|
|
81
|
-
|
|
55
|
+
error: 'Unauthorized',
|
|
56
|
+
message: 'x-frigg-admin-api-key header required',
|
|
82
57
|
});
|
|
83
58
|
expect(mockNext).not.toHaveBeenCalled();
|
|
84
59
|
});
|
|
60
|
+
});
|
|
85
61
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
error: 401,
|
|
90
|
-
reason: 'API key has expired',
|
|
91
|
-
code: 'EXPIRED_API_KEY',
|
|
92
|
-
});
|
|
62
|
+
describe('API key validation', () => {
|
|
63
|
+
it('should reject request with invalid API key', () => {
|
|
64
|
+
mockReq.headers['x-frigg-admin-api-key'] = 'invalid-key';
|
|
93
65
|
|
|
94
|
-
|
|
66
|
+
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
95
67
|
|
|
96
|
-
expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('expired-key');
|
|
97
68
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
98
69
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
99
|
-
error: '
|
|
100
|
-
|
|
70
|
+
error: 'Unauthorized',
|
|
71
|
+
message: 'Invalid admin API key',
|
|
101
72
|
});
|
|
102
73
|
expect(mockNext).not.toHaveBeenCalled();
|
|
103
74
|
});
|
|
104
75
|
|
|
105
|
-
it('should accept request with valid API key',
|
|
106
|
-
|
|
107
|
-
mockReq.headers.authorization = `Bearer ${validKey}`;
|
|
108
|
-
mockCommands.validateAdminApiKey.mockResolvedValue({
|
|
109
|
-
valid: true,
|
|
110
|
-
apiKey: {
|
|
111
|
-
id: 'key-id-1',
|
|
112
|
-
name: 'test-key',
|
|
113
|
-
keyLast4: 'e123',
|
|
114
|
-
},
|
|
115
|
-
});
|
|
76
|
+
it('should accept request with valid API key', () => {
|
|
77
|
+
mockReq.headers['x-frigg-admin-api-key'] = 'test-admin-key-123';
|
|
116
78
|
|
|
117
|
-
|
|
79
|
+
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
118
80
|
|
|
119
|
-
expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith(validKey);
|
|
120
|
-
expect(mockReq.adminApiKey).toBeDefined();
|
|
121
|
-
expect(mockReq.adminApiKey.name).toBe('test-key');
|
|
122
|
-
expect(mockReq.adminAudit).toBeDefined();
|
|
123
|
-
expect(mockReq.adminAudit.apiKeyName).toBe('test-key');
|
|
124
|
-
expect(mockReq.adminAudit.apiKeyLast4).toBe('e123');
|
|
125
|
-
expect(mockReq.adminAudit.ipAddress).toBe('127.0.0.1');
|
|
126
81
|
expect(mockNext).toHaveBeenCalled();
|
|
127
82
|
expect(mockRes.status).not.toHaveBeenCalled();
|
|
128
83
|
});
|
|
129
84
|
});
|
|
130
|
-
|
|
131
|
-
describe('Error handling', () => {
|
|
132
|
-
it('should handle validation errors gracefully', async () => {
|
|
133
|
-
mockReq.headers.authorization = 'Bearer some-key';
|
|
134
|
-
mockCommands.validateAdminApiKey.mockRejectedValue(
|
|
135
|
-
new Error('Database error')
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
await adminAuthMiddleware(mockReq, mockRes, mockNext);
|
|
139
|
-
|
|
140
|
-
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
141
|
-
expect(mockRes.json).toHaveBeenCalledWith({
|
|
142
|
-
error: 'Authentication failed',
|
|
143
|
-
code: 'AUTH_ERROR',
|
|
144
|
-
});
|
|
145
|
-
expect(mockNext).not.toHaveBeenCalled();
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
85
|
});
|
|
@@ -4,13 +4,8 @@ const { AdminScriptBase } = require('../../application/admin-script-base');
|
|
|
4
4
|
|
|
5
5
|
// Mock dependencies
|
|
6
6
|
jest.mock('../admin-auth-middleware', () => ({
|
|
7
|
-
|
|
8
|
-
// Mock auth -
|
|
9
|
-
req.adminAudit = {
|
|
10
|
-
apiKeyName: 'test-key',
|
|
11
|
-
apiKeyLast4: '1234',
|
|
12
|
-
ipAddress: '127.0.0.1',
|
|
13
|
-
};
|
|
7
|
+
validateAdminApiKey: (req, res, next) => {
|
|
8
|
+
// Mock auth - no audit trail with simplified auth
|
|
14
9
|
next();
|
|
15
10
|
},
|
|
16
11
|
}));
|
|
@@ -59,8 +54,8 @@ describe('Admin Script Router', () => {
|
|
|
59
54
|
};
|
|
60
55
|
|
|
61
56
|
mockCommands = {
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
createAdminProcess: jest.fn(),
|
|
58
|
+
findAdminProcessById: jest.fn(),
|
|
64
59
|
findRecentExecutions: jest.fn(),
|
|
65
60
|
};
|
|
66
61
|
|
|
@@ -143,7 +138,7 @@ describe('Admin Script Router', () => {
|
|
|
143
138
|
});
|
|
144
139
|
});
|
|
145
140
|
|
|
146
|
-
describe('POST /admin/scripts/:scriptName
|
|
141
|
+
describe('POST /admin/scripts/:scriptName', () => {
|
|
147
142
|
it('should execute script synchronously', async () => {
|
|
148
143
|
mockRunner.execute.mockResolvedValue({
|
|
149
144
|
executionId: 'exec-123',
|
|
@@ -154,7 +149,7 @@ describe('Admin Script Router', () => {
|
|
|
154
149
|
});
|
|
155
150
|
|
|
156
151
|
const response = await request(app)
|
|
157
|
-
.post('/admin/scripts/test-script
|
|
152
|
+
.post('/admin/scripts/test-script')
|
|
158
153
|
.send({
|
|
159
154
|
params: { foo: 'bar' },
|
|
160
155
|
mode: 'sync',
|
|
@@ -174,12 +169,12 @@ describe('Admin Script Router', () => {
|
|
|
174
169
|
});
|
|
175
170
|
|
|
176
171
|
it('should queue script for async execution', async () => {
|
|
177
|
-
mockCommands.
|
|
172
|
+
mockCommands.createAdminProcess.mockResolvedValue({
|
|
178
173
|
id: 'exec-456',
|
|
179
174
|
});
|
|
180
175
|
|
|
181
176
|
const response = await request(app)
|
|
182
|
-
.post('/admin/scripts/test-script
|
|
177
|
+
.post('/admin/scripts/test-script')
|
|
183
178
|
.send({
|
|
184
179
|
params: { foo: 'bar' },
|
|
185
180
|
mode: 'async',
|
|
@@ -198,12 +193,12 @@ describe('Admin Script Router', () => {
|
|
|
198
193
|
});
|
|
199
194
|
|
|
200
195
|
it('should default to async mode', async () => {
|
|
201
|
-
mockCommands.
|
|
196
|
+
mockCommands.createAdminProcess.mockResolvedValue({
|
|
202
197
|
id: 'exec-789',
|
|
203
198
|
});
|
|
204
199
|
|
|
205
200
|
const response = await request(app)
|
|
206
|
-
.post('/admin/scripts/test-script
|
|
201
|
+
.post('/admin/scripts/test-script')
|
|
207
202
|
.send({
|
|
208
203
|
params: { foo: 'bar' },
|
|
209
204
|
});
|
|
@@ -216,7 +211,7 @@ describe('Admin Script Router', () => {
|
|
|
216
211
|
mockFactory.has.mockReturnValue(false);
|
|
217
212
|
|
|
218
213
|
const response = await request(app)
|
|
219
|
-
.post('/admin/scripts/non-existent
|
|
214
|
+
.post('/admin/scripts/non-existent')
|
|
220
215
|
.send({
|
|
221
216
|
params: {},
|
|
222
217
|
});
|
|
@@ -226,15 +221,15 @@ describe('Admin Script Router', () => {
|
|
|
226
221
|
});
|
|
227
222
|
});
|
|
228
223
|
|
|
229
|
-
describe('GET /admin/executions/:executionId', () => {
|
|
224
|
+
describe('GET /admin/scripts/:scriptName/executions/:executionId', () => {
|
|
230
225
|
it('should return execution details', async () => {
|
|
231
|
-
mockCommands.
|
|
226
|
+
mockCommands.findAdminProcessById.mockResolvedValue({
|
|
232
227
|
id: 'exec-123',
|
|
233
228
|
scriptName: 'test-script',
|
|
234
229
|
status: 'COMPLETED',
|
|
235
230
|
});
|
|
236
231
|
|
|
237
|
-
const response = await request(app).get('/admin/executions/exec-123');
|
|
232
|
+
const response = await request(app).get('/admin/scripts/test-script/executions/exec-123');
|
|
238
233
|
|
|
239
234
|
expect(response.status).toBe(200);
|
|
240
235
|
expect(response.body.id).toBe('exec-123');
|
|
@@ -242,14 +237,14 @@ describe('Admin Script Router', () => {
|
|
|
242
237
|
});
|
|
243
238
|
|
|
244
239
|
it('should return 404 for non-existent execution', async () => {
|
|
245
|
-
mockCommands.
|
|
240
|
+
mockCommands.findAdminProcessById.mockResolvedValue({
|
|
246
241
|
error: 404,
|
|
247
242
|
reason: 'Execution not found',
|
|
248
243
|
code: 'EXECUTION_NOT_FOUND',
|
|
249
244
|
});
|
|
250
245
|
|
|
251
246
|
const response = await request(app).get(
|
|
252
|
-
'/admin/executions/non-existent'
|
|
247
|
+
'/admin/scripts/test-script/executions/non-existent'
|
|
253
248
|
);
|
|
254
249
|
|
|
255
250
|
expect(response.status).toBe(404);
|
|
@@ -257,24 +252,28 @@ describe('Admin Script Router', () => {
|
|
|
257
252
|
});
|
|
258
253
|
});
|
|
259
254
|
|
|
260
|
-
describe('GET /admin/executions', () => {
|
|
261
|
-
it('should list
|
|
255
|
+
describe('GET /admin/scripts/:scriptName/executions', () => {
|
|
256
|
+
it('should list executions for specific script', async () => {
|
|
262
257
|
mockCommands.findRecentExecutions.mockResolvedValue([
|
|
263
258
|
{ id: 'exec-1', scriptName: 'test-script', status: 'COMPLETED' },
|
|
264
259
|
{ id: 'exec-2', scriptName: 'test-script', status: 'RUNNING' },
|
|
265
260
|
]);
|
|
266
261
|
|
|
267
|
-
const response = await request(app).get('/admin/executions');
|
|
262
|
+
const response = await request(app).get('/admin/scripts/test-script/executions');
|
|
268
263
|
|
|
269
264
|
expect(response.status).toBe(200);
|
|
270
265
|
expect(response.body.executions).toHaveLength(2);
|
|
266
|
+
expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
|
|
267
|
+
scriptName: 'test-script',
|
|
268
|
+
limit: 50,
|
|
269
|
+
});
|
|
271
270
|
});
|
|
272
271
|
|
|
273
272
|
it('should accept query parameters', async () => {
|
|
274
273
|
mockCommands.findRecentExecutions.mockResolvedValue([]);
|
|
275
274
|
|
|
276
275
|
await request(app).get(
|
|
277
|
-
'/admin/
|
|
276
|
+
'/admin/scripts/test-script/executions?status=COMPLETED&limit=10'
|
|
278
277
|
);
|
|
279
278
|
|
|
280
279
|
expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
|
|
@@ -1,49 +1,11 @@
|
|
|
1
|
-
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Admin API Key Authentication Middleware
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Re-exports shared admin auth middleware from @friggframework/core.
|
|
5
|
+
* Uses simple ENV-based API key validation.
|
|
6
|
+
* Expects: x-frigg-admin-api-key header
|
|
8
7
|
*/
|
|
9
|
-
async function adminAuthMiddleware(req, res, next) {
|
|
10
|
-
try {
|
|
11
|
-
const authHeader = req.headers.authorization;
|
|
12
|
-
|
|
13
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
14
|
-
return res.status(401).json({
|
|
15
|
-
error: 'Missing or invalid Authorization header',
|
|
16
|
-
code: 'MISSING_AUTH'
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const apiKey = authHeader.substring(7); // Remove 'Bearer '
|
|
21
|
-
const commands = createAdminScriptCommands();
|
|
22
|
-
const result = await commands.validateAdminApiKey(apiKey);
|
|
23
|
-
|
|
24
|
-
if (result.error) {
|
|
25
|
-
return res.status(result.error).json({
|
|
26
|
-
error: result.reason,
|
|
27
|
-
code: result.code
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Attach validated key info to request for audit trail
|
|
32
|
-
req.adminApiKey = result.apiKey;
|
|
33
|
-
req.adminAudit = {
|
|
34
|
-
apiKeyName: result.apiKey.name,
|
|
35
|
-
apiKeyLast4: result.apiKey.keyLast4,
|
|
36
|
-
ipAddress: req.ip || req.connection?.remoteAddress || 'unknown'
|
|
37
|
-
};
|
|
38
8
|
|
|
39
|
-
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error('Admin auth middleware error:', error);
|
|
42
|
-
res.status(500).json({
|
|
43
|
-
error: 'Authentication failed',
|
|
44
|
-
code: 'AUTH_ERROR'
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
9
|
+
const { validateAdminApiKey } = require('@friggframework/core/handlers/middleware/admin-auth');
|
|
48
10
|
|
|
49
|
-
module.exports = {
|
|
11
|
+
module.exports = { validateAdminApiKey };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const serverless = require('serverless-http');
|
|
3
|
-
const {
|
|
3
|
+
const { validateAdminApiKey } = require('./admin-auth-middleware');
|
|
4
4
|
const { getScriptFactory } = require('../application/script-factory');
|
|
5
5
|
const { createScriptRunner } = require('../application/script-runner');
|
|
6
6
|
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
@@ -11,7 +11,7 @@ const { ScheduleManagementUseCase } = require('../application/schedule-managemen
|
|
|
11
11
|
const router = express.Router();
|
|
12
12
|
|
|
13
13
|
// Apply auth middleware to all admin routes
|
|
14
|
-
router.use(
|
|
14
|
+
router.use(validateAdminApiKey);
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Create ScheduleManagementUseCase instance
|
|
@@ -87,10 +87,10 @@ router.get('/scripts/:scriptName', async (req, res) => {
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* POST /admin/scripts/:scriptName
|
|
90
|
+
* POST /admin/scripts/:scriptName
|
|
91
91
|
* Execute a script (sync, async, or dry-run)
|
|
92
92
|
*/
|
|
93
|
-
router.post('/scripts/:scriptName
|
|
93
|
+
router.post('/scripts/:scriptName', async (req, res) => {
|
|
94
94
|
try {
|
|
95
95
|
const { scriptName } = req.params;
|
|
96
96
|
const { params = {}, mode = 'async', dryRun = false } = req.body;
|
|
@@ -110,7 +110,6 @@ router.post('/scripts/:scriptName/execute', async (req, res) => {
|
|
|
110
110
|
trigger: 'MANUAL',
|
|
111
111
|
mode: 'sync',
|
|
112
112
|
dryRun: true,
|
|
113
|
-
audit: req.adminAudit,
|
|
114
113
|
});
|
|
115
114
|
return res.json(result);
|
|
116
115
|
}
|
|
@@ -121,20 +120,18 @@ router.post('/scripts/:scriptName/execute', async (req, res) => {
|
|
|
121
120
|
const result = await runner.execute(scriptName, params, {
|
|
122
121
|
trigger: 'MANUAL',
|
|
123
122
|
mode: 'sync',
|
|
124
|
-
audit: req.adminAudit,
|
|
125
123
|
});
|
|
126
124
|
return res.json(result);
|
|
127
125
|
}
|
|
128
126
|
|
|
129
127
|
// Async execution - queue and return immediately
|
|
130
128
|
const commands = createAdminScriptCommands();
|
|
131
|
-
const execution = await commands.
|
|
129
|
+
const execution = await commands.createAdminProcess({
|
|
132
130
|
scriptName,
|
|
133
131
|
scriptVersion: factory.get(scriptName).Definition.version,
|
|
134
132
|
trigger: 'MANUAL',
|
|
135
133
|
mode: 'async',
|
|
136
134
|
input: params,
|
|
137
|
-
audit: req.adminAudit,
|
|
138
135
|
});
|
|
139
136
|
|
|
140
137
|
// Queue the execution
|
|
@@ -161,14 +158,14 @@ router.post('/scripts/:scriptName/execute', async (req, res) => {
|
|
|
161
158
|
});
|
|
162
159
|
|
|
163
160
|
/**
|
|
164
|
-
* GET /admin/executions/:executionId
|
|
165
|
-
* Get execution status
|
|
161
|
+
* GET /admin/scripts/:scriptName/executions/:executionId
|
|
162
|
+
* Get execution status for specific script
|
|
166
163
|
*/
|
|
167
|
-
router.get('/executions/:executionId', async (req, res) => {
|
|
164
|
+
router.get('/scripts/:scriptName/executions/:executionId', async (req, res) => {
|
|
168
165
|
try {
|
|
169
166
|
const { executionId } = req.params;
|
|
170
167
|
const commands = createAdminScriptCommands();
|
|
171
|
-
const execution = await commands.
|
|
168
|
+
const execution = await commands.findAdminProcessById(executionId);
|
|
172
169
|
|
|
173
170
|
if (execution.error) {
|
|
174
171
|
return res.status(execution.error).json({
|
|
@@ -185,12 +182,13 @@ router.get('/executions/:executionId', async (req, res) => {
|
|
|
185
182
|
});
|
|
186
183
|
|
|
187
184
|
/**
|
|
188
|
-
* GET /admin/executions
|
|
189
|
-
* List recent executions
|
|
185
|
+
* GET /admin/scripts/:scriptName/executions
|
|
186
|
+
* List recent executions for specific script
|
|
190
187
|
*/
|
|
191
|
-
router.get('/executions', async (req, res) => {
|
|
188
|
+
router.get('/scripts/:scriptName/executions', async (req, res) => {
|
|
192
189
|
try {
|
|
193
|
-
const { scriptName
|
|
190
|
+
const { scriptName } = req.params;
|
|
191
|
+
const { status, limit = 50 } = req.query;
|
|
194
192
|
const commands = createAdminScriptCommands();
|
|
195
193
|
|
|
196
194
|
const executions = await commands.findRecentExecutions({
|
|
@@ -21,7 +21,7 @@ async function handler(event) {
|
|
|
21
21
|
|
|
22
22
|
// If executionId provided (async from API), update existing record
|
|
23
23
|
if (executionId) {
|
|
24
|
-
await commands.
|
|
24
|
+
await commands.updateAdminProcessState(executionId, 'RUNNING');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const result = await runner.execute(scriptName, params, {
|
|
@@ -45,7 +45,7 @@ async function handler(event) {
|
|
|
45
45
|
if (executionId) {
|
|
46
46
|
const commands = createAdminScriptCommands();
|
|
47
47
|
await commands
|
|
48
|
-
.
|
|
48
|
+
.completeAdminProcess(executionId, {
|
|
49
49
|
status: 'FAILED',
|
|
50
50
|
error: {
|
|
51
51
|
name: error.name,
|