Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Public attestation endpoints #16

Merged
merged 3 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ module.exports ={
preset: "ts-jest",
testEnvironment : "node",
testMatch : ["**/**/*test.ts"],
coveragePathIgnorePatterns: [
"<rootDir>/src/app/app.ts",
"<rootDir>/src/app/server.ts",
"<rootDir>/src/app/utils/utils.ts",
],
verbose: true,
forceExit: true,
testTimeout : 50000,
Expand Down
91 changes: 91 additions & 0 deletions openapi/PublicProjects.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
openapi: 3.0.0
info:
title: Public Projects API
version: 1.0.0
paths:
/public/projects:
get:
summary: View all projects
responses:
'200':
description: List of all public projects
content:
application/json:
schema:
type: array
items:
$ref: './schemas/Response/ProjectResponse.yaml'
/public/projects/{projectId}:
get:
summary: Public Project Overview
parameters:
- in: path
name: projectId
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Project details with history and events
content:
application/json:
schema:
type: object
properties:
projectDetails:
$ref: './schemas/Response/ProjectResponse.yaml'
events:
type: array
items:
oneOf:
- $ref: './schemas/Response/EventResponse.yaml'
- $ref: './schemas/Response/ProjectDiffResponse.yaml'
'404':
description: Project ID not found
/public/projects/{projectId}/events:
get:
summary: View all Events on a public project
parameters:
- in: path
name: projectId
required: true
schema:
type: string
format: uuid
responses:
'200':
description: List of events for the project
content:
application/json:
schema:
type: array
items:
$ref: './schemas/Response/EventResponse.yaml'
'404':
description: Project ID not found
/public/projects/{projectId}/events/{eventId}:
get:
summary: View Single Event on a public project
parameters:
- in: path
name: projectId
required: true
schema:
type: string
format: uuid
- in: path
name: eventId
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Event details
content:
application/json:
schema:
$ref: './schemas/Response/EventResponse.yaml'
'404':
description: Project ID or Event ID not found
2 changes: 1 addition & 1 deletion openapi/projects.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
openapi: 3.0.0
info:
title: Project Collaborators API
title: Projects API
version: 1.0.0
tags:
- name: projectCollaborators_other
Expand Down
5 changes: 5 additions & 0 deletions openapi/schemas/Request/ProjectRequest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ properties:
type: string
maxLength: 1024
pattern: '^\S(.*\S)?$'
public:
type: boolean
# Default value is false
default: false
description: Can only make projects public on creation, once project is public it cannot be made private
projectStatus:
type: string
enum:
Expand Down
4 changes: 4 additions & 0 deletions openapi/schemas/Response/ProjectResponse.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ properties:
type: array
items:
$ref: './ProjectCollaborator.yaml'
public:
type: boolean

required:
- projectId
- startedDate
- projectName
- projectStatus
- ProjectCollaborators
- public
12 changes: 8 additions & 4 deletions redocly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ apis:
applications:
root: ./openapi/applications.yaml
x-openapi-ts:
output: .\src\app\models\openapi\applications.ts
output: ./src/app/models/openapi/applications.ts
authentication:
root: ./openapi/authentication.yaml
x-openapi-ts:
output: .\src\app\models\openapi\authentication.ts
output: ./src/app/models/openapi/authentication.ts
collaborators:
root: ./openapi/collaborators.yaml
x-openapi-ts:
output: .\src\app\models\openapi\collaborators.ts
output: ./src/app/models/openapi/collaborators.ts
projects:
root: ./openapi/projects.yaml
x-openapi-ts:
output: .\src\app\models\openapi\projects.ts
output: ./src/app/models/openapi/projects.ts
publicProjects:
root: ./openapi/PublicProjects.yaml
x-openapi-ts:
output: ./src/app/models/openapi/PublicProjects.ts
6 changes: 5 additions & 1 deletion src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import { ApplicationRouter } from "./routes/applicationsRouter";
import { authenticationRouter } from "./routes/AuthenticationRouter";
import { collaboratorsRouter } from "./routes/CollaboratorsRouter";
import { ProjectEventRouter } from "./routes/ProjectsEventsRouter";
import { PublicProjectsRouter } from "./routes/publicProjectsRouter";

export const app = express();

app.use(morgan("dev"));
if (process.env.NODE_ENV !== 'test') {
app.use(morgan("dev"));
}
app.use(express.json());

app.use("/user", authenticationRouter);
app.use("/projects", ProjectEventRouter);
app.use("/public/projects", PublicProjectsRouter);
app.use("/collaborators", collaboratorsRouter);
app.use("/app", ApplicationRouter);

Expand Down
6 changes: 0 additions & 6 deletions src/app/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,6 @@ export class UserInputError extends HttpError {
}
}

export class NotImplimentedError extends HttpError {
constructor(message: string) {
super(501, message);
}
}

export class MethodNotAllowedError extends HttpError {
constructor(message: string) {
super(405, message);
Expand Down
38 changes: 37 additions & 1 deletion src/app/models/database/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import mongoose, { HydratedDocument, Model, model, Schema } from "mongoose";

import { ServerError } from "../../errors/errors";
import { ServerError, UserInputError } from "../../errors/errors";
import {
EventResponse,
ProjectCollaborator,
Expand All @@ -26,6 +26,7 @@ interface IProjectArgs {
collaboratorTenancy: mongoose.Types.ObjectId;
diffs: ProjectDiffResponse[];
collaborators: mongoose.Types.ObjectId[];
public: boolean;
}

// Declare the attributes of the model
Expand Down Expand Up @@ -75,6 +76,7 @@ const ProjectSchema = new Schema<IProject, IProjectModel, IProjectMethods, IProj
events: [{ type: Schema.Types.ObjectId, ref: "Event" }],
diffs: [{ type: Schema.Types.Mixed, required: true }],
collaborators: [{ type: Schema.Types.ObjectId, ref: "diffCollaborators" }],
public: { type: Boolean, required: true },
});

ProjectSchema.static(
Expand All @@ -85,6 +87,20 @@ ProjectSchema.static(
): Promise<ProjectDocument> {
const customMetaData = (projectInfo?.customMetaData as { [key: string]: string }) || {};
const collaborators = projectInfo.collaborators?.map((id) => new mongoose.Types.ObjectId(id)) || [];
const _public = projectInfo.public || false;

// If the project is public, add the public tenant to the collaborators
const PublicTenant = await Tenancy.getPublicTenant();

if (_public) {
collaborators.push(PublicTenant._id);
} else {
// check that the public tenant is not in the collaborators list
// do a string comparison of the ids to make sure that the public tenant is not in the list
if (collaborators.map((id) => id.toString()).includes(PublicTenant._id.toString())) {
throw new UserInputError("Cannot add public tenant to a private project");
}
}

const projectObj: IProject = {
_id: new mongoose.Types.ObjectId(),
Expand All @@ -98,6 +114,7 @@ ProjectSchema.static(
OwnerTenancy: tenancy._id,
collaboratorTenancy: tenancy._id,
diffs: [],
public: _public,

// technically the owner shares the project with themselves, makes it easier to apply diffs and push events
collaborators: [tenancy._id, ...collaborators],
Expand Down Expand Up @@ -137,6 +154,7 @@ ProjectSchema.static(
collaboratorTenancy: collaborator._id,
diffs: [],
collaborators: project.collaborators,
public: project.public,
};

// push the project to the collaborator's project list
Expand Down Expand Up @@ -176,6 +194,7 @@ ProjectSchema.method("ToProjectResponse", async function ToProjectResponse(): Pr
projectDescription: this.projectDescription,
projectStatus: this.projectStatus,
ProjectCollaborators: await this.ListProjectCollaborators(),
public: this.public,
};
return obj;
});
Expand All @@ -186,6 +205,12 @@ ProjectSchema.method("ListProjectCollaborators", async function ListProjectColla
// Filter out owner from the collaborators
const collaborators = this.collaborators.filter((id) => !id.equals(this.OwnerTenancy));

// if public, filter out the public tenant
if (this.public) {
const PublicTenant = await Tenancy.getPublicTenant();
collaborators.filter((id) => !id.equals(PublicTenant._id));
}

// Map each collaborator ID to a promise of fetching the collaborator
const collaboratorPromises = collaborators.map((collaboratorID) => Tenancy.findById(collaboratorID));

Expand Down Expand Up @@ -278,8 +303,19 @@ ProjectSchema.method("applyDiff", async function applyDiff(diff: ProjectDiffRequ
// The new collaborators value that will be set
const Collaborators = [...new Set([this.OwnerTenancy, ...diffCollaborators])];

// Make sure that the public tenant is always in the collaborators list
const PublicTenant = await Tenancy.getPublicTenant();
if (this.public) {
Collaborators.push(PublicTenant._id);
}
// Get the new collaborators that were added and create a copy of the project for them
const newCollaborators = diffCollaborators.filter((id) => !this.collaborators.includes(id));

// make sure no one is trying to add the public tenant as a collaborator without the project being public
if (newCollaborators.includes(PublicTenant._id) && !this.public) {
throw new UserInputError("Cannot add public tenant to a private project");
}

await tenancy.CheckCollaboratorsAreActive(newCollaborators);
await this.CreateCopiesForCollaborators(newCollaborators);

Expand Down
44 changes: 36 additions & 8 deletions src/app/models/database/tenancy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { ProjectCollaborator, ProjectResponse } from "../types/projects";
import { Project } from "./project";
import { RelationshipManager } from "./relationshipManager";

const publicTenantName = "public-transparency-service";

export interface NewTenancyOptions {
newPublicTenant?: boolean;
}

// Declare the attributes of the model
interface ITenancy {
_id: mongoose.Types.ObjectId;
Expand Down Expand Up @@ -37,7 +43,8 @@ export type TenancyDocument = HydratedDocument<ITenancy, ITenancyMethods>;

// declare the Full Model along with any static methods
export interface ITenancyModel extends Model<ITenancy, ITenancyQueryHelpers, ITenancyMethods> {
NewTenancy: (companyName: string) => Promise<TenancyDocument>;
NewTenancy: (companyName: string, opts?: NewTenancyOptions) => Promise<TenancyDocument>;
getPublicTenant: () => Promise<TenancyDocument>;
}

// Create the schema
Expand All @@ -48,13 +55,34 @@ const TenancySchema = new Schema<ITenancy, ITenancyModel, ITenancyMethods>({
relationships: [{ type: Schema.Types.ObjectId, ref: RelationshipManager }],
});

TenancySchema.static("NewTenancy", async function NewTenancy(companyName: string): Promise<TenancyDocument> {
return this.create({
_id: new mongoose.Types.ObjectId(),
projects: [],
relationships: [],
companyName: companyName,
});
TenancySchema.static(
"NewTenancy",
async function NewTenancy(companyName: string, opts = { newPublicTenant: false }): Promise<TenancyDocument> {
// check if the tenancy is trying to overwrite the public tenant
if (!opts.newPublicTenant) {
if (companyName === publicTenantName) {
throw new UserInputError("Invalid company name");
}
}

return this.create({
_id: new mongoose.Types.ObjectId(),
projects: [],
relationships: [],
companyName: companyName,
});
}
);

TenancySchema.static("getPublicTenant", async function getPublicTenant(): Promise<TenancyDocument> {
// if the public tenant does not exist, create it
let publicTenant = await this.findOne({ companyName: publicTenantName });

if (publicTenant == null) {
publicTenant = await this.NewTenancy(publicTenantName, { newPublicTenant: true });
}

return publicTenant;
});

TenancySchema.method("ListProjects", async function ListProjects(): Promise<ProjectResponse[]> {
Expand Down
Loading
Loading