Expanded client API
Open sourcing an additional set of APIs that we use for the client experience.
This commit is contained in:
Родитель
735361dfae
Коммит
d1baf801ff
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
|
||||
import { jsonError } from '../../middleware/jsonError';
|
||||
import { IProviders, ReposAppRequest } from '../../transitional';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// TODO: move to modern w/administration experience, optionally
|
||||
|
||||
router.get('/', (req: ReposAppRequest, res, next) => {
|
||||
const { config } = req.app.settings.providers as IProviders;
|
||||
const text = config?.serviceMessage?.banner || null;
|
||||
const link = config.serviceMessage?.link;
|
||||
const details = config.serviceMessage?.details;
|
||||
let banner = text ? { text, link, details } : null;
|
||||
return res.json({banner});
|
||||
});
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this banner route', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { TeamJsonFormat, Team, Organization } from '../../../business';
|
||||
import { TeamJoinApprovalEntity } from '../../../entities/teamJoinApproval/teamJoinApproval';
|
||||
import { jsonError } from '../../../middleware';
|
||||
import { ApprovalPair, Approvals_getTeamMaintainerApprovals, Approvals_getUserRequests, closeOldRequest } from '../../../routes/settings/approvals';
|
||||
import { ReposAppRequest, IProviders } from '../../../transitional';
|
||||
import { IndividualContext } from '../../../user';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const approvalPairToJson = (pair: ApprovalPair) => {
|
||||
return {
|
||||
request: pair.request,
|
||||
team: pair.team.asJson(TeamJsonFormat.Augmented),
|
||||
};
|
||||
};
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { approvalProvider, operations } = req.app.settings.providers as IProviders;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json({
|
||||
teamResponsibilities: [],
|
||||
usersRequests: [],
|
||||
isLinked: false,
|
||||
});
|
||||
}
|
||||
try {
|
||||
// const username = activeContext.getGitHubIdentity().username;
|
||||
const id = activeContext.getGitHubIdentity().id;
|
||||
const aggregateTeams = await activeContext.aggregations.teams();
|
||||
const teamResponsibilities = await Approvals_getTeamMaintainerApprovals(operations, aggregateTeams, approvalProvider);
|
||||
const usersRequests = await Approvals_getUserRequests(operations, id.toString(), approvalProvider);
|
||||
const state = {
|
||||
teamResponsibilities: teamResponsibilities.map(approvalPairToJson),
|
||||
usersRequests: usersRequests.map(approvalPairToJson),
|
||||
};
|
||||
return res.json(state);
|
||||
} catch (error) {
|
||||
return next(jsonError(error));
|
||||
}
|
||||
}));
|
||||
|
||||
// -- individual request
|
||||
|
||||
router.get('/:approvalId', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const approvalId = req.params.approvalId;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json({});
|
||||
}
|
||||
const { approvalProvider, operations } = req.app.settings.providers as IProviders;
|
||||
const corporateId = activeContext.corporateIdentity.id;
|
||||
let request: TeamJoinApprovalEntity = null;
|
||||
try {
|
||||
let isMaintainer = false;
|
||||
let team: Team = null;
|
||||
const username = activeContext.getGitHubIdentity().username;
|
||||
const id = activeContext.getGitHubIdentity().id;
|
||||
let organization: Organization = null;
|
||||
request = await approvalProvider.getApprovalEntity(approvalId);
|
||||
organization = operations.getOrganization(request.organizationName);
|
||||
team = organization.team(Number(request.teamId));
|
||||
await team.getDetails();
|
||||
if (corporateId === request.corporateId) {
|
||||
return res.json(approvalPairToJson({ request, team }));
|
||||
}
|
||||
const isOrgSudoer = await organization.isSudoer(username, activeContext.link);
|
||||
isMaintainer = isOrgSudoer;
|
||||
const maintainers = await team.getOfficialMaintainers();
|
||||
if (!isMaintainer) {
|
||||
for (let i = 0; i < maintainers.length; i++) {
|
||||
if (String(maintainers[i].id) == String(id)) {
|
||||
isMaintainer = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isMaintainer) {
|
||||
return res.json(approvalPairToJson({ request, team }));
|
||||
}
|
||||
throw jsonError('This request does not exist or was created by another user', 400);
|
||||
} catch (error) {
|
||||
// Edge case: the team no longer exists.
|
||||
if (error.innerError && error.innerError.innerError && error.innerError.innerError.statusCode == 404) {
|
||||
return closeOldRequest(true, request, req, res, next);
|
||||
}
|
||||
return next(jsonError(error));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('*', (req: ReposAppRequest, res, next) => {
|
||||
return next(jsonError('Contextual API or route not found within approvals', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { Organization } from '../../../business';
|
||||
|
||||
import { jsonError } from '../../../middleware';
|
||||
import getCompanySpecificDeployment from '../../../middleware/companySpecificDeployment';
|
||||
import { ErrorHelper, IProviders, ReposAppRequest } from '../../../transitional';
|
||||
import { IndividualContext } from '../../../user';
|
||||
|
||||
import RouteApprovals from './approvals';
|
||||
import RouteIndividualContextualOrganization from './organization';
|
||||
import RouteOrgs from './orgs';
|
||||
import RouteRepos from './repos';
|
||||
import RouteTeams from './teams';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const deployment = getCompanySpecificDeployment();
|
||||
deployment?.routes?.api?.context?.index && deployment?.routes?.api?.context?.index(router);
|
||||
|
||||
router.use('/approvals', RouteApprovals);
|
||||
|
||||
router.get('/', (req: ReposAppRequest, res) => {
|
||||
const { config } = req.app.settings.providers as IProviders;
|
||||
const { continuousDeployment } = config;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
const isGitHubAuthenticated = !!activeContext.getSessionBasedGitHubIdentity()?.id;
|
||||
const data = {
|
||||
corporateIdentity: activeContext.corporateIdentity,
|
||||
githubIdentity: activeContext.getGitHubIdentity(),
|
||||
isAuthenticated: true,
|
||||
isGitHubAuthenticated,
|
||||
isLinked: !!activeContext.link,
|
||||
build: continuousDeployment,
|
||||
};
|
||||
return res.json(data);
|
||||
});
|
||||
|
||||
router.get('/accountDetails', asyncHandler(async (req: ReposAppRequest, res) => {
|
||||
const { operations} = req.app.settings.providers as IProviders;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
const gh = activeContext.getGitHubIdentity();
|
||||
if (!gh || !gh.id) {
|
||||
res.status(400);
|
||||
res.end();
|
||||
}
|
||||
const accountFromId = operations.getAccount(gh.id);
|
||||
const accountDetails = await accountFromId.getDetails();
|
||||
res.json(accountDetails);
|
||||
}));
|
||||
|
||||
router.get('/orgs', RouteOrgs);
|
||||
|
||||
router.get('/repos', RouteRepos);
|
||||
|
||||
router.get('/teams', RouteTeams);
|
||||
|
||||
router.use('/orgs/:orgName', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { orgName } = req.params;
|
||||
const { operations } = req.app.settings.providers as IProviders;
|
||||
// const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
// if (!activeContext.link) {
|
||||
// return next(jsonError('Account is not linked', 400));
|
||||
// }
|
||||
let organization: Organization = null;
|
||||
try {
|
||||
organization = operations.getOrganization(orgName);
|
||||
// CONSIDER: what if they are not currently a member of the org?
|
||||
req.organization = organization;
|
||||
return next();
|
||||
} catch (noOrgError) {
|
||||
if (ErrorHelper.IsNotFound(noOrgError)) {
|
||||
res.status(404);
|
||||
return res.end();
|
||||
}
|
||||
return next(jsonError(noOrgError, 500));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('/orgs/:orgName', RouteIndividualContextualOrganization);
|
||||
|
||||
router.use('*', (req: ReposAppRequest, res, next) => {
|
||||
return next(jsonError('Contextual API or route not found', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,146 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { GitHubTeamRole, Organization, OrganizationMembershipRole, Team, TeamJsonFormat } from '../../../../business';
|
||||
import { jsonError } from '../../../../middleware';
|
||||
import getCompanySpecificDeployment from '../../../../middleware/companySpecificDeployment';
|
||||
import { ReposAppRequest } from '../../../../transitional';
|
||||
import { IndividualContext } from '../../../../user';
|
||||
|
||||
import RouteRepos from './repos';
|
||||
import RouteTeams from './teams';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json(false);
|
||||
}
|
||||
const membership = await organization.getOperationalMembership(activeContext.getGitHubIdentity().username);
|
||||
if (!membership) {
|
||||
return res.json(false);
|
||||
}
|
||||
return res.json({
|
||||
user: toSanitizedUser(membership.user),
|
||||
organization: toSanitizedOrg(membership.organization),
|
||||
role: membership.role,
|
||||
state: membership.state,
|
||||
});
|
||||
}));
|
||||
|
||||
router.get('/sudo', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json({ isSudoer: false });
|
||||
}
|
||||
return res.json({
|
||||
isSudoer: await organization.isSudoer(activeContext.getGitHubIdentity().username, activeContext.link),
|
||||
});
|
||||
}));
|
||||
|
||||
router.get('/isOwner', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json({ isOrganizationOwner: false });
|
||||
}
|
||||
try {
|
||||
const username = activeContext.getGitHubIdentity().username;
|
||||
const membership = await organization.getOperationalMembership(username);
|
||||
const isOrganizationOwner = membership?.role === OrganizationMembershipRole.Admin;
|
||||
return res.json({
|
||||
isOrganizationOwner,
|
||||
});
|
||||
} catch (error) {
|
||||
return jsonError(error, 400);
|
||||
}
|
||||
}));
|
||||
|
||||
router.delete('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
// "Leave" / remove my context
|
||||
const { organization } = req;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return next(jsonError('You are not linked', 400));
|
||||
}
|
||||
const login = activeContext.getGitHubIdentity().username;
|
||||
const id = activeContext.getGitHubIdentity().id;
|
||||
try {
|
||||
await organization.removeMember(login, id);
|
||||
return res.json({
|
||||
message: `Your ${login} account has been removed from ${organization.name}.`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
return next(jsonError(error, 400));
|
||||
}
|
||||
}));
|
||||
|
||||
router.get('/personalizedTeams', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
try {
|
||||
const organization = req.organization as Organization;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json({personalizedTeams: []});
|
||||
}
|
||||
const userAggregateContext = activeContext.aggregations;
|
||||
const maintainedTeams = new Set<string>();
|
||||
const broadTeams = new Set<number>(req.organization.broadAccessTeams);
|
||||
const userTeams = userAggregateContext.reduceOrganizationTeams(organization, await userAggregateContext.teams());
|
||||
userTeams.maintainer.map(maintainedTeam => maintainedTeams.add(maintainedTeam.id.toString()));
|
||||
const combinedTeams = new Map<string, Team>();
|
||||
userTeams.maintainer.map(team => combinedTeams.set(team.id.toString(), team));
|
||||
userTeams.member.map(team => combinedTeams.set(team.id.toString(), team));
|
||||
const personalizedTeams = Array.from(combinedTeams.values()).map(combinedTeam => {
|
||||
const entry = combinedTeam.asJson(TeamJsonFormat.Augmented);
|
||||
entry.role = maintainedTeams.has(combinedTeam.id.toString()) ? GitHubTeamRole.Maintainer : GitHubTeamRole.Member;
|
||||
return entry;
|
||||
});
|
||||
return res.json({
|
||||
personalizedTeams,
|
||||
});
|
||||
} catch (error) {
|
||||
return next(jsonError(error, 400));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('/repos', RouteRepos);
|
||||
router.use('/teams', RouteTeams);
|
||||
|
||||
const deployment = getCompanySpecificDeployment();
|
||||
deployment?.routes?.api?.context?.organization?.index && deployment?.routes?.api?.context?.organization?.index(router);
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available: client>organization', 404));
|
||||
});
|
||||
|
||||
const toSanitizedUser = user => {
|
||||
if (!user || !user.login) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: user.id,
|
||||
login: user.login,
|
||||
avatar_url: user.avatar_url,
|
||||
}
|
||||
};
|
||||
const toSanitizedOrg = org => {
|
||||
if (!org || !org.login) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: org.id,
|
||||
login: org.login,
|
||||
avatar_url: org.avatar_url,
|
||||
description: org.description,
|
||||
}
|
||||
};
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { AddRepositoryPermissionsToRequest, getContextualRepositoryPermissions } from '../../../../middleware/github/repoPermissions';
|
||||
import { jsonError } from '../../../../middleware';
|
||||
import getCompanySpecificDeployment from '../../../../middleware/companySpecificDeployment';
|
||||
import { ReposAppRequest } from '../../../../transitional';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/permissions', AddRepositoryPermissionsToRequest, asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const permissions = getContextualRepositoryPermissions(req);
|
||||
return res.json(permissions);
|
||||
}));
|
||||
|
||||
const deployment = getCompanySpecificDeployment();
|
||||
deployment?.routes?.api?.context?.organization?.repo && deployment?.routes?.api?.context?.organization?.repo(router);
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError(`no API or ${req.method} function available for repo`, 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { OrganizationMembershipState, Repository } from '../../../../business';
|
||||
import { jsonError } from '../../../../middleware';
|
||||
import { setContextualRepository } from '../../../../middleware/github/repoPermissions';
|
||||
|
||||
import { ReposAppRequest } from '../../../../transitional';
|
||||
import { IndividualContext } from '../../../../user';
|
||||
import { createRepositoryFromClient } from '../../newOrgRepo';
|
||||
|
||||
import RouteContextualRepo from './repo';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function validateActiveMembership(req: ReposAppRequest, res, next) {
|
||||
const { organization } = req;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return next(jsonError('You must be linked and a member of the organization to create and manage repos', 400));
|
||||
}
|
||||
const membership = await organization.getOperationalMembership(activeContext.getGitHubIdentity().username);
|
||||
if (!membership || membership.state !== OrganizationMembershipState.Active) {
|
||||
return next(jsonError('You must be a member of the organization to create and manage repos', 400));
|
||||
}
|
||||
req['knownRequesterMailAddress'] = activeContext.link.corporateMailAddress;
|
||||
return next();
|
||||
}
|
||||
|
||||
router.post('/', asyncHandler(validateActiveMembership), asyncHandler(createRepositoryFromClient));
|
||||
|
||||
router.use('/:repoName', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const { repoName } = req.params;
|
||||
let repository: Repository = null;
|
||||
repository = organization.repository(repoName);
|
||||
setContextualRepository(req, repository);
|
||||
return next();
|
||||
}));
|
||||
|
||||
router.use('/:repoName', RouteContextualRepo);
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available for repos', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,203 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { ITeamMembershipRoleState, OrganizationMembershipState } from '../../../../business';
|
||||
import { TeamJoinApprovalEntity } from '../../../../entities/teamJoinApproval/teamJoinApproval';
|
||||
import { jsonError } from '../../../../middleware';
|
||||
import { AddTeamMembershipToRequest, AddTeamPermissionsToRequest, getContextualTeam, getTeamMembershipFromRequest, getTeamPermissionsFromRequest } from '../../../../middleware/github/teamPermissions';
|
||||
import { submitTeamJoinRequest } from '../../../../routes/org/team';
|
||||
import { postActionDecision, TeamApprovalDecision } from '../../../../routes/org/team/approval';
|
||||
import { PermissionWorkflowEngine } from '../../../../routes/org/team/approvals';
|
||||
import { IProviders, ReposAppRequest } from '../../../../transitional';
|
||||
import { IndividualContext } from '../../../../user';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
interface ITeamRequestJsonResponse {
|
||||
request?: TeamJoinApprovalEntity;
|
||||
}
|
||||
|
||||
interface ITeamApprovalsJsonResponse {
|
||||
allowAdministration: boolean;
|
||||
approvals?: TeamJoinApprovalEntity[];
|
||||
}
|
||||
|
||||
router.get('/permissions',
|
||||
asyncHandler(AddTeamPermissionsToRequest),
|
||||
asyncHandler(AddTeamMembershipToRequest),
|
||||
asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const membership = getTeamMembershipFromRequest(req);
|
||||
const permissions = getTeamPermissionsFromRequest(req);
|
||||
return res.json({ permissions, membership });
|
||||
})
|
||||
);
|
||||
|
||||
router.get('/join/request', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { approvalProvider } = req.app.settings.providers as IProviders;
|
||||
const team = getContextualTeam(req);
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
let request: TeamJoinApprovalEntity = null;
|
||||
if (activeContext.link) {
|
||||
// no point query currently implemented
|
||||
let approvals = await approvalProvider.queryPendingApprovalsForTeam(String(team.id));
|
||||
approvals = approvals.filter(approval => approval.corporateId === activeContext.corporateIdentity.id);
|
||||
request = approvals.length > 0 ? approvals[0] : null;
|
||||
}
|
||||
const response: ITeamRequestJsonResponse = { request };
|
||||
return res.json(response);
|
||||
}));
|
||||
|
||||
router.post('/join',
|
||||
asyncHandler(AddTeamMembershipToRequest),
|
||||
asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
try {
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const { approvalProvider } = providers;
|
||||
const membership = getTeamMembershipFromRequest(req);
|
||||
if (!membership.isLinked) {
|
||||
return res.json({ error: 'You have not linked your GitHub account to your corporate identity yet' });
|
||||
}
|
||||
if (membership.membershipState === OrganizationMembershipState.Active) {
|
||||
return res.json({ error: 'You already have an active team membership' });
|
||||
}
|
||||
const team = getContextualTeam(req);
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
// no point query currently implemented
|
||||
let approvals = await approvalProvider.queryPendingApprovalsForTeam(String(team.id));
|
||||
approvals = approvals.filter(approval => approval.corporateId === activeContext.corporateIdentity.id);
|
||||
const request = approvals.length > 0 ? approvals[0] : null;
|
||||
if (request) {
|
||||
return res.json({ error: 'You already have a pending team join request' });
|
||||
}
|
||||
//
|
||||
const justification = (req.body.justification || '') as string;
|
||||
const hostname = req.hostname;
|
||||
const correlationId = req.correlationId;
|
||||
const outcome = await submitTeamJoinRequest(providers, activeContext, team, justification, correlationId, hostname);
|
||||
return res.json(outcome);
|
||||
} catch (error) {
|
||||
return next(jsonError(error));
|
||||
}
|
||||
}));
|
||||
|
||||
router.post('/join/approvals/:approvalId',
|
||||
asyncHandler(AddTeamPermissionsToRequest),
|
||||
asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { approvalId: id } = req.params;
|
||||
if (!id) {
|
||||
return next(jsonError('invalid approval', 400));
|
||||
}
|
||||
const permissions = getTeamPermissionsFromRequest(req);
|
||||
if (!permissions.allowAdministration) {
|
||||
return next(jsonError('you do not have permission to administer this team', 401));
|
||||
}
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const { approvalProvider, operations } = providers;
|
||||
const team = getContextualTeam(req);
|
||||
const request = await approvalProvider.getApprovalEntity(id);
|
||||
if (String(request.teamId) !== String(team.id)) {
|
||||
return next(jsonError('mismatch on team', 400));
|
||||
}
|
||||
const requestingUser = await operations.getAccountWithDetailsAndLink(request.thirdPartyId);
|
||||
const approvalPackage = { request, requestingUser, id };
|
||||
const engine = new PermissionWorkflowEngine(team, approvalPackage);
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
const text = req.body.text as string;
|
||||
const dv = req.body.decision as string;
|
||||
let decision: TeamApprovalDecision = null;
|
||||
switch (dv) {
|
||||
case 'approve':
|
||||
decision = TeamApprovalDecision.Approve;
|
||||
break;
|
||||
case 'deny':
|
||||
decision = TeamApprovalDecision.Deny;
|
||||
break;
|
||||
case 'reopen':
|
||||
decision = TeamApprovalDecision.Reopen;
|
||||
break;
|
||||
default:
|
||||
return next(jsonError('invalid or no decision type', 400));
|
||||
}
|
||||
const teamBaseUrl = `/orgs/${team.organization.name}/teams/${team.slug}/`; // trailing?
|
||||
try {
|
||||
const outcome = await postActionDecision(providers, activeContext, engine, teamBaseUrl, decision, text);
|
||||
if (outcome.error) {
|
||||
throw outcome.error;
|
||||
}
|
||||
return res.json(outcome);
|
||||
} catch (outcomeError) {
|
||||
return next(jsonError(outcomeError, 500));
|
||||
}
|
||||
}));
|
||||
|
||||
router.get('/join/approvals/:approvalId',
|
||||
asyncHandler(AddTeamPermissionsToRequest),
|
||||
asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { approvalId: id } = req.params;
|
||||
if (!id) {
|
||||
return next(jsonError('invalid approval', 400));
|
||||
}
|
||||
const permissions = getTeamPermissionsFromRequest(req);
|
||||
if (!permissions.allowAdministration) {
|
||||
return next(jsonError('you do not have permission to administer this team', 401));
|
||||
}
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const { approvalProvider, operations } = providers;
|
||||
const team = getContextualTeam(req);
|
||||
const request = await approvalProvider.getApprovalEntity(id);
|
||||
if (String(request.teamId) !== String(team.id)) {
|
||||
return next(jsonError('mismatch on team', 400));
|
||||
}
|
||||
return res.json({ approval: request });
|
||||
}));
|
||||
|
||||
router.get('/join/approvals',
|
||||
asyncHandler(AddTeamPermissionsToRequest),
|
||||
asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { approvalProvider } = req.app.settings.providers as IProviders;
|
||||
const team = getContextualTeam(req);
|
||||
const permissions = getTeamPermissionsFromRequest(req);
|
||||
let response: ITeamApprovalsJsonResponse = {
|
||||
allowAdministration: false,
|
||||
};
|
||||
if (permissions.allowAdministration) {
|
||||
response.allowAdministration = permissions.allowAdministration;
|
||||
response.approvals = await approvalProvider.queryPendingApprovalsForTeam(String(team.id));
|
||||
}
|
||||
return res.json(response);
|
||||
}));
|
||||
|
||||
router.post('/role/:login',
|
||||
asyncHandler(AddTeamPermissionsToRequest),
|
||||
asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { role } = req.body;
|
||||
const { login } = req.params;
|
||||
if (!login) {
|
||||
return next(jsonError('invalid login', 400));
|
||||
}
|
||||
const permissions = getTeamPermissionsFromRequest(req);
|
||||
if (!permissions.allowAdministration) {
|
||||
return next(jsonError('you do not have permission to administer this team', 401));
|
||||
}
|
||||
const team = getContextualTeam(req);
|
||||
try {
|
||||
const currentRole = await team.getMembership(login, { backgroundRefresh : false, maxAgeSeconds: -1 });
|
||||
if (!currentRole || (currentRole as ITeamMembershipRoleState).state !== OrganizationMembershipState.Active) {
|
||||
return next(jsonError(`${login} is not currently a member of the team`, 400));
|
||||
}
|
||||
const response = await team.addMembership(login, { role });
|
||||
return res.json(response);
|
||||
} catch (outcomeError) {
|
||||
return next(jsonError(outcomeError, 500));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available for contextual team', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { Team } from '../../../../business';
|
||||
import { jsonError } from '../../../../middleware';
|
||||
import { setContextualTeam } from '../../../../middleware/github/teamPermissions';
|
||||
import { ReposAppRequest } from '../../../../transitional';
|
||||
|
||||
import RouteTeam from './team';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// CONSIDER: list their teams router.get('/ ')
|
||||
|
||||
router.use('/:teamSlug', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const { teamSlug } = req.params;
|
||||
let team: Team = null;
|
||||
try {
|
||||
team = await organization.getTeamFromSlug(teamSlug);
|
||||
setContextualTeam(req, team);
|
||||
} catch (error) {
|
||||
console.dir(error);
|
||||
return next(error);
|
||||
}
|
||||
return next();
|
||||
}));
|
||||
|
||||
router.use('/:teamSlug', RouteTeam);
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available for repos', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { ReposAppRequest } from '../../../transitional';
|
||||
import { IndividualContext } from '../../../user';
|
||||
|
||||
export default asyncHandler(async (req: ReposAppRequest, res) => {
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json({
|
||||
member: [],
|
||||
admin: [],
|
||||
isLinked: false,
|
||||
});
|
||||
}
|
||||
const orgs = await activeContext.aggregations.getQueryCacheOrganizations();
|
||||
const data = {
|
||||
isLinked: true,
|
||||
member: orgs.member.map(org => {
|
||||
return {
|
||||
name: org.name,
|
||||
id: org.id,
|
||||
};
|
||||
}),
|
||||
admin: orgs.admin.map(org => {
|
||||
return {
|
||||
name: org.name,
|
||||
id: org.id,
|
||||
}
|
||||
}),
|
||||
};
|
||||
return res.json(data);
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { ReposAppRequest } from '../../../transitional';
|
||||
import { IndividualContext } from '../../../user';
|
||||
import { GitHubRepositoryPermission } from '../../../entities/repositoryMetadata/repositoryMetadata';
|
||||
|
||||
export default asyncHandler(async (req: ReposAppRequest, res) => {
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json({
|
||||
isLinked: false,
|
||||
repositories: [],
|
||||
});
|
||||
}
|
||||
let permissions = await activeContext.aggregations.getQueryCacheRepositoryPermissions();
|
||||
permissions = permissions.filter(perm => {
|
||||
if (perm.bestComputedPermission !== GitHubRepositoryPermission.Pull) {
|
||||
return true;
|
||||
}
|
||||
let fromBroadAccess = false;
|
||||
perm.teamPermissions.map(tp => {
|
||||
if (tp.team.isBroadAccessTeam) {
|
||||
fromBroadAccess = true;
|
||||
}
|
||||
});
|
||||
if (fromBroadAccess) {
|
||||
return false;
|
||||
}
|
||||
if (perm.repository.private) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return res.json({
|
||||
isLinked: true,
|
||||
repositories: permissions.map(perm => {
|
||||
return {
|
||||
bestComputedPermission: perm.bestComputedPermission,
|
||||
collaboratorPermission: perm.collaboratorPermission,
|
||||
repository: perm.repository.asJson(),
|
||||
teamPermissions: perm.teamPermissions.map(tp => tp.asJson()),
|
||||
// TODO: would be nice for team permission for repos to also store the team slug in the query cache!
|
||||
};
|
||||
}),
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { ReposAppRequest } from '../../../transitional';
|
||||
import { IndividualContext } from '../../../user';
|
||||
import { TeamJsonFormat } from '../../../business';
|
||||
|
||||
export default asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
if (!activeContext.link) {
|
||||
return res.json({
|
||||
isLinked: false,
|
||||
member: [],
|
||||
maintainer: [],
|
||||
})
|
||||
}
|
||||
const permissions = await activeContext.aggregations.getQueryCacheTeams();
|
||||
return res.json({
|
||||
isLinked: true,
|
||||
member: permissions.member.map(t => t.asJson(TeamJsonFormat.Augmented)),
|
||||
maintainer: permissions.maintainer.map(t => t.asJson(TeamJsonFormat.Augmented)),
|
||||
});
|
||||
});
|
|
@ -7,16 +7,29 @@ import express from 'express';
|
|||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { apiContextMiddleware, AddLinkToRequest, requireAccessTokenClient, setIdentity, jsonError } from '../../middleware';
|
||||
import { ReposAppRequest } from '../../transitional';
|
||||
import { IProviders, ReposAppRequest } from '../../transitional';
|
||||
|
||||
import getCompanySpecificDeployment from '../../middleware/companySpecificDeployment';
|
||||
|
||||
import RouteClientNewRepo from './newRepo';
|
||||
|
||||
import RouteContext from './context';
|
||||
import RouteOrganizations from './organizations';
|
||||
import RouteLinking from './linking';
|
||||
import RouteSession from './session';
|
||||
import RouteBanner from './banner';
|
||||
import RouteCrossOrganizationPeople from './people';
|
||||
import RouteCrossOrganizationRepos from './repos';
|
||||
import RouteCrossOrganizationTeams from './teams';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use((req: ReposAppRequest, res, next) => {
|
||||
return req.isAuthenticated() ? next() : next(jsonError('Session is not authenticated', 401));
|
||||
const { config } = req.app.settings.providers as IProviders;
|
||||
if (config?.features?.allowApiClient) {
|
||||
return req.isAuthenticated() ? next() : next(jsonError('Session is not authenticated', 401));
|
||||
}
|
||||
return next(jsonError('Client API features unavailable', 403));
|
||||
});
|
||||
|
||||
router.use(asyncHandler(requireAccessTokenClient));
|
||||
|
@ -26,8 +39,18 @@ router.use(asyncHandler(AddLinkToRequest));
|
|||
|
||||
router.use('/newRepo', RouteClientNewRepo);
|
||||
|
||||
router.use('/context', RouteContext);
|
||||
|
||||
router.use('/banner', RouteBanner);
|
||||
router.use('/orgs', RouteOrganizations);
|
||||
router.use('/link', RouteLinking);
|
||||
router.use('/signout', RouteSession);
|
||||
router.use('/people', RouteCrossOrganizationPeople);
|
||||
router.use('/repos', RouteCrossOrganizationRepos);
|
||||
router.use('/teams', RouteCrossOrganizationTeams);
|
||||
|
||||
const dynamicStartupInstance = getCompanySpecificDeployment();
|
||||
dynamicStartupInstance?.routes?.connectCorporateApiRoutes && dynamicStartupInstance.routes.connectCorporateApiRoutes(router);
|
||||
dynamicStartupInstance?.routes?.api?.index && dynamicStartupInstance?.routes?.api?.index(router);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
return next(jsonError('The resource or endpoint you are looking for is not there', 404));
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { jsonError } from '../../middleware';
|
||||
|
||||
const maxPageSize = 50;
|
||||
const defaultPageSize = 30;
|
||||
|
||||
type Response = {
|
||||
json: (obj: any) => void;
|
||||
}
|
||||
|
||||
type Request = {
|
||||
query: any;
|
||||
}
|
||||
|
||||
export default class JsonPager<T> {
|
||||
pageSize: number;
|
||||
page: number;
|
||||
|
||||
// computed:
|
||||
total: number;
|
||||
lastPage: number;
|
||||
begin: number;
|
||||
end: number;
|
||||
subsetReturnSize: number;
|
||||
|
||||
res: Response;
|
||||
|
||||
constructor(req: Request, res: Response) {
|
||||
this.res = res;
|
||||
const { query } = req;
|
||||
const requestedPageSize = query.pageSize ? Number(query.pageSize) : defaultPageSize;
|
||||
const requestedPage = query.page ? Number(query.page) : 0;
|
||||
this.pageSize = Math.min(requestedPageSize, maxPageSize);
|
||||
const page = requestedPage || 1;
|
||||
if (page < 0 || isNaN(page)) {
|
||||
throw jsonError('Invalid page', 400);
|
||||
}
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
slice(array: T[]) {
|
||||
this.total = array.length;
|
||||
this.lastPage = Math.ceil(this.total / this.pageSize);
|
||||
// TODO: this can go past the end, i.e. search while on page 7, it will not return page 1 results
|
||||
this.begin = ((this.page - 1) * this.pageSize);
|
||||
this.end = this.begin + this.pageSize;
|
||||
const subset = array.slice(this.begin, this.end);
|
||||
this.subsetReturnSize = subset.length;
|
||||
return subset;
|
||||
}
|
||||
|
||||
sendJson(mappedValues: any[]) {
|
||||
if (mappedValues && mappedValues.length !== this.subsetReturnSize) {
|
||||
console.warn(`The mapped values length ${mappedValues.length} !== ${this.subsetReturnSize} that was computed`);
|
||||
}
|
||||
return this.res.json({
|
||||
values: mappedValues,
|
||||
total: this.total,
|
||||
lastPage: this.lastPage,
|
||||
nextPage: this.page + 1, // TODO: should not return if it's the end of the road
|
||||
page: this.page,
|
||||
pageSize: this.pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
sliceAndSend(array: T[]) {
|
||||
const subset = this.slice(array);
|
||||
return this.sendJson(subset);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { ICorporateLink, Operations } from '../../business';
|
||||
|
||||
const defaultMinutes = 5;
|
||||
|
||||
export default class LeakyLocalCache<K, T> {
|
||||
// TODO: use one of many NPMs to do this better and cleanup behind the scenes
|
||||
private _map: Map<K, T>;
|
||||
private _expires: Map<K, Date>;
|
||||
private _expireMs: number;
|
||||
|
||||
constructor(localExpirationMinutes?: number) {
|
||||
this._map = new Map();
|
||||
this._expires = new Map();
|
||||
const minutes = localExpirationMinutes || defaultMinutes;
|
||||
this._expireMs = 1000 * 60 * minutes;
|
||||
}
|
||||
|
||||
get(key: K): T {
|
||||
const expires = this._expires.get(key);
|
||||
const now = new Date();
|
||||
if (!expires) {
|
||||
this._map.delete(key);
|
||||
return;
|
||||
}
|
||||
const value = this._map.get(key);
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (now < expires) {
|
||||
return value;
|
||||
}
|
||||
this._map.delete(key);
|
||||
this._expires.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
set(key: K, value: T) {
|
||||
if (value !== undefined) {
|
||||
const now = new Date();
|
||||
const expires = new Date(now.getTime() + this._expireMs);
|
||||
this._map.set(key, value);
|
||||
this._expires.set(key, expires);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const leakyLocalCacheLinks = new LeakyLocalCache<boolean, ICorporateLink[]>();
|
||||
|
||||
export async function getLinksLightCache(operations: Operations) {
|
||||
const cached = leakyLocalCacheLinks.get(true);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const links = await operations.getLinks();
|
||||
leakyLocalCacheLinks.set(true, links);
|
||||
return links;
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { IndividualContext } from '../../user';
|
||||
import { jsonError } from '../../middleware';
|
||||
import { IProviders, ReposAppRequest } from '../../transitional';
|
||||
import { unlinkInteractive } from '../../routes/unlink';
|
||||
import { interactiveLinkUser } from '../../routes/link';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function validateLinkOk(req: ReposAppRequest, res, next) {
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const insights = providers.insights;
|
||||
const config = providers.config;
|
||||
let validateAndBlockGuests = false;
|
||||
if (config && config.activeDirectory && config.activeDirectory.blockGuestUserTypes) {
|
||||
validateAndBlockGuests = true;
|
||||
}
|
||||
// If the app has not been configured to check whether a user is a guest before linking, continue:
|
||||
if (!validateAndBlockGuests) {
|
||||
return next();
|
||||
}
|
||||
const aadId = activeContext.corporateIdentity.id;
|
||||
// If the app is configured to check guest status, do this now, before linking:
|
||||
const graphProvider = providers.graphProvider;
|
||||
// REFACTOR: delegate the decision to the auth provider
|
||||
if (!graphProvider || !graphProvider.getUserById) {
|
||||
return next(jsonError('No configured graph provider', 500));
|
||||
}
|
||||
insights.trackEvent({
|
||||
name: 'LinkValidateNotGuestStart',
|
||||
properties: {
|
||||
aadId: aadId,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const details = await graphProvider.getUserById(aadId);
|
||||
const userType = details.userType;
|
||||
const displayName = details.displayName;
|
||||
const userPrincipalName = details.userPrincipalName;
|
||||
let block = userType as string === 'Guest';
|
||||
let blockedRecord = block ? 'BLOCKED' : 'not blocked';
|
||||
insights.trackEvent({
|
||||
name: 'LinkValidateNotGuestGraphSuccess',
|
||||
properties: {
|
||||
aadId: aadId,
|
||||
userType: userType,
|
||||
displayName: displayName,
|
||||
userPrincipalName: userPrincipalName,
|
||||
blocked: blockedRecord,
|
||||
},
|
||||
});
|
||||
if (block) {
|
||||
insights.trackMetric({ name: 'LinksBlockedForGuests', value: 1 });
|
||||
const err = jsonError(`This system is not available to guests. You are currently signed in as ${displayName} ${userPrincipalName}. Please sign out or try a private browser window.`, 400);
|
||||
insights?.trackException({exception: err});
|
||||
return next(err);
|
||||
}
|
||||
const manager = await providers.graphProvider.getManagerByIdAsync(aadId);
|
||||
if (!manager || !manager.userPrincipalName) {
|
||||
return next(jsonError('You do not have an active manager entry in the directory, so cannot yet use this app to link.', 400));
|
||||
}
|
||||
return next();
|
||||
} catch (graphError) {
|
||||
insights.trackException({
|
||||
exception: graphError,
|
||||
properties: {
|
||||
aadId: aadId,
|
||||
name: 'LinkValidateNotGuestGraphFailure',
|
||||
},
|
||||
});
|
||||
return next(jsonError('Generic graph error', 500));
|
||||
}
|
||||
}
|
||||
|
||||
router.delete('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
return unlinkInteractive(true, activeContext, req, res, next);
|
||||
}));
|
||||
|
||||
router.post('/',
|
||||
validateLinkOk,
|
||||
asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
return interactiveLinkUser(true, activeContext, req, res, next);
|
||||
}));
|
||||
|
||||
|
||||
router.use('*', (req: ReposAppRequest, res, next) => {
|
||||
return next(jsonError('API or route not found', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import RouteRepos from './repos';
|
||||
import RouteTeams from './teams';
|
||||
import RoutePeople from './people';
|
||||
import RouteNewRepoMetadata from './newRepoMetadata';
|
||||
import { ReposAppRequest } from '../../../transitional';
|
||||
import { jsonError } from '../../../middleware';
|
||||
import getCompanySpecificDeployment from '../../../middleware/companySpecificDeployment';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const deployment = getCompanySpecificDeployment();
|
||||
deployment?.routes?.api?.organization?.index && deployment?.routes?.api?.organization?.index(router);
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
return res.json(organization.asClientJson());
|
||||
}));
|
||||
|
||||
router.get('/accountDetails', asyncHandler(async (req: ReposAppRequest, res) => {
|
||||
const { organization } = req;
|
||||
const entity = organization.getEntity();
|
||||
if (entity) {
|
||||
return res.json(entity);
|
||||
}
|
||||
const details = await organization.getDetails();
|
||||
return res.json(details);
|
||||
}));
|
||||
|
||||
router.use('/repos', RouteRepos);
|
||||
router.use('/teams', RouteTeams);
|
||||
router.use('/people', RoutePeople);
|
||||
router.use('/newRepoMetadata', RouteNewRepoMetadata);
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { jsonError } from '../../../middleware/jsonError';
|
||||
import { ReposAppRequest } from '../../../transitional';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const metadata = organization.getRepositoryCreateMetadata();
|
||||
res.json(metadata);
|
||||
}));
|
||||
|
||||
router.get('/byProjectReleaseType', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const options = {
|
||||
projectType: req.query.projectType,
|
||||
};
|
||||
const metadata = organization.getRepositoryCreateMetadata(options);
|
||||
res.json(metadata);
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this path', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,139 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { jsonError } from '../../../middleware/jsonError';
|
||||
import { IProviders, NoCacheNoBackground, ReposAppRequest } from '../../../transitional';
|
||||
import { Organization } from '../../../business/organization';
|
||||
import { MemberSearch } from '../../../business/memberSearch';
|
||||
import { Operations } from '../../../business/operations';
|
||||
import { corporateLinkToJson } from '../../../business/corporateLink';
|
||||
import { OrganizationMember } from '../../../business/organizationMember';
|
||||
import { Team } from '../../../business/team';
|
||||
import { TeamMember } from '../../../business/teamMember';
|
||||
import LeakyLocalCache, { getLinksLightCache } from '../leakyLocalCache';
|
||||
import JsonPager from '../jsonPager';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// BAD PRACTICE: leaky local cache
|
||||
// CONSIDER: use a better approach
|
||||
const leakyLocalCacheOrganizationMembers = new LeakyLocalCache<string, OrganizationMember[]>();
|
||||
const leakyLocalCacheTeamMembers = new LeakyLocalCache<string, TeamMember[]>();
|
||||
|
||||
async function getTeamMembers(options?: PeopleSearchOptions) {
|
||||
if (!options?.team) {
|
||||
return;
|
||||
}
|
||||
if (!options.forceRefresh) {
|
||||
const value = leakyLocalCacheTeamMembers.get(options.team.slug);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
const refreshOptions = options.forceRefresh ? NoCacheNoBackground : undefined;
|
||||
const teamMembers = await options.team.getMembers(refreshOptions);
|
||||
leakyLocalCacheTeamMembers.set(options.team.slug, teamMembers);
|
||||
return teamMembers;
|
||||
}
|
||||
|
||||
async function getPeopleForOrganization(operations: Operations, org: string, options?: PeopleSearchOptions) {
|
||||
const teamMembers = await getTeamMembers(options);
|
||||
const value = leakyLocalCacheOrganizationMembers.get(org);
|
||||
if (value) {
|
||||
return { organizationMembers: value, teamMembers };
|
||||
}
|
||||
const organization = operations.getOrganization(org);
|
||||
const organizationMembers = await organization.getMembers();
|
||||
leakyLocalCacheOrganizationMembers.set(org, organizationMembers);
|
||||
return { organizationMembers, teamMembers };
|
||||
}
|
||||
|
||||
type PeopleSearchOptions = {
|
||||
team: Team;
|
||||
forceRefresh: boolean;
|
||||
}
|
||||
|
||||
export async function equivalentLegacyPeopleSearch(req: ReposAppRequest, options?: PeopleSearchOptions) {
|
||||
const { operations } = req.app.settings.providers as IProviders;
|
||||
const links = await getLinksLightCache(operations);
|
||||
const org = req.organization ? req.organization.name : null;
|
||||
const orgId = req.organization ? (req.organization as Organization).id : null;
|
||||
const { organizationMembers, teamMembers } = await getPeopleForOrganization(operations, org, options);
|
||||
const page = req.query.page_number ? Number(req.query.page_number) : 1;
|
||||
let phrase = req.query.q as string;
|
||||
let type = req.query.type as string;
|
||||
const validTypes = new Set([
|
||||
'linked',
|
||||
'active',
|
||||
'unlinked',
|
||||
'former',
|
||||
'serviceAccount',
|
||||
'unknownAccount',
|
||||
'owners',
|
||||
]);
|
||||
if (!validTypes.has(type)) {
|
||||
type = null;
|
||||
}
|
||||
const filters = [];
|
||||
if (type) {
|
||||
filters.push({
|
||||
type: 'type',
|
||||
value: type,
|
||||
displayValue: type === 'former' ? 'formerly known' : type,
|
||||
displaySuffix: 'members',
|
||||
});
|
||||
}
|
||||
if (phrase) {
|
||||
filters.push({
|
||||
type: 'phrase',
|
||||
value: phrase,
|
||||
displayPrefix: 'matching',
|
||||
});
|
||||
}
|
||||
const search = new MemberSearch({
|
||||
phrase,
|
||||
type,
|
||||
pageSize: 1000000, // temporary, just return it all, we'll slice it locally
|
||||
links,
|
||||
providers: operations.providers,
|
||||
orgId,
|
||||
organizationMembers,
|
||||
// crossOrganizationMembers,
|
||||
// isOrganizationScoped: !!org, // Whether this view is specific to an org or not
|
||||
isOrganizationScoped: true,
|
||||
// team2AddType: null, // req.team2AddType, // Used to enable the "add a member" or maintainer experience for teams
|
||||
teamMembers, // Used to filter team members in ./org/ORG/team/TEAM/members and other views
|
||||
});
|
||||
await search.search(page, req.query.sort as string);
|
||||
return search;
|
||||
}
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const pager = new JsonPager<OrganizationMember>(req, res);
|
||||
try {
|
||||
const searcher = await equivalentLegacyPeopleSearch(req);
|
||||
const members = searcher.members;
|
||||
const slice = pager.slice(members);
|
||||
return pager.sendJson(slice.map(organizationMember => {
|
||||
const obj = Object.assign({
|
||||
link: organizationMember.link ? corporateLinkToJson(organizationMember.link) : null,
|
||||
}, organizationMember.getEntity());
|
||||
return obj;
|
||||
}),
|
||||
);
|
||||
} catch (repoError) {
|
||||
console.dir(repoError);
|
||||
return next(jsonError(repoError));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this people list', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,229 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { jsonError } from '../../../middleware/jsonError';
|
||||
import { ErrorHelper, IProviders, ReposAppRequest } from '../../../transitional';
|
||||
import { Repository } from '../../../business/repository';
|
||||
import { IndividualContext } from '../../../user';
|
||||
import NewRepositoryLockdownSystem from '../../../features/newRepositoryLockdown';
|
||||
import { AddRepositoryPermissionsToRequest, getContextualRepositoryPermissions } from '../../../middleware/github/repoPermissions';
|
||||
import { renameRepositoryDefaultBranchEndToEnd } from '../../../routes/org/repos';
|
||||
import getCompanySpecificDeployment from '../../../middleware/companySpecificDeployment';
|
||||
|
||||
import RouteRepoPermissions from './repoPermissions';
|
||||
|
||||
export enum LocalApiRepoAction {
|
||||
Delete = 'delete',
|
||||
Archive = 'archive',
|
||||
}
|
||||
|
||||
type RequestWithRepo = ReposAppRequest & {
|
||||
repository: Repository;
|
||||
};
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const deployment = getCompanySpecificDeployment();
|
||||
deployment?.routes?.api?.organization?.repo && deployment?.routes?.api?.organization?.repo(router);
|
||||
|
||||
router.use('/permissions', RouteRepoPermissions);
|
||||
|
||||
router.get('/', asyncHandler(async (req: RequestWithRepo, res, next) => {
|
||||
const { repository } = req;
|
||||
try {
|
||||
await repository.getDetails();
|
||||
|
||||
const clone = Object.assign({}, repository.getEntity());
|
||||
delete clone.temp_clone_token; // never share this back
|
||||
delete clone.cost;
|
||||
|
||||
return res.json(repository.getEntity());
|
||||
} catch (repoError) {
|
||||
if (ErrorHelper.IsNotFound(repoError)) {
|
||||
// // Attempt fallback by ID (?)
|
||||
}
|
||||
return next(jsonError(repoError));
|
||||
}
|
||||
}));
|
||||
|
||||
router.get('/exists', asyncHandler(async (req: RequestWithRepo, res, next) => {
|
||||
let exists = false;
|
||||
let name: string = undefined;
|
||||
const { repository } = req;
|
||||
try {
|
||||
const originalName = repository.name;
|
||||
await repository.getDetails();
|
||||
if (repository && repository.name) {
|
||||
name = repository.getEntity().name as string;
|
||||
if (name.toLowerCase() !== originalName.toLowerCase()) {
|
||||
// A renamed repository will return the new name here
|
||||
exists = false;
|
||||
} else {
|
||||
exists = true;
|
||||
}
|
||||
}
|
||||
} catch (repoError) {
|
||||
}
|
||||
return res.json({ exists, name });
|
||||
}));
|
||||
|
||||
router.patch('/renameDefaultBranch', asyncHandler(AddRepositoryPermissionsToRequest), asyncHandler(async function (req: RequestWithRepo, res, next) {
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
const repoPermissions = getContextualRepositoryPermissions(req);
|
||||
const targetBranchName = req.body.default_branch;
|
||||
const { repository } = req;
|
||||
try {
|
||||
const result = await renameRepositoryDefaultBranchEndToEnd(providers, activeContext, repoPermissions, repository, targetBranchName, true /* wait for refresh before sending response */);
|
||||
return res.json(result);
|
||||
} catch (error) {
|
||||
return next(jsonError(error));
|
||||
}
|
||||
}));
|
||||
|
||||
router.post('/archive', asyncHandler(AddRepositoryPermissionsToRequest), asyncHandler(async function (req: RequestWithRepo, res, next) {
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const { insights } = providers;
|
||||
const repoPermissions = getContextualRepositoryPermissions(req);
|
||||
if (!repoPermissions.allowAdministration) {
|
||||
return next(jsonError('You do not have permission to archive this repo', 403));
|
||||
}
|
||||
const insightsPrefix = 'ArchiveRepo';
|
||||
const { repository } = req;
|
||||
try {
|
||||
insights?.trackEvent({
|
||||
name: `${insightsPrefix}Started`,
|
||||
properties: {
|
||||
requestedById: activeContext.link.corporateId,
|
||||
repoName: repository.name,
|
||||
orgName: repository.organization.name,
|
||||
repoId: repository.id ? String(repository.id) : 'unknown',
|
||||
},
|
||||
});
|
||||
const currentRepositoryState = deployment?.features?.repositoryActions?.getCurrentRepositoryState ? (await deployment.features.repositoryActions.getCurrentRepositoryState(providers, repository)) : null;
|
||||
await repository.archive();
|
||||
if (deployment?.features?.repositoryActions?.sendActionReceipt) {
|
||||
deployment.features.repositoryActions.sendActionReceipt(providers, activeContext, repository, LocalApiRepoAction.Archive, currentRepositoryState).then(ok => {}).catch(() => {});
|
||||
}
|
||||
insights?.trackMetric({
|
||||
name: `${insightsPrefix}s`,
|
||||
value: 1,
|
||||
});
|
||||
insights?.trackEvent({
|
||||
name: `${insightsPrefix}Success`,
|
||||
properties: {
|
||||
requestedById: activeContext.link.corporateId,
|
||||
repoName: repository.name,
|
||||
orgName: repository.organization.name,
|
||||
repoId: repository.id ? String(repository.id) : 'unknown',
|
||||
},
|
||||
});
|
||||
//return res.json(result);
|
||||
return res.json({
|
||||
message: `You archived: ${repository.full_name}`,
|
||||
});
|
||||
} catch (error) {
|
||||
insights?.trackException({ exception: error });
|
||||
insights?.trackEvent({
|
||||
name: `${insightsPrefix}Failed`,
|
||||
properties: {
|
||||
requestedById: activeContext.link.corporateId,
|
||||
repoName: repository.name,
|
||||
orgName: repository.organization.name,
|
||||
repoId: repository.id ? String(repository.id) : 'unknown',
|
||||
},
|
||||
});
|
||||
return next(jsonError(error));
|
||||
}
|
||||
}));
|
||||
|
||||
router.delete('/', asyncHandler(AddRepositoryPermissionsToRequest), asyncHandler(async function (req: RequestWithRepo, res, next) {
|
||||
// NOTE: duplicated code from /routes/org/repos.ts
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const { insights } = providers;
|
||||
const insightsPrefix = 'DeleteRepo';
|
||||
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
|
||||
const { organization, repository } = req;
|
||||
const repoPermissions = getContextualRepositoryPermissions(req);
|
||||
if (repoPermissions.allowAdministration) {
|
||||
try {
|
||||
insights?.trackEvent({
|
||||
name: `${insightsPrefix}Started`,
|
||||
properties: {
|
||||
requestedById: activeContext.link.corporateId,
|
||||
repoName: repository.name,
|
||||
orgName: repository.organization.name,
|
||||
repoId: repository.id ? String(repository.id) : 'unknown',
|
||||
},
|
||||
});
|
||||
const currentRepositoryState = deployment?.features?.repositoryActions?.getCurrentRepositoryState ? (await deployment.features.repositoryActions.getCurrentRepositoryState(providers, repository)) : null;
|
||||
await repository.delete();
|
||||
if (deployment?.features?.repositoryActions?.sendActionReceipt) {
|
||||
deployment.features.repositoryActions.sendActionReceipt(providers, activeContext, repository, LocalApiRepoAction.Delete, currentRepositoryState).then(ok => {}).catch(() => {});
|
||||
}
|
||||
insights?.trackMetric({
|
||||
name: `${insightsPrefix}s`,
|
||||
value: 1,
|
||||
});
|
||||
insights?.trackEvent({
|
||||
name: `${insightsPrefix}Success`,
|
||||
properties: {
|
||||
requestedById: activeContext.link.corporateId,
|
||||
repoName: repository.name,
|
||||
orgName: repository.organization.name,
|
||||
repoId: repository.id ? String(repository.id) : 'unknown',
|
||||
},
|
||||
});
|
||||
return res.json({
|
||||
message: `You deleted: ${repository.full_name}`,
|
||||
});
|
||||
} catch (error) {
|
||||
insights?.trackException({ exception: error });
|
||||
insights?.trackEvent({
|
||||
name: `${insightsPrefix}Failed`,
|
||||
properties: {
|
||||
requestedById: activeContext.link.corporateId,
|
||||
repoName: repository.name,
|
||||
orgName: repository.organization.name,
|
||||
repoId: repository.id ? String(repository.id) : 'unknown',
|
||||
},
|
||||
});
|
||||
return next(jsonError(error));
|
||||
}
|
||||
}
|
||||
if (!organization.isNewRepositoryLockdownSystemEnabled) {
|
||||
return next(jsonError('This endpoint is not available as configured in this app.', 400));
|
||||
}
|
||||
const daysAfterCreateToAllowSelfDelete = 21; // could be a config setting if anyone cares
|
||||
try {
|
||||
// make sure ID is known
|
||||
if (await repository.isDeleted()) {
|
||||
return next(jsonError('The repository has already been deleted', 404));
|
||||
}
|
||||
const metadata = await repository.getRepositoryMetadata();
|
||||
await NewRepositoryLockdownSystem.ValidateUserCanSelfDeleteRepository(repository, metadata, activeContext, daysAfterCreateToAllowSelfDelete);
|
||||
} catch (noExistingMetadata) {
|
||||
if (noExistingMetadata.status === 404) {
|
||||
return next(jsonError('This repository does not have any metadata available regarding who can setup it up. No further actions available.', 400));
|
||||
}
|
||||
return next(jsonError(noExistingMetadata, 404));
|
||||
}
|
||||
const { operations, repositoryMetadataProvider } = req.app.settings.providers as IProviders;
|
||||
const lockdownSystem = new NewRepositoryLockdownSystem({ operations, organization, repository, repositoryMetadataProvider });
|
||||
await lockdownSystem.deleteLockedRepository(false /* delete for any reason */, true /* deleted by the original user instead of ops */);
|
||||
return res.json({
|
||||
message: `You deleted your repo, ${repository.full_name}.`,
|
||||
});
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this specific repo', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { jsonError } from '../../../middleware/jsonError';
|
||||
import { ReposAppRequest } from '../../../transitional';
|
||||
import { Repository } from '../../../business/repository';
|
||||
import { findRepoCollaboratorsExcludingOwners } from '../../../routes/org/repos';
|
||||
|
||||
type RequestWithRepo = ReposAppRequest & {
|
||||
repository: Repository;
|
||||
};
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', asyncHandler(async (req: RequestWithRepo, res, next) => {
|
||||
const { repository, organization } = req;
|
||||
try {
|
||||
const teamPermissions = await repository.getTeamPermissions();
|
||||
const owners = await organization.getOwners();
|
||||
const { collaborators, outsideCollaborators, memberCollaborators } = await findRepoCollaboratorsExcludingOwners(repository, owners);
|
||||
for (let teamPermission of teamPermissions) {
|
||||
try {
|
||||
teamPermission.resolveTeamMembers();
|
||||
} catch (ignoredError) { /* ignored */ }
|
||||
}
|
||||
return res.json({
|
||||
teamPermissions: teamPermissions.map(tp => tp.asJson()),
|
||||
collaborators: collaborators.map(c => c.asJson()),
|
||||
outsideCollaborators: outsideCollaborators.map(oc => oc.asJson()),
|
||||
memberCollaborators: memberCollaborators.map(oc => oc.asJson()),
|
||||
});
|
||||
} catch (error) {
|
||||
return next(jsonError(error));
|
||||
}
|
||||
}));
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,223 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { jsonError } from '../../../middleware/jsonError';
|
||||
import { IProviders, ReposAppRequest } from '../../../transitional';
|
||||
import { Repository } from '../../../business/repository';
|
||||
|
||||
import RouteRepo from './repo';
|
||||
import JsonPager from '../jsonPager';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const pager = new JsonPager<Repository>(req, res);
|
||||
const searchOptions = {
|
||||
q: (req.query.q || '') as string,
|
||||
type: (req.query.type || '') as string, // CONSIDER: TS: stronger typing
|
||||
}
|
||||
try {
|
||||
const repos = await searchRepos(providers, String(organization.id), RepositorySearchSortOrder.Updated, searchOptions);
|
||||
const slice = pager.slice(repos);
|
||||
return pager.sendJson(slice.map(repo => {
|
||||
return repo.asJson();
|
||||
}));
|
||||
} catch (repoError) {
|
||||
console.dir(repoError);
|
||||
return next(jsonError(repoError));
|
||||
}
|
||||
}));
|
||||
|
||||
// --- Search reimplementation ---
|
||||
|
||||
export enum RepoListSearchType {
|
||||
All = '',
|
||||
Public = 'public',
|
||||
Private = 'private',
|
||||
Sources = 'sources',
|
||||
Forks = 'forks',
|
||||
}
|
||||
|
||||
export function repoListSearchTypeToDisplayName(v: RepoListSearchType) {
|
||||
switch (v) {
|
||||
case RepoListSearchType.All:
|
||||
return 'All';
|
||||
case RepoListSearchType.Forks:
|
||||
return 'Forks';
|
||||
case RepoListSearchType.Private:
|
||||
return 'Private';
|
||||
case RepoListSearchType.Public:
|
||||
return 'Public';
|
||||
case RepoListSearchType.Sources:
|
||||
return 'Sources';
|
||||
default:
|
||||
throw new Error('Not a supported type');
|
||||
}
|
||||
}
|
||||
|
||||
export function repoSearchTypeFilterFromStringToEnum(value: string) {
|
||||
value = value || '';
|
||||
switch (value) {
|
||||
case RepoListSearchType.All:
|
||||
case RepoListSearchType.Public:
|
||||
case RepoListSearchType.Private:
|
||||
case RepoListSearchType.Forks:
|
||||
case RepoListSearchType.Sources:
|
||||
return value as RepoListSearchType;
|
||||
default:
|
||||
return RepoListSearchType.All;
|
||||
}
|
||||
}
|
||||
|
||||
export enum RepositorySearchSortOrder {
|
||||
Recent = 'recent',
|
||||
Stars = 'stars',
|
||||
Forks = 'forks',
|
||||
Name = 'name',
|
||||
Updated = 'updated',
|
||||
Created = 'created',
|
||||
Size = 'size',
|
||||
}
|
||||
|
||||
type RepoFilterFunction = (a: Repository) => boolean;
|
||||
|
||||
function getFilter(type: RepoListSearchType): RepoFilterFunction {
|
||||
switch (type) {
|
||||
case RepoListSearchType.Forks:
|
||||
return repo => { return repo.fork; };
|
||||
case RepoListSearchType.Sources:
|
||||
return repo => { return !repo.fork; }; // ? is this what 'Sources' means on GitHub?
|
||||
case RepoListSearchType.Public:
|
||||
return repo => { return !repo.private; };
|
||||
case RepoListSearchType.Private:
|
||||
return repo => { return repo.private; };
|
||||
case RepoListSearchType.All:
|
||||
default:
|
||||
return repo => { return true; };
|
||||
}
|
||||
}
|
||||
|
||||
type RepoSortFunction = (a: Repository, b: Repository) => number;
|
||||
|
||||
function sortDates(fieldName: string, a: Repository, b: Repository): number { // Inverted sort (newest first)
|
||||
const aa = a[fieldName] ? (typeof(a[fieldName]) === 'string' ? new Date(a[fieldName]) : a[fieldName]) : new Date(0);
|
||||
const bb = b[fieldName] ? (typeof(b[fieldName]) === 'string' ? new Date(b[fieldName]) : b[fieldName]) : new Date(0);
|
||||
return aa == bb ? 0 : (aa < bb) ? 1 : -1;
|
||||
}
|
||||
|
||||
function getSorter(search: RepositorySearchSortOrder): RepoSortFunction {
|
||||
switch (search) {
|
||||
case RepositorySearchSortOrder.Recent: {
|
||||
return sortDates.bind(null, 'pushed_at');
|
||||
}
|
||||
case RepositorySearchSortOrder.Created: {
|
||||
return sortDates.bind(null, 'created_at');
|
||||
}
|
||||
case RepositorySearchSortOrder.Updated: {
|
||||
return sortDates.bind(null, 'updated_at');
|
||||
}
|
||||
case RepositorySearchSortOrder.Forks: {
|
||||
return (a, b) => { return b.forks_count - a.forks_count };
|
||||
}
|
||||
case RepositorySearchSortOrder.Name: {
|
||||
return (a, b) => {
|
||||
const nameA = a.name.toLowerCase();
|
||||
const nameB = b.name.toLowerCase();
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
case RepositorySearchSortOrder.Size: {
|
||||
return (a, b) => {
|
||||
if (a.size > b.size) {
|
||||
return -1;
|
||||
} else if (a.size < b.size) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
case RepositorySearchSortOrder.Stars: {
|
||||
return (a, b) => {
|
||||
return b.stargazers_count - a.stargazers_count;
|
||||
};
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new Error('Not a supported search type');
|
||||
}
|
||||
|
||||
function repoMatchesPhrase(phrase: string, repo: Repository) {
|
||||
// assumes string is already lowercase
|
||||
const string = ((repo.name || '') + (repo.description || '') + (repo.id || '')).toLowerCase();
|
||||
return string.includes(phrase);
|
||||
}
|
||||
|
||||
interface ISearchReposOptions {
|
||||
q?: string;
|
||||
type?: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export async function searchRepos(providers: IProviders, organizationId: string, sort: RepositorySearchSortOrder, options: ISearchReposOptions) {
|
||||
const { queryCache } = providers;
|
||||
|
||||
const { q, type } = options;
|
||||
|
||||
// TODO: aggressive in-memory caching for each org
|
||||
let repositories = (
|
||||
organizationId ? (await queryCache.organizationRepositories(organizationId.toString())) : (await queryCache.allRepositories())
|
||||
).map(wrapper => wrapper.repository);
|
||||
|
||||
// Filters
|
||||
if (q) {
|
||||
let phrase = q.toLowerCase();
|
||||
repositories = repositories.filter(repoMatchesPhrase.bind(null, phrase));
|
||||
}
|
||||
|
||||
// TODO: const language = null;
|
||||
|
||||
if (type) {
|
||||
const t = repoSearchTypeFilterFromStringToEnum(type);
|
||||
if (t !== RepoListSearchType.All) {
|
||||
repositories = repositories.filter(getFilter(t));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
repositories.sort(getSorter(sort));
|
||||
|
||||
return repositories;
|
||||
}
|
||||
|
||||
// --- End of search reimplementation ---
|
||||
|
||||
router.use('/:repoName', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const { repoName } = req.params;
|
||||
// does not confirm the name
|
||||
(req as any).repository = organization.repository(repoName);
|
||||
return next();
|
||||
}));
|
||||
|
||||
router.use('/:repoName', RouteRepo);
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this repos endpoint', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { corporateLinkToJson, ICorporateLink } from '../../../business/corporateLink';
|
||||
import { OrganizationMember } from '../../../business/organizationMember';
|
||||
import { TeamJsonFormat } from '../../../business/team';
|
||||
import { TeamRepositoryPermission } from '../../../business/teamRepositoryPermission';
|
||||
import { getContextualTeam } from '../../../middleware/github/teamPermissions';
|
||||
|
||||
import { jsonError } from '../../../middleware/jsonError';
|
||||
import { sortRepositoriesByNameCaseInsensitive } from '../../../routes/org/team';
|
||||
import { IProviders, NoCacheNoBackground, ReposAppRequest } from '../../../transitional';
|
||||
import JsonPager from '../jsonPager';
|
||||
import { getLinksLightCache } from '../leakyLocalCache';
|
||||
import { equivalentLegacyPeopleSearch } from './people';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const team = getContextualTeam(req);
|
||||
return res.json(team.asJson(TeamJsonFormat.Augmented /* includes corporateMetadata */));
|
||||
}));
|
||||
|
||||
router.get('/repos', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
try {
|
||||
const forceRefresh = !!req.query.refresh;
|
||||
const pager = new JsonPager<TeamRepositoryPermission>(req, res);
|
||||
const team = getContextualTeam(req);
|
||||
// const onlySourceRepositories = {
|
||||
// type: GitHubRepositoryType.,
|
||||
// };
|
||||
let reposWithPermissions = null;
|
||||
const cacheOptions = forceRefresh ? NoCacheNoBackground : undefined;
|
||||
reposWithPermissions = await team.getRepositories(cacheOptions);
|
||||
const repositories = reposWithPermissions.sort(sortRepositoriesByNameCaseInsensitive);
|
||||
const slice = pager.slice(repositories);
|
||||
return pager.sendJson(slice.map(rp => {
|
||||
return rp.asJson();
|
||||
}));
|
||||
} catch (repoError) {
|
||||
console.dir(repoError);
|
||||
return next(jsonError(repoError));
|
||||
}
|
||||
}));
|
||||
|
||||
router.get('/members', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
try {
|
||||
const forceRefresh = !!req.query.refresh;
|
||||
const team = getContextualTeam(req);
|
||||
const pager = new JsonPager<OrganizationMember>(req, res); // or Org Member?
|
||||
const searcher = await equivalentLegacyPeopleSearch(req, { team, forceRefresh });
|
||||
const members = searcher.members;
|
||||
const slice = pager.slice(members);
|
||||
return pager.sendJson(slice.map(organizationMember => {
|
||||
const obj = Object.assign({
|
||||
link: organizationMember.link ? corporateLinkToJson(organizationMember.link) : null,
|
||||
}, organizationMember.getEntity());
|
||||
return obj;
|
||||
}));
|
||||
} catch (error) {
|
||||
console.dir(error);
|
||||
return next(jsonError(error));
|
||||
}
|
||||
}));
|
||||
|
||||
router.get('/maintainers', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { operations } = req.app.settings.providers as IProviders;
|
||||
try {
|
||||
const forceRefresh = !!req.query.refresh;
|
||||
const team = getContextualTeam(req);
|
||||
const links = await getLinksLightCache(operations);
|
||||
const cacheOptions = forceRefresh ? NoCacheNoBackground : undefined;
|
||||
const maintainers = await team.getMaintainers(cacheOptions);
|
||||
const idSet = new Set<number>();
|
||||
maintainers.forEach(maintainer => idSet.add(Number(maintainer.id)));
|
||||
const ls = new Map<number, ICorporateLink>();
|
||||
links.forEach(link => {
|
||||
if (idSet.has(Number(link.thirdPartyId))) {
|
||||
ls.set(Number(link.thirdPartyId), link);
|
||||
}
|
||||
});
|
||||
return res.json(maintainers.map(maintainer => {
|
||||
return {
|
||||
member: maintainer.asJson(),
|
||||
isSystemAccount: operations.isSystemAccountByUsername(maintainer.login),
|
||||
link: corporateLinkToJson(ls.get(Number(maintainer.id))),
|
||||
};
|
||||
}));
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available for this specific team', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { Organization } from '../../../business/organization';
|
||||
import { Team, TeamJsonFormat } from '../../../business/team';
|
||||
import { setContextualTeam } from '../../../middleware/github/teamPermissions';
|
||||
|
||||
import { jsonError } from '../../../middleware/jsonError';
|
||||
import { ReposAppRequest } from '../../../transitional';
|
||||
import JsonPager from '../jsonPager';
|
||||
import LeakyLocalCache from '../leakyLocalCache';
|
||||
|
||||
import RouteTeam from './team';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// BAD PRACTICE: leaky local cache
|
||||
// CONSIDER: use a better approach
|
||||
const leakyLocalCache = new LeakyLocalCache<number, Team[]>();
|
||||
|
||||
router.use('/:teamSlug', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const { teamSlug } = req.params;
|
||||
let team: Team = null;
|
||||
try {
|
||||
team = await organization.getTeamFromSlug(teamSlug);
|
||||
setContextualTeam(req, team);
|
||||
return next();
|
||||
} catch (teamError) {
|
||||
return next(jsonError(teamError));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('/:teamSlug', RouteTeam);
|
||||
|
||||
async function getTeamsForOrganization(organization: Organization): Promise<Team[]> {
|
||||
const cached = leakyLocalCache.get(organization.id);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const options = {
|
||||
backgroundRefresh: true,
|
||||
maxAgeSeconds: 60 * 10 /* 10 minutes */,
|
||||
individualMaxAgeSeconds: 60 * 30 /* 30 minutes */,
|
||||
};
|
||||
let list: Team[] = null;
|
||||
list = await organization.getTeams(options);
|
||||
leakyLocalCache.set(organization.id, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { organization } = req;
|
||||
const pager = new JsonPager<Team>(req, res);
|
||||
const q: string = (req.query.q ? req.query.q as string : null) || '';
|
||||
try {
|
||||
// TODO: need to do lots of caching to make this awesome!
|
||||
// const repos = await organization.getRepositories();
|
||||
let teams = await getTeamsForOrganization(organization);
|
||||
if (q) {
|
||||
teams = teams.filter(team => {
|
||||
let string = ((team.name || '') + (team.description || '') + (team.id || '') + (team.slug || '')).toLowerCase();
|
||||
return string.includes(q.toLowerCase());
|
||||
});
|
||||
}
|
||||
const slice = pager.slice(teams);
|
||||
return pager.sendJson(slice.map(team => {
|
||||
return team.asJson(TeamJsonFormat.Augmented);
|
||||
}));
|
||||
} catch (repoError) {
|
||||
console.dir(repoError);
|
||||
return next(jsonError(repoError));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this team', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { jsonError } from '../../middleware/jsonError';
|
||||
import { ErrorHelper, IProviders, ReposAppRequest } from '../../transitional';
|
||||
|
||||
import RouteOrganization from './organization';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { operations } = req.app.settings.providers as IProviders;
|
||||
try {
|
||||
const orgs = operations.getOrganizations();
|
||||
const dd = orgs.map(org => { return org.asClientJson(); });
|
||||
return res.json(dd);
|
||||
} catch (error) {
|
||||
throw jsonError(error, 400);
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('/:orgName', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { operations } = req.app.settings.providers as IProviders;
|
||||
const { orgName } = req.params;
|
||||
try {
|
||||
const org = operations.getOrganization(orgName);
|
||||
if (org) {
|
||||
req.organization = org;
|
||||
return next();
|
||||
}
|
||||
throw jsonError('managed organization not found', 404);
|
||||
} catch (orgNotFoundError) {
|
||||
if (ErrorHelper.IsNotFound(orgNotFoundError)) {
|
||||
return next(jsonError(orgNotFoundError, 404));
|
||||
} else {
|
||||
return next(jsonError(orgNotFoundError));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('/:orgName', RouteOrganization);
|
||||
|
||||
router.use('*', (req: ReposAppRequest, res, next) => {
|
||||
return next(jsonError('orgs API not found', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,138 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { IndividualContext } from '../../user';
|
||||
import LeakyLocalCache, { getLinksLightCache } from './leakyLocalCache';
|
||||
import { corporateLinkToJson, ICorporateLink, ICrossOrganizationMembersResult, MemberSearch, Operations, Organization } from '../../business';
|
||||
import { jsonError } from '../../middleware';
|
||||
import { ReposAppRequest, IProviders } from '../../transitional';
|
||||
import JsonPager from './jsonPager';
|
||||
import getCompanySpecificDeployment from '../../middleware/companySpecificDeployment';
|
||||
|
||||
import RouteGetPerson from './person';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const deployment = getCompanySpecificDeployment();
|
||||
|
||||
// BAD PRACTICE: leaky local cache
|
||||
// CONSIDER: use a better approach
|
||||
const leakyLocalCachePeople = new LeakyLocalCache<boolean, ICrossOrganizationMembersResult>();
|
||||
|
||||
async function getPeopleAcrossOrganizations(operations: Operations) {
|
||||
const value = leakyLocalCachePeople.get(true);
|
||||
if (value) {
|
||||
return { crossOrganizationMembers: value };
|
||||
}
|
||||
const crossOrganizationMembers = await operations.getMembers();
|
||||
leakyLocalCachePeople.set(true, crossOrganizationMembers);
|
||||
return { crossOrganizationMembers };
|
||||
}
|
||||
|
||||
export async function equivalentLegacyPeopleSearch(req: ReposAppRequest) {
|
||||
const { operations } = req.app.settings.providers as IProviders;
|
||||
const links = await getLinksLightCache(operations);
|
||||
const org = req.organization ? req.organization.name : null;
|
||||
const orgId = req.organization ? (req.organization as Organization).id : null;
|
||||
const { crossOrganizationMembers } = await getPeopleAcrossOrganizations(operations);
|
||||
const page = req.query.page_number ? Number(req.query.page_number) : 1;
|
||||
let phrase = req.query.q as string;
|
||||
let type = req.query.type as string;
|
||||
const validTypes = new Set([
|
||||
'linked',
|
||||
'active',
|
||||
'unlinked',
|
||||
'former',
|
||||
'serviceAccount',
|
||||
'unknownAccount',
|
||||
'owners',
|
||||
]);
|
||||
if (!validTypes.has(type)) {
|
||||
type = null;
|
||||
}
|
||||
const filters = [];
|
||||
if (type) {
|
||||
filters.push({
|
||||
type: 'type',
|
||||
value: type,
|
||||
displayValue: type === 'former' ? 'formerly known' : type,
|
||||
displaySuffix: 'members',
|
||||
});
|
||||
}
|
||||
if (phrase) {
|
||||
filters.push({
|
||||
type: 'phrase',
|
||||
value: phrase,
|
||||
displayPrefix: 'matching',
|
||||
});
|
||||
}
|
||||
const search = new MemberSearch({
|
||||
phrase,
|
||||
type,
|
||||
pageSize: 1000000, // we'll slice it locally
|
||||
links,
|
||||
providers: operations.providers,
|
||||
orgId,
|
||||
crossOrganizationMembers,
|
||||
isOrganizationScoped: false,
|
||||
});
|
||||
await search.search(page, req.query.sort as string);
|
||||
return search;
|
||||
}
|
||||
|
||||
interface ISimpleAccount {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface ICrossOrganizationMemberResponse {
|
||||
account: ISimpleAccount;
|
||||
link?: ICorporateLink;
|
||||
organizations: string[];
|
||||
}
|
||||
|
||||
export interface ICrossOrganizationSearchedMember {
|
||||
id: number;
|
||||
account: ISimpleAccount;
|
||||
link?: ICorporateLink;
|
||||
orgs: IOrganizationMembershipAccount;
|
||||
}
|
||||
|
||||
interface IOrganizationMembershipAccount {
|
||||
[id: string]: ISimpleAccount;
|
||||
}
|
||||
|
||||
router.get('/:login', RouteGetPerson);
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const pager = new JsonPager<ICrossOrganizationSearchedMember>(req, res);
|
||||
try {
|
||||
const searcher = await equivalentLegacyPeopleSearch(req);
|
||||
const members = searcher.members as unknown as ICrossOrganizationSearchedMember[];
|
||||
const slice = pager.slice(members);
|
||||
return pager.sendJson(slice.map(xMember => {
|
||||
const obj = Object.assign({
|
||||
link: xMember.link ? corporateLinkToJson(xMember.link) : null,
|
||||
id: xMember.id,
|
||||
organizations: xMember.orgs ? Object.getOwnPropertyNames(xMember.orgs) : [],
|
||||
}, xMember.account || { id: xMember.id });
|
||||
return obj;
|
||||
}),
|
||||
);
|
||||
} catch (repoError) {
|
||||
console.dir(repoError);
|
||||
return next(jsonError(repoError));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this cross-organization people list', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { jsonError } from '../../middleware/jsonError';
|
||||
import { IProviders, ReposAppRequest } from '../../transitional';
|
||||
|
||||
import { AccountJsonFormat } from '../../business';
|
||||
|
||||
export default asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const { operations, queryCache } = providers;
|
||||
const login = req.params.login as string;
|
||||
try {
|
||||
const account = await operations.getAccountByUsername(login);
|
||||
const idAsString = String(account.id);
|
||||
await account.tryGetLink();
|
||||
const json = account.asJson(AccountJsonFormat.UplevelWithLink);
|
||||
const orgs = await queryCache.userOrganizations(idAsString);
|
||||
const teams = await queryCache.userTeams(idAsString);
|
||||
for (let team of teams) {
|
||||
if (!team.team.slug) {
|
||||
try {
|
||||
await team.team.getDetails();
|
||||
} catch (ignoreSlugError) {
|
||||
console.warn(`get team slug or details error: team ID=${team.team.id} error=${ignoreSlugError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const collabs = await queryCache.userCollaboratorRepositories(idAsString);
|
||||
const combined = Object.assign({
|
||||
orgs: orgs.map(o => {
|
||||
return {
|
||||
organization: o.organization.name,
|
||||
role: o.role,
|
||||
organizationId: o.organization.id,
|
||||
};
|
||||
}),
|
||||
teams: teams.map(t => {
|
||||
return {
|
||||
role: t.role,
|
||||
slug: t.team.slug,
|
||||
organization: t.team.organization.name,
|
||||
teamId: t.team.id,
|
||||
};
|
||||
}),
|
||||
collaborator: collabs.map(c => {
|
||||
return {
|
||||
affiliation: c.affiliation,
|
||||
permission: c.permission,
|
||||
organization: c.repository.organization.name,
|
||||
repository: c.repository.name,
|
||||
repositoryId: c.repository.id,
|
||||
private: c.repository.private,
|
||||
};
|
||||
}),
|
||||
}, json);
|
||||
return res.json(combined);
|
||||
} catch (error) {
|
||||
return next(jsonError(`login ${login} error: ${error}`, 500));
|
||||
}
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { Repository } from '../../business/repository';
|
||||
|
||||
import { jsonError } from '../../middleware/jsonError';
|
||||
import { IProviders, ReposAppRequest } from '../../transitional';
|
||||
|
||||
import JsonPager from './jsonPager';
|
||||
import { RepositorySearchSortOrder, searchRepos } from './organization/repos';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const providers = req.app.settings.providers as IProviders;
|
||||
const pager = new JsonPager<Repository>(req, res);
|
||||
const searchOptions = {
|
||||
q: (req.query.q || '') as string,
|
||||
type: (req.query.type || '') as string, // CONSIDER: TS: stronger typing
|
||||
}
|
||||
try {
|
||||
const repos = await searchRepos(providers, null, RepositorySearchSortOrder.Updated, searchOptions);
|
||||
const slice = pager.slice(repos);
|
||||
return pager.sendJson(slice.map(repo => {
|
||||
return repo.asJson();
|
||||
}));
|
||||
} catch (repoError) {
|
||||
console.dir(repoError);
|
||||
return next(jsonError(repoError));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this cross-organization repps list', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
|
||||
import { jsonError } from '../../middleware/jsonError';
|
||||
import { IAppSession, ReposAppRequest } from '../../transitional';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// This route is /api/client/signout*
|
||||
|
||||
router.post('/', (req: ReposAppRequest, res) => {
|
||||
req.logout();
|
||||
const session = req.session as IAppSession;
|
||||
if (session) {
|
||||
delete session.enableMultipleAccounts;
|
||||
delete session.selectedGithubId;
|
||||
}
|
||||
res.status(204);
|
||||
res.end();
|
||||
});
|
||||
|
||||
router.post('/github', (req: ReposAppRequest, res) => {
|
||||
const session = req.session as IAppSession;
|
||||
if (session?.passport?.user?.github) {
|
||||
delete session.passport.user.github;
|
||||
}
|
||||
res.status(204);
|
||||
res.end();
|
||||
});
|
||||
|
||||
router.use('*', (req: ReposAppRequest, res, next) => {
|
||||
return next(jsonError('API or route not found', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { ICrossOrganizationMembershipByOrganization, Operations } from '../../business/operations';
|
||||
import { Team, TeamJsonFormat } from '../../business/team';
|
||||
|
||||
import { jsonError } from '../../middleware/jsonError';
|
||||
import { IProviders, ReposAppRequest } from '../../transitional';
|
||||
|
||||
import JsonPager from './jsonPager';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function getCrossOrganizationTeams(operations: Operations): Promise<Team[]> {
|
||||
const options = {
|
||||
backgroundRefresh: true,
|
||||
maxAgeSeconds: 60 * 10 /* 10 minutes */,
|
||||
individualMaxAgeSeconds: 60 * 30 /* 30 minutes */,
|
||||
};
|
||||
let list: Team[] = null;
|
||||
list = [];
|
||||
const crossOrgTeams = await operations.getCrossOrganizationTeams(options);
|
||||
const allReducedTeams = Array.from(crossOrgTeams.values());
|
||||
allReducedTeams.forEach((reducedTeam: ICrossOrganizationMembershipByOrganization) => {
|
||||
const orgs = Object.getOwnPropertyNames(reducedTeam.orgs);
|
||||
const firstOrg = orgs[0];
|
||||
const organization = operations.getOrganization(firstOrg);
|
||||
const entry = organization.teamFromEntity(reducedTeam.orgs[firstOrg]);
|
||||
list.push(entry);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
router.get('/', asyncHandler(async (req: ReposAppRequest, res, next) => {
|
||||
const { operations } = req.app.settings.providers as IProviders;
|
||||
const pager = new JsonPager<Team>(req, res);
|
||||
const q: string = (req.query.q ? req.query.q as string : null) || '';
|
||||
try {
|
||||
// TODO: need to do lots of caching to make this awesome!
|
||||
let teams = await getCrossOrganizationTeams(operations);
|
||||
if (q) {
|
||||
teams = teams.filter(team => {
|
||||
let string = ((team.name || '') + (team.description || '') + (team.id || '') + (team.slug || '')).toLowerCase();
|
||||
return string.includes(q.toLowerCase());
|
||||
});
|
||||
}
|
||||
const slice = pager.slice(teams);
|
||||
return pager.sendJson(slice.map(team => {
|
||||
return team.asJson(TeamJsonFormat.Detailed);
|
||||
}));
|
||||
} catch (repoError) {
|
||||
console.dir(repoError);
|
||||
return next(jsonError(repoError));
|
||||
}
|
||||
}));
|
||||
|
||||
router.use('*', (req, res, next) => {
|
||||
return next(jsonError('no API or function available within this cross-organization teams list', 404));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -7,5 +7,6 @@
|
|||
"allowOrganizationSudo": "env://FEATURE_FLAG_ALLOW_ORG_SUDO?trueIf=1&default=1",
|
||||
"allowPortalSudo": "env://FEATURE_FLAG_ALLOW_PORTAL_SUDO?trueIf=1",
|
||||
"allowAdministratorManualLinking": "env://FEATURE_FLAG_ALLOW_ADMIN_MANUAL_LINKING?trueIf=1",
|
||||
"allowApiClient": "env://FEATURE_FLAG_ALLOW_API_CLIENT?trueIf=1",
|
||||
"allowFossFundElections": "env://FEATURE_FLAG_FOSS_FUND_ELECTIONS?trueIf=1"
|
||||
}
|
|
@ -24,6 +24,11 @@ variable value to `1`.
|
|||
- Purpose: Locks repositories that are forks until they are approved by an administrator
|
||||
- Requirements: depends on the new repo lockdown system already being enabled and in use
|
||||
|
||||
- allowApiClient
|
||||
|
||||
- Variable: `FEATURE_FLAG_ALLOW_API_CLIENT`
|
||||
- Purpose: Allows session-based client APIs, used for powering a modern front-end app connected to the site
|
||||
|
||||
## Temporary features
|
||||
|
||||
An optional set of features are being developed for use in summer 2020 as part
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { Router } from 'express';
|
||||
import { IDictionary } from '../../transitional';
|
||||
|
||||
export interface ICorporationAdministrationSection {
|
||||
urls: IDictionary<string>;
|
||||
setupRoutes?: (router: Router) => void;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { IndividualContext } from '../../../user';
|
||||
|
||||
export interface ICompanySpecificFeatureDemo {
|
||||
isDemoUser: (activeContext: IndividualContext) => boolean;
|
||||
getDemoUsers(): IDemoUser[];
|
||||
}
|
||||
|
||||
export interface IDemoUser {
|
||||
login: string;
|
||||
avatar: string;
|
||||
displayName: string;
|
||||
alias: string;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { ICompanySpecificFeatureDemo } from './demo';
|
||||
import { ICompanySpecificFeatureOrganizationSudo } from './organizationSudo';
|
||||
import { ICompanySpecificFeaturePortalSudo } from './portalSudo';
|
||||
import { ICompanySpecificFeatureRepositoryState } from './repositoryActions';
|
||||
|
||||
export * from './organizationSudo';
|
||||
export * from './portalSudo';
|
||||
export * from './demo';
|
||||
export * from './repositoryActions';
|
||||
|
||||
export interface ICompanySpecificFeatures {
|
||||
organizationSudo?: ICompanySpecificFeatureOrganizationSudo;
|
||||
portalSudo?: ICompanySpecificFeaturePortalSudo;
|
||||
demo?: ICompanySpecificFeatureDemo;
|
||||
repositoryActions?: ICompanySpecificFeatureRepositoryState;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { Organization } from '../../../business';
|
||||
import { IOrganizationSudo } from '../../../features';
|
||||
import { IProviders } from '../../../transitional';
|
||||
|
||||
export interface ICompanySpecificFeatureOrganizationSudo {
|
||||
tryCreateInstance: (providers: IProviders, organization: Organization) => IOrganizationSudo;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { IPortalSudo } from '../../../features';
|
||||
import { IProviders } from '../../../transitional';
|
||||
|
||||
export interface ICompanySpecificFeaturePortalSudo {
|
||||
tryCreateInstance: (providers: IProviders) => IPortalSudo;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { LocalApiRepoAction } from '../../../api/client/organization/repo';
|
||||
import { Repository } from '../../../business';
|
||||
import { IProviders } from '../../../transitional';
|
||||
import { IndividualContext } from '../../../user';
|
||||
|
||||
export interface ICompanySpecificRepositoryStateStatus {}
|
||||
|
||||
export interface ICompanySpecificFeatureRepositoryState {
|
||||
getCurrentRepositoryState(providers: IProviders, repository: Repository): Promise<ICompanySpecificRepositoryStateStatus>;
|
||||
sendActionReceipt(providers: IProviders, context: IndividualContext, repository: Repository, action: LocalApiRepoAction, currentState: ICompanySpecificRepositoryStateStatus): Promise<void>;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
export * from './routes';
|
||||
export * from './features';
|
||||
export * from './middleware';
|
||||
export * from './strings';
|
||||
export * from './administration';
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { Repository } from '../../business';
|
||||
import { IContextualRepositoryPermissions } from '../../middleware/github/repoPermissions';
|
||||
import { IProviders } from '../../transitional';
|
||||
import { IndividualContext } from '../../user';
|
||||
|
||||
export interface ICompanySpecificRepoPermissionsMiddlewareCalls {
|
||||
afterPermissionsInitialized?: (providers: IProviders, permissions: IContextualRepositoryPermissions, activeContext: IndividualContext) => void;
|
||||
afterPermissionsComputed?: (providers: IProviders, permissions: IContextualRepositoryPermissions, activeContext: IndividualContext, repository: Repository) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IAttachCompanySpecificMiddleware {
|
||||
repoPermissions?: ICompanySpecificRepoPermissionsMiddlewareCalls;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { ConnectRouter } from '..';
|
||||
|
||||
export interface IAttachCompanySpecificRoutesApi {
|
||||
index?: ConnectRouter;
|
||||
context?: IAttachCompanySpecificRoutesApiContextual;
|
||||
organization?: IAttachCompanySpecificRoutesApiOrganization;
|
||||
people?: ConnectRouter;
|
||||
}
|
||||
|
||||
export interface IAttachCompanySpecificRoutesApiContextual {
|
||||
index?: ConnectRouter;
|
||||
organization?: IAttachCompanySpecificRoutesApiContextualOrganization;
|
||||
}
|
||||
|
||||
export interface IAttachCompanySpecificRoutesApiContextualOrganization {
|
||||
index?: ConnectRouter;
|
||||
repo?: ConnectRouter;
|
||||
}
|
||||
|
||||
export interface IAttachCompanySpecificRoutesApiOrganization {
|
||||
index?: ConnectRouter;
|
||||
repo?: ConnectRouter;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { Router } from 'express';
|
||||
import { IAttachCompanySpecificRoutesApi } from './api';
|
||||
|
||||
export * from './api';
|
||||
|
||||
export type ConnectRouter = (router: Router) => void;
|
||||
|
||||
export interface IAttachCompanySpecificRoutes {
|
||||
connectAuthenticatedRoutes: (router: Router, reactRoute: any) => void;
|
||||
api?: IAttachCompanySpecificRoutesApi;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
export interface IAttachCompanySpecificStrings {
|
||||
largeTeamProtectionDetailsLink: string;
|
||||
linkWarningMessages: string[];
|
||||
linkInformationMessage: string;
|
||||
linkInformationUrl: string;
|
||||
linkInformationPolicyLink: string;
|
||||
linkInformationMail: string;
|
||||
linkInformationHeading: string;
|
||||
linkInformationUrlTitle: string;
|
||||
linkInformationMailTitle: string;
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
//
|
||||
// Copyright (c) Microsoft.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
import { Router } from 'express';
|
||||
import { Organization } from '../business';
|
||||
import { Repository } from '../business/repository';
|
||||
import { IOrganizationSudo, IPortalSudo } from '../features/sudo';
|
||||
import { IContextualRepositoryPermissions } from '../middleware/github/repoPermissions';
|
||||
import { IDictionary, IProviders } from '../transitional';
|
||||
import { IndividualContext } from '../user';
|
||||
|
||||
// We're great at long variable names at Microsoft!
|
||||
|
||||
export interface IAttachCompanySpecificRoutes {
|
||||
connectAuthenticatedRoutes: (router: Router, reactRoute: any) => void;
|
||||
connectCorporateApiRoutes: (router: Router) => void;
|
||||
}
|
||||
|
||||
export interface ICompanySpecificFeatureOrganizationSudo {
|
||||
tryCreateInstance: (providers: IProviders, organization: Organization) => IOrganizationSudo;
|
||||
}
|
||||
|
||||
export interface ICompanySpecificFeaturePortalSudo {
|
||||
tryCreateInstance: (providers: IProviders) => IPortalSudo;
|
||||
}
|
||||
|
||||
export interface ICompanySpecificFeatures {
|
||||
organizationSudo?: ICompanySpecificFeatureOrganizationSudo;
|
||||
portalSudo?: ICompanySpecificFeaturePortalSudo;
|
||||
}
|
||||
|
||||
export interface ICompanySpecificStartupProperties {
|
||||
routes?: IAttachCompanySpecificRoutes;
|
||||
middleware?: IAttachCompanySpecificMiddleware;
|
||||
administrationSection?: ICorporationAdministrationSection;
|
||||
strings?: IAttachCompanySpecificStrings;
|
||||
features?: ICompanySpecificFeatures;
|
||||
}
|
||||
|
||||
export interface IAttachCompanySpecificMiddleware {
|
||||
repoPermissions?: ICompanySpecificRepoPermissionsMiddlewareCalls;
|
||||
}
|
||||
|
||||
export interface IAttachCompanySpecificStrings {
|
||||
largeTeamProtectionDetailsLink: string;
|
||||
linkWarningMessages: string[];
|
||||
linkInformationMessage: string;
|
||||
linkInformationUrl: string;
|
||||
linkInformationPolicyLink: string;
|
||||
linkInformationMail: string;
|
||||
linkInformationHeading: string;
|
||||
linkInformationUrlTitle: string;
|
||||
linkInformationMailTitle: string;
|
||||
}
|
||||
|
||||
export interface ICorporationAdministrationSection {
|
||||
urls: IDictionary<string>;
|
||||
setupRoutes?: (router: Router) => void;
|
||||
}
|
||||
|
||||
export interface ICompanySpecificRepoPermissionsMiddlewareCalls {
|
||||
afterPermissionsInitialized?: (providers: IProviders, permissions: IContextualRepositoryPermissions, activeContext: IndividualContext) => void;
|
||||
afterPermissionsComputed?: (providers: IProviders, permissions: IContextualRepositoryPermissions, activeContext: IndividualContext, repository: Repository) => Promise<void>;
|
||||
}
|
||||
|
||||
export type ICompanySpecificStartupFunction = (config: any, p: IProviders, rootdir: string) => Promise<void>;
|
||||
|
||||
export type ICompanySpecificStartup = ICompanySpecificStartupFunction & ICompanySpecificStartupProperties;
|
|
@ -3,4 +3,21 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
export * from './companySpecificLightup';
|
||||
import { IProviders } from '../transitional';
|
||||
import { IAttachCompanySpecificRoutes, IAttachCompanySpecificMiddleware, ICorporationAdministrationSection, IAttachCompanySpecificStrings, ICompanySpecificFeatures } from './companySpecific';
|
||||
|
||||
export * from './companySpecific';
|
||||
|
||||
// We're great at long variable names!
|
||||
|
||||
export interface ICompanySpecificStartupProperties {
|
||||
routes?: IAttachCompanySpecificRoutes;
|
||||
middleware?: IAttachCompanySpecificMiddleware;
|
||||
administrationSection?: ICorporationAdministrationSection;
|
||||
strings?: IAttachCompanySpecificStrings;
|
||||
features?: ICompanySpecificFeatures;
|
||||
}
|
||||
|
||||
export type ICompanySpecificStartupFunction = (config: any, p: IProviders, rootdir: string) => Promise<void>;
|
||||
|
||||
export type ICompanySpecificStartup = ICompanySpecificStartupFunction & ICompanySpecificStartupProperties;
|
||||
|
|
Загрузка…
Ссылка в новой задаче