This commit is contained in:
Jeff Wilcox 2023-06-11 17:42:22 -07:00
Родитель 7ecc386125
Коммит 4ca3b70ae1
180 изменённых файлов: 1830 добавлений и 1499 удалений

Просмотреть файл

@ -840,9 +840,6 @@
"Whois",
"withmaintainers",
"withservicetree",
"workboard",
"Workboard",
"workboarding",
"xamarinhq",
"Xcache",
"xlink",

148
.vscode/launch.json поставляемый
Просмотреть файл

@ -4,34 +4,7 @@
{
"type": "node",
"request": "launch",
"name": "Launch site 3000",
"program": "${workspaceFolder}/dist/bin/www.js",
"cwd": "${workspaceFolder}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development",
"DEBUG": "startup,g:server,context,github:tokens",
"MORE_DEBUG": "appinsights,cache,restapi,pg,querycache,user,redis-cross-org,health"
}
},
{
"type": "node",
"request": "launch",
"name": "Jest Tests",
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"args": ["-i"],
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"internalConsoleOptions": "openOnSessionStart",
"console": "externalTerminal",
"outFiles": ["${workspaceRoot}/dist/**/*"]
},
{
"type": "node",
"request": "launch",
"name": "Launch site 4000 sudo",
"name": "Launch site :4000 sudo",
"program": "${workspaceFolder}/dist/bin/www.js",
"cwd": "${workspaceFolder}/dist",
"preLaunchTask": "tsbuild",
@ -48,7 +21,7 @@
{
"type": "node",
"request": "launch",
"name": "Launch site 4000 sudo OFF",
"name": "Launch site :4000 sudo OFF",
"program": "${workspaceFolder}/dist/bin/www.js",
"cwd": "${workspaceFolder}/dist",
"preLaunchTask": "tsbuild",
@ -66,8 +39,23 @@
{
"type": "node",
"request": "launch",
"name": "Background Job: Firehose",
"program": "${workspaceRoot}/dist/jobs/firehose/index.js",
"name": "Launch server-rendered legacy site :3000",
"program": "${workspaceFolder}/dist/bin/www.js",
"cwd": "${workspaceFolder}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development",
"DEBUG": "startup,g:server,context,github:tokens",
"MORE_DEBUG": "appinsights,cache,restapi,pg,querycache,user,redis-cross-org,health"
}
},
{
"type": "node",
"request": "launch",
"name": "Background Job: Event firehose",
"program": "${workspaceRoot}/dist/jobs/firehose.js",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
@ -77,6 +65,18 @@
"DEBUG": "startup,querycache"
}
},
{
"type": "node",
"request": "launch",
"name": "Jest Tests",
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"args": ["-i"],
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"internalConsoleOptions": "openOnSessionStart",
"console": "externalTerminal",
"outFiles": ["${workspaceRoot}/dist/**/*"]
},
{
"type": "node",
"request": "launch",
@ -118,7 +118,7 @@
"sourceMaps": true,
"env": {
"NODE_ENV": "development",
"DEBUG": "appinsights,restapi,startup,redis-cross-org,appinsights",
"DEBUG": "appinsights,startup",
"DEBUG_GITHUB_PORTAL_SUDO_OFF": "1",
"DEBUG_GITHUB_ORG_SUDO_OFF": "1"
}
@ -137,39 +137,11 @@
"DEBUG": "startup"
}
},
{
"type": "node",
"request": "launch",
"name": "Job: Cleanup invitations",
"program": "${workspaceRoot}/dist/jobs/cleanupInvites/index.js",
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development",
"DEBUG": "redis,restapi,startup,appinsights"
}
},
{
"type": "node",
"request": "launch",
"name": "Job: Cleanup blob cache",
"program": "${workspaceRoot}/dist/jobs/cleanupBlobCache/index.js",
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development",
"DEBUG": "startup"
}
},
{
"type": "node",
"request": "launch",
"name": "Job: Backfill aliases (3)",
"program": "${workspaceRoot}/dist/jobs/refreshUsernames/index.js",
"program": "${workspaceRoot}/dist/jobs/refreshUsernames.js",
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
@ -184,21 +156,21 @@
"type": "node",
"request": "launch",
"name": "Job: User attributes hygiene (4)",
"program": "${workspaceRoot}/dist/jobs/refreshUsernames/index.js",
"program": "${workspaceRoot}/dist/jobs/refreshUsernames.js",
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development",
"DEBUG": "redis,restapi,startup,appinsights,cache"
"DEBUG": "startup,appinsights"
}
},
{
"type": "node",
"request": "launch",
"name": "Job: Consistency: All (6)",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache/index.js",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache.js",
"args": ["all"],
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
@ -213,7 +185,7 @@
"type": "node",
"request": "launch",
"name": "Job: Consistency: Deleted repos (7)",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache/deletedRepositories.js",
"program": "${workspaceRoot}/dist/jobs/deletedRepositoriesCache.js",
"args": [],
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
@ -228,7 +200,7 @@
"type": "node",
"request": "launch",
"name": "Job: Consistency: Teams (8)",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache/index.js",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache.js",
"args": ["teams"],
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
@ -243,7 +215,7 @@
"type": "node",
"request": "launch",
"name": "Job: Consistency: Org members (9)",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache/index.js",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache.js",
"args": ["organizations"],
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
@ -258,7 +230,7 @@
"type": "node",
"request": "launch",
"name": "Job: Consistency: Repo collaborators (10)",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache/index.js",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache.js",
"args": ["collaborators"],
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
@ -273,7 +245,7 @@
"type": "node",
"request": "launch",
"name": "Job: Consistency: Team permissions (11)",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache/index.js",
"program": "${workspaceRoot}/dist/jobs/refreshQueryCache.js",
"args": ["permissions"],
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
@ -301,8 +273,8 @@
{
"type": "node",
"request": "launch",
"name": "Job: Migrate links",
"program": "${workspaceRoot}/dist/jobs/migrateLinks/index.js",
"name": "Job: Migrate links (14)",
"program": "${workspaceRoot}/dist/jobs/migrateLinks.js",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
@ -315,8 +287,8 @@
{
"type": "node",
"request": "launch",
"name": "Job: System Team Permissions",
"program": "${workspaceRoot}/dist/jobs/permissions/index.js",
"name": "Job: System Team Permissions (15)",
"program": "${workspaceRoot}/dist/jobs/permissions.js",
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
@ -326,6 +298,34 @@
"DEBUG": "startup,appinsights"
}
},
{
"type": "node",
"request": "launch",
"name": "Job: Cleanup invitations (16)",
"program": "${workspaceRoot}/dist/jobs/cleanupInvites.js",
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development",
"DEBUG": "redis,restapi,startup,appinsights"
}
},
{
"type": "node",
"request": "launch",
"name": "Job: Cleanup blob cache (17)",
"program": "${workspaceRoot}/dist/jobs/cleanupBlobCache.js",
"cwd": "${workspaceRoot}/dist",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development",
"DEBUG": "startup"
}
},
{
"type": "node",
"request": "launch",
@ -345,7 +345,7 @@
"type": "node",
"request": "launch",
"name": "Script: Import audit CSV",
"program": "${workspaceRoot}/dist/jobs/importAuditLog/index.js",
"program": "${workspaceRoot}/dist/jobs/importAuditLog.js",
"preLaunchTask": "tsbuild",
"sourceMaps": true,
"console": "integratedTerminal",

Просмотреть файл

@ -3,7 +3,7 @@
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
#
ARG IMAGE_NAME=mcr.microsoft.com/cbl-mariner/base/nodejs:16
ARG IMAGE_NAME=mcr.microsoft.com/cbl-mariner/base/nodejs:18
FROM $IMAGE_NAME AS build

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import { ReposAppRequest } from '../../interfaces';
import { jsonError } from '../../middleware';
@ -13,7 +13,7 @@ const router: Router = Router();
// TODO: move to modern w/administration experience, optionally
router.get('/', (req: ReposAppRequest, res, next) => {
router.get('/', (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { config } = getProviders(req);
const text = config?.serviceMessage?.banner || null;
const link = config.serviceMessage?.link;
@ -22,7 +22,7 @@ router.get('/', (req: ReposAppRequest, res, next) => {
return res.json({ banner });
});
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within this banner route', 404));
});

Просмотреть файл

@ -3,8 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Organization } from '../../../../business/organization';
import { ReposAppRequest } from '../../../../interfaces';
import { getIsCorporateAdministrator, jsonError } from '../../../../middleware';
@ -20,7 +21,7 @@ interface IRequestWithAdministration extends ReposAppRequest {
}
router.use(
asyncHandler(async (req: IRequestWithAdministration, res, next) => {
asyncHandler(async (req: IRequestWithAdministration, res: Response, next: NextFunction) => {
req.isSystemAdministrator = await getIsCorporateAdministrator(req);
return next();
})
@ -28,7 +29,7 @@ router.use(
router.get(
'/',
asyncHandler(async (req: IRequestWithAdministration, res, next) => {
asyncHandler(async (req: IRequestWithAdministration, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
const isAdministrator = req.isSystemAdministrator;
if (!isAdministrator) {
@ -44,13 +45,13 @@ router.get(
})
);
router.use((req: IRequestWithAdministration, res, next) => {
router.use((req: IRequestWithAdministration, res: Response, next: NextFunction) => {
return req.isSystemAdministrator ? next() : next(jsonError('Not authorized', 403));
});
router.use(
'/organization/:orgName',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { orgName } = req.params;
const { operations } = getProviders(req);
let organization: Organization = null;
@ -74,7 +75,7 @@ const deployment = getCompanySpecificDeployment();
deployment?.routes?.api?.context?.administration?.index &&
deployment?.routes?.api?.context.administration.index(router);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available: context/administration', 404));
});

Просмотреть файл

@ -3,8 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { ReposAppRequest } from '../../../../../interfaces';
import { jsonError } from '../../../../../middleware';
@ -16,7 +17,7 @@ router.use('/settings', routeSettings);
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
return res.json({
organization: organization.asClientJson(),
@ -24,7 +25,7 @@ router.get(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available in administration - organization', 404));
});

Просмотреть файл

@ -3,8 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { OrganizationSetting } from '../../../../../entities/organizationSettings/organizationSetting';
import { ReposAppRequest } from '../../../../../interfaces';
import { jsonError } from '../../../../../middleware';
@ -17,7 +18,7 @@ interface IOrganizationSettings extends ReposAppRequest {
}
router.use(
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { organization } = req;
const { organizationSettingsProvider } = getProviders(req);
try {
@ -34,7 +35,7 @@ router.use(
router.get(
'/',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { dynamicSettings } = req;
return res.json({
dynamicSettings,
@ -46,7 +47,7 @@ router.get(
router.get(
'/features',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { dynamicSettings, organization } = req;
const { features } = dynamicSettings;
return res.json({
@ -58,7 +59,7 @@ router.get(
router.get(
'/feature/:flag',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { dynamicSettings, organization } = req;
const flag = req.params.flag as string;
return res.json({
@ -71,7 +72,7 @@ router.get(
router.put(
'/feature/:flag',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { dynamicSettings, organization } = req;
const { insights, organizationSettingsProvider } = getProviders(req);
const { features } = dynamicSettings;
@ -103,7 +104,7 @@ router.put(
router.delete(
'/feature/:flag',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { organization, dynamicSettings } = req;
const { organizationSettingsProvider, insights } = getProviders(req);
const { features } = dynamicSettings;
@ -137,7 +138,7 @@ router.delete(
router.get(
'/properties',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { dynamicSettings, organization } = req;
const { properties } = dynamicSettings;
return res.json({
@ -149,7 +150,7 @@ router.get(
router.get(
'/property/:flag',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { dynamicSettings, organization } = req;
const propertyName = req.params.propertyName as string;
const { properties } = dynamicSettings;
@ -163,7 +164,7 @@ router.get(
router.put(
'/property/:propertyName',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { organization, dynamicSettings } = req;
const { insights, organizationSettingsProvider } = getProviders(req);
const { properties } = dynamicSettings;
@ -209,7 +210,7 @@ router.put(
router.delete(
'/property/:propertyName',
asyncHandler(async (req: IOrganizationSettings, res, next) => {
asyncHandler(async (req: IOrganizationSettings, res: Response, next: NextFunction) => {
const { organization, dynamicSettings } = req;
const { organizationSettingsProvider, insights } = getProviders(req);
const { properties } = dynamicSettings;
@ -245,7 +246,7 @@ router.delete(
//
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available in administration - organization', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Team, Organization } from '../../../business';
@ -30,7 +30,7 @@ const approvalPairToJson = (pair: ApprovalPair) => {
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { approvalProvider, operations } = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext.link) {
@ -65,7 +65,7 @@ router.get(
router.get(
'/:approvalId',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const approvalId = req.params.approvalId;
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext.link) {
@ -112,7 +112,7 @@ router.get(
})
);
router.use('*', (req: ReposAppRequest, res, next) => {
router.use('*', (req: ReposAppRequest, res: Response, next: NextFunction) => {
return next(jsonError('Contextual API or route not found within approvals', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Organization } from '../../../business';
@ -48,7 +48,7 @@ router.get('/', (req: ReposAppRequest, res) => {
router.get(
'/specialized/multipleLinkGitHubIdentities',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
const links = (activeContext?.link ? [activeContext.link, ...activeContext.additionalLinks] : []).map(
@ -79,7 +79,7 @@ router.get(
router.get(
'/accountDetails',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
try {
@ -107,7 +107,7 @@ router.use('/sample', routeSample);
router.use(
'/orgs/:orgName',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { orgName } = req.params;
const providers = getProviders(req);
const { operations } = providers;
@ -151,7 +151,7 @@ async function isUnmanagedOrganization(providers: IProviders, orgName: string):
router.use('/orgs/:orgName', routeIndividualContextualOrganization);
router.use('*', (req: ReposAppRequest, res, next) => {
router.use('*', (req: ReposAppRequest, res: Response, next: NextFunction) => {
return next(jsonError('Contextual API or route not found', 404));
});

Просмотреть файл

@ -3,8 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Organization, Team } from '../../../../business';
import {
ReposAppRequest,
@ -23,7 +24,7 @@ const router: Router = Router();
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext.link) {
@ -46,7 +47,7 @@ router.get(
router.get(
'/sudo',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext.link) {
@ -60,7 +61,7 @@ router.get(
router.get(
'/isOwner',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext.link) {
@ -81,7 +82,7 @@ router.get(
router.delete(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
// "Leave" / remove my context
const { organization } = req;
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
@ -104,7 +105,7 @@ router.delete(
router.get(
'/personalizedTeams',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
try {
const organization = req.organization as Organization;
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
@ -145,7 +146,7 @@ const deployment = getCompanySpecificDeployment();
deployment?.routes?.api?.context?.organization?.index &&
deployment?.routes?.api?.context?.organization?.index(router);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available: client>organization', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import {
@ -21,7 +21,7 @@ const router: Router = Router();
router.get(
'/permissions',
AddRepositoryPermissionsToRequest,
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const permissions = getContextualRepositoryPermissions(req);
return res.json(permissions);
})
@ -33,7 +33,7 @@ const deployment = getCompanySpecificDeployment();
deployment?.routes?.api?.context?.organization?.repo &&
deployment?.routes?.api?.context?.organization?.repo(router);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError(`no API or ${req.method} function available for repo`, 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../../../middleware';
@ -20,7 +20,7 @@ import NewRepositoryLockdownSystem from '../../../../features/newRepositories/ne
const router: Router = Router();
router.use(
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const organization = req.organization as Organization;
if (!organization.isNewRepositoryLockdownSystemEnabled()) {
return next(jsonError('This endpoint is not available as configured for the organization', 400));
@ -42,7 +42,7 @@ router.use(
router.post(
'/approve',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
const repository = getContextualRepository(req);
const repositoryMetadataProvider = getRepositoryMetadataProvider(operations);
@ -70,7 +70,7 @@ router.post(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError(`no API or ${req.method} function available for repo fork unlock`, 404));
});

Просмотреть файл

@ -3,8 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Repository } from '../../../../business';
import { jsonError } from '../../../../middleware';
import { setContextualRepository } from '../../../../middleware/github/repoPermissions';
@ -17,7 +18,7 @@ import RouteContextualRepo from './repo';
const router: Router = Router();
async function validateActiveMembership(req: ReposAppRequest, res, next) {
async function validateActiveMembership(req: ReposAppRequest, res: Response, next: NextFunction) {
const { organization } = req;
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext.link) {
@ -37,7 +38,7 @@ router.post('/', asyncHandler(validateActiveMembership), asyncHandler(createRepo
router.use(
'/:repoName',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const { repoName } = req.params;
let repository: Repository = null;
@ -49,7 +50,7 @@ router.use(
router.use('/:repoName', RouteContextualRepo);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available for repos', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { TeamJoinApprovalEntity } from '../../../../entities/teamJoinApproval/teamJoinApproval';
@ -42,7 +42,7 @@ router.get(
'/permissions',
asyncHandler(AddTeamPermissionsToRequest),
asyncHandler(AddTeamMembershipToRequest),
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const membership = getTeamMembershipFromRequest(req);
const permissions = getTeamPermissionsFromRequest(req);
return res.json({ permissions, membership });
@ -51,7 +51,7 @@ router.get(
router.get(
'/join/request',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { approvalProvider } = getProviders(req);
const team = getContextualTeam(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
@ -70,7 +70,7 @@ router.get(
router.post(
'/join',
asyncHandler(AddTeamMembershipToRequest),
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
try {
const providers = getProviders(req);
const { approvalProvider } = providers;
@ -112,7 +112,7 @@ router.post(
router.post(
'/join/approvals/:approvalId',
asyncHandler(AddTeamPermissionsToRequest),
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { approvalId: id } = req.params;
if (!id) {
return next(jsonError('invalid approval', 400));
@ -164,7 +164,7 @@ router.post(
router.get(
'/join/approvals/:approvalId',
asyncHandler(AddTeamPermissionsToRequest),
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { approvalId: id } = req.params;
if (!id) {
return next(jsonError('invalid approval', 400));
@ -195,7 +195,7 @@ router.get(
router.get(
'/join/approvals',
asyncHandler(AddTeamPermissionsToRequest),
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { approvalProvider } = getProviders(req);
const team = getContextualTeam(req);
const permissions = getTeamPermissionsFromRequest(req);
@ -213,7 +213,7 @@ router.get(
router.post(
'/role/:login',
asyncHandler(AddTeamPermissionsToRequest),
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { role } = req.body;
const { login } = req.params;
if (!login) {
@ -240,7 +240,7 @@ router.post(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available for contextual team', 404));
});

Просмотреть файл

@ -3,8 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Team } from '../../../../business';
import { jsonError } from '../../../../middleware';
import { setContextualTeam } from '../../../../middleware/github/teamPermissions';
@ -18,7 +19,7 @@ const router: Router = Router();
router.use(
'/:teamSlug',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const { teamSlug } = req.params;
let team: Team = null;
@ -35,7 +36,7 @@ router.use(
router.use('/:teamSlug', RouteTeam);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available for repos', 404));
});

Просмотреть файл

@ -3,12 +3,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import asyncHandler from 'express-async-handler';
import { GitHubRepositoryPermission, ReposAppRequest } from '../../../interfaces';
import { IndividualContext } from '../../../business/user';
export default asyncHandler(async (req: ReposAppRequest, res, next) => {
export default asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
try {
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext.link) {

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { sendLinkedAccountMail } from '../../../business/operations/link';
@ -16,7 +16,7 @@ const router: Router = Router();
router.get(
'/:templateName',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
const templateName = req.params.templateName as string;
@ -43,7 +43,7 @@ router.get(
})
);
router.use('*', (req: ReposAppRequest, res, next) => {
router.use('*', (req: ReposAppRequest, res: Response, next: NextFunction) => {
return next(jsonError('Contextual API or route not found within samples', 404));
});

Просмотреть файл

@ -4,11 +4,12 @@
//
import asyncHandler from 'express-async-handler';
import { NextFunction, Response } from 'express';
import { ReposAppRequest, TeamJsonFormat } from '../../../interfaces';
import { IndividualContext } from '../../../business/user';
export default asyncHandler(async (req: ReposAppRequest, res, next) => {
export default asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext.link) {
return res.json({

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import {
@ -34,7 +34,7 @@ import routeCrossOrganizationTeams from './teams';
const router: Router = Router();
router.use((req: ReposAppRequest, res, next) => {
router.use((req: ReposAppRequest, res: Response, next: NextFunction) => {
const { config } = getProviders(req);
if (config?.features?.allowApiClient) {
if (req.isAuthenticated()) {
@ -126,7 +126,7 @@ router.get('/', (req: ReposAppRequest, res) => {
return res.send(JSON.stringify(data, null, 2));
});
router.use((req, res, next) => {
router.use((req, res: Response, next: NextFunction) => {
return next(jsonError('The resource or endpoint you are looking for is not there', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { IndividualContext } from '../../business/user';
@ -15,7 +15,7 @@ import { ReposAppRequest } from '../../interfaces';
const router: Router = Router();
async function validateLinkOk(req: ReposAppRequest, res, next) {
async function validateLinkOk(req: ReposAppRequest, res: Response, next: NextFunction) {
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
const providers = getProviders(req);
const insights = providers.insights;
@ -91,7 +91,7 @@ async function validateLinkOk(req: ReposAppRequest, res, next) {
}
}
router.get('/banner', (req: ReposAppRequest, res, next) => {
router.get('/banner', (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { config } = getProviders(req);
const offline = config?.github?.links?.provider?.linkingOfflineMessage;
return res.json({ offline });
@ -99,7 +99,7 @@ router.get('/banner', (req: ReposAppRequest, res, next) => {
router.delete(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
return unlinkInteractive(true, activeContext, req, res, next);
})
@ -108,13 +108,13 @@ router.delete(
router.post(
'/',
validateLinkOk,
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
return interactiveLinkUser(true, activeContext, req, res, next);
})
);
router.use('*', (req: ReposAppRequest, res, next) => {
router.use('*', (req: ReposAppRequest, res: Response, next: NextFunction) => {
return next(jsonError('API or route not found', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
const router: Router = Router();
@ -25,7 +25,7 @@ interface ILocalApiRequest extends ReposAppRequest {
knownRequesterMailAddress?: string;
}
router.get('/metadata', (req: ILocalApiRequest, res, next) => {
router.get('/metadata', (req: ILocalApiRequest, res: Response, next: NextFunction) => {
try {
const options = {
projectType: req.query.projectType,
@ -40,7 +40,7 @@ router.get('/metadata', (req: ILocalApiRequest, res, next) => {
router.get(
'/personalizedTeams',
asyncHandler(async (req: ILocalApiRequest, res, next) => {
asyncHandler(async (req: ILocalApiRequest, res: Response, next: NextFunction) => {
try {
const organization = req.organization as Organization;
const userAggregateContext = req.apiContext.aggregations;
@ -76,7 +76,7 @@ router.get(
router.get(
'/teams',
asyncHandler(async (req: ILocalApiRequest, res, next) => {
asyncHandler(async (req: ILocalApiRequest, res: Response, next: NextFunction) => {
const providers = getProviders(req);
const queryCache = providers.queryCache;
const organization = req.organization as Organization;
@ -154,7 +154,7 @@ router.get(
})
);
export async function discoverUserIdentities(req: ReposAppRequest, res, next) {
export async function discoverUserIdentities(req: ReposAppRequest, res: Response, next: NextFunction) {
const apiContext = req.apiContext as IndividualContext;
const providers = getProviders(req);
const mailAddressProvider = providers.mailAddressProvider;
@ -177,7 +177,7 @@ export async function discoverUserIdentities(req: ReposAppRequest, res, next) {
router.post('/repo/:repo', asyncHandler(discoverUserIdentities), asyncHandler(createRepositoryFromClient));
export async function createRepositoryFromClient(req: ILocalApiRequest, res, next) {
export async function createRepositoryFromClient(req: ILocalApiRequest, res: Response, next: NextFunction) {
const providers = getProviders(req);
const { insights, diagnosticsDrop, customizedNewRepositoryLogic, graphProvider } = providers;
const individualContext = req.watchdogContextOverride || req.individualContext || req.apiContext;

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
const router: Router = Router();
import { getProviders } from '../../transitional';
@ -12,7 +12,7 @@ import { jsonError } from '../../middleware/jsonError';
import newOrgRepo from './newOrgRepo';
import { ReposAppRequest } from '../../interfaces';
router.use('/org/:org', (req: ReposAppRequest, res, next) => {
router.use('/org/:org', (req: ReposAppRequest, res: Response, next: NextFunction) => {
const orgName = req.params.org;
const { operations } = getProviders(req);
try {

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../middleware';
@ -20,7 +20,7 @@ router.get(
})
);
router.use('*', (req: ReposAppRequest, res, next) => {
router.use('*', (req: ReposAppRequest, res: Response, next: NextFunction) => {
return next(jsonError('API or route not found within news', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../../middleware/jsonError';
@ -35,7 +35,7 @@ type IRequestWithOrganizationAnnotations = IReposAppRequestWithOrganizationManag
router.use(
'/',
checkIsCorporateAdministrator,
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res, next) => {
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res: Response, next: NextFunction) => {
const { organizationAnnotationsProvider } = getProviders(req);
const { organization, organizationManagementType, organizationProfile } = req;
const organizationId =
@ -55,7 +55,7 @@ router.use(
router.get(
'/',
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res, next) => {
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res: Response, next: NextFunction) => {
const { annotations } = req;
// Limited redaction
const isSystemAdministrator = await getIsCorporateAdministrator(req);
@ -68,7 +68,11 @@ router.get(
router.use(ensureOrganizationProfileMiddleware);
async function ensureAnnotations(req: IRequestWithOrganizationAnnotations, res, next) {
async function ensureAnnotations(
req: IRequestWithOrganizationAnnotations,
res: Response,
next: NextFunction
) {
if (!req.annotations) {
const { organizationAnnotationsProvider } = getProviders(req);
try {
@ -89,7 +93,7 @@ router.put('*', AuthorizeOnlyCorporateAdministrators, ensureAnnotations);
router.put(
'/',
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res, next) => {
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res: Response, next: NextFunction) => {
// No-op mostly, since ensureAnnotations precedes
return res.json({
annotations: req.annotations,
@ -125,7 +129,7 @@ function addChangeNote(
router.put(
'/property/:propertyName',
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res, next) => {
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res: Response, next: NextFunction) => {
const { annotations } = req;
const providers = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
@ -152,7 +156,7 @@ router.put(
router.delete(
'/property/:propertyName',
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res, next) => {
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res: Response, next: NextFunction) => {
const { annotations } = req;
const providers = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
@ -183,7 +187,7 @@ router.delete(
router.put(
'/feature/:flag',
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res, next) => {
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res: Response, next: NextFunction) => {
const { annotations } = req;
const providers = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
@ -211,7 +215,7 @@ router.put(
router.delete(
'/feature/:flag',
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res, next) => {
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res: Response, next: NextFunction) => {
const { annotations } = req;
const providers = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
@ -241,7 +245,7 @@ router.delete(
router.patch(
'/',
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res, next) => {
asyncHandler(async (req: IRequestWithOrganizationAnnotations, res: Response, next: NextFunction) => {
const { annotations } = req;
const providers = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
@ -288,7 +292,7 @@ async function applyPatch(
// features, properties
// flag
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within the organization annotations route', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { ReposAppRequest } from '../../../interfaces';
@ -63,19 +63,21 @@ asClientJson() {
*/
router.get(
'/',
asyncHandler(async (req: IReposAppRequestWithOrganizationManagementType, res, next) => {
const { organization, organizationProfile, organizationManagementType } = req;
if (organizationManagementType === OrganizationManagementType.Unmanaged) {
asyncHandler(
async (req: IReposAppRequestWithOrganizationManagementType, res: Response, next: NextFunction) => {
const { organization, organizationProfile, organizationManagementType } = req;
if (organizationManagementType === OrganizationManagementType.Unmanaged) {
return res.json({
managementType: req.organizationManagementType,
id: organizationProfile.id,
});
}
return res.json({
managementType: req.organizationManagementType,
id: organizationProfile.id,
...organization.asClientJson(),
});
}
return res.json({
managementType: req.organizationManagementType,
...organization.asClientJson(),
});
})
)
);
router.use('/annotations', routeAnnotations);
@ -93,7 +95,7 @@ router.get('/newRepoBanner', (req: ReposAppRequest, res) => {
return res.json({ newRepositoriesOffline });
});
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../../middleware/jsonError';
@ -13,7 +13,7 @@ const router: Router = Router();
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const metadata = organization.getRepositoryCreateMetadata();
res.json(metadata);
@ -22,7 +22,7 @@ router.get(
router.get(
'/byProjectReleaseType',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const options = {
projectType: req.query.projectType,
@ -32,7 +32,7 @@ router.get(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within this path', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../../middleware';
@ -118,7 +118,7 @@ export async function equivalentLegacyPeopleSearch(req: ReposAppRequest, options
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const pager = new JsonPager<OrganizationMember>(req, res);
try {
const searcher = await equivalentLegacyPeopleSearch(req);
@ -142,7 +142,7 @@ router.get(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within this people list', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../../middleware';
@ -44,7 +44,7 @@ router.use('/permissions', RouteRepoPermissions);
router.get(
'/',
asyncHandler(async (req: RequestWithRepo, res, next) => {
asyncHandler(async (req: RequestWithRepo, res: Response, next: NextFunction) => {
const { repository } = req;
try {
await repository.getDetails({ backgroundRefresh: false });
@ -64,7 +64,7 @@ router.get(
router.get(
'/exists',
asyncHandler(async (req: RequestWithRepo, res, next) => {
asyncHandler(async (req: RequestWithRepo, res: Response, next: NextFunction) => {
let exists = false;
let name: string = undefined;
const { repository } = req;
@ -88,7 +88,7 @@ router.get(
router.patch(
'/renameDefaultBranch',
asyncHandler(AddRepositoryPermissionsToRequest),
asyncHandler(async function (req: RequestWithRepo, res, next) {
asyncHandler(async function (req: RequestWithRepo, res: Response, next: NextFunction) {
const providers = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
const repoPermissions = getContextualRepositoryPermissions(req);
@ -122,7 +122,12 @@ router.post(
asyncHandler(archiveUnArchiveRepositoryHandler.bind(null, ArchivalAction.UnArchive))
);
async function archiveUnArchiveRepositoryHandler(action: ArchivalAction, req: RequestWithRepo, res, next) {
async function archiveUnArchiveRepositoryHandler(
action: ArchivalAction,
req: RequestWithRepo,
res: Response,
next: NextFunction
) {
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
const providers = getProviders(req);
const { insights } = providers;
@ -200,7 +205,7 @@ async function archiveUnArchiveRepositoryHandler(action: ArchivalAction, req: Re
router.delete(
'/',
asyncHandler(AddRepositoryPermissionsToRequest),
asyncHandler(async function (req: RequestWithRepo, res, next) {
asyncHandler(async function (req: RequestWithRepo, res: Response, next: NextFunction) {
// NOTE: duplicated code from /routes/org/repos.ts
const providers = getProviders(req);
const { insights } = providers;
@ -310,7 +315,7 @@ router.delete(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
console.warn(req.baseUrl);
return next(jsonError('no API or function available within this specific repo', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../../middleware/jsonError';
@ -19,7 +19,7 @@ const router: Router = Router();
router.get(
'/',
asyncHandler(async (req: RequestWithRepo, res, next) => {
asyncHandler(async (req: RequestWithRepo, res: Response, next: NextFunction) => {
const { repository, organization } = req;
try {
const teamPermissions = await repository.getTeamPermissions();

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../../middleware';
@ -19,7 +19,7 @@ const router: Router = Router();
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const providers = getProviders(req);
const pager = new JsonPager<Repository>(req, res);
@ -238,7 +238,7 @@ export async function searchRepos(
router.use(
'/:repoName',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const { repoName } = req.params;
// does not confirm the name
@ -249,7 +249,7 @@ router.use(
router.use('/:repoName', RouteRepo);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within this repos endpoint', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { getContextualTeam } from '../../../middleware/github/teamPermissions';
@ -21,7 +21,7 @@ const router: Router = Router();
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const team = getContextualTeam(req);
return res.json(team.asJson(TeamJsonFormat.Augmented /* includes corporateMetadata */));
})
@ -29,7 +29,7 @@ router.get(
router.get(
'/repos',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
try {
const forceRefresh = !!req.query.refresh;
const pager = new JsonPager<TeamRepositoryPermission>(req, res);
@ -56,7 +56,7 @@ router.get(
router.get(
'/members',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
try {
const forceRefresh = !!req.query.refresh;
const team = getContextualTeam(req);
@ -84,7 +84,7 @@ router.get(
router.get(
'/maintainers',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
try {
const forceRefresh = !!req.query.refresh;
@ -115,7 +115,7 @@ router.get(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available for this specific team', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Organization } from '../../../business/organization';
@ -24,7 +24,7 @@ const leakyLocalCache = new LeakyLocalCache<number, Team[]>();
router.use(
'/:teamSlug',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { organization } = req;
const { teamSlug } = req.params;
let team: Team = null;
@ -61,15 +61,19 @@ async function getTeamsForOrganization(
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
return await getClientApiOrganizationTeamsResponse(req, res, next);
})
);
export async function getClientApiOrganizationTeamsResponse(req: ReposAppRequest, res, next) {
export async function getClientApiOrganizationTeamsResponse(
req: ReposAppRequest,
res: Response,
next: NextFunction
) {
const organization = (req.organization || (req as any).aeOrganization) as Organization;
if (!organization) {
return next(jsonError('No available organization'), 400);
return next(jsonError('No available organization', 400));
}
const pager = new JsonPager<Team>(req, res);
const q: string = (req.query.q ? (req.query.q as string) : null) || '';
@ -101,7 +105,7 @@ export async function getClientApiOrganizationTeamsResponse(req: ReposAppRequest
}
}
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within this team', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../middleware';
@ -36,7 +36,7 @@ type HighlightedOrganization = {
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
try {
const orgs = operations.getOrganizations();
@ -52,7 +52,7 @@ router.get(
router.get(
'/annotations',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const providers = getProviders(req);
const { organizationAnnotationsProvider } = providers;
const projection = typeof req.query.projection === 'string' ? req.query.projection : undefined;
@ -113,7 +113,7 @@ router.get(
router.get(
'/list.txt',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
try {
const orgs = operations.getOrganizations();
@ -130,41 +130,43 @@ router.get(
router.use(
'/:orgName',
asyncHandler(async (req: IReposAppRequestWithOrganizationManagementType, res, next) => {
const { operations } = getProviders(req);
const { orgName } = req.params;
req.organizationName = orgName;
try {
const org = operations.getOrganization(orgName);
if (org) {
req.organizationManagementType = OrganizationManagementType.Managed;
asyncHandler(
async (req: IReposAppRequestWithOrganizationManagementType, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
const { orgName } = req.params;
req.organizationName = orgName;
try {
const org = operations.getOrganization(orgName);
if (org) {
req.organizationManagementType = OrganizationManagementType.Managed;
req.organization = org;
return next();
}
} catch (orgNotFoundError) {
if (!ErrorHelper.IsNotFound(orgNotFoundError)) {
return next(orgNotFoundError);
}
}
try {
const org = operations.getUncontrolledOrganization(orgName);
req.organizationManagementType = OrganizationManagementType.Unmanaged;
req.organization = org;
return next();
}
} catch (orgNotFoundError) {
if (!ErrorHelper.IsNotFound(orgNotFoundError)) {
return next(orgNotFoundError);
await setOrganizationProfileForRequest(req);
} catch (orgProfileError) {
if (ErrorHelper.IsNotFound(orgProfileError)) {
return next(CreateError.NotFound(`The organization ${orgName} does not exist`));
} else {
return next(orgProfileError);
}
}
return next();
}
try {
const org = operations.getUncontrolledOrganization(orgName);
req.organizationManagementType = OrganizationManagementType.Unmanaged;
req.organization = org;
await setOrganizationProfileForRequest(req);
} catch (orgProfileError) {
if (ErrorHelper.IsNotFound(orgProfileError)) {
return next(CreateError.NotFound(`The organization ${orgName} does not exist`));
} else {
return next(orgProfileError);
}
}
return next();
})
)
);
router.use('/:orgName', RouteOrganization);
router.use('*', (req: ReposAppRequest, res, next) => {
router.use('*', (req: ReposAppRequest, res: Response, next: NextFunction) => {
return next(jsonError('orgs API not found', 404));
});

Просмотреть файл

@ -3,12 +3,12 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { corporateLinkToJson } from '../../business';
import { jsonError } from '../../middleware';
import { ICorporateLink, ReposAppRequest } from '../../interfaces';
import { type GitHubSimpleAccount, type ICorporateLink, ReposAppRequest } from '../../interfaces';
import JsonPager from './jsonPager';
import getCompanySpecificDeployment from '../../middleware/companySpecificDeployment';
@ -20,34 +20,28 @@ const router: Router = Router();
const deployment = getCompanySpecificDeployment();
deployment?.routes?.api?.people && deployment.routes.api.people(router);
interface ISimpleAccount {
login: string;
avatar_url: string;
id: number;
}
export interface ICrossOrganizationMemberResponse {
account: ISimpleAccount;
account: GitHubSimpleAccount;
link?: ICorporateLink;
organizations: string[];
}
export interface ICrossOrganizationSearchedMember {
id: number;
account: ISimpleAccount;
account: GitHubSimpleAccount;
link?: ICorporateLink;
orgs: IOrganizationMembershipAccount;
}
interface IOrganizationMembershipAccount {
[id: string]: ISimpleAccount;
[id: string]: GitHubSimpleAccount;
}
router.get('/:login', RouteGetPerson);
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const pager = new JsonPager<ICrossOrganizationSearchedMember>(req, res);
try {
const searcher = await equivalentLegacyPeopleSearch(req);
@ -73,7 +67,7 @@ router.get(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within this cross-organization people list', 404));
});

Просмотреть файл

@ -4,13 +4,15 @@
//
import asyncHandler from 'express-async-handler';
import { NextFunction, Response } from 'express';
import { ReposAppRequest, AccountJsonFormat } from '../../interfaces';
import { IGraphEntry } from '../../lib/graphProvider';
import { jsonError } from '../../middleware';
import { getProviders } from '../../transitional';
export default asyncHandler(async (req: ReposAppRequest, res, next) => {
export default asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const providers = getProviders(req);
const { operations, queryCache, graphProvider } = providers;
const login = req.params.login as string;

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Repository } from '../../business';
@ -17,7 +17,7 @@ const router: Router = Router();
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const providers = getProviders(req);
const pager = new JsonPager<Repository>(req, res);
const searchOptions = {
@ -39,7 +39,7 @@ router.get(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within this cross-organization repps list', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import { jsonError } from '../../middleware/jsonError';
import { IAppSession, ReposAppRequest } from '../../interfaces';
@ -42,7 +42,7 @@ router.post('/github', (req: ReposAppRequest, res) => {
res.end();
});
router.use('*', (req: ReposAppRequest, res, next) => {
router.use('*', (req: ReposAppRequest, res: Response, next: NextFunction) => {
return next(jsonError('API or route not found', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Operations, Team } from '../../business';
@ -40,7 +40,7 @@ async function getCrossOrganizationTeams(operations: Operations): Promise<Team[]
router.get(
'/',
asyncHandler(async (req: ReposAppRequest, res, next) => {
asyncHandler(async (req: ReposAppRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
const pager = new JsonPager<Team>(req, res);
const q: string = (req.query.q ? (req.query.q as string) : null) || '';
@ -71,7 +71,7 @@ router.get(
})
);
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('no API or function available within this cross-organization teams list', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Response, Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
const router: Router = Router();
@ -19,9 +19,9 @@ import { PersonalAccessToken } from '../entities/token/token';
const thisApiScopeName = 'extension';
interface IExtensionResponse extends Response {
type ExtensionResponse = Response & {
localKey?: any;
}
};
interface IConnectionInformation {
link?: any;
@ -29,7 +29,7 @@ interface IConnectionInformation {
auth?: any;
}
router.use(function (req: IApiRequest, res, next) {
router.use(function (req: IApiRequest, res: Response, next: NextFunction) {
const token = req.apiKeyToken;
if (!token.scopes) {
return next(jsonError('The key is not authorized for specific APIs', 403));
@ -40,7 +40,7 @@ router.use(function (req: IApiRequest, res, next) {
return next();
});
function overwriteUserContext(req: IApiRequest, res, next) {
function overwriteUserContext(req: IApiRequest, res: Response, next: NextFunction) {
const token = req.apiKeyToken;
const corporateId = token.corporateId;
if (!corporateId) {
@ -121,7 +121,7 @@ router.get('/', (req: IApiRequest, res) => {
router.get(
'/metadata',
asyncHandler(getLocalEncryptionKeyMiddleware),
(req: IApiRequest, res: IExtensionResponse) => {
(req: IApiRequest, res: ExtensionResponse) => {
const apiContext = req.apiContext;
const localKey = res.localKey;
@ -186,7 +186,11 @@ function getSanitizedOrganizations(operations) {
return value;
}
async function getLocalEncryptionKeyMiddleware(req: IApiRequest, res, next): Promise<void> {
async function getLocalEncryptionKeyMiddleware(
req: IApiRequest,
res: ExtensionResponse,
next: NextFunction
): Promise<void> {
const providers = getProviders(req);
const localExtensionKeyProvider = providers.localExtensionKeyProvider;
const apiKeyToken = req.apiKeyToken;
@ -250,7 +254,7 @@ async function getOrCreateLocalEncryptionKey(
return await createLocalEncryptionKey(insights, localExtensionKeyProvider, corporateId);
}
router.use('*', (req, res, next) => {
router.use('*', (req, res: Response, next: NextFunction) => {
return next(jsonError('API not found', 404));
});

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
const router: Router = Router();
@ -36,7 +36,7 @@ function isClientRoute(req: ReposAppRequest) {
router.use('/webhook', apiWebhook);
router.use((req: IApiRequest, res, next) => {
router.use((req: IApiRequest, res: Response, next: NextFunction) => {
if (isClientRoute(req)) {
// The frontend client routes are hooked into Express after
// the session middleware. The client route does not require
@ -91,7 +91,7 @@ router.post('/:org/repos', aadAndCustomProviders);
router.post(
'/:org/repos',
requireAadApiAuthorizedScope(['repo/create', 'createRepo']),
function (req: IApiRequest, res, next) {
function (req: IApiRequest, res: Response, next: NextFunction) {
const orgName = req.params.org;
if (!req.apiKeyToken.organizationScopes) {
return next(jsonError('There is a problem with the key configuration (no organization scopes)', 412));
@ -116,7 +116,7 @@ router.post(
router.post(
'/:org/repos',
asyncHandler(async function (req: ReposAppRequest, res, next) {
asyncHandler(async function (req: ReposAppRequest, res: Response, next: NextFunction) {
const providers = getProviders(req);
const organization = req.organization;
const convergedObject = Object.assign({}, req.headers);
@ -185,7 +185,7 @@ router.post(
})
);
router.use((req: IApiRequest, res, next) => {
router.use((req: IApiRequest, res: Response, next: NextFunction) => {
if (isClientRoute(req)) {
// The frontend client routes are hooked into Express after
// the session middleware. The client route does not require

Просмотреть файл

@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import { getProviders } from '../transitional';
export default function JsonErrorHandler(err, req, res, next) {
export default function JsonErrorHandler(err, req, res: Response, next: NextFunction) {
if (err && err['json']) {
// jsonError objects should bubble up like before
return next(err);

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import { json404 } from '../../middleware/jsonError';

Просмотреть файл

@ -3,6 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import { getProviders } from '../../transitional';
import { jsonError } from '../../middleware';
import { IApiRequest } from '../../middleware/apiReposAuth';
@ -15,7 +17,7 @@ const supportedApiVersions = new Set([
'2019-10-01',
]);
export default async function postLinkApi(req: IApiRequest, res, next) {
export default async function postLinkApi(req: IApiRequest, res: Response, next: NextFunction) {
const providers = getProviders(req);
const { operations } = providers;
const token = req.apiKeyToken;

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { jsonError } from '../../middleware';
@ -26,7 +26,7 @@ const extendedLinkApiVersions = [
'2019-02-01',
];
router.use(function (req: IApiRequest, res, next) {
router.use(function (req: IApiRequest, res: Response, next: NextFunction) {
const token = req.apiKeyToken;
if (!token.scopes) {
return next(jsonError('The key is not authorized for specific APIs', 401));
@ -41,7 +41,7 @@ router.post('/', asyncHandler(postLinkApi));
router.get(
'/',
asyncHandler(async (req: IApiRequest, res, next) => {
asyncHandler(async (req: IApiRequest, res: Response, next: NextFunction) => {
const { operations } = getProviders(req);
const skipOrganizations = req.query.showOrganizations !== undefined && !!req.query.showOrganizations;
const showTimestamps = req.query.showTimestamps !== undefined && req.query.showTimestamps === 'true';
@ -54,7 +54,7 @@ router.get(
router.get(
'/:linkid',
asyncHandler(async (req: IApiRequest, res, next) => {
asyncHandler(async (req: IApiRequest, res: Response, next: NextFunction) => {
if (unsupportedApiVersions.includes(req.apiVersion)) {
return next(jsonError('This API is not supported by the API version you are using.', 400));
}
@ -103,7 +103,7 @@ router.get(
router.get(
'/github/:username',
asyncHandler(async (req: IApiRequest, res, next) => {
asyncHandler(async (req: IApiRequest, res: Response, next: NextFunction) => {
if (unsupportedApiVersions.includes(req.apiVersion)) {
return next(jsonError('This API is not supported by the API version you are using.', 400));
}
@ -150,7 +150,7 @@ router.get(
router.get(
'/aad/userPrincipalName/:upn',
asyncHandler(async (req: IApiRequest, res, next) => {
asyncHandler(async (req: IApiRequest, res: Response, next: NextFunction) => {
const upn = req.params.upn;
const { operations } = getProviders(req);
const skipOrganizations = req.query.showOrganizations !== undefined && !!req.query.showOrganizations;
@ -212,7 +212,7 @@ router.get(
router.get(
'/aad/:id',
asyncHandler(async (req: IApiRequest, res, next) => {
asyncHandler(async (req: IApiRequest, res: Response, next: NextFunction) => {
if (req.apiVersion == '2016-12-01') {
return next(jsonError('This API is not supported by the API version you are using.', 400));
}

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import { ICorporateLink, UnlinkPurpose } from '../../interfaces';
@ -17,7 +17,7 @@ interface ILinksApiRequestWithUnlink extends IApiRequest {
unlink?: ICorporateLink;
}
router.use(function (req: ILinksApiRequestWithUnlink, res, next) {
router.use(function (req: ILinksApiRequestWithUnlink, res: Response, next: NextFunction) {
const token = req.apiKeyToken;
if (!token.scopes) {
return next(jsonError('The key is not authorized for specific APIs', 401));
@ -30,7 +30,7 @@ router.use(function (req: ILinksApiRequestWithUnlink, res, next) {
router.use(
'/github/id/:id',
asyncHandler(async (req: ILinksApiRequestWithUnlink, res, next) => {
asyncHandler(async (req: ILinksApiRequestWithUnlink, res: Response, next: NextFunction) => {
const { linkProvider } = getProviders(req);
const id = req.params.id;
try {
@ -46,11 +46,11 @@ router.use(
})
);
router.use('*', (req: ILinksApiRequestWithUnlink, res, next) => {
router.use('*', (req: ILinksApiRequestWithUnlink, res: Response, next: NextFunction) => {
return next(req.unlink ? undefined : jsonError('No link available for operation', 404));
});
router.delete('*', (req: ILinksApiRequestWithUnlink, res, next) => {
router.delete('*', (req: ILinksApiRequestWithUnlink, res: Response, next: NextFunction) => {
const { config, operations } = getProviders(req);
const link = req.unlink;
let purpose: UnlinkPurpose = null;

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import asyncHandler from 'express-async-handler';
import moment from 'moment';
@ -21,7 +21,7 @@ interface IRequestWithRaw extends ReposAppRequest {
}
router.use(
asyncHandler(async (req: IRequestWithRaw, res, next) => {
asyncHandler(async (req: IRequestWithRaw, res: Response, next: NextFunction) => {
if (!isWebhookIngestionEndpointEnabled(req)) {
return next(
jsonError(

Просмотреть файл

@ -9,13 +9,15 @@ import Debug from 'debug';
const debug = Debug.debug('g:server');
const debugInitialization = Debug.debug('startup');
import app from '../app';
import http from 'http';
import https from 'https';
import fs from 'fs';
import path from 'path';
import { createExpressApplication } from '..';
const app = createExpressApplication();
function normalizePort(val) {
const port = parseInt(val, 10);

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { IReposApplication } from '../../interfaces';
import { ExecutionEnvironment } from '../../interfaces';
import { CreateError } from '../../transitional';
import Debug from 'debug';
@ -146,6 +146,7 @@ export interface IGitHubAppConfiguration {
}
export interface IGitHubAppsOptions {
app: IReposApplication;
// app: IReposApplication;
configurations: Map<AppPurposeTypes, IGitHubAppConfiguration>;
executionEnvironment: ExecutionEnvironment;
}

Просмотреть файл

@ -65,9 +65,10 @@ export class GitHubTokenManager {
if (!options) {
throw new Error('options required');
}
const executionEnvironment = options.executionEnvironment;
this.#options = options;
GitHubTokenManager._forceBackgroundTokens =
options.app.isBackgroundJob && !options.app.enableAllGitHubApps;
executionEnvironment.isJob && !executionEnvironment.enableAllGitHubApps;
}
private getFallbackList(input: AppPurposeTypes[]) {

Просмотреть файл

@ -21,6 +21,7 @@ import {
IOperationsCentralOperationsToken,
IAuthorizationHeaderValue,
SiteConfiguration,
ExecutionEnvironment,
} from '../../interfaces';
import { RestLibrary } from '../../lib/github';
import { CreateError } from '../../transitional';
@ -35,6 +36,7 @@ export interface IOperationsCoreOptions {
github: RestLibrary;
providers: IProviders;
baseUrl?: string;
executionEnvironment: ExecutionEnvironment;
}
export enum CacheDefault {

Просмотреть файл

@ -86,13 +86,7 @@ export interface ICrossOrganizationMembersResult
extends Map<number, ICrossOrganizationMembershipByOrganization> {}
export interface IOperationsOptions extends IOperationsCoreOptions {
// cacheProvider: ICacheHelper;
// config: any;
github: RestLibrary;
// insights: TelemetryClient;
// linkProvider: ILinkProvider;
// mailAddressProvider: IMailAddressProvider;
// mailProvider: IMailProvider;
repositoryMetadataProvider: IRepositoryMetadataProvider;
}
@ -179,7 +173,7 @@ export class Operations
}
this._tokenManager = new GitHubTokenManager({
configurations: purposesToConfigurations,
app: this.providers.app,
executionEnvironment: options.executionEnvironment,
});
GitHubTokenManager.RegisterManagerForOperations(this, this._tokenManager);
this._dynamicOrganizationIds = new Set();

Просмотреть файл

@ -24,6 +24,7 @@ import { CacheDefault, getMaxAgeSeconds, getPageSize, OperationsCore } from './o
import {
CoreCapability,
GitHubAuditLogEntry,
GitHubOrganizationInvite,
GitHubRepositoryVisibility,
IAccountBasics,
IAddOrganizationMembershipOptions,
@ -512,10 +513,6 @@ export class Organization {
return this._settings.hasFeature(OrganizationFeature.Hidden) || false;
}
get pilot_program() {
return this._settings.properties['1es'];
}
get createRepositoriesOnGitHub(): boolean {
return this._settings.hasFeature(OrganizationFeature.CreateNativeRepositories) || false;
}
@ -1331,7 +1328,7 @@ export class Organization {
}
}
async getMembershipInvitations(): Promise<any> {
async getMembershipInvitations(): Promise<GitHubOrganizationInvite[]> {
const operations = throwIfNotGitHubCapable(this._operations);
const parameters = {
org: this.name,
@ -1342,7 +1339,7 @@ export class Organization {
'orgs.listPendingInvitations',
parameters
);
return invitations;
return invitations as GitHubOrganizationInvite[];
} catch (error) {
if (error.status == /* loose */ 404) {
return null;

Просмотреть файл

@ -47,6 +47,7 @@ import {
IOperationsRepositoryMetadataProvider,
IOperationsUrls,
GitHubRepositoryPermission,
GitHubRepositoryVisibility,
} from '../interfaces';
import { IListPullsParameters, GitHubPullRequestState } from '../lib/github/collections';
@ -256,6 +257,9 @@ export class Repository {
get private(): boolean {
return this._entity ? this._entity.private : false;
}
get visibility(): GitHubRepositoryVisibility {
return this._entity ? this._entity.visibility : null;
}
get html_url(): string {
return this._entity ? this._entity.html_url : null;
}
@ -403,6 +407,17 @@ export class Repository {
};
}
async getId(options?: ICacheOptions): Promise<number> {
// Repositories by name may not actually have the ID; this ensures it's available
// and a number. Similar to previously checking "isDeleted" or "getDetails" first.
if (!this.id) {
await this.getDetails(options);
}
if (this.id) {
return typeof this.id === 'number' ? this.id : parseInt(this.id, 10);
}
}
async isDeleted(options?: ICacheOptions): Promise<boolean> {
try {
await this.getDetails(options);

Просмотреть файл

@ -9,10 +9,7 @@ import { AppPurpose } from './githubApps';
import {
IPurposefulGetAuthorizationHeader,
IOperationsInstance,
IGetBranchesOptions,
IGitHubBranch,
throwIfNotGitHubCapable,
IGetPullsOptions,
ICacheOptions,
IGetAuthorizationHeader,
} from '../interfaces';

Просмотреть файл

@ -6,15 +6,34 @@ if you choose to use them.
Jobs are an alternate entrypoint into the application, and have full use of
the same set of [providers](./providers.md).
## list of cronjobs
## Webhook event firehose
Several jobs are available in the container or the `jobs/` folder. These can
optionally provide useful operational and services support. Often a Kubernetes
CronJob can help.
> The primary consistency and event processing loop for the entire app. [firehose](../jobs/firehose.ts)
- `cleanupInvites`: if configured for an org, cleanup old unaccepted invites
- `firehose`: ongoing processing of GitHub events for keeping cache up-to-date
- `managers`: cache the last-known manager for links, to use in notifications after a departure may remove someone from the graph
- `permissions`: updating permissions for all-write/all-read/all-admin teams when configured
- `refreshUsernames`: keeping link data fresh with GitHub username renames, corporate username and display name updates, and removing links for deleted GitHub users who remove their accounts permanently from GitHub.com
- `reports`: processing the building of report data about use, abandoned repos, etc. **this job is broken**
Ongoing processing of GitHub events for keeping cache up-to-date, locking down new repos, etc.
## Cleanup organization invitations
> [cleanupInvites](../jobs/cleanupInvites.ts)
If configured for an org, cleanup old unaccepted invites. This predates
GitHub-native expiration of invites.
## System Team permissions
> [permissions](../jobs/permissions.ts)
Updating permissions for all-write/all-read/all-admin teams when configured
## Refresh usernames and other link attributes
> [refreshUsernames](../jobs/refreshUsernames.ts)
Keeps link data fresh with GitHub username renames, corporate username and display name updates,
and removing links for deleted GitHub users who remove their accounts permanently from GitHub.com.
## Cleanup blob cache
Removes expired cache entities from the blob cache.
> [cleanupBlobCache](../jobs/cleanupBlobCache.ts)

Просмотреть файл

@ -16,6 +16,7 @@ import {
QueryBase,
} from '../lib/entityMetadataProvider';
import { PostgresConfiguration, PostgresSettings } from '../lib/entityMetadataProvider/postgres';
import { ErrorHelper } from '../transitional';
const type = new EntityMetadataType('RepositoryDetails');
const typeColumnValue = 'repositorydetails';
@ -312,6 +313,21 @@ for (let i = 0; i < fieldNames.length; i++) {
}
}
export async function tryGetRepositoryEntity(
repositoryProvider: IRepositoryProvider,
repositoryId: number
): Promise<RepositoryEntity> {
try {
const repositoryEntity = await repositoryProvider.get(repositoryId);
return repositoryEntity;
} catch (error) {
if (ErrorHelper.IsNotFound(error)) {
return null;
}
throw error;
}
}
export const EntityImplementation = {
Type: type,
EnsureDefinitions: () => {},

Просмотреть файл

@ -4,8 +4,8 @@
//
import { EntityField } from '../../lib/entityMetadataProvider/entityMetadataProvider';
import { IEntityMetadata } from '../../lib/entityMetadataProvider/entityMetadata';
import { IEntityMetadataFixedQuery, FixedQueryType } from '../../lib/entityMetadataProvider/query';
import type { IEntityMetadata } from '../../lib/entityMetadataProvider/entityMetadata';
import { type IEntityMetadataFixedQuery, FixedQueryType } from '../../lib/entityMetadataProvider/query';
import {
EntityMetadataMappings,
MetadataMappingDefinition,
@ -17,7 +17,7 @@ import {
PostgresSettings,
PostgresConfiguration,
} from '../../lib/entityMetadataProvider/postgres';
import { TableConfiguration, TableSettings } from '../../lib/entityMetadataProvider/table';
import { TableSettings } from '../../lib/entityMetadataProvider/table';
import { MemoryConfiguration, MemorySettings } from '../../lib/entityMetadataProvider/memory';
import { odata, TableEntityQueryOptions } from '@azure/data-tables';
import {

91
index.ts Normal file
Просмотреть файл

@ -0,0 +1,91 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import express from 'express';
import Debug from 'debug';
import type { ExecutionEnvironment, IReposApplication, SiteConfiguration } from './interfaces';
import configResolver from './lib/config';
import initialize from './middleware/initialize';
// Library framework
export * from './interfaces';
// Application framework
type InitializeCall = (
executionEnvironment: ExecutionEnvironment,
config: SiteConfiguration,
configurationError: Error
) => Promise<ExecutionEnvironment>;
export function createExpressApplication(): IReposApplication {
Debug.debug('startup')('starting web framework...');
const app = express() as any as IReposApplication;
app.initializeApplication = initializeApp.bind(undefined, app, express, __dirname);
app.startupApplication = commonStartup.bind(
undefined,
app.initializeApplication,
false /* not a job */,
app
);
return app;
}
function initializeApp(
app: IReposApplication,
express: any,
dirname: string,
executionEnvironment: ExecutionEnvironment,
config: SiteConfiguration,
configurationError: Error
) {
return initialize(executionEnvironment, app, express, dirname, config, configurationError);
}
export async function commonStartup(call: InitializeCall, isJob: boolean, app?: IReposApplication) {
const executionEnvironment: ExecutionEnvironment = {
isJob,
enableAllGitHubApps: undefined,
//
expressApplication: app,
//
providers: undefined,
skipModules: new Set(),
//
started: new Date(),
};
let painlessConfigResolver = null;
try {
painlessConfigResolver = configResolver();
} catch (error) {
console.warn('Painless config resolver initialization error:');
console.error(error);
throw error;
}
let config: any = null;
let configurationError: Error = null;
try {
config = await painlessConfigResolver.resolve();
} catch (error) {
configurationError = error;
}
if (isJob) {
executionEnvironment.skipModules.add('web');
}
try {
await call(executionEnvironment, config, configurationError);
} catch (startupError) {
console.error(`Startup error: ${startupError}`);
if (startupError.stack) {
console.error(startupError.stack);
}
process.exit(1);
}
return executionEnvironment;
}

Просмотреть файл

@ -3,14 +3,21 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Application } from 'express';
import { Application, Response, NextFunction } from 'express';
import { IProviders } from './providers';
import type { RuntimeConfiguration } from './config';
import { ReposAppRequest } from './web';
export interface IApplicationProfile {
applicationName: string;
customErrorHandlerRender?: (errorView: any, err: Error, req: any, res: any, next: any) => Promise<void>;
customErrorHandlerRender?: (
errorView: unknown,
err: Error,
req: ReposAppRequest,
res: Response,
next: NextFunction
) => Promise<void | unknown>;
customRoutes?: () => Promise<void>;
logDependencies: boolean;
serveClientAssets: boolean;
@ -32,24 +39,43 @@ export interface IReposApplication extends Application {
enableAllGitHubApps: boolean;
runtimeConfiguration: RuntimeConfiguration;
executionEnvironment: ExecutionEnvironment;
startServer: () => Promise<void>;
initializeApplication: (config: any, configurationError: Error) => Promise<IReposApplication>;
initializeJob: (config: any, configurationError: Error) => Promise<IReposApplication>;
initializeApplication: (
executionEnvironment: ExecutionEnvironment,
config: any,
configurationError: Error
) => Promise<IReposApplication>;
startupApplication: () => Promise<IReposApplication>;
startupJob: () => Promise<IReposApplication>;
runJob: (
job: (job: IReposJob) => Promise<IReposJobResult | void>,
options?: IReposJobOptions
) => Promise<IReposApplication>;
) => Promise<IReposJobResult | void>;
}
export type ExecutionEnvironment = {
isJob: boolean;
enableAllGitHubApps: boolean;
expressApplication: IReposApplication | null;
providers: IProviders;
skipModules: Set<string>;
started: Date;
};
export interface IReposJob {
app: IReposApplication;
started: Date;
providers: IProviders;
parameters: any;
args: string[];
executionEnvironment: ExecutionEnvironment;
}
export interface IReposJobResult {

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import { IDictionary } from '../../interfaces';

Просмотреть файл

@ -4,7 +4,7 @@
//
import { PassportStatic } from 'passport';
import { IAuthenticationHelperMethods } from '../../middleware/passport-routes';
import type { IAuthenticationHelperMethods } from '../../middleware/passport-routes';
export interface ICompanySpecificPassportMiddleware {
configure?: (app: any, config: any, passport: PassportStatic) => void;

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
import { IAttachCompanySpecificRoutesApi } from './api';
export * from './api';

Просмотреть файл

@ -3,8 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { ICacheOptions, IPagedCacheOptions } from '.';
import { ICorporateLink } from '..';
import type { ICacheOptions, IPagedCacheOptions } from '.';
import type { ICorporateLink } from '..';
import { OrganizationMember } from '../../business';
import { Repository } from '../../business/repository';
@ -97,3 +97,24 @@ export interface IOrganizationMembership {
organization: any;
user: any;
}
export type GitHubSimpleAccount = {
login: string;
avatar_url: string;
id: number;
};
export type GitHubOrganizationInvite = {
created_at: string;
email: string;
failed_at: string;
failed_reason: string;
id: number;
invitation_source: string; // 'member'
invitation_teams_url: string;
inviter: GitHubSimpleAccount;
login: string;
node_id: string;
role: string; // 'direct_member'
team_count: number;
};

Просмотреть файл

@ -16,7 +16,8 @@ export * from './providers';
export * from './web';
export * from './config';
import {
import type { ExecutionEnvironment } from './app';
import type {
IAttachCompanySpecificRoutes,
IAttachCompanySpecificMiddleware,
ICorporationAdministrationSection,
@ -25,8 +26,9 @@ import {
IAttachCompanySpecificViews,
IAttachCompanySpecificUrls,
} from './companySpecific';
import { ICompanySpecificPassportMiddleware } from './companySpecific/passport';
import { IProviders } from './providers';
import type { ICompanySpecificPassportMiddleware } from './companySpecific/passport';
import type { SiteConfiguration } from './config';
import type { IProviders } from './providers';
// We're great at long variable names!
@ -42,6 +44,11 @@ export interface ICompanySpecificStartupProperties {
urls?: IAttachCompanySpecificUrls;
}
export type ICompanySpecificStartupFunction = (config: any, p: IProviders, rootdir: string) => Promise<void>;
export type ICompanySpecificStartupFunction = (
executionEnvironment: ExecutionEnvironment,
config: SiteConfiguration,
p: IProviders,
rootdir: string
) => Promise<void>;
export type ICompanySpecificStartup = ICompanySpecificStartupFunction & ICompanySpecificStartupProperties;

Просмотреть файл

@ -1,3 +1,8 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
// eslint-disable-next-line n/no-unpublished-import
import type { Config } from 'jest';

Просмотреть файл

@ -3,69 +3,27 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import express from 'express';
import { hostname } from 'os';
import { IReposApplication, IReposJob, IReposJobOptions, IReposJobResult } from './interfaces';
import configResolver from './lib/config';
import initialize from './middleware/initialize';
import { quitInTenSeconds } from './utils';
const app = express() as any as IReposApplication;
import Debug from 'debug';
Debug.debug('startup')('starting...');
app.initializeApplication = initialize.bind(undefined, app, express, __dirname);
import type {
ExecutionEnvironment,
IProviders,
IReposJob,
IReposJobOptions,
IReposJobResult,
SiteConfiguration,
} from './interfaces';
import { commonStartup } from '.';
import { quitInTenSeconds } from './utils';
import initialize from './middleware/initialize';
app.initializeJob = function initializeJob(config, configurationError) {
if (config) {
config.isJobInternal = true;
config.skipModules = new Set(['web']);
} else {
console.warn(`Configuration did not resolve successfully`, configurationError);
}
return initialize(app, express, __dirname, config, configurationError);
};
async function startup(startupApplication: boolean) {
let painlessConfigResolver = null;
try {
painlessConfigResolver = configResolver();
} catch (error) {
console.warn('Painless config resolver initialization error:');
console.error(error);
throw error;
}
let config: any = null;
let configurationError: Error = null;
try {
config = await painlessConfigResolver.resolve();
} catch (error) {
configurationError = error;
}
try {
if (startupApplication) {
await app.initializeApplication(config, configurationError);
} else {
await app.initializeJob(config, configurationError);
}
} catch (startupError) {
console.error(`Startup error: ${startupError}`);
process.exit(1); // throw startupError;
}
return app;
}
app.startupApplication = startup.bind(null, true);
app.startupJob = startup.bind(null, false);
app.runJob = async function (
export async function runJob(
job: (job: IReposJob) => Promise<IReposJobResult | void>,
options?: IReposJobOptions
): Promise<IReposApplication> {
): Promise<IReposJobResult | void> {
Debug.debug('startup')('starting job...');
options = options || {};
// TODO: automatically track elapsed job time
const started = new Date();
@ -79,20 +37,19 @@ app.runJob = async function (
if (options.defaultDebugOutput && !process.env.DEBUG) {
process.env.DEBUG = options.defaultDebugOutput;
}
app.isBackgroundJob = true;
if (options.enableAllGitHubApps) {
app.enableAllGitHubApps = true;
}
let executionEnvironment: ExecutionEnvironment = null;
try {
await app.startupJob();
executionEnvironment = await commonStartup(initializeJob, true /* job */, null /* app */);
} catch (startupError) {
console.error(`Job startup error before runJob: ${startupError}`);
quitInTenSeconds(false);
return app;
return;
}
if (options.insightsPrefix && app.providers.insights) {
const providers = executionEnvironment?.providers;
if (options.insightsPrefix && providers?.insights) {
try {
app.providers.insights.trackEvent({
providers?.insights?.trackEvent({
name: `${options.insightsPrefix}Started`,
properties: {
hostname: hostname(),
@ -103,17 +60,19 @@ app.runJob = async function (
}
}
const jobObject = {
app,
providers: app.providers,
app: providers?.app,
executionEnvironment,
providers,
started,
parameters: options && options.parameters ? options.parameters : {},
args: process.argv.length > 2 ? process.argv.slice(2) : [],
};
let result: IReposJobResult = null;
try {
const result = await job.call(null, jobObject);
if (result && result.successProperties && app.providers.insights && options.insightsPrefix) {
result = (await job.call(null, jobObject)) as IReposJobResult;
if (result?.successProperties && providers?.insights && options.insightsPrefix) {
try {
app.providers.insights.trackEvent({
providers?.insights?.trackEvent({
name: `${options.insightsPrefix}Success`,
properties: Object.assign(
{
@ -128,14 +87,17 @@ app.runJob = async function (
}
} catch (jobError) {
console.error(`The job failed: ${jobError}`);
if (jobError.stack) {
console.error(jobError.stack);
}
// by default, let's not show the whole inner error
const simpleError = { ...jobError };
simpleError?.cause && delete simpleError.cause;
console.dir(simpleError);
quitInTenSeconds(false);
if (options.insightsPrefix && app.providers.insights) {
if (options.insightsPrefix) {
try {
app.providers.insights.trackException({
providers?.insights?.trackException({
exception: jobError,
properties: {
name: `${options.insightsPrefix}Failure`,
@ -145,12 +107,50 @@ app.runJob = async function (
console.error(`insights error: ${ignoreInsightsError}`);
}
}
return app;
return result;
}
// CONSIDER: insights metric for job time
console.log();
console.log('The job was successful.');
quitInTenSeconds(true);
return app;
return result;
}
function initializeJob(
executionEnvironment: ExecutionEnvironment,
config: SiteConfiguration,
configurationError: Error
) {
if (!config || configurationError) {
console.warn(`Configuration did not resolve successfully`, configurationError);
}
return initialize(
executionEnvironment,
null /* app */,
null /* express */,
__dirname,
config,
configurationError
);
}
const job = {
runBackgroundJob: async (
script: (providers: IProviders, jobParameters?: IReposJob) => Promise<IReposJobResult | void>,
options?: IReposJobOptions
) => {
return runJob(async function (jobParameters: IReposJob) {
return (await script(jobParameters.providers, jobParameters)) || {};
}, Object.assign({ enableAllGitHubApps: false }, options || {}));
},
run: async (
script: (providers: IProviders, jobParameters?: IReposJob) => Promise<IReposJobResult | void>,
options?: IReposJobOptions
) => {
return runJob(async function (jobParameters: IReposJob) {
return (await script(jobParameters.providers, jobParameters)) || {};
}, Object.assign({ enableAllGitHubApps: true }, options || {}));
},
};
export default app;
export default job;

Просмотреть файл

@ -3,10 +3,15 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import BlobCache from '../../lib/caching/blob';
import { IReposJob } from '../../interfaces';
// Job 17: Cleanup blob cache
export default async function go({ providers }: IReposJob): Promise<void> {
import BlobCache from '../lib/caching/blob';
import job from '../job';
import { IProviders } from '../interfaces';
job.runBackgroundJob(cleanup);
async function cleanup(providers: IProviders): Promise<void> {
for (const providerName in providers) {
const provider = providers[providerName];
if (provider && provider['expiringBlobCache']) {

Просмотреть файл

@ -1,9 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import Job from './task';
import app from '../../app';
app.runJob(Job);

Просмотреть файл

@ -3,48 +3,50 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import moment from 'moment';
import { IReposJob } from '../../interfaces';
// Job 16: cleanup invites
// Organization invitations cleanup: remove any invitations that are older than a
// set period of time from the organization.
import { GitHubOrganizationInvite, IProviders } from '../interfaces';
import job from '../job';
import { daysInMilliseconds } from '../utils';
const defaultMaximumInvitationAgeDays = 4;
export default async function cleanup({ providers }: IReposJob): Promise<void> {
job.runBackgroundJob(cleanup, {
timeoutMinutes: 90,
insightsPrefix: 'JobOrganizationInvitationsCleanup',
});
async function cleanup(providers: IProviders) {
const insights = providers.insights;
let maximumInvitationAgeDays = defaultMaximumInvitationAgeDays;
const { config, operations } = providers;
if (
config.github &&
config.github.jobs &&
config.github.jobs.cleanup &&
config.github.jobs.cleanup.maximumInvitationAgeDays
) {
if (config?.github?.jobs?.cleanup?.maximumInvitationAgeDays) {
maximumInvitationAgeDays = config.github.jobs.cleanup.maximumInvitationAgeDays;
}
const maximumAgeMoment = moment().subtract(maximumInvitationAgeDays, 'days');
const maximumAgeDate = new Date(new Date().getTime() - daysInMilliseconds(maximumInvitationAgeDays));
const organizations = operations.getOrganizations();
const removedInvitations = 0;
for (const organization of organizations) {
let invitations: any[];
let invitations: GitHubOrganizationInvite[];
try {
invitations = await organization.getMembershipInvitations();
} catch (getInvitationsError) {
insights.trackException({ exception: getInvitationsError });
insights?.trackException({ exception: getInvitationsError });
console.dir(getInvitationsError);
continue;
}
if (!invitations || invitations.length === 0) {
continue;
}
const invitationsToRemove = [];
const invitationsToRemove: string[] = [];
let emailInvitations = 0;
for (let i = 0; i < invitations.length; i++) {
const invite = invitations[i];
const createdAt = moment(invite.created_at);
if (createdAt.isBefore(maximumAgeMoment)) {
const createdAt = new Date(invite.created_at);
if (createdAt < maximumAgeDate) {
if (invite.login) {
invitationsToRemove.push(invite.login);
} else {
@ -52,8 +54,7 @@ export default async function cleanup({ providers }: IReposJob): Promise<void> {
console.warn(`An e-mail based invitation to ${invite.email} cannot be automatically canceled`);
}
const data = {
createdAt: createdAt.format(),
invitedAgo: createdAt.fromNow(),
createdAt: createdAt.toISOString(),
login: invite.login,
inviter: invite && invite.inviter && invite.inviter.login ? invite.inviter.login : undefined,
role: invite.role,
@ -62,7 +63,7 @@ export default async function cleanup({ providers }: IReposJob): Promise<void> {
const eventName = invite.login
? 'JobOrganizationInviteCleanupInvitationNeeded'
: 'JobOrganizationInviteCleanupInvitationNotUser';
insights.trackEvent({
insights?.trackEvent({
name: eventName,
properties: data,
});
@ -80,8 +81,8 @@ export default async function cleanup({ providers }: IReposJob): Promise<void> {
try {
await organization.removeMember(login);
} catch (removeError) {
insights.trackException({ exception: removeError });
insights.trackEvent({
insights?.trackException({ exception: removeError });
insights?.trackEvent({
name: 'JobOrganizationInvitationsCleanupInvitationFailed',
properties: {
login: login,
@ -92,5 +93,5 @@ export default async function cleanup({ providers }: IReposJob): Promise<void> {
}
}
console.log(`Job finishing. Removed ${removedInvitations} expired invitations.`);
insights.trackMetric({ name: 'JobOrganizationInvitationsExpired', value: removedInvitations });
insights?.trackMetric({ name: 'JobOrganizationInvitationsExpired', value: removedInvitations });
}

Просмотреть файл

@ -1,13 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import Job from './task';
import app from '../../app';
app.runJob(Job, {
timeoutMinutes: 90,
defaultDebugOutput: 'restapi',
insightsPrefix: 'JobOrganizationInvitationsCleanup',
});

Просмотреть файл

@ -1,9 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import Job from './task';
import app from '../../app';
app.runJob(Job, { timeoutMinutes: 90, defaultDebugOutput: 'restapi', insightsPrefix: 'JobCleanupKeys' });

Просмотреть файл

@ -1,98 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import throat from 'throat';
import { IReposJob, IReposJobResult } from '../../interfaces';
import { sleep } from '../../utils';
import { IGraphProvider } from '../../lib/graphProvider';
import { LocalExtensionKey } from '../../entities/localExtensionKey/localExtensionKey';
async function lookupCorporateId(
graphProvider: IGraphProvider,
knownUsers: Map<string, any>,
corporateId: string
): Promise<any> {
const entry = knownUsers.get(corporateId);
if (entry === false) {
return false;
} else if (entry) {
return true;
}
try {
const userDetails = await graphProvider.getUserById(corporateId);
if (!userDetails || !userDetails.userPrincipalName) {
knownUsers.set(corporateId, false);
return false;
}
knownUsers.set(corporateId, userDetails);
return true;
} catch (otherUserError) {
console.dir(otherUserError);
throw otherUserError;
}
}
export default async function cleanup({ providers }: IReposJob): Promise<IReposJobResult> {
const graphProvider = providers.graphProvider;
const localExtensionKeyProvider = providers.localExtensionKeyProvider;
const insights = providers.insights;
console.log('reading all keys');
const allKeys = await localExtensionKeyProvider.getAllKeys();
console.log(`read ${allKeys.length}`);
insights.trackEvent({ name: 'JobCleanupTokensKeysTokens', properties: { tokens: String(allKeys.length) } });
let errors = 0;
let deleted = 0;
let okUserTokens = 0;
const parallelUsers = 2;
const secondsDelayAfterSuccess = 0.25;
const knownUsers = new Map<string, any>();
const throttle = throat(parallelUsers);
await Promise.all(
allKeys.map((key: LocalExtensionKey) =>
throttle(async () => {
const corporateId = key.corporateId;
const userStatus = await lookupCorporateId(graphProvider, knownUsers, corporateId);
if (!userStatus) {
try {
++deleted;
console.log(`${deleted}: Deleting key for ${corporateId} that could not be found`);
await localExtensionKeyProvider.delete(key);
} catch (tokenDeleteError) {
--deleted;
console.dir(tokenDeleteError);
++errors;
insights.trackException({ exception: tokenDeleteError });
}
} else {
++okUserTokens;
console.log(`${okUserTokens}: valid`);
}
await sleep(secondsDelayAfterSuccess * 1000);
})
)
);
console.log(`deleted: ${deleted}`);
console.log(`okUserTokens: ${okUserTokens}`);
console.log();
return {
successProperties: {
deleted,
okUserTokens,
errors,
},
};
}

Просмотреть файл

@ -1,9 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import Job from './task';
import app from '../../app';
app.runJob(Job, { timeoutMinutes: 90, defaultDebugOutput: 'restapi', insightsPrefix: 'JobCleanupTokens' });

Просмотреть файл

@ -1,142 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import throat from 'throat';
import { IReposJob, IReposJobResult } from '../../interfaces';
// Revoke tokens of users that no longer resolve in the corporate graph and
// delete tokens that have been expired 30 days.
const expiredTokenDeleteThresholdDays = 30;
import { PersonalAccessToken } from '../../entities/token/token';
import { sleep } from '../../utils';
import { IGraphProvider } from '../../lib/graphProvider';
async function lookupCorporateId(
graphProvider: IGraphProvider,
knownUsers: Map<string, any>,
corporateId: string
): Promise<any> {
const entry = knownUsers.get(corporateId);
if (entry === false) {
return false;
} else if (entry) {
return true;
}
try {
const userDetails = await graphProvider.getUserById(corporateId);
if (!userDetails || !userDetails.userPrincipalName) {
knownUsers.set(corporateId, false);
return false;
}
knownUsers.set(corporateId, userDetails);
return true;
} catch (otherUserError) {
console.dir(otherUserError);
throw otherUserError;
}
}
export default async function cleanup({ providers }: IReposJob): Promise<IReposJobResult> {
const insights = providers.insights;
const graphProvider = providers.graphProvider;
const tokenProvider = providers.tokenProvider;
console.log('reading all tokens');
const allTokens = await tokenProvider.getAllTokens();
console.log(`read ${allTokens.length}`);
insights.trackEvent({
name: 'JobCleanupTokensReadTokens',
properties: { tokens: String(allTokens.length) },
});
let errors = 0;
let revokedUnresolved = 0;
let deleted = 0;
let serviceTokens = 0;
let okUserTokens = 0;
const parallelUsers = 1;
const secondsDelayAfterSuccess = 0.25;
const now = new Date();
const monthAgo = new Date(now.getTime() - 1000 * 60 * 60 * 24 * expiredTokenDeleteThresholdDays);
const knownUsers = new Map<string, any>();
const throttle = throat(parallelUsers);
await Promise.all(
allTokens.map((pat: PersonalAccessToken) =>
throttle(async () => {
const isGuidMeansADash = pat.corporateId && pat.corporateId.includes('-');
let wasUser = false;
if (isGuidMeansADash) {
wasUser = true;
const userStatus = await lookupCorporateId(graphProvider, knownUsers, pat.corporateId);
if (!userStatus && pat.active !== false) {
pat.active = false;
console.log(
`Revoking key for ${pat.getIdentifier()} - employee ${pat.corporateId} could not be found`
);
try {
await tokenProvider.updateToken(pat);
++revokedUnresolved;
} catch (tokenUpdateError) {
console.dir(tokenUpdateError);
++errors;
insights.trackException({ exception: tokenUpdateError });
}
}
} else {
++serviceTokens;
}
if (pat.isExpired()) {
const dateExpired = pat.expires;
if (dateExpired < monthAgo) {
console.log(`Deleting key for ${pat.getIdentifier()} that expired ${dateExpired}`);
try {
await tokenProvider.deleteToken(pat);
++deleted;
} catch (tokenDeleteError) {
console.dir(tokenDeleteError);
++errors;
insights.trackException({ exception: tokenDeleteError });
}
} else {
console.log(
`Expired key, keeping around ${pat.getIdentifier()} that expired ${dateExpired} for user notification purposes`
);
}
} else if (wasUser) {
++okUserTokens;
}
await sleep(secondsDelayAfterSuccess * 1000);
})
)
);
console.log(`deleted: ${deleted}`);
console.log(`revokedUnresolved: ${revokedUnresolved}`);
console.log(`okUserTokens: ${okUserTokens}`);
console.log(`serviceTokens: ${serviceTokens}`);
console.log();
return {
successProperties: {
deleted,
revokedUnresolved,
okUserTokens,
serviceTokens,
errors,
},
};
}

Просмотреть файл

@ -1,14 +1,21 @@
import app from '../../app';
import { Organization } from '../../business/organization';
import { RepositoryCollaboratorCacheEntity } from '../../entities/repositoryCollaboratorCache/repositoryCollaboratorCache';
import { RepositoryTeamCacheEntity } from '../../entities/repositoryTeamCache/repositoryTeamCache';
import { IProviders, IReposJob, IReposJobResult } from '../../interfaces';
import { ErrorHelper } from '../../transitional';
import { sleep } from '../../utils';
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
// Job: Consistency: Deleted repos (7)
import job from '../job';
import { Organization } from '../business/organization';
import { RepositoryCollaboratorCacheEntity } from '../entities/repositoryCollaboratorCache/repositoryCollaboratorCache';
import { RepositoryTeamCacheEntity } from '../entities/repositoryTeamCache/repositoryTeamCache';
import { IProviders, IReposJobResult } from '../interfaces';
import { ErrorHelper } from '../transitional';
import { sleep } from '../utils';
const killBitHours = 8;
app.runJob(byUserJob, {
job.runBackgroundJob(byUserJob, {
defaultDebugOutput: 'qcuser',
timeoutMinutes: 60 * killBitHours,
insightsPrefix: 'JobRefreshUserQC',
@ -202,7 +209,7 @@ async function processDeletedRepositories(providers: IProviders): Promise<void>
console.log(`removed collaborator repos: ${removedCollaboratorRepositories}`);
}
export default async function byUserJob({ providers, args }: IReposJob): Promise<IReposJobResult> {
async function byUserJob(providers: IProviders): Promise<IReposJobResult> {
const { config } = providers;
if (config?.jobs?.refreshWrites !== true) {
console.log('job is currently disabled to avoid metadata refresh/rewrites');

Просмотреть файл

@ -7,24 +7,29 @@
import os from 'os';
import { DateTime } from 'luxon';
import App from '../../app';
import ProcessOrganizationWebhook, { IGitHubWebhookProperties } from '../../webhooks/organizationProcessor';
import ProcessOrganizationWebhook, { IGitHubWebhookProperties } from '../webhooks/organizationProcessor';
import {
IGitHubAppInstallation,
IGitHubWebhookEnterprise,
IProviders,
IReposJob,
IReposJobResult,
} from '../../interfaces';
import { sleep } from '../../utils';
import { IQueueMessage } from '../../lib/queues';
import getCompanySpecificDeployment from '../../middleware/companySpecificDeployment';
} from '../interfaces';
import { sleep } from '../utils';
import { IQueueMessage } from '../lib/queues';
import getCompanySpecificDeployment from '../middleware/companySpecificDeployment';
import job from '../job';
const runningAsOngoingDeployment = true;
const hardAbortMs = 1000 * 60 * 5; // 5 minutes
export default async function firehose({ providers, started }: IReposJob): Promise<IReposJobResult> {
job.run(firehose, {
insightsPrefix: 'JobFirehose',
});
async function firehose(providers: IProviders, { started }: IReposJob): Promise<IReposJobResult> {
const processedEventTypes = {};
const interestingEvents = 0;
let processedEvents = 0;
@ -89,28 +94,24 @@ export default async function firehose({ providers, started }: IReposJob): Promi
console.log(
`Parallelism for this run will be ${parallelism} logical threads, offset by ${sliceDelayPerThread}s`
);
// const insights = app.settings.appInsightsClient;
if (insights) {
insights.trackEvent({
name: 'JobFirehoseStarted',
properties: {
hostname: os.hostname(),
// queue: serviceBusConfig.queue,
// subscription: serviceBusConfig.subscriptionName,
// messagesInQueue: messagesInQueue.toString(),
// deadLetters: deadLetters.toString(),
},
});
// insights.trackMetric({ name: 'FirehoseMessagesInQueue', value: messagesInQueue });
// insights.trackMetric({ name: 'FirehoseDeadLetters', value: deadLetters });
}
insights?.trackEvent({
name: 'JobFirehoseStarted',
properties: {
hostname: os.hostname(),
// queue: serviceBusConfig.queue,
// subscription: serviceBusConfig.subscriptionName,
// messagesInQueue: messagesInQueue.toString(),
// deadLetters: deadLetters.toString(),
},
});
// insights.trackMetric({ name: 'FirehoseMessagesInQueue', value: messagesInQueue });
// insights.trackMetric({ name: 'FirehoseDeadLetters', value: deadLetters });
const threads: Promise<void>[] = [];
let delay = 0;
for (let i = 0; i < parallelism; i++) {
threads.push(createThread(App, providers, i, delay));
threads.push(createThread(providers, i, delay));
delay += sliceDelayPerThread;
}
const ok = true;
await Promise.all(threads);
console.warn('Forever execution thread has completed.');
@ -119,7 +120,6 @@ export default async function firehose({ providers, started }: IReposJob): Promi
// -- end of job startup --
async function createThread(
app,
providers: IProviders,
threadNumber: number,
startupDelay: number
@ -136,7 +136,7 @@ export default async function firehose({ providers, started }: IReposJob): Promi
await iterate(providers, threadNumber);
}
} catch (error) {
const insights = app.settings.appInsightsClient;
const insights = providers.insights;
insights.trackException({ exception: error });
insights.trackEvent({
name: 'JobFirehoseFatalError',

Просмотреть файл

@ -1,13 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import Job from './task';
import app from '../../app';
app.runJob(Job, {
defaultDebugOutput: 'redis,restapi,querycache',
insightsPrefix: 'JobFirehose',
enableAllGitHubApps: true,
});

214
jobs/permissions.ts Normal file
Просмотреть файл

@ -0,0 +1,214 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
// Job 15: System Team Permissions
import { shuffle } from 'lodash';
import throat from 'throat';
import job from '../job';
import { TeamPermission } from '../business/teamPermission';
import { GitHubRepositoryPermission, IProviders, IReposJobResult } from '../interfaces';
import AutomaticTeamsWebhookProcessor from '../webhooks/tasks/automaticTeams';
import { sleep } from '../utils';
import { ErrorHelper } from '../transitional';
import { Organization } from '../business';
// Permissions processing: visit all repos and make sure that any designated read, write, admin
// teams for the organization are present on every repo. This job is designed to be run relatively
// regularly but is not looking to answer "the truth" - it will use the cache of repos and other
// assets to not abuse GitHub and its API exhaustively. Over time repos will converge to having
// the right permissions.
//
// If a repository is "compliance locked", the system teams are not enforced until the lock is removed.
const maxParallelism = 3;
const delayBetweenSeconds = 1;
let updatedPermissions = 0;
let updatedRepos = 0;
const missingTeams = new Set<number>();
job.runBackgroundJob(permissionsRun, {
insightsPrefix: 'JobPermissions',
});
async function permissionsRun(providers: IProviders): Promise<IReposJobResult> {
const { config, insights, operations } = providers;
if (config?.jobs?.refreshWrites !== true) {
console.log('job is currently disabled to avoid metadata refresh/rewrites');
return;
}
const throttle = throat(maxParallelism);
const organizations = shuffle(Array.from(operations.organizations.values()));
await Promise.allSettled(
organizations.map((organization, index) =>
throttle(async () => {
return reviewOrganizationSystemTeams(providers, organization, index, organizations.length);
})
)
);
console.log(`Updated ${updatedPermissions} permissions across ${organizations.length} organizations`);
insights?.trackMetric({ name: 'JobSystemTeamsUpdatedPermissions', value: updatedPermissions });
console.log(`Updated ${updatedRepos} repos across ${organizations.length} organizations`);
insights?.trackMetric({ name: 'JobSystemTeamsUpdatedRepos', value: updatedRepos });
return {};
}
async function reviewOrganizationSystemTeams(
providers: IProviders,
organization: Organization,
index: number,
count: number
) {
const { insights } = providers;
const prefix = `${index}/${count}: ${organization.name}: `;
console.log(`${prefix} Reviewing permissions for all repos in ${organization.name}...`);
try {
const repos = await organization.getRepositories();
console.log(`Repos in the ${organization.name} org: ${repos.length}`);
const automaticTeams = new AutomaticTeamsWebhookProcessor();
for (const repo of repos) {
let thisRepoUpdated = false;
console.log(`${organization.name}/${repo.name}`);
sleep(1000 * delayBetweenSeconds);
const cacheOptions = {
maxAgeSeconds: 10 * 60 /* 10m */,
backgroundRefresh: false,
};
const { specialTeamIds, specialTeamLevels } = automaticTeams.processOrgSpecialTeams(repo.organization);
let permissions: TeamPermission[] = null;
try {
permissions = await repo.getTeamPermissions(cacheOptions);
} catch (getError) {
if (ErrorHelper.IsNotFound(getError)) {
console.log(`Repo gone: ${repo.organization.name}/${repo.name}`);
} else {
console.log(
`There was a problem getting the permissions for the repo ${repo.name} from ${repo.organization.name}`
);
console.dir(getError);
}
continue;
}
let shouldSkipEnforcement = false;
const { customizedTeamPermissionsWebhookLogic } = providers;
if (customizedTeamPermissionsWebhookLogic) {
shouldSkipEnforcement = await customizedTeamPermissionsWebhookLogic.shouldSkipEnforcement(repo);
}
const currentPermissions = new Map<number, GitHubRepositoryPermission>();
permissions.forEach((entry) => {
currentPermissions.set(Number(entry.team.id), entry.permission);
});
const teamsToSet = new Set<number>();
specialTeamIds.forEach((specialTeamId) => {
if (!currentPermissions.has(specialTeamId)) {
teamsToSet.add(specialTeamId);
} else if (
isAtLeastPermissionLevel(
currentPermissions.get(specialTeamId),
specialTeamLevels.get(specialTeamId)
)
) {
// The team permission is already acceptable
} else {
console.log(
`Permission level for ${specialTeamId} is not good enough, expected ${specialTeamLevels.get(
specialTeamId
)} but currently ${currentPermissions.get(specialTeamId)}`
);
teamsToSet.add(specialTeamId);
}
});
const setArray = Array.from(teamsToSet.values());
for (const teamId of setArray) {
const newPermission = specialTeamLevels.get(teamId);
if (
shouldSkipEnforcement &&
(newPermission as GitHubRepositoryPermission) !== GitHubRepositoryPermission.Pull
) {
console.log(
`should add ${teamId} team with permission ${newPermission} to the repo ${repo.name}, but compliance lock prevents non-read system teams`
);
insights?.trackEvent({
name: 'JobSystemTeamsSkipped',
properties: {
org: organization.name,
repo: repo.name,
teamId,
reason: 'compliance lock',
newPermission,
},
});
} else {
try {
if (!missingTeams.has(teamId)) {
await repo.setTeamPermission(teamId, newPermission as GitHubRepositoryPermission);
++updatedPermissions;
thisRepoUpdated = true;
insights?.trackEvent({
name: 'JobSystemTeamsUpdated',
properties: {
org: organization.name,
repo: repo.name,
teamId,
newPermission,
},
});
}
} catch (error) {
if (ErrorHelper.IsNotFound(error)) {
missingTeams.add(teamId);
console.log(
`the team ID ${teamId} could not be found when setting to repo ${repo.name} in org ${organization.name} and should likely be removed from config...`
);
} else {
console.log(`${repo.name}`);
console.dir(error);
throw error;
}
}
}
}
if (thisRepoUpdated) {
++updatedRepos;
}
}
console.log(`Finished with repos in ${organization.name} organization`);
} catch (processOrganizationError) {
console.dir(processOrganizationError);
console.log(`moving past ${organization.name} processing due to error...`);
}
}
function isAtLeastPermissionLevel(value, expected) {
if (value !== 'admin' && value !== 'push' && value !== 'pull') {
throw new Error(`The permission type ${value} is not understood by isAtLeastPermissionLevel`);
}
if (value === expected) {
return true;
}
// Admin always wins
if (value === 'admin') {
return true;
} else if (expected === 'admin') {
return false;
}
if (expected === 'write' && value === expected) {
return true;
}
if (expected === 'read') {
return true;
}
return false;
}

Просмотреть файл

@ -1,14 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
// Job: System Team Permissions
import Job from './task';
import app from '../../app';
app.runJob(Job, {
insightsPrefix: 'JobPermissions',
defaultDebugOutput: 'cache,restapi',
});

Просмотреть файл

@ -1,155 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
// Job: System Team Permissions
import { shuffle } from 'lodash';
import { TeamPermission } from '../../business/teamPermission';
import { GitHubRepositoryPermission, IReposJob, IReposJobResult } from '../../interfaces';
import AutomaticTeamsWebhookProcessor from '../../webhooks/tasks/automaticTeams';
import { sleep } from '../../utils';
import { ErrorHelper } from '../../transitional';
// Permissions processing: visit all repos and make sure that any designated read, write, admin
// teams for the organization are present on every repo. This job is designed to be run relatively
// regularly but is not looking to answer "the truth" - it will use the cache of repos and other
// assets to not abuse GitHub and its API exhaustively. Over time repos will converge to having
// the right permissions.
//
// If a repository is "compliance locked", the system teams are not enforced until the lock is removed.
const maxParallelism = 1;
const delayBetweenSeconds = 1;
export default async function permissionsRun({ providers }: IReposJob): Promise<IReposJobResult> {
const { config, operations } = providers;
if (config?.jobs?.refreshWrites !== true) {
console.log('job is currently disabled to avoid metadata refresh/rewrites');
return;
}
for (const organization of shuffle(Array.from(operations.organizations.values()))) {
console.log(`Reviewing permissions for all repos in ${organization.name}...`);
try {
const repos = await organization.getRepositories();
console.log(`Repos in the ${organization.name} org: ${repos.length}`);
let z = 0;
const automaticTeams = new AutomaticTeamsWebhookProcessor();
for (const repo of repos) {
console.log(`${repo.organization.name}/${repo.name}`);
sleep(1000 * delayBetweenSeconds);
const cacheOptions = {
maxAgeSeconds: 10 * 60 /* 10m */,
backgroundRefresh: false,
};
++z;
if (z % 250 === 1) {
console.log('. ' + z);
}
const { specialTeamIds, specialTeamLevels } = automaticTeams.processOrgSpecialTeams(
repo.organization
);
let permissions: TeamPermission[] = null;
try {
permissions = await repo.getTeamPermissions(cacheOptions);
} catch (getError) {
if (getError.status == /* loose */ 404) {
console.log(`Repo gone: ${repo.organization.name}/${repo.name}`);
} else {
console.log(
`There was a problem getting the permissions for the repo ${repo.name} from ${repo.organization.name}`
);
console.dir(getError);
}
continue;
}
let shouldSkipEnforcement = false;
const { customizedTeamPermissionsWebhookLogic } = providers;
if (customizedTeamPermissionsWebhookLogic) {
shouldSkipEnforcement = await customizedTeamPermissionsWebhookLogic.shouldSkipEnforcement(repo);
}
const currentPermissions = new Map<number, GitHubRepositoryPermission>();
permissions.forEach((entry) => {
currentPermissions.set(Number(entry.team.id), entry.permission);
});
const teamsToSet = new Set<number>();
specialTeamIds.forEach((specialTeamId) => {
if (!currentPermissions.has(specialTeamId)) {
teamsToSet.add(specialTeamId);
} else if (
isAtLeastPermissionLevel(
currentPermissions.get(specialTeamId),
specialTeamLevels.get(specialTeamId)
)
) {
// The team permission is already acceptable
} else {
console.log(
`Permission level for ${specialTeamId} is not good enough, expected ${specialTeamLevels.get(
specialTeamId
)} but currently ${currentPermissions.get(specialTeamId)}`
);
teamsToSet.add(specialTeamId);
}
});
const setArray = Array.from(teamsToSet.values());
for (const teamId of setArray) {
const newPermission = specialTeamLevels.get(teamId);
if (
shouldSkipEnforcement &&
(newPermission as GitHubRepositoryPermission) !== GitHubRepositoryPermission.Pull
) {
console.log(
`should add ${teamId} team with permission ${newPermission} to the repo ${repo.name}, but compliance lock prevents non-read system teams`
);
} else {
try {
await repo.setTeamPermission(teamId, newPermission as GitHubRepositoryPermission);
} catch (error) {
if (ErrorHelper.IsNotFound(error)) {
console.log(
`the team ID ${teamId} could not be found when setting to repo ${repo.name} in org ${organization.name} and should likely be removed from config...`
);
} else {
console.log(`${repo.name}`);
console.dir(error);
throw error;
}
}
}
}
}
console.log(`Finished with repos in ${organization.name} organization`);
} catch (processOrganizationError) {
console.dir(processOrganizationError);
console.log(`moving past ${organization.name} processing due to error...`);
}
}
return {};
}
function isAtLeastPermissionLevel(value, expected) {
if (value !== 'admin' && value !== 'push' && value !== 'pull') {
throw new Error(`The permission type ${value} is not understood by isAtLeastPermissionLevel`);
}
if (value === expected) {
return true;
}
// Admin always wins
if (value === 'admin') {
return true;
} else if (expected === 'admin') {
return false;
}
if (expected === 'write' && value === expected) {
return true;
}
if (expected === 'read') {
return true;
}
return false;
}

Просмотреть файл

@ -6,7 +6,17 @@
import throat from 'throat';
import { shuffle } from 'lodash';
import { permissionsObjectToValue } from '../../transitional';
const killBitHours = 48;
import job from '../job';
job.runBackgroundJob(refreshQueryCache, {
defaultDebugOutput: 'querycache',
timeoutMinutes: 60 * killBitHours,
insightsPrefix: 'JobRefreshQueryCache',
});
import { permissionsObjectToValue } from '../transitional';
import {
Collaborator,
Operations,
@ -16,9 +26,9 @@ import {
Team,
TeamMember,
TeamPermission,
} from '../../business';
import { sleep, addArrayToSet } from '../../utils';
import QueryCache from '../../business/queryCache';
} from '../business';
import { sleep, addArrayToSet } from '../utils';
import QueryCache from '../business/queryCache';
import {
IPagedCacheOptions,
ICacheOptions,
@ -37,7 +47,8 @@ import {
QueryCacheOperation,
IReposJob,
IReposJobResult,
} from '../../interfaces';
IProviders,
} from '../interfaces';
interface IConsistencyStats {
new: number;
@ -628,7 +639,7 @@ async function cacheRepositoryCollaborators(
return operations.filter((real) => real);
}
export default async function refresh({ providers, args }: IReposJob): Promise<IReposJobResult> {
async function refreshQueryCache(providers: IProviders, { args }: IReposJob): Promise<IReposJobResult> {
const { config } = providers;
if (config?.jobs?.refreshWrites !== true) {
console.log('job is currently disabled to avoid metadata refresh/rewrites');

Просмотреть файл

@ -1,15 +0,0 @@
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import Job from './task';
import app from '../../app';
const killBitHours = 48;
app.runJob(Job, {
defaultDebugOutput: 'querycache',
timeoutMinutes: 60 * killBitHours,
insightsPrefix: 'JobRefreshQueryCache',
});

Просмотреть файл

@ -6,28 +6,29 @@
// Job: Backfill aliases (3)
// Job: User attributes hygiene - alias backfills (4)
import app from '../../app';
import job from '../job';
import throat from 'throat';
import { shuffle } from 'lodash';
import { sleep } from '../../utils';
import { IReposJob, IReposJobResult, UnlinkPurpose } from '../../interfaces';
import { sleep } from '../utils';
import { IProviders, IReposJobResult, UnlinkPurpose } from '../interfaces';
import { ErrorHelper } from '../transitional';
const backfillAliasesOnly = process.env.BACKFILL_ALIASES === '1';
app.runJob(refresh, {
defaultDebugOutput: 'cache,restapi',
job.runBackgroundJob(refresh, {
insightsPrefix: 'JobRefreshUsernames',
});
async function refresh({ providers }: IReposJob): Promise<IReposJobResult> {
async function refresh(providers: IProviders): Promise<IReposJobResult> {
const { config, operations, insights, linkProvider, graphProvider } = providers;
if (config?.jobs?.refreshWrites !== true) {
console.log('job is currently disabled to avoid metadata refresh/rewrites');
return;
}
const backfillAliasesOnly = config.process.get('BACKFILL_ALIASES') === '1';
const terminateLinksAndMemberships = config.process.get('REFRESH_USERNAMES_TERMINATE_ACCOUNTS') === '1';
console.log('reading all links');
let allLinks = shuffle(await linkProvider.getAll());
console.log(`READ: ${allLinks.length} links`);
@ -67,6 +68,9 @@ async function refresh({ providers }: IReposJob): Promise<IReposJobResult> {
allLinks.map((link) =>
throttle(async () => {
++i;
if (i % 100 === 0) {
console.log(`${i}/${allLinks.length}; total updates=${updates}, errors=${errors}`);
}
// Refresh GitHub username for the ID
const id = link.thirdPartyId;
@ -96,7 +100,19 @@ async function refresh({ providers }: IReposJob): Promise<IReposJobResult> {
++updatedAvatars;
}
} catch (githubError) {
console.dir(githubError);
if (ErrorHelper.IsNotFound(githubError)) {
console.warn(
`Deleted GitHub account, id=${id}, username_was=${link.thirdPartyUsername}; https://api.github.com/users/${link.thirdPartyUsername}`
);
insights.trackMetric({ name: 'JobRefreshUsernamesMissingGitHubAccounts', value: 1 });
insights.trackEvent({
name: 'JobRefreshUsernamesGitHubAccountNotFound',
properties: { githubid: id, error: githubError.message },
});
} else {
console.dir(githubError);
}
throw githubError;
}
try {
@ -129,7 +145,17 @@ async function refresh({ providers }: IReposJob): Promise<IReposJobResult> {
}
} catch (graphLookupError) {
// Ignore graph lookup issues, other jobs handle terminated employees
console.dir(graphLookupError);
if (ErrorHelper.IsNotFound(graphLookupError)) {
console.warn(`Deleted AAD account, id=${id}, username_was=${link.corporateUsername}`);
insights.trackMetric({ name: 'JobRefreshUsernamesMissingCorporateAccounts', value: 1 });
insights.trackEvent({
name: 'JobRefreshUsernamesCorporateAccountNotFound',
properties: { githubid: id, error: graphLookupError.message },
});
} else {
console.dir(graphLookupError);
}
throw graphLookupError;
}
if (changed) {
@ -138,28 +164,31 @@ async function refresh({ providers }: IReposJob): Promise<IReposJobResult> {
++updates;
}
} catch (getDetailsError) {
if (getDetailsError.status == /* loose compare */ '404') {
if (ErrorHelper.IsNotFound(getDetailsError)) {
++notFoundErrors;
insights.trackEvent({
name: 'JobRefreshUsernamesNotFound',
properties: { githubid: id, error: getDetailsError.message },
properties: { githubid: id, aadId: link.corporateId, error: getDetailsError.message },
});
try {
await operations.terminateLinkAndMemberships(id, { purpose: UnlinkPurpose.Deleted });
insights.trackEvent({
name: 'JobRefreshUsernamesUnlinkDelete',
properties: { githubid: id, error: getDetailsError.message },
});
} catch (unlinkDeletedAccountError) {
console.dir(unlinkDeletedAccountError);
insights.trackException({
exception: unlinkDeletedAccountError,
properties: { githubid: id, event: 'JobRefreshUsernamesDeleteError' },
});
if (terminateLinksAndMemberships) {
try {
await operations.terminateLinkAndMemberships(id, { purpose: UnlinkPurpose.Deleted });
insights.trackEvent({
name: 'JobRefreshUsernamesUnlinkDelete',
properties: { githubid: id, error: getDetailsError.message },
});
} catch (unlinkDeletedAccountError) {
console.dir(unlinkDeletedAccountError);
insights.trackException({
exception: unlinkDeletedAccountError,
properties: { githubid: id, event: 'JobRefreshUsernamesDeleteError' },
});
}
}
} else {
console.dir(getDetailsError);
++errors;
insights.trackMetric({ name: 'JobRefreshUsernamesErrors', value: 1 });
insights.trackException({
exception: getDetailsError,
properties: { name: 'JobRefreshUsernamesError' },
@ -188,8 +217,8 @@ async function refresh({ providers }: IReposJob): Promise<IReposJobResult> {
console.log(`Updates: ${updates}`);
console.log(`GitHub username changes: ${updatedUsernames}`);
console.log(`GitHub avatar changes: ${updatedAvatars}`);
console.log(`AAD name changes: ${updatedAadNames}`);
console.log(`AAD username changes: ${updatedAadUpns}`);
console.log(`Corporate name changes: ${updatedAadNames}`);
console.log(`Corporate username changes: ${updatedAadUpns}`);
console.log(`Updated corporate mails: ${updatedCorporateMails}`);
return {

Просмотреть файл

@ -3,20 +3,19 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
// JOB 13: refresh repository data
// This is very similar to the query cache, but using proper Postgres type entities, and
// not being used by the app today.
// Job 13: refresh repository data
// Implementation is initial and not as robust (will refresh everything, even things not touched
// since the last update; limited telemetry.)
// This is very similar to the query cache, but using proper Postgres type entities, and
// not being used by the app today at runtime. Possible optimizations include only
// targeting refreshes based on last-cached times. The act of refreshing these entities
// also helps keep the standard GitHub repository cache up to date.
import throat from 'throat';
import app from '../app';
import job from '../job';
import { Organization, sortByRepositoryDate } from '../business';
import { IRepositoryProvider, RepositoryEntity } from '../entities/repository';
import { IProviders, IReposJob, IReposJobResult } from '../interfaces';
import { ErrorHelper } from '../transitional';
import { RepositoryEntity, tryGetRepositoryEntity } from '../entities/repository';
import { IProviders, IReposJobResult } from '../interfaces';
import { sleep } from '../utils';
const sleepBetweenReposMs = 125;
@ -24,7 +23,7 @@ const maxParallel = 6;
const shouldUpdateCached = true;
async function refreshRepositories({ providers }: IReposJob): Promise<IReposJobResult> {
async function refreshRepositories(providers: IProviders): Promise<IReposJobResult> {
const { config, operations } = providers;
if (config?.jobs?.refreshWrites !== true) {
console.log('job is currently disabled to avoid metadata refresh/rewrites');
@ -62,7 +61,11 @@ async function processOrganization(
repos = repos.sort(sortByRepositoryDate);
for (let i = 0; i < repos.length; i++) {
const repo = repos[i];
const prefix = `org ${orgIndex}/${orgsLength}: repo ${i}/${repos.length}: `;
const prefix =
'org ' + `${orgIndex + 1}/${orgsLength}:`.padEnd(6) + ` repo ${i + 1}/${repos.length}: `.padEnd(17);
if (i % 100 === 0) {
console.log(`${prefix}(Processing ${organization.name}${i > 0 ? ' continues' : ''})`);
}
try {
let repositoryEntity = await tryGetRepositoryEntity(repositoryProvider, repo.id);
if (await repo.isDeleted()) {
@ -73,28 +76,33 @@ async function processOrganization(
continue;
}
const entity = repo.getEntity();
let update = false;
let updatedFields: string[] = null;
let replace = false;
if (!repositoryEntity) {
repositoryEntity = new RepositoryEntity();
setFields(repositoryProvider, repositoryEntity, entity);
setFields(repositoryEntity, entity, true);
await repositoryProvider.insert(repositoryEntity);
console.log(`${prefix}inserted ${organization.name}/${repositoryEntity.name}`);
continue;
} else {
setFields(repositoryProvider, repositoryEntity, entity);
// not detecting changes now
update = true;
updatedFields = setFields(repositoryEntity, entity, false /* not new */);
replace = !!updatedFields;
}
if (!update && shouldUpdateCached) {
update = true;
if (updatedFields.length === 0 && shouldUpdateCached) {
replace = true;
repositoryEntity.cached = new Date();
}
if (update) {
if (replace) {
await repositoryProvider.replace(repositoryEntity);
console.log(`${prefix}Updated all fields for ${organization.name}/${repo.name}`);
updatedFields.length > 0 &&
console.log(
`${prefix}Updated ${updatedFields.length} field${updatedFields.length === 1 ? '' : 's'} for ${
organization.name
}/${repo.name} [${updatedFields.join(', ')}]`
);
}
} catch (error) {
console.warn(`${prefix}repo error: ${repo.name} in organization ${organization.name}`);
console.warn(`${prefix}repo error: ${repo.name} in organization ${organization.name}: ${error}`);
}
await sleep(sleepBetweenReposMs);
@ -106,70 +114,237 @@ async function processOrganization(
return {};
}
function setFields(repositoryProvider: IRepositoryProvider, repositoryEntity: RepositoryEntity, entity: any) {
repositoryEntity.repositoryId = entity.id;
repositoryEntity.archived = entity.archived;
repositoryEntity.cached = new Date();
function setFields(repositoryEntity: RepositoryEntity, entity: any, isNew: boolean) {
const changed: string[] = [];
if (
(repositoryEntity.repositoryId || entity.id) &&
String(repositoryEntity.repositoryId) !== String(entity.id)
) {
repositoryEntity.repositoryId = parseInt(entity.id, 10);
changed.push('id');
}
if ((entity.archived || repositoryEntity.archived) && repositoryEntity.archived !== entity.archived) {
repositoryEntity.archived = entity.archived;
changed.push('archived');
}
if (entity.created_at) {
repositoryEntity.createdAt = new Date(entity.created_at);
}
repositoryEntity.defaultBranch = entity.default_branch;
repositoryEntity.description = entity.description;
repositoryEntity.disabled = entity.disabled;
repositoryEntity.fork = entity.fork;
repositoryEntity.forksCount = entity.forks_count;
repositoryEntity.hasDownloads = entity.has_downloads;
repositoryEntity.hasIssues = entity.has_issues;
repositoryEntity.hasPages = entity.has_pages;
repositoryEntity.hasProjects = entity.has_projects;
repositoryEntity.hasWiki = entity.has_wiki;
repositoryEntity.homepage = entity.homepage;
repositoryEntity.language = entity.language;
repositoryEntity.license = entity.license?.spdx_id;
repositoryEntity.fullName = entity.full_name;
repositoryEntity.organizationId = entity.organization?.id;
repositoryEntity.organizationLogin = entity.organization?.login;
repositoryEntity.name = entity.name;
repositoryEntity.networkCount = entity.network_count;
repositoryEntity.openIssuesCount = entity.open_issues_count;
repositoryEntity.organizationId = entity.organization?.id;
repositoryEntity.parentId = entity.parent?.id;
repositoryEntity.parentName = entity.parent?.login;
repositoryEntity.parentOrganizationId = entity.parent?.organization?.id;
repositoryEntity.parentOrganizationName = entity.parent?.organization?.login;
repositoryEntity.private = entity.private;
if (entity.pushed_at) {
repositoryEntity.pushedAt = new Date(entity.pushed_at);
}
repositoryEntity.size = entity.size;
repositoryEntity.stargazersCount = entity.stargazers_count;
repositoryEntity.subscribersCount = entity.subscribers_count;
repositoryEntity.topics = entity.topics;
if (entity.updated_at) {
repositoryEntity.updatedAt = new Date(entity.updated_at);
}
repositoryEntity.visibility = entity.visibility;
repositoryEntity.watchersCount = entity.watchers_count;
return repositoryEntity;
}
async function tryGetRepositoryEntity(
repositoryProvider: IRepositoryProvider,
repositoryId: number
): Promise<RepositoryEntity> {
try {
const repositoryEntity = await repositoryProvider.get(repositoryId);
return repositoryEntity;
} catch (error) {
if (ErrorHelper.IsNotFound(error)) {
return null;
const createdAt = new Date(entity.created_at);
const currentCreatedAt = repositoryEntity.createdAt ? new Date(repositoryEntity.createdAt) : null;
if (currentCreatedAt && createdAt && currentCreatedAt.toISOString() !== createdAt.toISOString()) {
repositoryEntity.pushedAt = createdAt;
changed.push('created_at');
} else if (!currentCreatedAt && createdAt) {
repositoryEntity.createdAt = createdAt;
changed.push('created_at');
}
throw error;
}
if (
(entity.default_branch || repositoryEntity.defaultBranch) &&
entity.default_branch !== repositoryEntity.defaultBranch
) {
repositoryEntity.defaultBranch = entity.default_branch;
changed.push('default_branch');
}
if (
(entity.description || repositoryEntity.description) &&
entity.description !== repositoryEntity.description
) {
repositoryEntity.description = entity.description;
changed.push('description');
}
if ((entity.disabled || repositoryEntity.disabled) && entity.disabled !== repositoryEntity.disabled) {
repositoryEntity.disabled = entity.disabled;
changed.push('disabled');
}
if ((entity.fork || repositoryEntity.fork) && entity.fork !== repositoryEntity.fork) {
repositoryEntity.fork = entity.fork;
changed.push('fork');
}
if (
(entity.forks_count || repositoryEntity.forksCount) &&
String(entity.forks_count) !== String(repositoryEntity.forksCount)
) {
repositoryEntity.forksCount = parseInt(entity.forks_count, 10);
changed.push('forks_count');
}
if (
(entity.has_downloads || repositoryEntity.hasDownloads) &&
entity.has_downloads !== repositoryEntity.hasDownloads
) {
repositoryEntity.hasDownloads = entity.has_downloads;
changed.push('has_downloads');
}
if ((entity.has_issues || repositoryEntity.hasIssues) && entity.has_issues !== repositoryEntity.hasIssues) {
repositoryEntity.hasIssues = entity.has_issues;
changed.push('has_issues');
}
if ((entity.has_pages || repositoryEntity.hasPages) && entity.has_pages !== repositoryEntity.hasPages) {
repositoryEntity.hasPages = entity.has_pages;
changed.push('has_pages');
}
if (
(entity.has_projects || repositoryEntity.hasProjects) &&
entity.has_projects !== repositoryEntity.hasProjects
) {
repositoryEntity.hasProjects = entity.has_projects;
changed.push('has_projects');
}
if ((entity.has_wiki || repositoryEntity.hasWiki) && entity.has_wiki !== repositoryEntity.hasWiki) {
repositoryEntity.hasWiki = entity.has_wiki;
changed.push('has_wiki');
}
if ((entity.homepage || repositoryEntity.homepage) && entity.homepage !== repositoryEntity.homepage) {
repositoryEntity.homepage = entity.homepage;
changed.push('homepage');
}
if ((entity.language || repositoryEntity.language) && entity.language !== repositoryEntity.language) {
repositoryEntity.language = entity.language;
changed.push('language');
}
if (entity.license?.spdx_id !== repositoryEntity.license) {
repositoryEntity.license = entity.license?.spdx_id;
changed.push('license.spdx_id');
}
if ((entity.full_name || repositoryEntity.fullName) && entity.full_name !== repositoryEntity.fullName) {
repositoryEntity.fullName = entity.full_name;
changed.push('full_name');
}
if (
(entity.organization?.id || repositoryEntity.organizationId) &&
String(entity.organization?.id) !== String(repositoryEntity.organizationId)
) {
repositoryEntity.organizationId = parseInt(entity.organization?.id, 10);
changed.push('organization.id');
}
if (entity.organization?.login !== repositoryEntity.organizationLogin) {
repositoryEntity.organizationLogin = entity.organization?.login;
changed.push('organization.login');
}
if ((entity.name || repositoryEntity.name) && entity.name !== repositoryEntity.name) {
repositoryEntity.name = entity.name;
changed.push('name');
}
if (
(entity.network_count || repositoryEntity.networkCount) &&
String(entity.network_count) !== String(repositoryEntity.networkCount)
) {
repositoryEntity.networkCount = parseInt(entity.network_count, 10);
changed.push('network_count');
}
if (
(entity.open_issues_count || repositoryEntity.openIssuesCount) &&
String(entity.open_issues_count) !== String(repositoryEntity.openIssuesCount)
) {
repositoryEntity.openIssuesCount = parseInt(entity.open_issues_count, 10);
changed.push('open_issues_count');
}
if (
(entity.parent?.id || repositoryEntity.parentId) &&
String(entity.parent?.id) !== String(repositoryEntity.parentId)
) {
repositoryEntity.parentId = parseInt(entity.parent?.id, 10);
changed.push('parent.id');
}
if (
(entity.parent?.login || repositoryEntity.parentName) &&
entity.parent?.login !== repositoryEntity.parentName
) {
repositoryEntity.parentName = entity.parent?.login;
changed.push('parent.login');
}
if (
(entity?.parent?.organization?.id || repositoryEntity.parentOrganizationId) &&
String(entity?.parent?.organization?.id) !== String(repositoryEntity.parentOrganizationId)
) {
repositoryEntity.parentOrganizationId = parseInt(entity.parent?.organization?.id, 10);
changed.push('parent.organization.id');
}
if (
(entity?.parent?.organization?.login || repositoryEntity.parentOrganizationName) &&
entity?.parent?.organization?.login !== repositoryEntity.parentOrganizationName
) {
repositoryEntity.parentOrganizationName = entity.parent?.organization?.login;
changed.push('parent.organization.login');
}
if ((entity.private || repositoryEntity.private) && entity.private !== repositoryEntity.private) {
repositoryEntity.private = entity.private;
changed.push('private');
}
if (entity.pushed_at) {
const pushedAt = new Date(entity.pushed_at);
const currentPushedAt = repositoryEntity.pushedAt ? new Date(repositoryEntity.pushedAt) : null;
if (currentPushedAt && pushedAt && currentPushedAt.toISOString() !== pushedAt.toISOString()) {
repositoryEntity.pushedAt = pushedAt;
changed.push('pushed_at');
} else if (!currentPushedAt && pushedAt) {
repositoryEntity.pushedAt = pushedAt;
changed.push('pushed_at');
}
}
if ((entity.size || repositoryEntity.size) && String(entity.size) !== String(repositoryEntity.size)) {
repositoryEntity.size = parseInt(entity.size, 10);
changed.push('size');
}
if (
(entity.stargazers_count || repositoryEntity.stargazersCount) &&
String(entity.stargazers_count) !== String(repositoryEntity.stargazersCount)
) {
repositoryEntity.stargazersCount = parseInt(entity.stargazers_count, 10);
changed.push('stargazers_count');
}
if (
(entity.subscribers_count || repositoryEntity.subscribersCount) &&
String(entity.subscribers_count) !== String(repositoryEntity.subscribersCount)
) {
repositoryEntity.subscribersCount = parseInt(entity.subscribers_count, 10);
changed.push('subscribers_count');
}
if (entity.topics && !repositoryEntity.topics) {
repositoryEntity.topics = entity.topics;
changed.push('topics');
} else if (!entity.topics && repositoryEntity.topics) {
repositoryEntity.topics = null;
changed.push('topics');
} else {
const storedTopics = [...(repositoryEntity.topics || [])].sort();
const entityTopics = [...(entity.topics || [])].sort();
if (storedTopics.join(',') !== entityTopics.join(',')) {
repositoryEntity.topics = entity.topics;
changed.push('topics');
}
}
if (entity.updated_at) {
const updatedAt = new Date(entity.updated_at);
const currentUpdatedAt = repositoryEntity.updatedAt ? new Date(repositoryEntity.updatedAt) : null;
if (currentUpdatedAt && updatedAt && currentUpdatedAt.toISOString() !== updatedAt.toISOString()) {
repositoryEntity.pushedAt = updatedAt;
changed.push('updated_at');
} else if (!currentUpdatedAt && updatedAt) {
repositoryEntity.updatedAt = updatedAt;
changed.push('updated_at');
}
}
if (
(entity.visibility || repositoryEntity.visibility) &&
entity.visibility !== repositoryEntity.visibility
) {
repositoryEntity.visibility = entity.visibility;
changed.push('visibility');
}
if (
(entity.watchers_count || repositoryEntity.watchersCount) &&
String(entity.watchers_count) !== String(repositoryEntity.watchersCount)
) {
repositoryEntity.watchersCount = parseInt(entity.watchers_count, 10);
changed.push('watchers_count');
}
if (changed.length > 0 || isNew) {
repositoryEntity.cached = new Date();
}
return changed;
}
app.runJob(refreshRepositories, {
job.run(refreshRepositories, {
timeoutMinutes: 320,
defaultDebugOutput: 'restapi',
insightsPrefix: 'JobRefreshRepositories',
});

Просмотреть файл

@ -10,7 +10,7 @@ import Debug from 'debug';
const debug = Debug.debug('redis');
const debugCrossOrganization = Debug.debug('redis-cross-org');
import { ICacheHelper } from '.';
import type { ICacheHelper } from '.';
import { gunzipBuffer, gzipString } from '../../utils';
export interface ISetCompressedOptions {

Просмотреть файл

@ -3,8 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { IEntityMetadata, EntityMetadataType } from './entityMetadata';
import { IEntityMetadataFixedQuery } from './query';
import { type IEntityMetadata, EntityMetadataType } from './entityMetadata';
import type { IEntityMetadataFixedQuery } from './query';
import { swapMap } from '../../utils';
export enum EntityField {

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { IEntityMetadataProvider } from './entityMetadataProvider';
import type { IEntityMetadataProvider } from './entityMetadataProvider';
import { EntityMetadataType, EntityMetadataBase } from './entityMetadata';
// Newer "entity" implementations have fully decoupled and no longer use this single query type.

Просмотреть файл

@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
@ -15,7 +16,7 @@ import getCompanySpecificDeployment from './companySpecificDeployment';
// CONSIDER: Caching of signing keys
export function requireAadApiAuthorizedScope(scope: string | string[]) {
return (req: IApiRequest, res, next) => {
return (req: IApiRequest, res: Response, next: NextFunction) => {
const { apiKeyToken } = req;
const scopes = typeof scope === 'string' ? [scope] : scope;
if (!apiKeyToken.hasAnyScope(scopes)) {
@ -25,7 +26,7 @@ export function requireAadApiAuthorizedScope(scope: string | string[]) {
};
}
export default function aadApiMiddleware(req: IApiRequest, res, next) {
export default function aadApiMiddleware(req: IApiRequest, res: Response, next: NextFunction) {
return validateAadAuthorization(req)
.then((ok) => {
return next();
@ -34,7 +35,7 @@ export default function aadApiMiddleware(req: IApiRequest, res, next) {
if ((err as any).immediate === true) {
console.warn(`AAD API authorization failed: ${err}`);
}
return isJsonError(err) ? next(err) : jsonError(err, 500);
return isJsonError(err) ? next(err) : (jsonError(err, 500) as unknown);
});
}

Просмотреть файл

@ -11,6 +11,7 @@
import basicAuth from 'basic-auth';
import crypto from 'crypto';
import { NextFunction, Response } from 'express';
import { jsonError } from './jsonError';
import { getProviders } from '../transitional';
@ -30,7 +31,7 @@ export interface IApiRequest extends ReposAppRequest {
userContextOverwriteRequest?: any; // refactor?
}
export default function ReposApiAuthentication(req: IApiRequest, res, next) {
export default function ReposApiAuthentication(req: IApiRequest, res: Response, next: NextFunction) {
const user = basicAuth(req);
const key = user ? user.pass || user.name : null;
if (!key) {

Просмотреть файл

@ -5,6 +5,7 @@
import axios from 'axios';
import asyncHandler from 'express-async-handler';
import { NextFunction, Response } from 'express';
import { jsonError } from './jsonError';
import { IApiRequest } from './apiReposAuth';
@ -14,7 +15,7 @@ import { getProviders } from '../transitional';
// TODO: consider better caching
const localMemoryCacheVstsToAadId = new Map();
const vstsAuth = asyncHandler(async (req: IApiRequest, res, next) => {
const vstsAuth = asyncHandler(async (req: IApiRequest, res: Response, next: NextFunction) => {
const config = getProviders(req).config;
if (!config) {
return next(new Error('Missing configuration for the application'));

Просмотреть файл

@ -3,13 +3,21 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import wrapOrCreateInsightsConsoleClient from '../lib/insights';
import Debug from 'debug';
const debug = Debug.debug('startup');
import { setup as appInsightsSetup, defaultClient } from 'applicationinsights';
import { IReposApplication, IProviders, ReposAppRequest } from '../interfaces';
import type {
IReposApplication,
IProviders,
ReposAppRequest,
SiteConfiguration,
ExecutionEnvironment,
} from '../interfaces';
function ignoreKubernetesProbes(envelope /* , context */) {
if ('RequestData' === envelope.data.baseType) {
@ -42,20 +50,22 @@ function filterTelemetry(envelope, context): boolean {
return true;
}
export default function initializeAppInsights(app: IReposApplication, config) {
export default function initializeAppInsights(
providers: IProviders,
executionEnvironment: ExecutionEnvironment,
app: IReposApplication,
config: SiteConfiguration
) {
let client = undefined;
if (!config) {
// Configuration failure happened ahead of this module
return;
}
const providers = app.settings.providers as IProviders;
let cs: string =
config?.telemetry?.applicationInsightsConnectionString || config?.telemetry?.applicationInsightsKey;
// Override the key with a job-specific one if this is a job execution instead
const jobCs: string =
config?.telemetry?.jobsApplicationInsightsConnectionString ||
config?.telemetry?.jobsApplicationInsightsKey;
if (jobCs && config.isJobInternal === true) {
const jobCs: string = config?.telemetry?.jobsApplicationInsightsConnectionString;
if (jobCs && executionEnvironment.isJob === true) {
cs = jobCs;
}
if (cs) {
@ -78,7 +88,7 @@ export default function initializeAppInsights(app: IReposApplication, config) {
debug('insights telemetry is not configured with a key or connection string');
}
app.use((req: ReposAppRequest, res, next) => {
app?.use((req: ReposAppRequest, res: Response, next: NextFunction) => {
// Acknowledge synthetic tests immediately without spending time in more middleware
if (
req.headers &&

Просмотреть файл

@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Router } from 'express';
import { NextFunction, Response, Router } from 'express';
const router: Router = Router();
import { ReposAppRequest } from '../../interfaces';
@ -20,7 +20,11 @@ function denyRoute(next) {
);
}
export function requirePortalAdministrationPermission(req: ReposAppRequest, res, next) {
export function requirePortalAdministrationPermission(
req: ReposAppRequest,
res: Response,
next: NextFunction
) {
req.individualContext
.isPortalAdministrator()
.then((isAdmin) => {

Просмотреть файл

@ -3,13 +3,15 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import { ReposAppRequest } from '../../interfaces';
import { getProviders } from '../../transitional';
import { wrapError } from '../../utils';
const cachedLinksRequestKeyName = 'cachedLinks';
export async function ensureAllLinksInMemory(req: ReposAppRequest, res, next) {
export async function ensureAllLinksInMemory(req: ReposAppRequest, res: Response, next: NextFunction) {
if (req[cachedLinksRequestKeyName]) {
return next();
}

Просмотреть файл

@ -17,6 +17,7 @@ import {
} from '../../business/user';
import { storeOriginalUrlAsReferrer } from '../../utils';
import getCompanySpecificDeployment from '../companySpecificDeployment';
import { Response, NextFunction } from 'express';
export async function requireAuthenticatedUserOrSignInExcluding(
exclusionPaths: string[],
@ -34,7 +35,7 @@ export async function requireAuthenticatedUserOrSignInExcluding(
return await requireAuthenticatedUserOrSignIn(req, res, next);
}
export async function requireAccessTokenClient(req: ReposAppRequest, res, next) {
export async function requireAccessTokenClient(req: ReposAppRequest, res: Response, next: NextFunction) {
if (req.oauthAccessToken) {
return next();
}
@ -83,7 +84,11 @@ function redirectToSignIn(req, res) {
);
}
export async function requireAuthenticatedUserOrSignIn(req: ReposAppRequest, res, next) {
export async function requireAuthenticatedUserOrSignIn(
req: ReposAppRequest,
res: Response,
next: NextFunction
) {
const companySpecific = getCompanySpecificDeployment();
const providers = getProviders(req);
const { config } = providers;
@ -111,7 +116,7 @@ export async function requireAuthenticatedUserOrSignIn(req: ReposAppRequest, res
return shouldRedirectToSignIn ? redirectToSignIn(req, res) : next();
}
export function setIdentity(req: ReposAppRequest, res, next) {
export function setIdentity(req: ReposAppRequest, res: Response, next: NextFunction) {
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
if (!activeContext) {
return next(new Error('No context available'));

Просмотреть файл

@ -6,6 +6,8 @@
// This route does not use GitHub as a source of truth but instead falls back to
// corporate assigned usernames or security group membership.
import { NextFunction, Response } from 'express';
import { ReposAppRequest } from '../../interfaces';
import { getProviders } from '../../transitional';
import { IndividualContext } from '../../business/user';
@ -29,7 +31,11 @@ function denyRoute(next, isApi: boolean) {
);
}
export async function AuthorizeOnlyCorporateAdministrators(req: ReposAppRequest, res, next) {
export async function AuthorizeOnlyCorporateAdministrators(
req: ReposAppRequest,
res: Response,
next: NextFunction
) {
const { operations } = getProviders(req);
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
const corporateId = activeContext.corporateIdentity?.id;

Просмотреть файл

@ -5,9 +5,15 @@
// This is a Microsoft-specific piece of middleware.
import { NextFunction, Response } from 'express';
import { ReposAppRequest } from '../../interfaces';
export function AuthorizeOnlyFullTimeEmployeesAndInterns(req: ReposAppRequest, res, next) {
export function AuthorizeOnlyFullTimeEmployeesAndInterns(
req: ReposAppRequest,
res: Response,
next: NextFunction
) {
const individualContext = req.individualContext;
if (!individualContext.corporateIdentity || !individualContext.corporateIdentity.username) {
return next(new Error('This resource is only available to authenticated users.'));

Просмотреть файл

@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import { ReposAppRequest } from '../../interfaces/web';
import { jsonError } from '../jsonError';
@ -21,7 +22,11 @@ export function getOrganizationManagementType(req: IReposAppRequestWithOrganizat
return req.organizationManagementType;
}
export function blockIfUnmanagedOrganization(req: IReposAppRequestWithOrganizationManagementType, res, next) {
export function blockIfUnmanagedOrganization(
req: IReposAppRequestWithOrganizationManagementType,
res: Response,
next: NextFunction
) {
const managementType = getOrganizationManagementType(req);
switch (managementType) {
case OrganizationManagementType.Unmanaged:

Просмотреть файл

@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import {
IndividualContext,
IIndividualContextOptions,
@ -11,9 +12,10 @@ import {
SessionUserProperties,
WebApiContext,
} from '../../business/user';
import { ReposAppRequest } from '../../interfaces';
import { getProviders } from '../../transitional';
export function webContextMiddleware(req, res, next) {
export function webContextMiddleware(req: ReposAppRequest, res: Response, next: NextFunction) {
const { operations, insights } = getProviders(req);
if (req.apiContext) {
const msg = 'INVALID: API and web contexts should not be mixed';
@ -46,7 +48,7 @@ export function webContextMiddleware(req, res, next) {
return next();
}
export function apiContextMiddleware(req, res, next) {
export function apiContextMiddleware(req, res: Response, next: NextFunction) {
const { operations, insights } = getProviders(req);
if (req.individualContext) {
const msg = 'INVALID: API and web contexts should not be mixed';

Просмотреть файл

@ -3,6 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import { getProviders } from '../transitional';
interface ICampaignData {
@ -23,7 +25,7 @@ export default function initializeCampaigns(app) {
// come through the app.
app.use('*', campaignMiddleware);
function campaignMiddleware(req, res, next) {
function campaignMiddleware(req, res: Response, next: NextFunction) {
process.nextTick(processCampaignTelemetry.bind(null, req));
// Immediate return to keep middleware going

Просмотреть файл

@ -3,13 +3,15 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import { ReposAppRequest } from '../interfaces';
// Assistant for when using Visual Studio Code to connect to a Codespace
// locally instead of the web. The default port forwarding experience is
// to toast the user to browse to 127.0.0.1:3000, but since AAD does not
// allow for IP-based callback URLs, the user must use localhost.
export function codespacesDevAssistant(req: ReposAppRequest, res, next) {
export function codespacesDevAssistant(req: ReposAppRequest, res: Response, next: NextFunction) {
if (req.hostname === '127.0.0.1') {
console.warn(
`${req.method} ${req.url}: WARNING: You're trying to connect to ${req.hostname} from your codespace.`

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше