diff --git a/.eslintrc.json b/.eslintrc.json index fb129879f..a7e652dce 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,7 +45,8 @@ "@typescript-eslint/no-explicit-any": "off", // temporary until TS refactor is complete "@typescript-eslint/no-unused-vars": "off", // temporary until TS refactor is complete "@typescript-eslint/no-require-imports": "off", // prevents error on old "require" imports - "@typescript-eslint/no-unused-expressions": "off" // prevents error on test "expect" expressions + "@typescript-eslint/no-unused-expressions": "off", // prevents error on test "expect" expressions + "new-cap": "off" // prevents error on express.Router() }, "settings": { "react": { diff --git a/index.ts b/index.ts index 880ccfe02..b633bd7c4 100755 --- a/index.ts +++ b/index.ts @@ -5,7 +5,7 @@ import { hideBin } from 'yargs/helpers'; import * as fs from 'fs'; import { configFile, setConfigFile, validate } from './src/config/file'; import proxy from './src/proxy'; -import service from './src/service'; +import * as service from './src/service'; const argv = yargs(hideBin(process.argv)) .usage('Usage: $0 [options]') diff --git a/package-lock.json b/package-lock.json index 5eb72a6f1..4807e3707 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,11 +66,17 @@ "@babel/preset-react": "^7.22.5", "@commitlint/cli": "^19.0.0", "@commitlint/config-conventional": "^19.0.0", + "@types/cors": "^2.8.19", "@types/express": "^5.0.1", "@types/express-http-proxy": "^1.6.6", + "@types/express-session": "^1.18.2", + "@types/jsonwebtoken": "^9.0.10", + "@types/jwk-to-pem": "^2.0.3", "@types/lodash": "^4.17.15", + "@types/lusca": "^1.7.5", "@types/mocha": "^10.0.10", "@types/node": "^22.13.5", + "@types/passport-local": "^1.0.38", "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", @@ -3688,6 +3694,16 @@ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", @@ -3723,6 +3739,16 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -3730,6 +3756,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/jwk-to-pem": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/jwk-to-pem/-/jwk-to-pem-2.0.3.tgz", + "integrity": "sha512-I/WFyFgk5GrNbkpmt14auGO3yFK1Wt4jXzkLuI+fDBNtO5ZI2rbymyGd6bKzfSBEuyRdM64ZUwxU1+eDcPSOEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.16", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", @@ -3737,6 +3781,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lusca": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/lusca/-/lusca-1.7.5.tgz", + "integrity": "sha512-l49gAf8pu2iMzbKejLcz6Pqj+51H2na6BgORv1ElnE8ByPFcBdh/eZ0WNR1Va/6ZuNSZa01Hoy1DTZ3IZ+y+kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -3750,6 +3804,13 @@ "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", @@ -3758,6 +3819,39 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", diff --git a/package.json b/package.json index f3da903db..03786134e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.18.2", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { - "cli": "node ./packages/git-proxy-cli/index.js", + "cli": "tsx ./packages/git-proxy-cli/index.ts", "client": "vite --config vite.config.js", "clientinstall": "npm install --prefix client", "server": "tsx index.ts", @@ -89,11 +89,17 @@ "@babel/preset-react": "^7.22.5", "@commitlint/cli": "^19.0.0", "@commitlint/config-conventional": "^19.0.0", + "@types/cors": "^2.8.19", "@types/express": "^5.0.1", "@types/express-http-proxy": "^1.6.6", + "@types/express-session": "^1.18.2", + "@types/jsonwebtoken": "^9.0.10", + "@types/jwk-to-pem": "^2.0.3", "@types/lodash": "^4.17.15", + "@types/lusca": "^1.7.5", "@types/mocha": "^10.0.10", "@types/node": "^22.13.5", + "@types/passport-local": "^1.0.38", "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", diff --git a/packages/git-proxy-cli/index.js b/packages/git-proxy-cli/index.ts old mode 100755 new mode 100644 similarity index 86% rename from packages/git-proxy-cli/index.js rename to packages/git-proxy-cli/index.ts index 142a58a33..db4c5b428 --- a/packages/git-proxy-cli/index.js +++ b/packages/git-proxy-cli/index.ts @@ -1,9 +1,11 @@ #!/usr/bin/env node -const axios = require('axios'); -const yargs = require('yargs/yargs'); -const { hideBin } = require('yargs/helpers'); -const fs = require('fs'); -const util = require('util'); +import axios from 'axios'; +import yargs from 'yargs/yargs'; +import { hideBin } from 'yargs/helpers'; +import fs from 'fs'; +import util from 'util'; + +import { CommitData, PushData, PushFilters } from './types'; const GIT_PROXY_COOKIE_FILE = 'git-proxy-cookie'; // GitProxy UI HOST and PORT (configurable via environment variable) @@ -19,7 +21,7 @@ axios.defaults.timeout = 30000; * @param {string} username The user name to login with * @param {string} password The password to use for the login */ -async function login(username, password) { +async function login(username: string, password: string) { try { let response = await axios.post( `${baseUrl}/api/auth/login`, @@ -44,7 +46,7 @@ async function login(username, password) { const user = `"${response.data.username}" <${response.data.email}>`; const isAdmin = response.data.admin ? ' (admin)' : ''; console.log(`Login ${user}${isAdmin}: OK`); - } catch (error) { + } catch (error: any) { if (error.response) { console.error(`Error: Login '${username}': '${error.response.status}'`); process.exitCode = 1; @@ -76,7 +78,7 @@ async function login(username, password) { * @param {boolean} filters.rejected - If not null, filters for pushes with * given attribute and status. */ -async function getGitPushes(filters) { +async function getGitPushes(filters: PushFilters) { if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { console.error('Error: List: Authentication required'); process.exitCode = 1; @@ -91,40 +93,46 @@ async function getGitPushes(filters) { params: filters, }); - const records = []; - response.data?.forEach((push) => { - const record = {}; - record.id = push.id; - record.timestamp = push.timestamp; - record.url = push.url; - record.allowPush = push.allowPush; - record.authorised = push.authorised; - record.blocked = push.blocked; - record.canceled = push.canceled; - record.error = push.error; - record.rejected = push.rejected; - - record.lastStep = { - stepName: push.lastStep?.stepName, - error: push.lastStep?.error, - errorMessage: push.lastStep?.errorMessage, - blocked: push.lastStep?.blocked, - blockedMessage: push.lastStep?.blockedMessage, + const records: PushData[] = []; + response.data.forEach((push: PushData) => { + const record: PushData = { + id: push.id, + timestamp: push.timestamp, + url: push.url, + allowPush: push.allowPush, + authorised: push.authorised, + blocked: push.blocked, + canceled: push.canceled, + error: push.error, + rejected: push.rejected, }; - record.commitData = []; - push.commitData?.forEach((pushCommitDataRecord) => { - record.commitData.push({ - message: pushCommitDataRecord.message, - committer: pushCommitDataRecord.committer, + if (push.lastStep) { + record.lastStep = { + stepName: push.lastStep?.stepName, + error: push.lastStep?.error, + errorMessage: push.lastStep?.errorMessage, + blocked: push.lastStep?.blocked, + blockedMessage: push.lastStep?.blockedMessage, + }; + } + + if (push.commitData) { + const commitData: CommitData[] = []; + push.commitData.forEach((pushCommitDataRecord: CommitData) => { + commitData.push({ + message: pushCommitDataRecord.message, + committer: pushCommitDataRecord.committer, + }); }); - }); + record.commitData = commitData; + } records.push(record); }); console.log(`${util.inspect(records, false, null, false)}`); - } catch (error) { + } catch (error: any) { // default error const errorMessage = `Error: List: '${error.message}'`; process.exitCode = 2; @@ -136,7 +144,7 @@ async function getGitPushes(filters) { * Authorise git push by ID * @param {string} id The ID of the git push to authorise */ -async function authoriseGitPush(id) { +async function authoriseGitPush(id: string) { if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { console.error('Error: Authorise: Authentication required'); process.exitCode = 1; @@ -168,7 +176,7 @@ async function authoriseGitPush(id) { ); console.log(`Authorise: ID: '${id}': OK`); - } catch (error) { + } catch (error: any) { // default error let errorMessage = `Error: Authorise: '${error.message}'`; process.exitCode = 2; @@ -192,7 +200,7 @@ async function authoriseGitPush(id) { * Reject git push by ID * @param {string} id The ID of the git push to reject */ -async function rejectGitPush(id) { +async function rejectGitPush(id: string) { if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { console.error('Error: Reject: Authentication required'); process.exitCode = 1; @@ -215,7 +223,7 @@ async function rejectGitPush(id) { ); console.log(`Reject: ID: '${id}': OK`); - } catch (error) { + } catch (error: any) { // default error let errorMessage = `Error: Reject: '${error.message}'`; process.exitCode = 2; @@ -239,7 +247,7 @@ async function rejectGitPush(id) { * Cancel git push by ID * @param {string} id The ID of the git push to cancel */ -async function cancelGitPush(id) { +async function cancelGitPush(id: string) { if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { console.error('Error: Cancel: Authentication required'); process.exitCode = 1; @@ -262,7 +270,7 @@ async function cancelGitPush(id) { ); console.log(`Cancel: ID: '${id}': OK`); - } catch (error) { + } catch (error: any) { // default error let errorMessage = `Error: Cancel: '${error.message}'`; process.exitCode = 2; @@ -299,7 +307,7 @@ async function logout() { headers: { Cookie: cookies }, }, ); - } catch (error) { + } catch (error: any) { console.log(`Warning: Logout: '${error.message}'`); } } @@ -323,7 +331,7 @@ async function reloadConfig() { await axios.post(`${baseUrl}/api/v1/admin/reload-config`, {}, { headers: { Cookie: cookies } }); console.log('Configuration reloaded successfully'); - } catch (error) { + } catch (error: any) { const errorMessage = `Error: Reload config: '${error.message}'`; process.exitCode = 2; console.error(errorMessage); @@ -462,8 +470,10 @@ yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused }) .command({ command: 'reload-config', - description: 'Reload GitProxy configuration without restarting', - action: reloadConfig, + describe: 'Reload GitProxy configuration without restarting', + handler() { + reloadConfig(); + }, }) .demandCommand(1, 'You need at least one command before moving on') .strict() diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json index 420cb17e9..eb5b8fb15 100644 --- a/packages/git-proxy-cli/package.json +++ b/packages/git-proxy-cli/package.json @@ -2,7 +2,9 @@ "name": "@finos/git-proxy-cli", "version": "0.1.0", "description": "Command line interface tool for FINOS GitProxy.", - "bin": "./index.js", + "bin": { + "git-proxy-cli": "./dist/index.js" + }, "dependencies": { "axios": "^1.10.0", "yargs": "^17.7.2", @@ -12,8 +14,9 @@ "chai": "^4.5.0" }, "scripts": { - "lint": "eslint --fix . --ext .js,.jsx", - "test": "NODE_ENV=test ts-mocha --exit --timeout 10000", + "build": "tsc", + "lint": "eslint \"./*.ts\" --fix", + "test": "NODE_ENV=test ts-mocha test/*.ts --exit --timeout 10000", "test-coverage": "nyc npm run test", "test-coverage-ci": "nyc --reporter=lcovonly --reporter=text --reporter=html npm run test" }, diff --git a/packages/git-proxy-cli/test/testCli.test.js b/packages/git-proxy-cli/test/testCli.test.ts similarity index 73% rename from packages/git-proxy-cli/test/testCli.test.js rename to packages/git-proxy-cli/test/testCli.test.ts index fbfce0fe3..7250667be 100644 --- a/packages/git-proxy-cli/test/testCli.test.js +++ b/packages/git-proxy-cli/test/testCli.test.ts @@ -1,15 +1,12 @@ /* eslint-disable max-len */ -const helper = require('./testCliUtils'); +import * as helper from './testCliUtils.ts'; +import path from 'path'; -const path = require('path'); +import { setConfigFile } from '../../../src/config/file'; -// set test proxy config file path *before* loading the proxy -require('../../../src/config/file').configFile = path.join( - process.cwd(), - 'test', - 'testCli.proxy.config.json', -); -const service = require('../../../src/service'); +import * as service from '../../../src/service/index'; + +setConfigFile(path.join(process.cwd(), 'test', 'testCli.proxy.config.json')); /* test constants */ // push ID which does not exist @@ -23,14 +20,16 @@ const TEST_REPO_CONFIG = { }; const TEST_REPO = 'finos/git-proxy-test.git'; +const cliPath = `tsx ${path.resolve(__dirname, '../index.ts')}`; + describe('test git-proxy-cli', function () { // *** help *** describe(`test git-proxy-cli :: help`, function () { it(`print help if no command or option is given`, async function () { - const cli = `npx -- @finos/git-proxy-cli`; + const cli = `${cliPath}`; const expectedExitCode = 1; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = [ 'Commands:', 'Options:', @@ -40,9 +39,9 @@ describe('test git-proxy-cli', function () { }); it(`print help if invalid command or option is given`, async function () { - const cli = `npx -- @finos/git-proxy-cli invalid --invalid`; + const cli = `${cliPath} invalid --invalid`; const expectedExitCode = 1; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = [ 'Commands:', 'Options:', @@ -52,10 +51,10 @@ describe('test git-proxy-cli', function () { }); it(`print help if "--help" option is given`, async function () { - const cli = `npx -- @finos/git-proxy-cli invalid --help`; + const cli = `${cliPath} invalid --help`; const expectedExitCode = 0; const expectedMessages = ['Commands:', 'Options:']; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); }); @@ -64,10 +63,10 @@ describe('test git-proxy-cli', function () { describe(`test git-proxy-cli :: version`, function () { it(`"--version" option prints version details `, async function () { - const cli = `npx -- @finos/git-proxy-cli --version`; + const cli = `${cliPath} --version`; const expectedExitCode = 0; const expectedMessages = ['0.1.0']; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); }); @@ -76,10 +75,10 @@ describe('test git-proxy-cli', function () { describe('test git-proxy-cli :: configuration', function () { it(`"config" command prints configuration details`, async function () { - const cli = `npx -- @finos/git-proxy-cli config`; + const cli = `${cliPath} config`; const expectedExitCode = 0; const expectedMessages = ['GitProxy URL:']; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); }); @@ -102,9 +101,9 @@ describe('test git-proxy-cli', function () { it('login should fail when server is down', async function () { const username = 'admin'; const password = 'admin'; - const cli = `npx -- @finos/git-proxy-cli login --username ${username} --password ${password}`; + const cli = `${cliPath} login --username ${username} --password ${password}`; const expectedExitCode = 2; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = [`Error: Login '${username}':`]; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -112,9 +111,9 @@ describe('test git-proxy-cli', function () { it('login should fail with invalid credentials', async function () { const username = 'unkn0wn'; const password = 'p4ssw0rd'; - const cli = `npx -- @finos/git-proxy-cli login --username ${username} --password ${password}`; + const cli = `${cliPath} login --username ${username} --password ${password}`; const expectedExitCode = 1; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = [`Error: Login '${username}': '401'`]; try { await helper.startServer(service); @@ -127,10 +126,10 @@ describe('test git-proxy-cli', function () { it('login shoud be successful with valid credentials (admin)', async function () { const username = 'admin'; const password = 'admin'; - const cli = `npx -- @finos/git-proxy-cli login --username ${username} --password ${password}`; + const cli = `${cliPath} login --username ${username} --password ${password}`; const expectedExitCode = 0; const expectedMessages = [`Login "${username}" (admin): OK`]; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; try { await helper.startServer(service); await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); @@ -140,10 +139,10 @@ describe('test git-proxy-cli', function () { }); it('login shoud be successful with valid credentials (non-admin)', async function () { - const cli = `npx -- @finos/git-proxy-cli login --username ${testUser} --password ${testPassword}`; + const cli = `${cliPath} login --username ${testUser} --password ${testPassword}`; const expectedExitCode = 0; const expectedMessages = [`Login "${testUser}" <${testEmail}>: OK`]; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; try { await helper.startServer(service); await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); @@ -159,25 +158,25 @@ describe('test git-proxy-cli', function () { it('logout shoud succeed when server is down (and not logged in before)', async function () { await helper.removeCookiesFile(); - const cli = `npx -- @finos/git-proxy-cli logout`; + const cli = `${cliPath} logout`; const expectedExitCode = 0; const expectedMessages = [`Logout: OK`]; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); it('logout should succeed when server is down (but logged in before)', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); } finally { await helper.closeServer(service.httpServer); } - const cli = `npx -- @finos/git-proxy-cli logout`; + const cli = `${cliPath} logout`; const expectedExitCode = 0; const expectedMessages = [`Logout: OK`]; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -185,10 +184,10 @@ describe('test git-proxy-cli', function () { try { await helper.createCookiesFileWithExpiredCookie(); - const cli = `npx -- @finos/git-proxy-cli logout`; + const cli = `${cliPath} logout`; const expectedExitCode = 0; const expectedMessages = [`Logout: OK`]; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.startServer(service); await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { @@ -199,12 +198,12 @@ describe('test git-proxy-cli', function () { it('logout shoud be successful when authenticated (server is up)', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli logout`; + const cli = `${cliPath} logout`; const expectedExitCode = 0; const expectedMessages = [`Logout: OK`]; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); @@ -231,15 +230,15 @@ describe('test git-proxy-cli', function () { try { // start server -> login -> stop server await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); } finally { await helper.closeServer(service.httpServer); } const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli authorise --id ${id}`; + const cli = `${cliPath} authorise --id ${id}`; const expectedExitCode = 2; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Authorise:']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -248,9 +247,9 @@ describe('test git-proxy-cli', function () { await helper.removeCookiesFile(); const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli authorise --id ${id}`; + const cli = `${cliPath} authorise --id ${id}`; const expectedExitCode = 1; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Authorise: Authentication required']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -260,9 +259,9 @@ describe('test git-proxy-cli', function () { await helper.createCookiesFileWithExpiredCookie(); await helper.startServer(service); const id = pushId; - const cli = `npx -- @finos/git-proxy-cli authorise --id ${id}`; + const cli = `${cliPath} authorise --id ${id}`; const expectedExitCode = 3; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Authorise: Authentication required']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { @@ -273,12 +272,12 @@ describe('test git-proxy-cli', function () { it('attempt to authorise should fail when git push ID not found', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli authorise --id ${id}`; + const cli = `${cliPath} authorise --id ${id}`; const expectedExitCode = 4; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = [`Error: Authorise: ID: '${id}': Not Found`]; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { @@ -306,15 +305,15 @@ describe('test git-proxy-cli', function () { try { // start server -> login -> stop server await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); } finally { await helper.closeServer(service.httpServer); } const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli cancel --id ${id}`; + const cli = `${cliPath} cancel --id ${id}`; const expectedExitCode = 2; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Cancel:']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -323,9 +322,9 @@ describe('test git-proxy-cli', function () { await helper.removeCookiesFile(); const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli cancel --id ${id}`; + const cli = `${cliPath} cancel --id ${id}`; const expectedExitCode = 1; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Cancel: Authentication required']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -335,9 +334,9 @@ describe('test git-proxy-cli', function () { await helper.createCookiesFileWithExpiredCookie(); await helper.startServer(service); const id = pushId; - const cli = `npx -- @finos/git-proxy-cli cancel --id ${id}`; + const cli = `${cliPath} cancel --id ${id}`; const expectedExitCode = 3; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Cancel: Authentication required']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); // }); @@ -349,12 +348,12 @@ describe('test git-proxy-cli', function () { it('attempt to cancel should fail when git push ID not found', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli cancel --id ${id}`; + const cli = `${cliPath} cancel --id ${id}`; const expectedExitCode = 4; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = [`Error: Cancel: ID: '${id}': Not Found`]; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { @@ -370,14 +369,14 @@ describe('test git-proxy-cli', function () { try { // start server -> login -> stop server await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); } finally { await helper.closeServer(service.httpServer); } - const cli = `npx -- @finos/git-proxy-cli ls`; + const cli = `${cliPath} ls`; const expectedExitCode = 2; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: List:']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -385,9 +384,9 @@ describe('test git-proxy-cli', function () { it('attempt to ls should fail when not authenticated', async function () { await helper.removeCookiesFile(); - const cli = `npx -- @finos/git-proxy-cli ls`; + const cli = `${cliPath} ls`; const expectedExitCode = 1; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: List: Authentication required']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -395,11 +394,11 @@ describe('test git-proxy-cli', function () { it('attempt to ls should fail when invalid option given', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli ls --invalid`; + const cli = `${cliPath} ls --invalid`; const expectedExitCode = 1; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Options:', 'Unknown argument: invalid']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { @@ -427,15 +426,15 @@ describe('test git-proxy-cli', function () { try { // start server -> login -> stop server await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); } finally { await helper.closeServer(service.httpServer); } const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli reject --id ${id}`; + const cli = `${cliPath} reject --id ${id}`; const expectedExitCode = 2; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Reject:']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -444,9 +443,9 @@ describe('test git-proxy-cli', function () { await helper.removeCookiesFile(); const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli reject --id ${id}`; + const cli = `${cliPath} reject --id ${id}`; const expectedExitCode = 1; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Reject: Authentication required']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); }); @@ -456,9 +455,9 @@ describe('test git-proxy-cli', function () { await helper.createCookiesFileWithExpiredCookie(); await helper.startServer(service); const id = pushId; - const cli = `npx -- @finos/git-proxy-cli reject --id ${id}`; + const cli = `${cliPath} reject --id ${id}`; const expectedExitCode = 3; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = ['Error: Reject: Authentication required']; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { @@ -469,12 +468,12 @@ describe('test git-proxy-cli', function () { it('attempt to reject should fail when git push ID not found', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); const id = GHOST_PUSH_ID; - const cli = `npx -- @finos/git-proxy-cli reject --id ${id}`; + const cli = `${cliPath} reject --id ${id}`; const expectedExitCode = 4; - const expectedMessages = null; + const expectedMessages = undefined; const expectedErrorMessages = [`Error: Reject: ID: '${id}': Not Found`]; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { @@ -504,9 +503,9 @@ describe('test git-proxy-cli', function () { it('attempt to ls should list existing push', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli ls --authorised false --blocked true --canceled false --rejected false`; + const cli = `${cliPath} ls --authorised false --blocked true --canceled false --rejected false`; const expectedExitCode = 0; const expectedMessages = [ pushId, @@ -517,7 +516,7 @@ describe('test git-proxy-cli', function () { 'error: false', 'rejected: false', ]; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); @@ -527,12 +526,12 @@ describe('test git-proxy-cli', function () { it('attempt to ls should not list existing push when filtered for authorised', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli ls --authorised true`; + const cli = `${cliPath} ls --authorised true`; const expectedExitCode = 0; const expectedMessages = ['[]']; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); @@ -542,12 +541,12 @@ describe('test git-proxy-cli', function () { it('attempt to ls should not list existing push when filtered for canceled', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli ls --canceled true`; + const cli = `${cliPath} ls --canceled true`; const expectedExitCode = 0; const expectedMessages = ['[]']; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); @@ -557,12 +556,12 @@ describe('test git-proxy-cli', function () { it('attempt to ls should not list existing push when filtered for rejected', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli ls --rejected true`; + const cli = `${cliPath} ls --rejected true`; const expectedExitCode = 0; const expectedMessages = ['[]']; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); @@ -572,12 +571,12 @@ describe('test git-proxy-cli', function () { it('attempt to ls should not list existing push when filtered for non-blocked', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli ls --blocked false`; + const cli = `${cliPath} ls --blocked false`; const expectedExitCode = 0; const expectedMessages = ['[]']; - const expectedErrorMessages = null; + const expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); @@ -587,24 +586,24 @@ describe('test git-proxy-cli', function () { it('authorise push and test if appears on authorised list', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - let cli = `npx -- @finos/git-proxy-cli ls --authorised true --canceled false --rejected false`; + let cli = `${cliPath} ls --authorised true --canceled false --rejected false`; let expectedExitCode = 0; let expectedMessages = ['[]']; - let expectedErrorMessages = null; + let expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); - cli = `npx -- @finos/git-proxy-cli authorise --id ${pushId}`; + cli = `${cliPath} authorise --id ${pushId}`; expectedExitCode = 0; expectedMessages = [`Authorise: ID: '${pushId}': OK`]; - expectedErrorMessages = null; + expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); - cli = `npx -- @finos/git-proxy-cli ls --authorised true --canceled false --rejected false`; + cli = `${cliPath} ls --authorised true --canceled false --rejected false`; expectedExitCode = 0; expectedMessages = [pushId, TEST_REPO]; - expectedErrorMessages = null; + expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); @@ -614,24 +613,24 @@ describe('test git-proxy-cli', function () { it('reject push and test if appears on rejected list', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - let cli = `npx -- @finos/git-proxy-cli ls --authorised false --canceled false --rejected true`; + let cli = `${cliPath} ls --authorised false --canceled false --rejected true`; let expectedExitCode = 0; let expectedMessages = ['[]']; - let expectedErrorMessages = null; + let expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); - cli = `npx -- @finos/git-proxy-cli reject --id ${pushId}`; + cli = `${cliPath} reject --id ${pushId}`; expectedExitCode = 0; expectedMessages = [`Reject: ID: '${pushId}': OK`]; - expectedErrorMessages = null; + expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); - cli = `npx -- @finos/git-proxy-cli ls --authorised false --canceled false --rejected true`; + cli = `${cliPath} ls --authorised false --canceled false --rejected true`; expectedExitCode = 0; expectedMessages = [pushId, TEST_REPO]; - expectedErrorMessages = null; + expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); @@ -641,24 +640,24 @@ describe('test git-proxy-cli', function () { it('cancel push and test if appears on canceled list', async function () { try { await helper.startServer(service); - await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + await helper.runCli(`${cliPath} login --username admin --password admin`); - let cli = `npx -- @finos/git-proxy-cli ls --authorised false --canceled true --rejected false`; + let cli = `${cliPath} ls --authorised false --canceled true --rejected false`; let expectedExitCode = 0; let expectedMessages = ['[]']; - let expectedErrorMessages = null; + let expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); - cli = `npx -- @finos/git-proxy-cli cancel --id ${pushId}`; + cli = `${cliPath} cancel --id ${pushId}`; expectedExitCode = 0; expectedMessages = [`Cancel: ID: '${pushId}': OK`]; - expectedErrorMessages = null; + expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); - cli = `npx -- @finos/git-proxy-cli ls --authorised false --canceled true --rejected false`; + cli = `${cliPath} ls --authorised false --canceled true --rejected false`; expectedExitCode = 0; expectedMessages = [pushId, TEST_REPO]; - expectedErrorMessages = null; + expectedErrorMessages = undefined; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); } finally { await helper.closeServer(service.httpServer); diff --git a/packages/git-proxy-cli/test/testCliUtils.js b/packages/git-proxy-cli/test/testCliUtils.ts similarity index 75% rename from packages/git-proxy-cli/test/testCliUtils.js rename to packages/git-proxy-cli/test/testCliUtils.ts index 557857619..2daed3456 100644 --- a/packages/git-proxy-cli/test/testCliUtils.js +++ b/packages/git-proxy-cli/test/testCliUtils.ts @@ -1,13 +1,15 @@ -const fs = require('fs'); -const util = require('util'); -const { exec } = require('child_process'); -const execAsync = util.promisify(exec); -const { expect } = require('chai'); +import fs from 'fs'; +import util from 'util'; +import { exec } from 'child_process'; +import { expect } from 'chai'; +import http from 'http'; + +import { Action } from '../../../src/proxy/actions/Action'; +import { Step } from '../../../src/proxy/actions/Step'; +import * as processor from '../../../src/proxy/processors/push-action/audit'; +import * as db from '../../../src/db'; -const actions = require('../../../src/proxy/actions/Action'); -const steps = require('../../../src/proxy/actions/Step'); -const processor = require('../../../src/proxy/processors/push-action/audit'); -const db = require('../../../src/db'); +const execAsync = util.promisify(exec); // cookie file name const GIT_PROXY_COOKIE_FILE = 'git-proxy-cookie'; @@ -17,21 +19,21 @@ const GIT_PROXY_COOKIE_FILE = 'git-proxy-cookie'; * @param {string} cli - The CLI command to be executed. * @param {number} expectedExitCode - The expected exit code after the command * execution. Typically, `0` for successful execution. - * @param {string} expectedMessages - The array of expected messages included + * @param {string[]} expectedMessages - The array of expected messages included * in the output after the command execution. - * @param {string} expectedErrorMessages - The array of expected messages + * @param {string[]} expectedErrorMessages - The array of expected messages * included in the error output after the command execution. * @param {boolean} debug - Flag to enable detailed logging for debugging. * @throws {AssertionError} Throws an error if the actual exit code does not * match the `expectedExitCode`. */ async function runCli( - cli, - expectedExitCode = 0, - expectedMessages = null, - expectedErrorMessages = null, - debug = false, -) { + cli: string, + expectedExitCode: number = 0, + expectedMessages: string[] | undefined = undefined, + expectedErrorMessages: string[] | undefined = undefined, + debug: boolean = false, +): Promise { try { console.log(`cli: '${cli}'`); const { stdout, stderr } = await execAsync(cli); @@ -50,7 +52,7 @@ async function runCli( expect(stderr).to.include(expectedErrorMessage); }); } - } catch (error) { + } catch (error: any) { const exitCode = error.code; if (!exitCode) { // an AssertionError is thrown from failing some of the expectations @@ -85,7 +87,7 @@ async function runCli( * @return {Promise} A promise that resolves when the service has * successfully started. Does not return any value upon resolution. */ -async function startServer(service) { +async function startServer(service: any): Promise { await service.start(); } @@ -104,7 +106,7 @@ async function startServer(service) { * @throws {Error} If the server cannot be closed properly or if an error * occurs during the close operation. */ -async function closeServer(server, waitTime = 0) { +async function closeServer(server: http.Server, waitTime: number = 0): Promise { return new Promise((resolve, reject) => { server.closeAllConnections(); server.close((err) => { @@ -124,7 +126,7 @@ async function closeServer(server, waitTime = 0) { /** * Create local cookies file with an expired connect cookie. */ -async function createCookiesFileWithExpiredCookie() { +async function createCookiesFileWithExpiredCookie(): Promise { await removeCookiesFile(); const cookies = [ // eslint-disable-next-line max-len @@ -136,7 +138,7 @@ async function createCookiesFileWithExpiredCookie() { /** * Remove local cookies file. */ -async function removeCookiesFile() { +async function removeCookiesFile(): Promise { if (fs.existsSync(GIT_PROXY_COOKIE_FILE)) { fs.unlinkSync(GIT_PROXY_COOKIE_FILE); } @@ -144,12 +146,12 @@ async function removeCookiesFile() { /** * Add a new repo to the database. - * @param {object} newRepo The new repo attributes. + * @param {Object} newRepo The new repo attributes. * @param {boolean} debug Print debug messages to console if true. */ -async function addRepoToDb(newRepo, debug = false) { +async function addRepoToDb(newRepo: any, debug: boolean = false): Promise { const repos = await db.getRepos(); - const found = repos.find((y) => y.project === newRepo.project && newRepo.name === y.name); + const found = repos.find((y: any) => y.project === newRepo.project && newRepo.name === y.name); if (!found) { await db.createRepo(newRepo); await db.addUserCanPush(newRepo.name, 'admin'); @@ -168,7 +170,7 @@ async function addRepoToDb(newRepo, debug = false) { * Removes a repo from the DB. * @param {string} repoName The name of the repo to remove. */ -async function removeRepoFromDb(repoName) { +async function removeRepoFromDb(repoName: string): Promise { await db.deleteRepo(repoName); } @@ -179,8 +181,13 @@ async function removeRepoFromDb(repoName) { * @param {string} user The user who pushed the git push. * @param {boolean} debug Flag to enable logging for debugging. */ -async function addGitPushToDb(id, repo, user = null, debug = false) { - const action = new actions.Action( +async function addGitPushToDb( + id: string, + repo: string, + user: string | undefined = undefined, + debug: boolean = false, +): Promise { + const action = new Action( id, 'push', // type 'get', // method @@ -188,7 +195,7 @@ async function addGitPushToDb(id, repo, user = null, debug = false) { repo, ); action.user = user; - const step = new steps.Step( + const step = new Step( 'authBlock', // stepName false, // error null, // errorMessage @@ -204,6 +211,7 @@ async function addGitPushToDb(id, repo, user = null, debug = false) { committer: 'committer', commitTs: 'commitTs', message: 'message', + authorEmail: 'authorEmail', }); action.commitData = commitData; action.addStep(step); @@ -217,7 +225,7 @@ async function addGitPushToDb(id, repo, user = null, debug = false) { * Removes a push from the DB * @param {string} id */ -async function removeGitPushFromDb(id) { +async function removeGitPushFromDb(id: string): Promise { await db.deletePush(id); } @@ -230,7 +238,14 @@ async function removeGitPushFromDb(id) { * @param {boolean} admin Flag to make the user administrator. * @param {boolean} debug Flag to enable logging for debugging. */ -async function addUserToDb(username, password, email, gitAccount, admin = false, debug = false) { +async function addUserToDb( + username: string, + password: string, + email: string, + gitAccount: string, + admin: boolean = false, + debug: boolean = false, +): Promise { const result = await db.createUser(username, password, email, gitAccount, admin); if (debug) { console.log(`New user added to DB: ${util.inspect(result)}`); @@ -241,20 +256,20 @@ async function addUserToDb(username, password, email, gitAccount, admin = false, * Remove a user record from the database if present. * @param {string} username The user name. */ -async function removeUserFromDb(username) { +async function removeUserFromDb(username: string): Promise { await db.deleteUser(username); } -module.exports = { - runCli: runCli, - startServer: startServer, - closeServer: closeServer, - addRepoToDb: addRepoToDb, - removeRepoFromDb: removeRepoFromDb, - addGitPushToDb: addGitPushToDb, - removeGitPushFromDb: removeGitPushFromDb, - addUserToDb: addUserToDb, - removeUserFromDb: removeUserFromDb, - createCookiesFileWithExpiredCookie: createCookiesFileWithExpiredCookie, - removeCookiesFile: removeCookiesFile, +export { + runCli, + startServer, + closeServer, + addRepoToDb, + removeRepoFromDb, + addGitPushToDb, + removeGitPushFromDb, + addUserToDb, + removeUserFromDb, + createCookiesFileWithExpiredCookie, + removeCookiesFile, }; diff --git a/packages/git-proxy-cli/tsconfig.json b/packages/git-proxy-cli/tsconfig.json index 236bfabc5..3ee7e2022 100644 --- a/packages/git-proxy-cli/tsconfig.json +++ b/packages/git-proxy-cli/tsconfig.json @@ -5,14 +5,24 @@ "allowJs": true, "checkJs": false, "jsx": "react-jsx", - "moduleResolution": "Node", + "moduleResolution": "nodenext", "strict": true, - "noEmit": true, + "declaration": true, "skipLibCheck": true, "isolatedModules": true, - "module": "CommonJS", + "module": "NodeNext", "esModuleInterop": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "." }, - "include": ["index.js", "test", "coverage"] + "include": ["index.ts", "types.ts"], + "exclude": [ + "src/config/**/*", + "src/db/**/*", + "src/proxy/**/*", + "src/service/**/*", + "src/ui/**/*" + ] } diff --git a/packages/git-proxy-cli/types.ts b/packages/git-proxy-cli/types.ts new file mode 100644 index 000000000..22feb22b9 --- /dev/null +++ b/packages/git-proxy-cli/types.ts @@ -0,0 +1,35 @@ +export interface PushFilters { + allowPush?: boolean; + authorised?: boolean; + blocked?: boolean; + canceled?: boolean; + error?: boolean; + rejected?: boolean; +} + +export interface PushData { + id: string; + timestamp: number; + url: string; + allowPush: boolean; + authorised: boolean; + blocked: boolean; + canceled: boolean; + error: boolean; + rejected: boolean; + lastStep?: PushStep; + commitData?: CommitData[]; +} + +export interface PushStep { + stepName: string; + error: boolean; + errorMessage: string; + blocked: boolean; + blockedMessage: string; +} + +export interface CommitData { + message: string; + committer: string; +} diff --git a/src/config/index.ts b/src/config/index.ts index a3ea42136..81093f138 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -32,7 +32,7 @@ let _privateOrganizations: string[] = defaultSettings.privateOrganizations; let _urlShortener: string = defaultSettings.urlShortener; let _contactEmail: string = defaultSettings.contactEmail; let _csrfProtection: boolean = defaultSettings.csrfProtection; -let _domains: Record = defaultSettings.domains; +let _domains: Record = defaultSettings.domains; let _rateLimit: RateLimitConfig = defaultSettings.rateLimit; // These are not always present in the default config file, so casting is required diff --git a/src/config/types.ts b/src/config/types.ts index 291de4081..b2d66cc2f 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -21,7 +21,7 @@ export interface UserSettings { urlShortener: string; contactEmail: string; csrfProtection: boolean; - domains: Record; + domains: Record; rateLimit: RateLimitConfig; } @@ -48,7 +48,39 @@ export interface Database { export interface Authentication { type: string; enabled: boolean; - options?: Record; + oidcConfig?: OidcConfig; + adConfig?: AdConfig; + jwtConfig?: JwtConfig; + + // Deprecated fields for backwards compatibility + // TODO: remove in future release and keep the ones in adConfig + userGroup?: string; + adminGroup?: string; + domain?: string; +} + +export interface OidcConfig { + issuer: string; + clientID: string; + clientSecret: string; + callbackURL: string; + scope: string; +} + +export interface AdConfig { + url: string; + baseDN: string; + searchBase: string; + userGroup?: string; + adminGroup?: string; + domain?: string; +} + +export interface JwtConfig { + clientID: string; + authorityURL: string; + roleMapping: Record; + expectedAudience?: string; } export interface TempPasswordConfig { diff --git a/src/proxy/processors/push-action/pullRemote.ts b/src/proxy/processors/push-action/pullRemote.ts index 2f7c808a2..d62c2469e 100644 --- a/src/proxy/processors/push-action/pullRemote.ts +++ b/src/proxy/processors/push-action/pullRemote.ts @@ -1,11 +1,12 @@ import { Action, Step } from '../../actions'; import fs from 'fs'; -import git from 'isomorphic-git'; -import gitHttpClient from 'isomorphic-git/http/node'; const dir = './.remote'; const exec = async (req: any, action: Action): Promise => { + const git = await import('isomorphic-git'); + const gitHttpClient = await import('isomorphic-git/http/node/index.js'); + const step = new Step('pullRemote'); try { diff --git a/src/proxy/routes/index.ts b/src/proxy/routes/index.ts index 1e1cfff46..416a30568 100644 --- a/src/proxy/routes/index.ts +++ b/src/proxy/routes/index.ts @@ -48,7 +48,7 @@ const validGitRequest = (url: string, headers: any): boolean => { return false; } // https://www.git-scm.com/docs/http-protocol#_uploading_data - return agent.startsWith('git/') && accept.startsWith('application/x-git-') ; + return agent.startsWith('git/') && accept.startsWith('application/x-git-'); } return false; }; diff --git a/src/service/index.js b/src/service/index.js deleted file mode 100644 index 02e416aa0..000000000 --- a/src/service/index.js +++ /dev/null @@ -1,123 +0,0 @@ -const express = require('express'); -const session = require('express-session'); -const http = require('http'); -const cors = require('cors'); -const app = express(); -const path = require('path'); -const config = require('../config'); -const db = require('../db'); -const rateLimit = require('express-rate-limit'); -const lusca = require('lusca'); -const configLoader = require('../config/ConfigLoader'); -const proxy = require('../proxy'); - -const limiter = rateLimit(config.getRateLimit()); - -const { GIT_PROXY_UI_PORT: uiPort } = require('../config/env').serverConfig; - -const _httpServer = http.createServer(app); - -const corsOptions = { - credentials: true, - origin: true, -}; - -const createApp = async () => { - // configuration of passport is async - // Before we can bind the routes - we need the passport strategy - const passport = await require('./passport').configure(); - const routes = require('./routes'); - const absBuildPath = path.join(__dirname, '../../build'); - app.use(cors(corsOptions)); - app.set('trust proxy', 1); - app.use(limiter); - - // Add new admin-only endpoint to reload config - app.post('/api/v1/admin/reload-config', async (req, res) => { - if (!req.isAuthenticated() || !req.user.admin) { - return res.status(403).json({ error: 'Unauthorized' }); - } - - try { - // 1. Reload configuration - await configLoader.loadConfiguration(); - - // 2. Stop existing services - await proxy.stop(); - - // 3. Apply new configuration - config.validate(); - - // 4. Restart services with new config - await proxy.start(); - - console.log('Configuration reloaded and services restarted successfully'); - res.json({ status: 'success', message: 'Configuration reloaded and services restarted' }); - } catch (error) { - console.error('Failed to reload configuration and restart services:', error); - - // Attempt to restart with existing config if reload fails - try { - await proxy.start(); - } catch (startError) { - console.error('Failed to restart services:', startError); - } - - res.status(500).json({ error: 'Failed to reload configuration' }); - } - }); - - app.use( - session({ - store: config.getDatabase().type === 'mongo' ? db.getSessionStore(session) : null, - secret: config.getCookieSecret(), - resave: false, - saveUninitialized: false, - cookie: { - secure: 'auto', - httpOnly: true, - maxAge: config.getSessionMaxAgeHours() * 60 * 60 * 1000, - }, - }), - ); - if (config.getCSRFProtection() && process.env.NODE_ENV !== 'test') { - app.use( - lusca({ - csrf: { - cookie: { name: 'csrf' }, - }, - hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, - nosniff: true, - referrerPolicy: 'same-origin', - xframe: 'SAMEORIGIN', - xssProtection: true, - }), - ); - } - app.use(passport.initialize()); - app.use(passport.session()); - app.use(express.json()); - app.use(express.urlencoded({ extended: true })); - app.use('/', routes); - app.use('/', express.static(absBuildPath)); - app.get('/*', (req, res) => { - res.sendFile(path.join(`${absBuildPath}/index.html`)); - }); - - return app; -}; - -const start = async () => { - const app = await createApp(); - - _httpServer.listen(uiPort); - - console.log(`Service Listening on ${uiPort}`); - app.emit('ready'); - - return app; -}; - -module.exports.createApp = createApp; -module.exports.start = start; -module.exports.httpServer = _httpServer; diff --git a/src/service/index.ts b/src/service/index.ts new file mode 100644 index 000000000..778b7f960 --- /dev/null +++ b/src/service/index.ts @@ -0,0 +1,90 @@ +import express, { Express } from 'express'; +import session from 'express-session'; +import http from 'http'; +import cors from 'cors'; +import path from 'path'; +import rateLimit from 'express-rate-limit'; +import lusca from 'lusca'; + +import * as config from '../config'; +import * as db from '../db'; +import { configure as configurePassport } from './passport'; +import routes from './routes'; +import { serverConfig } from '../config/env'; + +const app: Express = express(); +const limiter = rateLimit(config.getRateLimit()); +const { GIT_PROXY_UI_PORT: uiPort } = serverConfig; + +const _httpServer = http.createServer(app); + +const corsOptions = { + credentials: true, + origin: true, +}; + +export const createApp = async (): Promise => { + const passport = await configurePassport(); + const absBuildPath = path.join(__dirname, '../../build'); + + app.use(cors(corsOptions)); + app.set('trust proxy', 1); + app.use(limiter); + + const sessionStore = + config.getDatabase().type === 'mongo' ? db.getSessionStore(session) : undefined; + + app.use( + session({ + store: sessionStore, + secret: config.getCookieSecret(), + resave: false, + saveUninitialized: false, + cookie: { + secure: 'auto', + httpOnly: true, + maxAge: config.getSessionMaxAgeHours() * 60 * 60 * 1000, + }, + }), + ); + + if (config.getCSRFProtection() && process.env.NODE_ENV !== 'test') { + app.use( + lusca({ + csrf: { + cookie: { name: 'csrf' }, + }, + hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, + nosniff: true, + referrerPolicy: 'same-origin', + xframe: 'SAMEORIGIN', + xssProtection: true, + }), + ); + } + + app.use(passport.initialize()); + app.use(passport.session()); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + app.use('/', routes); + app.use('/', express.static(absBuildPath)); + app.get('/*', (_req, res) => { + res.sendFile(path.join(`${absBuildPath}/index.html`)); + }); + + return app; +}; + +export const start = async (): Promise => { + const app = await createApp(); + + _httpServer.listen(uiPort); + console.log(`Service Listening on ${uiPort}`); + app.emit('ready'); + + return app; +}; + +export const httpServer = _httpServer; diff --git a/src/service/passport/activeDirectory.js b/src/service/passport/activeDirectory.ts similarity index 63% rename from src/service/passport/activeDirectory.js rename to src/service/passport/activeDirectory.ts index 8f681c823..cef397d00 100644 --- a/src/service/passport/activeDirectory.js +++ b/src/service/passport/activeDirectory.ts @@ -1,18 +1,33 @@ -const ActiveDirectoryStrategy = require('passport-activedirectory'); -const ldaphelper = require('./ldaphelper'); +import ActiveDirectoryStrategy from 'passport-activedirectory'; +import { PassportStatic } from 'passport'; +import * as ldaphelper from './ldaphelper'; +import * as db from '../../db'; +import { getAuthMethods } from '../../config'; -const type = 'activedirectory'; +export const type = 'activedirectory'; -const configure = (passport) => { - const db = require('../../db'); - - // We can refactor this by normalizing auth strategy config and pass it directly into the configure() function, - // ideally when we convert this to TS. - const authMethods = require('../../config').getAuthMethods(); +export const configure = async (passport: PassportStatic): Promise => { + const authMethods = getAuthMethods(); const config = authMethods.find((method) => method.type.toLowerCase() === type); + + if (!config || !config.adConfig) { + throw new Error('AD authentication method not enabled'); + } + const adConfig = config.adConfig; - const { userGroup, adminGroup, domain } = config; + if (!adConfig) { + throw new Error('Invalid Active Directory configuration'); + } + + // Handle legacy config + const userGroup = adConfig.userGroup || config.userGroup; + const adminGroup = adConfig.adminGroup || config.adminGroup; + const domain = adConfig.domain || config.domain; + + if (!userGroup || !adminGroup || !domain) { + throw new Error('Invalid Active Directory configuration'); + } console.log(`AD User Group: ${userGroup}, AD Admin Group: ${adminGroup}`); @@ -24,7 +39,7 @@ const configure = (passport) => { integrated: false, ldap: adConfig, }, - async function (req, profile, ad, done) { + async function (req: any, profile: any, ad: any, done: (err: any, user: any) => void) { try { profile.username = profile._json.sAMAccountName?.toLowerCase(); profile.email = profile._json.mail; @@ -43,8 +58,7 @@ const configure = (passport) => { const message = `User it not a member of ${userGroup}`; return done(message, null); } - } catch (err) { - console.log('ad test (isUser): e', err); + } catch (err: any) { const message = `An error occurred while checking if the user is a member of the user group: ${err.message}`; return done(message, null); } @@ -54,8 +68,8 @@ const configure = (passport) => { try { isAdmin = await ldaphelper.isUserInAdGroup(req, profile, ad, domain, adminGroup); - } catch (err) { - const message = `An error occurred while checking if the user is a member of the admin group: ${err.message}`; + } catch (err: any) { + const message = `An error occurred while checking if the user is a member of the admin group: ${JSON.stringify(err)}`; console.error(message, err); // don't return an error for this case as you may still be a user } @@ -73,24 +87,21 @@ const configure = (passport) => { await db.updateUser(user); return done(null, user); - } catch (err) { + } catch (err: any) { console.log(`Error authenticating AD user: ${err.message}`); return done(err, null); } - }, - ), + } + ) ); - passport.serializeUser(function (user, done) { + passport.serializeUser(function (user: any, done: (err: any, user: any) => void) { done(null, user); }); - passport.deserializeUser(function (user, done) { + passport.deserializeUser(function (user: any, done: (err: any, user: any) => void) { done(null, user); }); - passport.type = "ActiveDirectory"; return passport; }; - -module.exports = { configure, type }; diff --git a/src/service/passport/index.js b/src/service/passport/index.js deleted file mode 100644 index 72918282f..000000000 --- a/src/service/passport/index.js +++ /dev/null @@ -1,36 +0,0 @@ -const passport = require("passport"); -const local = require('./local'); -const activeDirectory = require('./activeDirectory'); -const oidc = require('./oidc'); -const config = require('../../config'); - -// Allows obtaining strategy config function and type -// Keep in mind to add AuthStrategy enum when refactoring this to TS -const authStrategies = { - local: local, - activedirectory: activeDirectory, - openidconnect: oidc, -}; - -const configure = async () => { - passport.initialize(); - - const authMethods = config.getAuthMethods(); - - for (const auth of authMethods) { - const strategy = authStrategies[auth.type.toLowerCase()]; - if (strategy && typeof strategy.configure === "function") { - await strategy.configure(passport); - } - } - - if (authMethods.some(auth => auth.type.toLowerCase() === "local")) { - await local.createDefaultAdmin(); - } - - return passport; -}; - -const getPassport = () => passport; - -module.exports = { authStrategies, configure, getPassport }; diff --git a/src/service/passport/index.ts b/src/service/passport/index.ts new file mode 100644 index 000000000..07852508a --- /dev/null +++ b/src/service/passport/index.ts @@ -0,0 +1,39 @@ +import passport, { PassportStatic } from 'passport'; +import * as local from './local'; +import * as activeDirectory from './activeDirectory'; +import * as oidc from './oidc'; +import * as config from '../../config'; +import { Authentication } from '../../config/types'; + +type StrategyModule = { + configure: (passport: PassportStatic) => Promise; + createDefaultAdmin?: () => Promise; + type: string; +}; + +export const authStrategies: Record = { + local, + activedirectory: activeDirectory, + openidconnect: oidc, +}; + +export const configure = async (): Promise => { + passport.initialize(); + + const authMethods: Authentication[] = config.getAuthMethods(); + + for (const auth of authMethods) { + const strategy = authStrategies[auth.type.toLowerCase()]; + if (strategy && typeof strategy.configure === 'function') { + await strategy.configure(passport); + } + } + + if (authMethods.some(auth => auth.type.toLowerCase() === 'local')) { + await local.createDefaultAdmin?.(); + } + + return passport; +}; + +export const getPassport = (): PassportStatic => passport; diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js deleted file mode 100644 index 32c81304d..000000000 --- a/src/service/passport/jwtAuthHandler.js +++ /dev/null @@ -1,53 +0,0 @@ -const { assignRoles, validateJwt } = require('./jwtUtils'); - -/** - * Middleware function to handle JWT authentication. - * @param {*} overrideConfig optional configuration to override the default JWT configuration (e.g. for testing) - * @return {Function} the middleware function - */ -const jwtAuthHandler = (overrideConfig = null) => { - return async (req, res, next) => { - const apiAuthMethods = - overrideConfig - ? [{ type: "jwt", jwtConfig: overrideConfig }] - : require('../../config').getAPIAuthMethods(); - - const jwtAuthMethod = apiAuthMethods.find((method) => method.type.toLowerCase() === "jwt"); - if (!overrideConfig && (!jwtAuthMethod || !jwtAuthMethod.enabled)) { - return next(); - } - - if (req.isAuthenticated()) { - return next(); - } - - const token = req.header("Authorization"); - if (!token) { - return res.status(401).send("No token provided\n"); - } - - const { clientID, authorityURL, expectedAudience, roleMapping } = jwtAuthMethod.jwtConfig; - const audience = expectedAudience || clientID; - - if (!authorityURL) { - return res.status(500).send("OIDC authority URL is not configured\n"); - } - - if (!clientID) { - return res.status(500).send("OIDC client ID is not configured\n"); - } - - const tokenParts = token.split(" "); - const { verifiedPayload, error } = await validateJwt(tokenParts[1], authorityURL, audience, clientID); - if (error) { - return res.status(401).send(error); - } - - req.user = verifiedPayload; - assignRoles(roleMapping, verifiedPayload, req.user); - - return next(); - } -} - -module.exports = jwtAuthHandler; diff --git a/src/service/passport/jwtAuthHandler.ts b/src/service/passport/jwtAuthHandler.ts new file mode 100644 index 000000000..b95440d1d --- /dev/null +++ b/src/service/passport/jwtAuthHandler.ts @@ -0,0 +1,65 @@ +import { assignRoles, validateJwt } from './jwtUtils'; +import { Request, Response, NextFunction } from 'express'; +import { getAPIAuthMethods } from '../../config'; +import { JwtConfig, Authentication } from '../../config/types'; +import { RoleMapping } from './types'; + +export const jwtAuthHandler = (overrideConfig: JwtConfig | null = null) => { + return async (req: Request, res: Response, next: NextFunction): Promise => { + const apiAuthMethods: Authentication[] = overrideConfig + ? [{ type: 'jwt', enabled: true, jwtConfig: overrideConfig }] + : getAPIAuthMethods(); + + const jwtAuthMethod = apiAuthMethods.find( + (method) => method.type.toLowerCase() === 'jwt' + ); + + if (!overrideConfig && (!jwtAuthMethod || !jwtAuthMethod.enabled)) { + return next(); + } + + if (req.isAuthenticated?.()) { + return next(); + } + + const token = req.header('Authorization'); + if (!token) { + res.status(401).send('No token provided\n'); + return; + } + + const config = jwtAuthMethod!.jwtConfig!; + const { clientID, authorityURL, expectedAudience, roleMapping } = config; + const audience = expectedAudience || clientID; + + if (!authorityURL) { + res.status(500).send('OIDC authority URL is not configured\n'); + return; + } + + if (!clientID) { + res.status(500).send('OIDC client ID is not configured\n'); + return; + } + + const tokenParts = token.split(' '); + const accessToken = tokenParts.length === 2 ? tokenParts[1] : tokenParts[0]; + + const { verifiedPayload, error } = await validateJwt( + accessToken, + authorityURL, + audience, + clientID + ); + + if (error || !verifiedPayload) { + res.status(401).send(error || 'JWT validation failed\n'); + return; + } + + req.user = verifiedPayload; + assignRoles(roleMapping as RoleMapping, verifiedPayload, req.user); + + next(); + }; +}; diff --git a/src/service/passport/jwtUtils.js b/src/service/passport/jwtUtils.js deleted file mode 100644 index 45bda4cc9..000000000 --- a/src/service/passport/jwtUtils.js +++ /dev/null @@ -1,93 +0,0 @@ -const axios = require("axios"); -const jwt = require("jsonwebtoken"); -const jwkToPem = require("jwk-to-pem"); - -/** - * Obtain the JSON Web Key Set (JWKS) from the OIDC authority. - * @param {string} authorityUrl the OIDC authority URL. e.g. https://login.microsoftonline.com/{tenantId} - * @return {Promise} the JWKS keys - */ -async function getJwks(authorityUrl) { - try { - const { data } = await axios.get(`${authorityUrl}/.well-known/openid-configuration`); - const jwksUri = data.jwks_uri; - - const { data: jwks } = await axios.get(jwksUri); - return jwks.keys; - } catch (error) { - console.error("Error fetching JWKS:", error); - throw new Error("Failed to fetch JWKS"); - } -} - -/** - * Validate a JWT token using the OIDC configuration. - * @param {*} token the JWT token - * @param {*} authorityUrl the OIDC authority URL - * @param {*} clientID the OIDC client ID - * @param {*} expectedAudience the expected audience for the token - * @param {*} getJwksInject the getJwks function to use (for dependency injection). Defaults to the built-in getJwks function. - * @return {Promise} the verified payload or an error - */ -async function validateJwt(token, authorityUrl, clientID, expectedAudience, getJwksInject = getJwks) { - try { - const jwks = await getJwksInject(authorityUrl); - - const decodedHeader = await jwt.decode(token, { complete: true }); - if (!decodedHeader || !decodedHeader.header || !decodedHeader.header.kid) { - throw new Error("Invalid JWT: Missing key ID (kid)"); - } - - const { kid } = decodedHeader.header; - const jwk = jwks.find((key) => key.kid === kid); - if (!jwk) { - throw new Error("No matching key found in JWKS"); - } - - const pubKey = jwkToPem(jwk); - - const verifiedPayload = jwt.verify(token, pubKey, { - algorithms: ["RS256"], - issuer: authorityUrl, - audience: expectedAudience, - }); - - if (verifiedPayload.azp !== clientID) { - throw new Error("JWT client ID does not match"); - } - - return { verifiedPayload }; - } catch (error) { - const errorMessage = `JWT validation failed: ${error.message}\n`; - console.error(errorMessage); - return { error: errorMessage }; - } -} - -/** - * Assign roles to the user based on the role mappings provided in the jwtConfig. - * - * If no role mapping is provided, the user will not have any roles assigned (i.e. user.admin = false). - * @param {*} roleMapping the role mapping configuration - * @param {*} payload the JWT payload - * @param {*} user the req.user object to assign roles to - */ -function assignRoles(roleMapping, payload, user) { - if (roleMapping) { - for (const role of Object.keys(roleMapping)) { - const claimValuePair = roleMapping[role]; - const claim = Object.keys(claimValuePair)[0]; - const value = claimValuePair[claim]; - - if (payload[claim] && payload[claim] === value) { - user[role] = true; - } - } - } -} - -module.exports = { - getJwks, - validateJwt, - assignRoles, -}; diff --git a/src/service/passport/jwtUtils.ts b/src/service/passport/jwtUtils.ts new file mode 100644 index 000000000..7effa59f4 --- /dev/null +++ b/src/service/passport/jwtUtils.ts @@ -0,0 +1,99 @@ +import axios from 'axios'; +import jwt, { JwtPayload } from 'jsonwebtoken'; +import jwkToPem from 'jwk-to-pem'; + +import { JwkKey, JwksResponse, JwtValidationResult, RoleMapping } from './types'; + +/** + * Obtain the JSON Web Key Set (JWKS) from the OIDC authority. + * @param {string} authorityUrl the OIDC authority URL. e.g. https://login.microsoftonline.com/{tenantId} + * @return {Promise} the JWKS keys + */ +export async function getJwks(authorityUrl: string): Promise { + try { + const { data } = await axios.get(`${authorityUrl}/.well-known/openid-configuration`); + const jwksUri: string = data.jwks_uri; + + const { data: jwks }: { data: JwksResponse } = await axios.get(jwksUri); + return jwks.keys; + } catch (error) { + console.error('Error fetching JWKS:', error); + throw new Error('Failed to fetch JWKS'); + } +} + +/** + * Validate a JWT token using the OIDC configuration. + * @param {string} token the JWT token + * @param {string} authorityUrl the OIDC authority URL + * @param {string} expectedAudience the expected audience for the token + * @param {string} clientID the OIDC client ID + * @param {Function} getJwksInject the getJwks function to use (for dependency injection). Defaults to the built-in getJwks function. + * @return {Promise} the verified payload or an error + */ +export async function validateJwt( + token: string, + authorityUrl: string, + expectedAudience: string, + clientID: string, + getJwksInject: (authorityUrl: string) => Promise = getJwks +): Promise { + try { + const jwks = await getJwksInject(authorityUrl); + + const decoded = jwt.decode(token, { complete: true }); + if (!decoded || typeof decoded !== 'object' || !decoded.header?.kid) { + throw new Error('Invalid JWT: Missing key ID (kid)'); + } + + const { kid } = decoded.header; + const jwk = jwks.find((key) => key.kid === kid); + if (!jwk) { + throw new Error('No matching key found in JWKS'); + } + + const pubKey = jwkToPem(jwk as any); + + const verifiedPayload = jwt.verify(token, pubKey, { + algorithms: ['RS256'], + issuer: authorityUrl, + audience: expectedAudience, + }) as JwtPayload; + + if (verifiedPayload.azp && verifiedPayload.azp !== clientID) { + throw new Error('JWT client ID does not match'); + } + + return { verifiedPayload, error: null }; + } catch (error: any) { + const errorMessage = `JWT validation failed: ${error.message}\n`; + console.error(errorMessage); + return { error: errorMessage, verifiedPayload: null }; + } +} + +/** + * Assign roles to the user based on the role mappings provided in the jwtConfig. + * + * If no role mapping is provided, the user will not have any roles assigned (i.e. user.admin = false). + * @param {RoleMapping} roleMapping the role mapping configuration + * @param {JwtPayload} payload the JWT payload + * @param {Record} user the req.user object to assign roles to + */ +export function assignRoles( + roleMapping: RoleMapping | undefined, + payload: JwtPayload, + user: Record +): void { + if (!roleMapping) return; + + for (const role of Object.keys(roleMapping)) { + const claimMap = roleMapping[role]; + const claim = Object.keys(claimMap)[0]; + const value = claimMap[claim]; + + if (payload[claim] && payload[claim] === value) { + user[role] = true; + } + } +} diff --git a/src/service/passport/ldaphelper.js b/src/service/passport/ldaphelper.js deleted file mode 100644 index 00ba01f00..000000000 --- a/src/service/passport/ldaphelper.js +++ /dev/null @@ -1,51 +0,0 @@ -const thirdpartyApiConfig = require('../../config').getAPIs(); -const axios = require('axios'); - -const isUserInAdGroup = (req, profile, ad, domain, name) => { - // determine, via config, if we're using HTTP or AD directly - if (thirdpartyApiConfig?.ls?.userInADGroup) { - return isUserInAdGroupViaHttp(profile.username, domain, name); - } else { - return isUserInAdGroupViaAD(req, profile, ad, domain, name); - } -}; - -const isUserInAdGroupViaAD = (req, profile, ad, domain, name) => { - return new Promise((resolve, reject) => { - ad.isUserMemberOf(profile.username, name, function (err, isMember) { - if (err) { - const msg = 'ERROR isUserMemberOf: ' + JSON.stringify(err); - reject(msg); - } else { - console.log(profile.username + ' isMemberOf ' + name + ': ' + isMember); - resolve(isMember); - } - }); - }); -}; - -const isUserInAdGroupViaHttp = (id, domain, name) => { - const url = String(thirdpartyApiConfig.ls.userInADGroup) - .replace('', domain) - .replace('', name) - .replace('', id); - - const client = axios.create({ - responseType: 'json', - headers: { - 'content-type': 'application/json', - }, - }); - - console.log(`checking if user is in group ${url}`); - return client - .get(url) - .then((res) => res.data) - .catch(() => { - return false; - }); -}; - -module.exports = { - isUserInAdGroup, -}; diff --git a/src/service/passport/ldaphelper.ts b/src/service/passport/ldaphelper.ts new file mode 100644 index 000000000..c34d1271c --- /dev/null +++ b/src/service/passport/ldaphelper.ts @@ -0,0 +1,66 @@ +import axios from 'axios'; +import { Request } from 'express'; + +import { getAPIs } from '../../config'; +import { AD } from './types'; + +const thirdpartyApiConfig = getAPIs(); + +export const isUserInAdGroup = ( + req: Request, + profile: { username: string }, + ad: AD, + domain: string, + name: string +): Promise => { + // determine, via config, if we're using HTTP or AD directly + if ((thirdpartyApiConfig?.ls as any).userInADGroup !== '') { + return isUserInAdGroupViaHttp(profile.username, domain, name); + } else { + return isUserInAdGroupViaAD(req, profile, ad, domain, name); + } +}; + +const isUserInAdGroupViaAD = ( + req: Request, + profile: { username: string }, + ad: AD, + domain: string, + name: string +): Promise => { + return new Promise((resolve, reject) => { + ad.isUserMemberOf(profile.username, name, (err, isMember) => { + if (err) { + const msg = `ERROR isUserMemberOf: ${JSON.stringify(err)}`; + reject(msg); + } else { + console.log(`${profile.username} isMemberOf ${name}: ${isMember}`); + resolve(isMember); + } + }); + }); +}; + +const isUserInAdGroupViaHttp = ( + id: string, + domain: string, + name: string +): Promise => { + const url = String((thirdpartyApiConfig?.ls as any).userInADGroup) + .replace('', domain) + .replace('', name) + .replace('', id); + + const client = axios.create({ + responseType: 'json', + headers: { + 'content-type': 'application/json', + }, + }); + + console.log(`checking if user is in group ${url}`); + return client + .get(url) + .then((res) => res.data) + .catch(() => false); +}; diff --git a/src/service/passport/local.js b/src/service/passport/local.ts similarity index 57% rename from src/service/passport/local.js rename to src/service/passport/local.ts index 579d47234..6f1460a32 100644 --- a/src/service/passport/local.js +++ b/src/service/passport/local.ts @@ -1,12 +1,13 @@ -const bcrypt = require("bcryptjs"); -const LocalStrategy = require("passport-local").Strategy; -const db = require("../../db"); +import bcrypt from "bcryptjs"; +import { Strategy as LocalStrategy } from "passport-local"; +import type { PassportStatic } from "passport"; +import * as db from "../../db"; -const type = "local"; +export const type = "local"; -const configure = async (passport) => { +export const configure = async (passport: PassportStatic): Promise => { passport.use( - new LocalStrategy(async (username, password, done) => { + new LocalStrategy(async (username: string, password: string, done) => { try { const user = await db.findUser(username); if (!user) { @@ -20,21 +21,21 @@ const configure = async (passport) => { return done(null, user); } catch (err) { - return done(err); + return done(err as Error); } }) ); - passport.serializeUser((user, done) => { + passport.serializeUser((user: any, done) => { done(null, user.username); }); - passport.deserializeUser(async (username, done) => { + passport.deserializeUser(async (username: string, done) => { try { const user = await db.findUser(username); done(null, user); } catch (err) { - done(err, null); + done(err as Error, null); } }); @@ -44,11 +45,9 @@ const configure = async (passport) => { /** * Create the default admin user if it doesn't exist */ -const createDefaultAdmin = async () => { +export const createDefaultAdmin = async (): Promise => { const admin = await db.findUser("admin"); if (!admin) { await db.createUser("admin", "admin", "admin@place.com", "none", true); } }; - -module.exports = { configure, createDefaultAdmin, type }; diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.ts similarity index 52% rename from src/service/passport/oidc.js rename to src/service/passport/oidc.ts index 7e2aa5ee0..151091e59 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.ts @@ -1,42 +1,48 @@ -const db = require('../../db'); +import * as db from '../../db'; +import { PassportStatic } from 'passport'; +import { getAuthMethods } from '../../config/index.ts'; +import { UserInfoResponse } from './types'; -const type = 'openidconnect'; +export const type = 'openidconnect'; -const configure = async (passport) => { - // Temp fix for ERR_REQUIRE_ESM, will be changed when we refactor to ESM +export const configure = async (passport: PassportStatic): Promise => { + // Use dynamic imports to avoid ESM/CommonJS issues const { discovery, fetchUserInfo } = await import('openid-client'); const { Strategy } = await import('openid-client/passport'); - const authMethods = require('../../config').getAuthMethods(); + + const authMethods = getAuthMethods(); const oidcConfig = authMethods.find( (method) => method.type.toLowerCase() === 'openidconnect', )?.oidcConfig; - const { issuer, clientID, clientSecret, callbackURL, scope } = oidcConfig; if (!oidcConfig || !oidcConfig.issuer) { throw new Error('Missing OIDC issuer in configuration'); } + const { issuer, clientID, clientSecret, callbackURL, scope } = oidcConfig; + const server = new URL(issuer); let config; try { config = await discovery(server, clientID, clientSecret); - } catch (error) { + } catch (error: any) { console.error('Error during OIDC discovery:', error); throw new Error('OIDC setup error (discovery): ' + error.message); } try { - const strategy = new Strategy({ callbackURL, config, scope }, async (tokenSet, done) => { - // Validate token sub for added security - const idTokenClaims = tokenSet.claims(); - const expectedSub = idTokenClaims.sub; - const userInfo = await fetchUserInfo(config, tokenSet.access_token, expectedSub); - handleUserAuthentication(userInfo, done); - }); + const strategy = new Strategy( + { callbackURL, config, scope }, + async (tokenSet: any, done: (err: any, user?: any) => void) => { + const idTokenClaims = tokenSet.claims(); + const expectedSub = idTokenClaims.sub; + const userInfo = await fetchUserInfo(config, tokenSet.access_token, expectedSub); + handleUserAuthentication(userInfo, done); + } + ); - // currentUrl must be overridden to match the callback URL - strategy.currentUrl = function (request) { + strategy.currentUrl = function (request: any) { const callbackUrl = new URL(callbackURL); const currentUrl = Strategy.prototype.currentUrl.call(this, request); currentUrl.host = callbackUrl.host; @@ -44,24 +50,23 @@ const configure = async (passport) => { return currentUrl; }; - // Prevent default strategy name from being overridden with the server host passport.use(type, strategy); - passport.serializeUser((user, done) => { + passport.serializeUser((user: any, done) => { done(null, user.oidcId || user.username); }); - passport.deserializeUser(async (id, done) => { + passport.deserializeUser(async (id: string, done) => { try { const user = await db.findUserByOIDC(id); done(null, user); } catch (err) { - done(err); + done(err as Error); } }); return passport; - } catch (error) { + } catch (error: any) { console.error('Error during OIDC passport setup:', error); throw new Error('OIDC setup error (strategy): ' + error.message); } @@ -69,11 +74,11 @@ const configure = async (passport) => { /** * Handles user authentication with OIDC. - * @param {Object} userInfo the OIDC user info object - * @param {Function} done the callback function - * @return {Promise} a promise with the authenticated user or an error + * @param {UserInfoResponse} userInfo - The user info response from the OIDC provider + * @param {Function} done - The callback function to handle the user authentication + * @return {Promise} - A promise that resolves when the user authentication is complete */ -const handleUserAuthentication = async (userInfo, done) => { +const handleUserAuthentication = async (userInfo: UserInfoResponse, done: (err: any, user?: any) => void): Promise => { console.log('handleUserAuthentication called'); try { const user = await db.findUserByOIDC(userInfo.sub); @@ -88,7 +93,14 @@ const handleUserAuthentication = async (userInfo, done) => { oidcId: userInfo.sub, }; - await db.createUser(newUser.username, null, newUser.email, 'Edit me', false, newUser.oidcId); + await db.createUser( + newUser.username, + '', + newUser.email, + 'Edit me', + false, + newUser.oidcId, + ); return done(null, newUser); } @@ -100,26 +112,24 @@ const handleUserAuthentication = async (userInfo, done) => { /** * Extracts email from OIDC profile. - * This function is necessary because OIDC providers have different ways of storing emails. - * @param {object} profile the profile object from OIDC provider - * @return {string | null} the email address + * Different providers use different fields to store the email. + * @param {any} profile - The user profile from the OIDC provider + * @return {string | null} - The email address from the profile */ -const safelyExtractEmail = (profile) => { +const safelyExtractEmail = (profile: any): string | null => { return ( profile.email || (profile.emails && profile.emails.length > 0 ? profile.emails[0].value : null) ); }; /** - * Generates a username from email address. + * Generates a username from an email address. * This helps differentiate users within the specific OIDC provider. * Note: This is incompatible with multiple providers. Ideally, users are identified by * OIDC ID (requires refactoring the database). - * @param {string} email the email address - * @return {string} the username + * @param {string} email - The email address to generate a username from + * @return {string} - The username generated from the email address */ -const getUsername = (email) => { +const getUsername = (email: string): string => { return email ? email.split('@')[0] : ''; }; - -module.exports = { configure, type }; diff --git a/src/service/passport/types.ts b/src/service/passport/types.ts new file mode 100644 index 000000000..235e1b9ef --- /dev/null +++ b/src/service/passport/types.ts @@ -0,0 +1,70 @@ +import { JwtPayload } from "jsonwebtoken"; + +export type JwkKey = { + kty: string; + kid: string; + use: string; + n?: string; + e?: string; + x5c?: string[]; + [key: string]: any; +}; + +export type JwksResponse = { + keys: JwkKey[]; +}; + +export type JwtValidationResult = { + verifiedPayload: JwtPayload | null; + error: string | null; +} + +/** + * The JWT role mapping configuration. + * + * The key is the in-app role name (e.g. "admin"). + * The value is a pair of claim name and expected value. + * + * For example, the following role mapping will assign the "admin" role to users whose "name" claim is "John Doe": + * + * { + * "admin": { + * "name": "John Doe" + * } + * } + */ +export type RoleMapping = Record>; + +export type AD = { + isUserMemberOf: ( + username: string, + groupName: string, + callback: (err: Error | null, isMember: boolean) => void + ) => void; +} + +/** + * The UserInfoResponse type from openid-client (to fix some type errors) + */ +export type UserInfoResponse = { + readonly sub: string; + readonly name?: string; + readonly given_name?: string; + readonly family_name?: string; + readonly middle_name?: string; + readonly nickname?: string; + readonly preferred_username?: string; + readonly profile?: string; + readonly picture?: string; + readonly website?: string; + readonly email?: string; + readonly email_verified?: boolean; + readonly gender?: string; + readonly birthdate?: string; + readonly zoneinfo?: string; + readonly locale?: string; + readonly phone_number?: string; + readonly updated_at?: number; + readonly address?: any; + readonly [claim: string]: any; +} diff --git a/src/service/routes/auth.js b/src/service/routes/auth.ts similarity index 61% rename from src/service/routes/auth.js rename to src/service/routes/auth.ts index 4a78c774c..6edf44cbb 100644 --- a/src/service/routes/auth.js +++ b/src/service/routes/auth.ts @@ -1,28 +1,27 @@ -const express = require('express'); -const router = new express.Router(); -const passport = require('../passport').getPassport(); -const { getAuthMethods } = require('../../config'); -const passportLocal = require('../passport/local'); -const passportAD = require('../passport/activeDirectory'); -const authStrategies = require('../passport').authStrategies; -const db = require('../../db'); -const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 3000 } = - process.env; - -router.get('/', (req, res) => { +import express, { Request, Response, NextFunction } from 'express'; +import { getPassport, authStrategies } from '../passport'; +import { getAuthMethods } from '../../config'; + +import * as db from '../../db'; +import * as passportLocal from '../passport/local'; +import * as passportAD from '../passport/activeDirectory'; + +import { User } from '../../db/types'; +import { Authentication } from '../../config/types'; + +const router = express.Router(); +const passport = getPassport(); + +const { + GIT_PROXY_UI_HOST: uiHost = 'http://localhost', + GIT_PROXY_UI_PORT: uiPort = '3000', +} = process.env; + +router.get('/', (_req: Request, res: Response) => { res.status(200).json({ - login: { - action: 'post', - uri: '/api/auth/login', - }, - profile: { - action: 'get', - uri: '/api/auth/profile', - }, - logout: { - action: 'post', - uri: '/api/auth/logout', - }, + login: { action: 'post', uri: '/api/auth/login' }, + profile: { action: 'get', uri: '/api/auth/profile' }, + logout: { action: 'post', uri: '/api/auth/logout' }, }); }); @@ -34,7 +33,7 @@ const appropriateLoginStrategies = [passportLocal.type, passportAD.type]; const getLoginStrategy = () => { // returns only enabled auth methods // returns at least one enabled auth method - const enabledAppropriateLoginStrategies = getAuthMethods().filter((am) => + const enabledAppropriateLoginStrategies = getAuthMethods().filter((am: Authentication) => appropriateLoginStrategies.includes(am.type.toLowerCase()), ); // for where no login strategies which work for /login are enabled @@ -50,7 +49,7 @@ const getLoginStrategy = () => { // TODO: if providing separate auth methods, inform the frontend so it has relevant UI elements and appropriate client-side behavior router.post( '/login', - (req, res, next) => { + (req: Request, res: Response, next: NextFunction) => { const authType = getLoginStrategy(); if (authType === null) { res.status(403).send('Username and Password based Login is not enabled at this time').end(); @@ -59,10 +58,10 @@ router.post( console.log('going to auth with', authType); return passport.authenticate(authType)(req, res, next); }, - async (req, res) => { + async (req: Request, res: Response) => { try { - const currentUser = { ...req.user }; - delete currentUser.password; + const currentUser = { ...req.user } as User; + delete (currentUser as any).password; console.log( `serivce.routes.auth.login: user logged in, username=${ currentUser.username @@ -72,7 +71,7 @@ router.post( message: 'success', user: currentUser, }); - } catch (e) { + } catch (e: any) { console.log(`service.routes.auth.login: Error logging user in ${JSON.stringify(e)}`); res.status(500).send('Failed to login').end(); return; @@ -80,10 +79,13 @@ router.post( }, ); -router.get('/oidc', passport.authenticate(authStrategies['openidconnect'].type)); +router.get( + '/oidc', + passport.authenticate(authStrategies['openidconnect'].type) +); -router.get('/oidc/callback', (req, res, next) => { - passport.authenticate(authStrategies['openidconnect'].type, (err, user, info) => { +router.get('/oidc/callback', (req: Request, res: Response, next: NextFunction) => { + passport.authenticate(authStrategies['openidconnect'].type, (err: any, user: any, info: any) => { if (err) { console.error('Authentication error:', err); return res.status(401).end(); @@ -92,43 +94,46 @@ router.get('/oidc/callback', (req, res, next) => { console.error('No user found:', info); return res.status(401).end(); } + req.logIn(user, (err) => { if (err) { console.error('Login error:', err); return res.status(401).end(); } + console.log('Logged in successfully. User:', user); return res.redirect(`${uiHost}:${uiPort}/dashboard/profile`); }); })(req, res, next); }); -router.post('/logout', (req, res, next) => { - req.logout(req.user, (err) => { +router.post('/logout', (req: Request, res: Response, next: NextFunction) => { + req.logout((err) => { if (err) return next(err); + res.clearCookie('connect.sid'); + res.send({ isAuth: req.isAuthenticated?.(), user: req.user }); }); - res.clearCookie('connect.sid'); - res.send({ isAuth: req.isAuthenticated(), user: req.user }); }); -router.get('/profile', async (req, res) => { +router.get('/profile', async (req: Request, res: Response) => { if (req.user) { - const userVal = await db.findUser(req.user.username); - delete userVal.password; + const userVal = await db.findUser((req.user as User).username); + delete (userVal as any).password; res.send(userVal); } else { res.status(401).end(); } }); -router.post('/gitAccount', async (req, res) => { +router.post('/gitAccount', async (req: Request, res: Response) => { if (req.user) { try { - let username = - req.body.username == null || req.body.username == 'undefined' + let username: string = + req.body.username == null || req.body.username === 'undefined' ? req.body.id : req.body.username; - username = username?.split('@')[0]; + + username = username?.split('@')[0] ?? ''; if (!username) { res.status(400).send('Error: Missing username. Git account not updated').end(); @@ -136,12 +141,12 @@ router.post('/gitAccount', async (req, res) => { } const user = await db.findUser(username); + console.log('Adding gitAccount:', req.body.gitAccount); - console.log('Adding gitAccount' + req.body.gitAccount); user.gitAccount = req.body.gitAccount; - db.updateUser(user); + await db.updateUser(user); res.status(200).end(); - } catch (e) { + } catch (e: any) { res .status(500) .send({ @@ -154,16 +159,19 @@ router.post('/gitAccount', async (req, res) => { } }); -router.get('/me', async (req, res) => { +router.get('/me', async (req: Request, res: Response) => { if (req.user) { const user = JSON.parse(JSON.stringify(req.user)); if (user && user.password) delete user.password; + const login = user.username; const userVal = await db.findUser(login); - if (userVal && userVal.password) delete userVal.password; + + if (userVal?.password) delete userVal.password; res.send(userVal); } else { res.status(401).end(); } }); -module.exports = router; + +export default router; diff --git a/src/service/routes/config.js b/src/service/routes/config.js deleted file mode 100644 index e80d70b5b..000000000 --- a/src/service/routes/config.js +++ /dev/null @@ -1,22 +0,0 @@ -const express = require('express'); -const router = new express.Router(); - -const config = require('../../config'); - -router.get('/attestation', function ({ res }) { - res.send(config.getAttestationConfig()); -}); - -router.get('/urlShortener', function ({ res }) { - res.send(config.getURLShortener()); -}); - -router.get('/contactEmail', function ({ res }) { - res.send(config.getContactEmail()); -}); - -router.get('/uiRouteAuth', function ({ res }) { - res.send(config.getUIRouteAuth()); -}); - -module.exports = router; diff --git a/src/service/routes/config.ts b/src/service/routes/config.ts new file mode 100644 index 000000000..0d8796fde --- /dev/null +++ b/src/service/routes/config.ts @@ -0,0 +1,22 @@ +import express, { Request, Response } from 'express'; +import * as config from '../../config'; + +const router = express.Router(); + +router.get('/attestation', (_req: Request, res: Response) => { + res.send(config.getAttestationConfig()); +}); + +router.get('/urlShortener', (_req: Request, res: Response) => { + res.send(config.getURLShortener()); +}); + +router.get('/contactEmail', (_req: Request, res: Response) => { + res.send(config.getContactEmail()); +}); + +router.get('/uiRouteAuth', (_req: Request, res: Response) => { + res.send(config.getUIRouteAuth()); +}); + +export default router; diff --git a/src/service/routes/healthcheck.js b/src/service/routes/healthcheck.js deleted file mode 100644 index 4745a8275..000000000 --- a/src/service/routes/healthcheck.js +++ /dev/null @@ -1,10 +0,0 @@ -const express = require('express'); -const router = new express.Router(); - -router.get('/', function (req, res) { - res.send({ - message: 'ok', - }); -}); - -module.exports = router; diff --git a/src/service/routes/healthcheck.ts b/src/service/routes/healthcheck.ts new file mode 100644 index 000000000..c1518bec0 --- /dev/null +++ b/src/service/routes/healthcheck.ts @@ -0,0 +1,11 @@ +import express, { Request, Response } from 'express'; + +const router = express.Router(); + +router.get('/', function (req: Request, res: Response) { + res.send({ + message: 'ok', + }); +}); + +export default router; diff --git a/src/service/routes/home.js b/src/service/routes/home.js deleted file mode 100644 index ce11503f6..000000000 --- a/src/service/routes/home.js +++ /dev/null @@ -1,14 +0,0 @@ -const express = require('express'); -const router = new express.Router(); - -const resource = { - healthcheck: '/api/v1/healthcheck', - push: '/api/v1/push', - auth: '/api/auth', -}; - -router.get('/', function (req, res) { - res.send(resource); -}); - -module.exports = router; diff --git a/src/service/routes/home.ts b/src/service/routes/home.ts new file mode 100644 index 000000000..07a4d5b06 --- /dev/null +++ b/src/service/routes/home.ts @@ -0,0 +1,15 @@ +import express, { Request, Response } from 'express'; + +const router = express.Router(); + +const resource = { + healthcheck: '/api/v1/healthcheck', + push: '/api/v1/push', + auth: '/api/auth', +}; + +router.get('/', function (req: Request, res: Response) { + res.send(resource); +}); + +export default router; diff --git a/src/service/routes/index.js b/src/service/routes/index.js deleted file mode 100644 index 45b276c17..000000000 --- a/src/service/routes/index.js +++ /dev/null @@ -1,20 +0,0 @@ -const express = require('express'); -const auth = require('./auth'); -const push = require('./push'); -const home = require('./home'); -const repo = require('./repo'); -const users = require('./users'); -const healthcheck = require('./healthcheck'); -const config = require('./config'); -const jwtAuthHandler = require('../passport/jwtAuthHandler'); -const router = new express.Router(); - -router.use('/api', home); -router.use('/api/auth', auth); -router.use('/api/v1/healthcheck', healthcheck); -router.use('/api/v1/push', jwtAuthHandler(), push); -router.use('/api/v1/repo', jwtAuthHandler(), repo); -router.use('/api/v1/user', jwtAuthHandler(), users); -router.use('/api/v1/config', config); - -module.exports = router; diff --git a/src/service/routes/index.ts b/src/service/routes/index.ts new file mode 100644 index 000000000..2b760afb0 --- /dev/null +++ b/src/service/routes/index.ts @@ -0,0 +1,21 @@ +import express from 'express'; +import auth from './auth'; +import push from './push'; +import home from './home'; +import repo from './repo'; +import users from './users'; +import healthcheck from './healthcheck'; +import config from './config'; +import { jwtAuthHandler } from '../passport/jwtAuthHandler'; + +const router = express.Router(); + +router.use('/api', home); +router.use('/api/auth', auth); +router.use('/api/v1/healthcheck', healthcheck); +router.use('/api/v1/push', jwtAuthHandler(), push); +router.use('/api/v1/repo', jwtAuthHandler(), repo); +router.use('/api/v1/user', jwtAuthHandler(), users); +router.use('/api/v1/config', config); + +export default router; diff --git a/src/service/routes/push.js b/src/service/routes/push.js deleted file mode 100644 index 9750375ca..000000000 --- a/src/service/routes/push.js +++ /dev/null @@ -1,184 +0,0 @@ -const express = require('express'); -const router = new express.Router(); -const db = require('../../db'); - -router.get('/', async (req, res) => { - const query = { - type: 'push', - }; - - for (const k in req.query) { - if (!k) continue; - - if (k === 'limit') continue; - if (k === 'skip') continue; - let v = req.query[k]; - if (v === 'false') v = false; - if (v === 'true') v = true; - query[k] = v; - } - - res.send(await db.getPushes(query)); -}); - -router.get('/:id', async (req, res) => { - const id = req.params.id; - const push = await db.getPush(id); - if (push) { - res.send(push); - } else { - res.status(404).send({ - message: 'not found', - }); - } -}); - -router.post('/:id/reject', async (req, res) => { - if (req.user) { - const id = req.params.id; - console.log({ id }); - - // Get the push request - const push = await db.getPush(id); - console.log({ push }); - - // Get the Internal Author of the push via their Git Account name - const gitAccountauthor = push.user; - const list = await db.getUsers({ gitAccount: gitAccountauthor }); - console.log({ list }); - - if (list.length === 0) { - res.status(401).send({ - message: `The git account ${gitAccountauthor} could not be found`, - }); - return; - } - - if (list[0].username.toLowerCase() === req.user.username.toLowerCase() && !list[0].admin) { - res.status(401).send({ - message: `Cannot reject your own changes`, - }); - return; - } - - const isAllowed = await db.canUserApproveRejectPush(id, req.user.username); - console.log({ isAllowed }); - - if (isAllowed) { - const result = await db.reject(id); - console.log(`user ${req.user.username} rejected push request for ${id}`); - res.send(result); - } else { - res.status(401).send({ - message: 'User is not authorised to reject changes', - }); - } - } else { - res.status(401).send({ - message: 'not logged in', - }); - } -}); - -router.post('/:id/authorise', async (req, res) => { - console.log({ req }); - - const questions = req.body.params?.attestation; - console.log({ questions }); - - const attestationComplete = questions?.every((question) => !!question.checked); - console.log({ attestationComplete }); - - if (req.user && attestationComplete) { - const id = req.params.id; - console.log({ id }); - - // Get the push request - const push = await db.getPush(id); - console.log({ push }); - - // Get the Internal Author of the push via their Git Account name - const gitAccountauthor = push.user; - const list = await db.getUsers({ gitAccount: gitAccountauthor }); - console.log({ list }); - - if (list.length === 0) { - res.status(401).send({ - message: `The git account ${gitAccountauthor} could not be found`, - }); - return; - } - - if (list[0].username.toLowerCase() === req.user.username.toLowerCase() && !list[0].admin) { - res.status(401).send({ - message: `Cannot approve your own changes`, - }); - return; - } - - // If we are not the author, now check that we are allowed to authorise on this - // repo - const isAllowed = await db.canUserApproveRejectPush(id, req.user.username); - if (isAllowed) { - console.log(`user ${req.user.username} approved push request for ${id}`); - - const reviewerList = await db.getUsers({ username: req.user.username }); - console.log({ reviewerList }); - - const reviewerGitAccount = reviewerList[0].gitAccount; - console.log({ reviewerGitAccount }); - - if (!reviewerGitAccount) { - res.status(401).send({ - message: 'You must associate a GitHub account with your user before approving...', - }); - return; - } - - const attestation = { - questions, - timestamp: new Date(), - reviewer: { - username: req.user.username, - gitAccount: reviewerGitAccount, - }, - }; - const result = await db.authorise(id, attestation); - res.send(result); - } else { - res.status(401).send({ - message: `user ${req.user.username} not authorised to approve push's on this project`, - }); - } - } else { - res.status(401).send({ - message: 'You are unauthorized to perform this action...', - }); - } -}); - -router.post('/:id/cancel', async (req, res) => { - if (req.user) { - const id = req.params.id; - - const isAllowed = await db.canUserCancelPush(id, req.user.username); - - if (isAllowed) { - const result = await db.cancel(id); - console.log(`user ${req.user.username} canceled push request for ${id}`); - res.send(result); - } else { - console.log(`user ${req.user.username} not authorised to cancel push request for ${id}`); - res.status(401).send({ - message: - 'User ${req.user.username)} not authorised to cancel push requests on this project.', - }); - } - } else { - res.status(401).send({ - message: 'not logged in', - }); - } -}); - -module.exports = router; diff --git a/src/service/routes/push.ts b/src/service/routes/push.ts new file mode 100644 index 000000000..0178fd504 --- /dev/null +++ b/src/service/routes/push.ts @@ -0,0 +1,134 @@ +import express, { Request, Response } from 'express'; +import * as db from '../../db'; +import { AuthenticatedRequest } from './types'; + +const router = express.Router(); + +router.get('/', async (req: Request, res: Response) => { + const query: Record = { type: 'push' }; + + for (const k in req.query) { + if (!k || k === 'limit' || k === 'skip') continue; + let v = req.query[k]; + if (v === 'false') v = false as any; + if (v === 'true') v = true as any; + query[k] = v; + } + + res.send(await db.getPushes(query)); +}); + +router.get('/:id', async (req: Request, res: Response) => { + const { id } = req.params; + const push = await db.getPush(id); + if (push) { + res.send(push); + } else { + res.status(404).send({ message: 'not found' }); + } +}); + +router.post('/:id/reject', async (req: AuthenticatedRequest, res: any) => { + try { + if (!req.user) { + return res.status(401).send({ message: 'not logged in' }); + } + + const { id } = req.params; + const push = await db.getPush(id); + const gitAccountAuthor = push.user; + const users = await db.getUsers({ gitAccount: gitAccountAuthor }); + + if (users.length === 0) { + return res.status(401).send({ message: `The git account ${gitAccountAuthor} could not be found` }); + } + + if (users[0].username.toLowerCase() === req.user.username.toLowerCase() && !users[0].admin) { + return res.status(401).send({ message: `Cannot reject your own changes` }); + } + + const isAllowed = await db.canUserApproveRejectPush(id, req.user.username); + if (isAllowed) { + const result = await db.reject(id); + return res.status(200).send(result); + } else { + return res.status(401).send({ message: 'User is not authorised to reject changes' }); + } + } catch (error) { + console.error('Error rejecting push:', error); + return res.status(500).send({ message: 'Internal server error' }); + } +}); + +router.post('/:id/authorise', async (req: AuthenticatedRequest, res: any) => { + const questions = req.body.params?.attestation; + + const attestationComplete = Array.isArray(questions) + ? questions.every((q) => !!q.checked) + : false; + + if (!req.user || !attestationComplete) { + return res.status(401).send({ message: 'You are unauthorized to perform this action...' }); + } + + const { id } = req.params; + const push = await db.getPush(id); + const gitAccountAuthor = push.user; + const users = await db.getUsers({ gitAccount: gitAccountAuthor }); + + if (users.length === 0) { + return res.status(401).send({ message: `The git account ${gitAccountAuthor} could not be found` }); + } + + if (users[0].username.toLowerCase() === req.user.username.toLowerCase() && !users[0].admin) { + return res.status(401).send({ message: `Cannot approve your own changes` }); + } + + const isAllowed = await db.canUserApproveRejectPush(id, req.user.username); + if (!isAllowed) { + return res.status(401).send({ + message: `user ${req.user.username} not authorised to approve push's on this project`, + }); + } + + const reviewerList = await db.getUsers({ username: req.user.username }); + const reviewerGitAccount = reviewerList[0].gitAccount; + + if (!reviewerGitAccount) { + return res.status(401).send({ + message: 'You must associate a GitHub account with your user before approving...', + }); + } + + const attestation = { + questions, + timestamp: new Date(), + reviewer: { + username: req.user.username, + gitAccount: reviewerGitAccount, + }, + }; + + const result = await db.authorise(id, attestation); + res.send(result); +}); + +router.post('/:id/cancel', async (req: AuthenticatedRequest, res: any) => { + if (!req.user) { + return res.status(401).send({ message: 'not logged in' }); + } + + const { id } = req.params; + const isAllowed = await db.canUserCancelPush(id, req.user.username); + + if (isAllowed) { + const result = await db.cancel(id); + res.send(result); + } else { + res.status(401).send({ + message: `User ${req.user.username} not authorised to cancel push requests on this project.`, + }); + } +}); + +export default router; diff --git a/src/service/routes/repo.js b/src/service/routes/repo.js deleted file mode 100644 index cc70cec16..000000000 --- a/src/service/routes/repo.js +++ /dev/null @@ -1,154 +0,0 @@ -const express = require('express'); -const router = new express.Router(); -const db = require('../../db'); -const { getProxyURL } = require('../urls'); - -router.get('/', async (req, res) => { - const proxyURL = getProxyURL(req); - const query = {}; - - for (const k in req.query) { - if (!k) continue; - - if (k === 'limit') continue; - if (k === 'skip') continue; - let v = req.query[k]; - if (v === 'false') v = false; - if (v === 'true') v = true; - query[k] = v; - } - - const qd = await db.getRepos(query); - res.send(qd.map((d) => ({ ...d, proxyURL }))); -}); - -router.get('/:name', async (req, res) => { - const proxyURL = getProxyURL(req); - const name = req.params.name; - const qd = await db.getRepo(name); - res.send({ ...qd, proxyURL }); -}); - -router.patch('/:name/user/push', async (req, res) => { - if (req.user && req.user.admin) { - const repoName = req.params.name; - const username = req.body.username.toLowerCase(); - const user = await db.findUser(username); - - if (!user) { - res.status(400).send({ error: 'User does not exist' }); - return; - } - - await db.addUserCanPush(repoName, username); - res.send({ message: 'created' }); - } else { - res.status(401).send({ - message: 'You are not authorised to perform this action...', - }); - } -}); - -router.patch('/:name/user/authorise', async (req, res) => { - if (req.user && req.user.admin) { - const repoName = req.params.name; - const username = req.body.username; - const user = await db.findUser(username); - - if (!user) { - res.status(400).send({ error: 'User does not exist' }); - return; - } - - await db.addUserCanAuthorise(repoName, username); - res.send({ message: 'created' }); - } else { - res.status(401).send({ - message: 'You are not authorised to perform this action...', - }); - } -}); - -router.delete('/:name/user/authorise/:username', async (req, res) => { - if (req.user && req.user.admin) { - const repoName = req.params.name; - const username = req.params.username; - const user = await db.findUser(username); - - if (!user) { - res.status(400).send({ error: 'User does not exist' }); - return; - } - - await db.removeUserCanAuthorise(repoName, username); - res.send({ message: 'created' }); - } else { - res.status(401).send({ - message: 'You are not authorised to perform this action...', - }); - } -}); - -router.delete('/:name/user/push/:username', async (req, res) => { - if (req.user && req.user.admin) { - const repoName = req.params.name; - const username = req.params.username; - const user = await db.findUser(username); - - if (!user) { - res.status(400).send({ error: 'User does not exist' }); - return; - } - - await db.removeUserCanPush(repoName, username); - res.send({ message: 'created' }); - } else { - res.status(401).send({ - message: 'You are not authorised to perform this action...', - }); - } -}); - -router.delete('/:name/delete', async (req, res) => { - if (req.user && req.user.admin) { - const repoName = req.params.name; - - await db.deleteRepo(repoName); - res.send({ message: 'deleted' }); - } else { - res.status(401).send({ - message: 'You are not authorised to perform this action...', - }); - } -}); - -router.post('/', async (req, res) => { - if (req.user && req.user.admin) { - if (!req.body.name) { - res.status(400).send({ - message: 'Repository name is required', - }); - return; - } - - const repo = await db.getRepo(req.body.name); - if (repo) { - res.status(409).send({ - message: 'Repository already exists!', - }); - } else { - try { - await db.createRepo(req.body); - res.send({ message: 'created' }); - } catch { - res.send('Failed to create repository'); - } - } - } else { - res.status(401).send({ - message: 'You are not authorised to perform this action...', - }); - } -}); - -module.exports = router; diff --git a/src/service/routes/repo.ts b/src/service/routes/repo.ts new file mode 100644 index 000000000..e1b34d956 --- /dev/null +++ b/src/service/routes/repo.ts @@ -0,0 +1,125 @@ +import express, { Request, Response } from 'express'; +import * as db from '../../db'; +import { getProxyURL } from '../urls'; +import { AuthenticatedRequest } from './types'; + +const router = express.Router(); + +router.get('/', async (req: Request, res: Response) => { + const proxyURL = getProxyURL(req); + const query: Record = {}; + + for (const k in req.query) { + if (!k || k === 'limit' || k === 'skip') continue; + let v = req.query[k]; + if (v === 'false') v = false as any; + if (v === 'true') v = true as any; + query[k] = v; + } + + const repos = await db.getRepos(query); + res.send(repos.map((r: any) => ({ ...r, proxyURL }))); +}); + +router.get('/:name', async (req: Request, res: Response) => { + const proxyURL = getProxyURL(req); + const name = req.params.name; + const repo = await db.getRepo(name); + res.send({ ...repo, proxyURL }); +}); + +router.patch('/:name/user/push', async (req: AuthenticatedRequest, res: any) => { + if (req.user?.admin) { + const repoName = req.params.name; + const username = req.body.username.toLowerCase(); + const user = await db.findUser(username); + + if (!user) return res.status(400).send({ error: 'User does not exist' }); + + await db.addUserCanPush(repoName, username); + return res.send({ message: 'created' }); + } + + res.status(401).send({ message: 'You are not authorised to perform this action...' }); +}); + +router.patch('/:name/user/authorise', async (req: AuthenticatedRequest, res: any) => { + if (req.user?.admin) { + const repoName = req.params.name; + const username = req.body.username; + const user = await db.findUser(username); + + if (!user) return res.status(400).send({ error: 'User does not exist' }); + + await db.addUserCanAuthorise(repoName, username); + return res.send({ message: 'created' }); + } + + res.status(401).send({ message: 'You are not authorised to perform this action...' }); +}); + +router.delete('/:name/user/authorise/:username', async (req: AuthenticatedRequest, res: any) => { + if (req.user?.admin) { + const repoName = req.params.name; + const username = req.params.username; + const user = await db.findUser(username); + + if (!user) return res.status(400).send({ error: 'User does not exist' }); + + await db.removeUserCanAuthorise(repoName, username); + return res.send({ message: 'created' }); + } + + res.status(401).send({ message: 'You are not authorised to perform this action...' }); +}); + +router.delete('/:name/user/push/:username', async (req: AuthenticatedRequest, res: any) => { + if (req.user?.admin) { + const repoName = req.params.name; + const username = req.params.username; + const user = await db.findUser(username); + + if (!user) return res.status(400).send({ error: 'User does not exist' }); + + await db.removeUserCanPush(repoName, username); + return res.send({ message: 'created' }); + } + + res.status(401).send({ message: 'You are not authorised to perform this action...' }); +}); + +router.delete('/:name/delete', async (req: AuthenticatedRequest, res: any) => { + if (req.user?.admin) { + const repoName = req.params.name; + await db.deleteRepo(repoName); + return res.send({ message: 'deleted' }); + } + + res.status(401).send({ message: 'You are not authorised to perform this action...' }); +}); + +router.post('/', async (req: AuthenticatedRequest, res: any) => { + if (req.user?.admin) { + const repoName = req.body.name; + if (!repoName) { + return res.status(400).send({ message: 'Repository name is required' }); + } + + const repo = await db.getRepo(repoName); + if (repo) { + return res.status(409).send({ message: 'Repository already exists!' }); + } + + try { + await db.createRepo(req.body); + return res.send({ message: 'created' }); + } catch (error) { + console.error('Failed to create repository:', error); + return res.status(500).send({ message: 'Failed to create repository' }); + } + } + + res.status(401).send({ message: 'You are not authorised to perform this action...' }); +}); + +export default router; diff --git a/src/service/routes/types.ts b/src/service/routes/types.ts new file mode 100644 index 000000000..9c5b2239f --- /dev/null +++ b/src/service/routes/types.ts @@ -0,0 +1,6 @@ +import { Request } from 'express'; +import { User } from '../../db/types'; + +export interface AuthenticatedRequest extends Request { + user: User; +} diff --git a/src/service/routes/users.js b/src/service/routes/users.js deleted file mode 100644 index 118243d70..000000000 --- a/src/service/routes/users.js +++ /dev/null @@ -1,32 +0,0 @@ -const express = require('express'); -const router = new express.Router(); -const db = require('../../db'); - -router.get('/', async (req, res) => { - const query = {}; - - console.log(`fetching users = query path =${JSON.stringify(req.query)}`); - for (const k in req.query) { - if (!k) continue; - - if (k === 'limit') continue; - if (k === 'skip') continue; - let v = req.query[k]; - if (v === 'false') v = false; - if (v === 'true') v = true; - query[k] = v; - } - - res.send(await db.getUsers(query)); -}); - -router.get('/:id', async (req, res) => { - const username = req.params.id.toLowerCase(); - console.log(`Retrieving details for user: ${username}`); - const data = await db.findUser(username); - const user = JSON.parse(JSON.stringify(data)); - if (user && user.password) delete user.password; - res.send(user); -}); - -module.exports = router; diff --git a/src/service/routes/users.ts b/src/service/routes/users.ts new file mode 100644 index 000000000..c4b3b1d8c --- /dev/null +++ b/src/service/routes/users.ts @@ -0,0 +1,53 @@ +// src/service/routes/users.ts + +import express, { Request, Response } from 'express'; +import * as db from '../../db'; +import { User } from '../../db/types'; // adjust this import path to your actual User type + +const router = express.Router(); + +router.get('/', async (req: Request, res: Response) => { + const query: Record = {}; + + console.log(`Fetching users with query: ${JSON.stringify(req.query)}`); + for (const k in req.query) { + if (!k || k === 'limit' || k === 'skip') continue; + + let v = req.query[k]; + if (v === 'false') v = false as any; + if (v === 'true') v = true as any; + query[k] = v; + } + + try { + const users = await db.getUsers(query); + res.send(users); + } catch (err) { + console.error('Error fetching users:', err); + res.status(500).send({ message: 'Internal server error' }); + } +}); + +router.get('/:id', async (req: Request, res: any) => { + const username = req.params.id.toLowerCase(); + console.log(`Retrieving details for user: ${username}`); + + try { + const data = await db.findUser(username); + + if (!data) { + return res.status(404).send({ message: 'User not found' }); + } + + // Clone and sanitize user data + const user: Partial = { ...JSON.parse(JSON.stringify(data)) }; + if ('password' in user) delete user.password; + + res.send(user); + } catch (err) { + console.error('Error retrieving user:', err); + res.status(500).send({ message: 'Internal server error' }); + } +}); + +export default router; diff --git a/src/service/urls.js b/src/service/urls.js deleted file mode 100644 index 2d1a60de9..000000000 --- a/src/service/urls.js +++ /dev/null @@ -1,20 +0,0 @@ -const { GIT_PROXY_SERVER_PORT: PROXY_HTTP_PORT, GIT_PROXY_UI_PORT: UI_PORT } = - require('../config/env').serverConfig; -const config = require('../config'); - -module.exports = { - getProxyURL: (req) => { - const defaultURL = `${req.protocol}://${req.headers.host}`.replace( - `:${UI_PORT}`, - `:${PROXY_HTTP_PORT}`, - ); - return config.getDomains().proxy ?? defaultURL; - }, - getServiceUIURL: (req) => { - const defaultURL = `${req.protocol}://${req.headers.host}`.replace( - `:${PROXY_HTTP_PORT}`, - `:${UI_PORT}`, - ); - return config.getDomains().service ?? defaultURL; - }, -}; diff --git a/src/service/urls.ts b/src/service/urls.ts new file mode 100644 index 000000000..3609bb2dc --- /dev/null +++ b/src/service/urls.ts @@ -0,0 +1,21 @@ +import { serverConfig } from '../config/env'; +import * as config from '../config'; +import { Request } from 'express'; + +const { GIT_PROXY_SERVER_PORT: PROXY_HTTP_PORT, GIT_PROXY_UI_PORT: UI_PORT } = serverConfig; + +export function getProxyURL(req: Request): string { + const defaultURL = `${req.protocol}://${req.headers.host}`.replace( + `:${UI_PORT}`, + `:${PROXY_HTTP_PORT}`, + ); + return config.getDomains().proxy ?? defaultURL; +} + +export function getServiceUIURL(req: Request): string { + const defaultURL = `${req.protocol}://${req.headers.host}`.replace( + `:${PROXY_HTTP_PORT}`, + `:${UI_PORT}`, + ); + return config.getDomains().service ?? defaultURL; +} diff --git a/src/types/passport-activedirectory.d.ts b/src/types/passport-activedirectory.d.ts new file mode 100644 index 000000000..1578409ae --- /dev/null +++ b/src/types/passport-activedirectory.d.ts @@ -0,0 +1,7 @@ +declare module 'passport-activedirectory' { + import { Strategy as PassportStrategy } from 'passport'; + class Strategy extends PassportStrategy { + constructor(options: any, verify: (...args: any[]) => void); + } + export = Strategy; +} diff --git a/test/chain.test.js b/test/chain.test.js index 1fc749248..7dbcf4fa7 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -74,7 +74,7 @@ describe('proxy chain', function () { mockPushProcessors = initMockPushProcessors(sandboxSinon); // Re-import the processors module after clearing the cache - processors = await import('../src/proxy/processors'); + processors = require('../src/proxy/processors/index.ts'); // Mock the processors module sandboxSinon.stub(processors, 'pre').value(mockPreProcessors); diff --git a/test/testJwtAuthHandler.test.js b/test/testJwtAuthHandler.test.js index af2bb1bb2..ffb29458d 100644 --- a/test/testJwtAuthHandler.test.js +++ b/test/testJwtAuthHandler.test.js @@ -5,7 +5,7 @@ const jwt = require('jsonwebtoken'); const { jwkToBuffer } = require('jwk-to-pem'); const { assignRoles, getJwks, validateJwt } = require('../src/service/passport/jwtUtils'); -const jwtAuthHandler = require('../src/service/passport/jwtAuthHandler'); +const { jwtAuthHandler } = require('../src/service/passport/jwtAuthHandler'); describe('getJwks', () => { it('should fetch JWKS keys from authority', async () => { diff --git a/tsconfig.json b/tsconfig.json index a389ca8c7..baaaf0b0e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,14 +5,15 @@ "allowJs": true, "checkJs": false, "jsx": "react-jsx", - "moduleResolution": "Node", + "moduleResolution": "nodenext", "strict": true, "noEmit": true, "declaration": true, "skipLibCheck": true, "isolatedModules": true, - "module": "CommonJS", + "module": "NodeNext", "esModuleInterop": true, + "allowImportingTsExtensions": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true },