Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions backend/api/github.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import axios from 'axios';

export { getContributors } from './github/contributors';
export { getChangelog, getLatestRelease } from './github/release';

const GitHubApi = () =>
axios.create({
headers: {
'User-Agent': 'github.com/FuturePortal/CIMonitor',
accept: 'application/json',
},
baseURL: 'https://api.github.com',
});
const GitHubApi = () => {
const request = async (url: string) => {
const response = await fetch(`https://api.github.com${url}`, {
headers: {
'User-Agent': 'github.com/FuturePortal/CIMonitor',
accept: 'application/json',
},
});

return response.json();
};

return {
get: (url: string) => request(url),
};
};

const GitHubApiSingleton = GitHubApi();

export default GitHubApi;
export default GitHubApiSingleton;
8 changes: 4 additions & 4 deletions backend/api/github/contributors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ let cachedContributors: Contributor[] = [];

export const getContributors = async (): Promise<Contributor[]> => {
try {
const response = await GitHubApi().get('repos/FuturePortal/CIMonitor/stats/contributors');

const contributors: GitHubContributor[] = response.data;
const contributors: GitHubContributor[] = await GitHubApi.get(
'repos/FuturePortal/CIMonitor/stats/contributors'
);
const cleanContributors = cleanResponse(contributors);

if (cleanContributors.length === 0) {
Expand Down Expand Up @@ -55,7 +55,7 @@ const enrichContributors = async (contributors: Contributor[]): Promise<Contribu

const enrichContributor = async (contributor: Contributor): Promise<Contributor> => {
try {
const response = await GitHubApi().get(`users/${contributor.username}`);
const response = await GitHubApi.get(`users/${contributor.username}`);

const user: GitHubUser = response.data;

Expand Down
8 changes: 2 additions & 6 deletions backend/api/github/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ import GitHubApi from 'backend/api/github';
import { GitHubChangelog, GitHubRelease } from 'types/github';

export const getLatestRelease = async (): Promise<GitHubRelease> => {
const response = await GitHubApi().get('/repos/FuturePortal/CIMonitor/releases/latest');

return response.data;
return GitHubApi.get('/repos/FuturePortal/CIMonitor/releases/latest');
};

export const getChangelog = async (): Promise<GitHubChangelog> => {
const response = await GitHubApi().get('/repos/FuturePortal/CIMonitor/releases');

return response.data;
return GitHubApi.get('/repos/FuturePortal/CIMonitor/releases');
};
3 changes: 2 additions & 1 deletion backend/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from 'express';

import authRouter from './route/auth';
import changelogRouter from './route/changelog';
import contributorRouter from './route/contributors';
import dashboardRouter from './route/dashboard';
Expand All @@ -9,13 +10,13 @@ import webhookRouter from './route/webhook';

const router = express.Router();

// Prevent search engine indexing
router.use((req, res, next) => {
res.setHeader('X-Robots-Tag', 'noindex, nofollow');
next();
});

router.use('/', dashboardRouter);
router.use('/auth', authRouter);
router.use('/webhook', webhookRouter);
router.use('/version', versionRouter);
router.use('/contributors', contributorRouter);
Expand Down
21 changes: 21 additions & 0 deletions backend/router/route/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import express from 'express';

const router = express.Router();

router.post('/validate', (request, response) => {
const dashboardPassword = process.env.DASHBOARD_PASSWORD || '';

if (!dashboardPassword) {
return response.status(401).json({ valid: false, reason: 'Dashboard password is not set.' });
}

const providedPassword = request.body.password || '';

if (providedPassword === dashboardPassword) {
return response.json({ valid: true });
}

return response.status(401).json({ valid: false, reason: 'Invalid password.' });
});

export default router;
24 changes: 23 additions & 1 deletion backend/router/route/dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import express from 'express';
import fs from 'fs';
import path from 'path';

const dashboardPath = path.resolve('dashboard');

const router = express.Router();

function getPasswordProtectedSetting(): 'dashboard' | 'settings' | 'no' {
const dashboardPassword = process.env.DASHBOARD_PASSWORD || '';
if (!dashboardPassword) {
return 'no';
}

const dashboardLock = process.env.DASHBOARD_LOCK || 'settings';
if (dashboardLock === 'dashboard') {
return 'dashboard';
}

return 'settings';
}

router.get('/robots.txt', (request, response) => {
response.type('text/plain');
response.send('User-agent: *\nDisallow: /');
});

router.get('/', (request, response) => {
console.log(`[route/dashboard] Serving dashboard.`);
response.sendFile(dashboardPath + '/index.html');

const htmlPath = path.join(dashboardPath, 'index.html');
const htmlContent = fs.readFileSync(htmlPath, 'utf-8');

const injectedScript = `<script>window.PASSWORD_PROTECTED = "${getPasswordProtectedSetting()}";</script>`;
const modifiedHtml = htmlContent.replace('</head>', `\t${injectedScript}\n\t</head>`);

response.send(modifiedHtml);
});

router.use(express.static(dashboardPath));
Expand Down
29 changes: 29 additions & 0 deletions backend/router/route/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,32 @@ import StatusManager from 'backend/status/manager';

const router = express.Router();

function isPasswordRequired(): boolean {
const dashboardPassword = process.env.DASHBOARD_PASSWORD || '';
return dashboardPassword !== '';
}

function verifyPassword(request: express.Request, response: express.Response): boolean {
if (!isPasswordRequired()) {
return true;
}

const dashboardPassword = process.env.DASHBOARD_PASSWORD || '';
const authorizationHeader = request.headers.authorization || '';

if (authorizationHeader === dashboardPassword) {
return true;
}

response.status(401).json({ error: 'Unauthorized' });
return false;
}

router.delete('/all', async (request, response) => {
if (!verifyPassword(request, response)) {
return;
}

console.log(`[route/status] Deleting ALL statuses.`);

StatusManager.deleteAllStatuses();
Expand All @@ -13,6 +38,10 @@ router.delete('/all', async (request, response) => {
});

router.delete('/:id', async (request, response) => {
if (!verifyPassword(request, response)) {
return;
}

console.log(`[route/status] Deleting status ${request.params.id}.`);

StatusManager.deleteStatus(request.params.id);
Expand Down
7 changes: 7 additions & 0 deletions backend/router/route/webhook/bitbucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import Parser from 'backend/parser/bitbucket';
import StatusManager from 'backend/status/manager';
import Status from 'types/status';

import verifySimpleSecret from './verify-secret';

const router = express.Router();

router.post('/', (request, response) => {
console.log('[route/webhook/bitbucket] Webhook received.');

if (!verifySimpleSecret(request)) {
console.log('[route/webhook/bitbucket] Invalid webhook secret.');
return response.status(403).json({ message: 'Invalid secret verification.' });
}

const webhookType: string = String(request.headers['x-event-key']);

let status: Status | null = null;
Expand Down
30 changes: 30 additions & 0 deletions backend/router/route/webhook/github.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import crypto from 'crypto';
import express from 'express';

import GitHubParser from 'backend/parser/github';
Expand All @@ -6,9 +7,38 @@ import Status from 'types/status';

const router = express.Router();

const verifyGitHubSignature = (request: express.Request): boolean => {
const webhookSecret = process.env.WEBHOOK_SECRET;

if (!webhookSecret) {
return true;
}

const signature = request.headers['x-hub-signature-256'] as string;

if (!signature) {
return false;
}

const body = JSON.stringify(request.body);
const hmac = crypto.createHmac('sha256', webhookSecret);
hmac.update(body);
const expectedSignature = `sha256=${hmac.digest('hex')}`;

return crypto.timingSafeEqual(
new Uint8Array(Buffer.from(signature)),
new Uint8Array(Buffer.from(expectedSignature))
);
};

router.post('/', (request, response) => {
console.log('[route/webhook/github] Webhook received.');

if (!verifyGitHubSignature(request)) {
console.log('[route/webhook/github] Invalid webhook signature.');
return response.status(403).json({ message: 'Invalid secret verification.' });
}

const githubWebhookType: string = String(request.headers['x-github-event']);

let status: Status | null = null;
Expand Down
21 changes: 21 additions & 0 deletions backend/router/route/webhook/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,32 @@ import StatusManager from 'backend/status/manager';
import GitLabWebhook from 'types/gitlab';
import Status from 'types/status';

import verifySimpleSecret from './verify-secret';

const router = express.Router();

const verifyGitLabSecret = (request: express.Request): boolean => {
const webhookSecret = process.env.WEBHOOK_SECRET;

if (!webhookSecret) {
return true;
}

if (request.headers['x-gitlab-token'] === webhookSecret) {
return true;
}

return verifySimpleSecret(request);
};

router.post('/', (request, response) => {
console.log('[route/webhook/gitlab] Webhook received.');

if (!verifyGitLabSecret(request)) {
console.log('[route/webhook/gitlab] Invalid webhook secret.');
return response.status(403).json({ message: 'Invalid secret verification.' });
}

const gitlabWebhook: GitLabWebhook = request.body;

let status: Status | null = null;
Expand Down
7 changes: 7 additions & 0 deletions backend/router/route/webhook/readthedocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import ReadTheDocsParser from 'backend/parser/readthedocs';
import StatusManager from 'backend/status/manager';
import ReadTheDocsBuild from 'types/readthedocs';

import verifySimpleSecret from './verify-secret';

const router = express.Router();

router.post('/', (request, response) => {
console.log('[route/webhook/readthedocs] Webhook received.');

if (!verifySimpleSecret(request)) {
console.log('[route/webhook/readthedocs] Invalid webhook secret.');
return response.status(403).json({ message: 'Invalid secret verification.' });
}

const webhook: ReadTheDocsBuild = request.body;

let status = null;
Expand Down
29 changes: 29 additions & 0 deletions backend/router/route/webhook/verify-secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import express from 'express';

const getWebhookSecret = (): string | null => {
return process.env.WEBHOOK_SECRET || null;
};

const verifySimpleSecret = (request: express.Request): boolean => {
const webhookSecret = getWebhookSecret();

if (!webhookSecret) {
return true;
}

if (request.headers['authorization'] === webhookSecret) {
return true;
}

if (request.headers['x-secret-token'] === webhookSecret) {
return true;
}

if (request.query.secret === webhookSecret) {
return true;
}

return false;
};

export default verifySimpleSecret;
Loading