зеркало из
1
0
Форкнуть 0
Open sourcing an additional set of APIs that we use for the client
experience.
This commit is contained in:
Jeff Wilcox 2021-03-18 15:19:39 -07:00
Родитель 735361dfae
Коммит d1baf801ff
44 изменённых файлов: 2567 добавлений и 74 удалений

28
api/client/banner.ts Normal file
Просмотреть файл

@ -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));

74
api/client/jsonPager.ts Normal file
Просмотреть файл

@ -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;
}

100
api/client/linking.ts Normal file
Просмотреть файл

@ -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;

138
api/client/people.ts Normal file
Просмотреть файл

@ -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;

65
api/client/person.ts Normal file
Просмотреть файл

@ -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));
}
});

41
api/client/repos.ts Normal file
Просмотреть файл

@ -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;

40
api/client/session.ts Normal file
Просмотреть файл

@ -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;

65
api/client/teams.ts Normal file
Просмотреть файл

@ -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;