Skip to content

Commit

Permalink
Merge pull request #243 from desci-labs/contributor-apis
Browse files Browse the repository at this point in the history
Contributor APIs
  • Loading branch information
hubsmoke authored Mar 20, 2024
2 parents 01af526 + 64afd0d commit 0d621c2
Show file tree
Hide file tree
Showing 36 changed files with 3,311 additions and 79 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
# Examples:
uses: actions/setup-node@v2
with:
node-version: 16
node-version: 18

- name: Set up docker-compose
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
# Examples:
uses: actions/setup-node@v2
with:
node-version: 16
node-version: 18

- name: Set up docker-compose
run: |
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.16.0
18.17.0
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:16.20.0-bookworm
FROM node:18.17.0-bookworm

VOLUME /root/.yarn

Expand Down
5 changes: 4 additions & 1 deletion desci-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"docker:test": "CI=true docker-compose --file ../docker-compose.test.yml --compatibility up --exit-code-from nodes_backend_test",
"docker:stage": "docker-compose --file docker-compose.yml --file docker-compose.stage.yml --compatibility up --build",
"docker:prod": "../dockerProd.sh",
"podman:dev": "podman-compose --file docker-compose.yml --file docker-compose.dev.yml up --build"
"podman:dev": "podman-compose --file docker-compose.yml --file docker-compose.dev.yml up --build",
"email-dev": "email dev --dir ./src/templates/emails --port 3777"
},
"dependencies": {
"@automerge/automerge": "2.1.10",
Expand All @@ -59,6 +60,7 @@
"@penseapp/discord-notification": "^2.0.9",
"@prisma/client": "4.2.1",
"@quixo3/prisma-session-store": "^3.1.0",
"@react-email/components": "0.0.15",
"@sendgrid/mail": "^7.7.0",
"@sentry/node": "^7.12.0",
"@sentry/tracing": "^7.12.0",
Expand Down Expand Up @@ -104,6 +106,7 @@
"pino-pretty": "^10.0.0",
"prisma": "4.2.1",
"promise-parallel-throttle": "^3.3.0",
"react-email": "2.1.0",
"redis": "^4.6.7",
"reflect-metadata": "^0.1.13",
"rimraf": "^5.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "NodeContribution" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"contributorId" TEXT NOT NULL,
"nodeId" INTEGER NOT NULL,
"userId" INTEGER,
"verified" BOOLEAN NOT NULL DEFAULT false,
"email" TEXT NOT NULL,

CONSTRAINT "NodeContribution_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "NodeContribution_contributorId_key" ON "NodeContribution"("contributorId");

-- CreateIndex
CREATE UNIQUE INDEX "NodeContribution_contributorId_nodeId_userId_key" ON "NodeContribution"("contributorId", "nodeId", "userId");

-- AddForeignKey
ALTER TABLE "NodeContribution" ADD CONSTRAINT "NodeContribution_nodeId_fkey" FOREIGN KEY ("nodeId") REFERENCES "Node"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "NodeContribution" ADD CONSTRAINT "NodeContribution_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "NodeContribution" ADD COLUMN "orcid" TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "NodeContribution" ALTER COLUMN "email" DROP NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "NodeContribution" ADD COLUMN "deleted" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "deletedAt" TIMESTAMP(3);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- DropIndex
DROP INDEX "PrivateShare_nodeUUID_key";

-- AlterTable
ALTER TABLE "PrivateShare" ADD COLUMN "memo" TEXT;

-- CreateIndex
CREATE INDEX "PrivateShare_nodeUUID_idx" ON "PrivateShare"("nodeUUID");
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "NodeContribution" ADD COLUMN "denied" BOOLEAN NOT NULL DEFAULT false;
33 changes: 28 additions & 5 deletions desci-server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ model Node {
ownerId Int
uuid String? @unique @default(uuid())
manifestDocumentId String @default("")
privateShare PrivateShare?
owner User @relation(fields: [ownerId], references: [id])
authorInvites AuthorInvite[]
transactions ChainTransaction[]
Expand All @@ -41,6 +40,8 @@ model Node {
NodeAttestation NodeAttestation[]
NodeThumbnails NodeThumbnails[]
PublishTaskQueue PublishTaskQueue[]
NodeContribution NodeContribution[]
PrivateShare PrivateShare[]
@@index([ownerId])
@@index([uuid])
Expand Down Expand Up @@ -173,7 +174,6 @@ model User {
maxDriveStorageLimitGb Int @default(500)
userOrganizations UserOrganizations[]
UploadJobs UploadJobs[]
// DraftNodeTree DraftNodeTree[]
NodeFeedItemEndorsement NodeFeedItemEndorsement[]
DesciCommunity DesciCommunity? @relation(fields: [desciCommunityId], references: [id])
desciCommunityId Int?
Expand All @@ -184,6 +184,7 @@ model User {
NodeAttestationReaction NodeAttestationReaction[]
ApiKey ApiKey[]
PublishTaskQueue PublishTaskQueue[]
NodeContribution NodeContribution[]
@@index([orcid])
@@index([walletAddress])
Expand Down Expand Up @@ -440,11 +441,13 @@ model PublicDataReferenceOnIpfsMirror {
model PrivateShare {
id Int @id @default(autoincrement())
shareId String @unique
nodeUUID String @unique
nodeUUID String
memo String?
node Node @relation(fields: [nodeUUID], references: [uuid])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([nodeUUID])
@@index([shareId])
}

Expand All @@ -470,6 +473,26 @@ model NodeThumbnails {
@@unique([nodeUuid, componentCid])
}

// The glue between a manifest contributor entry and a nodes profile
model NodeContribution {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
contributorId String @unique
nodeId Int
userId Int?
verified Boolean @default(false)
denied Boolean @default(false)
email String?
orcid String?
deleted Boolean @default(false)
deletedAt DateTime?
node Node @relation(fields: [nodeId], references: [id])
user User? @relation(fields: [userId], references: [id])
@@unique([contributorId, nodeId, userId])
}

model FriendReferral {
id Int @id @default(autoincrement())
uuid String @unique @default(uuid())
Expand Down Expand Up @@ -756,8 +779,8 @@ model PublishTaskQueue {
status PublishTaskQueueStatus
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
node Node @relation(fields: [uuid], references: [uuid])
user User @relation(fields: [userId], references: [id])
node Node @relation(fields: [uuid], references: [uuid])
user User @relation(fields: [userId], references: [id])
}

enum CommunityMembershipRole {
Expand Down
99 changes: 99 additions & 0 deletions desci-server/src/controllers/nodes/contributions/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Node, User } from '@prisma/client';
import sgMail from '@sendgrid/mail';
import { Request, Response } from 'express';

import { logger as parentLogger } from '../../../logger.js';
import { contributorService } from '../../../services/Contributors.js';
import { ContributorInviteEmailHtml } from '../../../templates/emails/utils/emailRenderer.js';

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

export type AddContributorReqBody = {
contributorId: string;
email?: string;
orcid?: string;
userId?: number;
};

export type AddContributorRequest = Request<never, never, AddContributorReqBody> & {
user: User; // added by auth middleware
node: Node; // added by ensureWriteAccess middleware
};

export type AddContributorResBody =
| {
ok: boolean;
message: string;
}
| {
error: string;
};

export const addContributor = async (req: AddContributorRequest, res: Response<AddContributorResBody>) => {
const node = req.node;
const user = req.user;

if (!node || !user)
throw Error('Middleware not properly setup for addContributor controller, requires req.node and req.user');

const { contributorId, orcid, userId } = req.body;
let { email } = req.body;
if (email) email = email.toLowerCase();
const logger = parentLogger.child({
module: 'Contributors::createController',
body: req.body,
uuid: node.uuid,
user: req.user,
nodeId: node.id,
});

if (!contributorId) {
return res.status(400).json({ error: 'contributorId required' });
}
if (!userId && !email && !orcid) {
return res.status(400).json({ error: 'userId, Email or Orcid required' });
}
// debugger;
// Add contributor to the db
try {
const contributorAdded = await contributorService.addNodeContribution({
node,
nodeOwner: user,
contributorId,
email,
orcid,
userId,
});
if (!contributorAdded) throw Error('Failed to add contributor');
if (user.id !== contributorAdded.userId && contributorAdded.email) {
// Generate a share code for the contributor if it's the node owner themselves
const shareCode = await contributorService.generatePrivShareCodeForContribution(contributorAdded, node);

// Future: make it count as a friend referral
const emailHtml = ContributorInviteEmailHtml({
inviter: user.name,
nodeUuid: node.uuid,
privShareCode: shareCode,
contributorId: contributorAdded.contributorId,
newUser: contributorAdded.userId !== undefined,
});
const emailMsg = {
to: email,
from: 'no-reply@desci.com',
subject: `[nodes.desci.com] ${user.name} has added you as a contributor to their research node.`,
text: `You've been added as a contributor to ${node.title}. Confirm your contribution to ensure you're credited for your work.
Your private share code: ${shareCode}`,
html: emailHtml,
};

sgMail.send(emailMsg);
}
logger.info({ contributorAdded }, 'Contributor added successfully');
return res.status(200).json({ ok: true, message: 'Contributor added successfully' });
} catch (e) {
logger.error({ e }, 'Failed to add contributor');
return res.status(500).json({ error: 'Failed to add contributor' });
}

return res.status(500).json({ error: 'Something went wrong' });
};
59 changes: 59 additions & 0 deletions desci-server/src/controllers/nodes/contributions/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Node, User } from '@prisma/client';
import { Request, Response } from 'express';

import { logger as parentLogger } from '../../../logger.js';
import { contributorService } from '../../../services/Contributors.js';

export type DeleteContributorReqBody = {
contributorId: string;
};

export type DeleteContributorRequest = Request<never, never, DeleteContributorReqBody> & {
user: User; // added by auth middleware
node: Node; // added by ensureWriteAccess middleware
};

export type DeleteContributorResBody =
| {
ok: boolean;
message: string;
}
| {
error: string;
};

export const deleteContributor = async (req: DeleteContributorRequest, res: Response<DeleteContributorResBody>) => {
const node = req.node;
const user = req.user;

if (!node || !user)
throw Error('Middleware not properly setup for addContributor controller, requires req.node and req.user');

const { contributorId } = req.body;

const logger = parentLogger.child({
module: 'Contributors::deleteContributorController',
body: req.body,
uuid: node.uuid,
user: (req as any).user,
nodeId: node.id,
});

if (!contributorId) {
return res.status(400).json({ error: 'contributorId required' });
}

// Remove contributor entry from the db
try {
const contributorRemoved = await contributorService.removeContributor(contributorId, node.id);
if (contributorRemoved) {
logger.info('Contributor deleted successfully');
return res.status(200).json({ ok: true, message: 'Contributor deleted successfully' });
}
} catch (e) {
logger.error({ e }, 'Failed to delete contributor');
return res.status(500).json({ error: 'Failed to delete contributor' });
}

return res.status(500).json({ error: 'Something went wrong' });
};
Loading

0 comments on commit 0d621c2

Please sign in to comment.