From abe53510a65af1e4a6f88b63a61d55a08f892a1a Mon Sep 17 00:00:00 2001 From: kadami Date: Thu, 18 Apr 2024 03:04:06 +0700 Subject: [PATCH 1/2] add controller for retrieving nodes shared with a user --- .../controllers/nodes/contributions/delete.ts | 2 +- .../src/controllers/nodes/sharedNodes.ts | 106 ++++++++++++++++++ desci-server/src/services/Contributors.ts | 2 +- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 desci-server/src/controllers/nodes/sharedNodes.ts diff --git a/desci-server/src/controllers/nodes/contributions/delete.ts b/desci-server/src/controllers/nodes/contributions/delete.ts index 0664362b8..07c01e6e7 100644 --- a/desci-server/src/controllers/nodes/contributions/delete.ts +++ b/desci-server/src/controllers/nodes/contributions/delete.ts @@ -27,7 +27,7 @@ export const deleteContributor = async (req: DeleteContributorRequest, res: Resp const user = req.user; if (!node || !user) - throw Error('Middleware not properly setup for addContributor controller, requires req.node and req.user'); + throw Error('Middleware not properly setup for deleteContributor controller, requires req.node and req.user'); const { contributorId } = req.body; diff --git a/desci-server/src/controllers/nodes/sharedNodes.ts b/desci-server/src/controllers/nodes/sharedNodes.ts new file mode 100644 index 000000000..6f8057a5f --- /dev/null +++ b/desci-server/src/controllers/nodes/sharedNodes.ts @@ -0,0 +1,106 @@ +import { IpldUrl, ResearchObjectV1Dpid } from '@desci-labs/desci-models'; +import { User } from '@prisma/client'; +import { Request, Response } from 'express'; + +import { prisma } from '../../client.js'; +import { logger as parentLogger } from '../../logger.js'; +import { PRIV_SHARE_CONTRIBUTION_PREFIX } from '../../services/Contributors.js'; +import { getManifestFromNode } from '../../services/data/processing.js'; +import { getIndexedResearchObjects } from '../../theGraph.js'; + +export type SharedNode = { + uuid: string; + manifestCid: string; + title?: string; + versions: number; + coverImageCid?: string | IpldUrl; + published?: boolean; + dpid?: ResearchObjectV1Dpid; + publishDate?: string; + shareKey: string; +}; + +export type GetSharedNodesRequest = Request & { + user: User; // added by auth middleware +}; + +export type GetSharedNodesResBody = + | { + ok: boolean; + sharedNodes: SharedNode[]; + } + | { + error: string; + }; + +export const getSharedNodes = async (req: GetSharedNodesRequest, res: Response) => { + const user = req.user; + + if (!user) throw Error('Middleware not properly setup for getSharedNodes controller, requires req.user'); + + const logger = parentLogger.child({ + module: 'PrivateShare::GetSharedNodesController', + body: req.body, + userId: user.id, + }); + + if (!user.email) { + logger.warn('User does not have an email, no nodes can be shared with the user.'); + return res.status(500).json({ error: 'User does not have an email' }); + } + + try { + const privSharedNodes = await prisma.privateShare.findMany({ + where: { + memo: `${PRIV_SHARE_CONTRIBUTION_PREFIX}${user.email}`, + }, + include: { + node: true, + }, + }); + + if (privSharedNodes?.length === 0) { + return res.status(200).json({ ok: true, sharedNodes: [] }); + } + + const nodeUuids = privSharedNodes.map((priv) => priv.node.uuid); + const { researchObjects } = await getIndexedResearchObjects(nodeUuids); + const publishedNodesMap = researchObjects.reduce((acc, ro) => { + // convert hex string to integer + const nodeUuidInt = Buffer.from(ro.id.substring(2), 'hex'); + // convert integer to hex + const nodeUuid = nodeUuidInt.toString('base64url'); + acc[nodeUuid] = ro; + }, {}); + + const filledSharedNodes = await Promise.all( + privSharedNodes.map(async (priv) => { + const { node } = priv; + const { manifest: latestManifest } = await getManifestFromNode(node); + const publishedEntry = publishedNodesMap[node.uuid]; + + return { + uuid: node.uuid, + manifestCid: node.manifestUrl, + title: latestManifest.title, + versions: publishedEntry?.versions.length, + coverImageCid: latestManifest.coverImage, + dpid: latestManifest.dpid, + publishDate: publishedEntry?.versions[0].time, + published: !!publishedEntry, + shareKey: priv.shareId, + }; + }), + ); + + if (filledSharedNodes) { + logger.info({ totalSharedNodesFound: filledSharedNodes.length }, 'Shared nodes retrieved successfully'); + return res.status(200).json({ ok: true, sharedNodes: filledSharedNodes }); + } + } catch (e) { + logger.error({ e }, 'Failed to retrieve shared nodes for user'); + return res.status(500).json({ error: 'Failed to retrieve shared nodes' }); + } + + return res.status(500).json({ error: 'Something went wrong' }); +}; diff --git a/desci-server/src/services/Contributors.ts b/desci-server/src/services/Contributors.ts index 55b9e72a3..9b52f9835 100644 --- a/desci-server/src/services/Contributors.ts +++ b/desci-server/src/services/Contributors.ts @@ -52,7 +52,7 @@ export type AddNodeContributionParams = { userId?: number; }; -const PRIV_SHARE_CONTRIBUTION_PREFIX = 'C-'; +export const PRIV_SHARE_CONTRIBUTION_PREFIX = 'C-'; class ContributorService { private logger = parentLogger.child({ module: 'Services::ContributorsService' }); From c74a2e421f8b4346c6726e28a105d866948af881 Mon Sep 17 00:00:00 2001 From: kadami Date: Thu, 18 Apr 2024 03:11:03 +0700 Subject: [PATCH 2/2] add route for retrieval --- desci-server/src/controllers/nodes/sharedNodes.ts | 10 +++++----- desci-server/src/routes/v1/nodes.ts | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/desci-server/src/controllers/nodes/sharedNodes.ts b/desci-server/src/controllers/nodes/sharedNodes.ts index 6f8057a5f..8c1de0e81 100644 --- a/desci-server/src/controllers/nodes/sharedNodes.ts +++ b/desci-server/src/controllers/nodes/sharedNodes.ts @@ -20,11 +20,11 @@ export type SharedNode = { shareKey: string; }; -export type GetSharedNodesRequest = Request & { +export type ListSharedNodesRequest = Request & { user: User; // added by auth middleware }; -export type GetSharedNodesResBody = +export type ListSharedNodesResBody = | { ok: boolean; sharedNodes: SharedNode[]; @@ -33,13 +33,13 @@ export type GetSharedNodesResBody = error: string; }; -export const getSharedNodes = async (req: GetSharedNodesRequest, res: Response) => { +export const listSharedNodes = async (req: ListSharedNodesRequest, res: Response) => { const user = req.user; - if (!user) throw Error('Middleware not properly setup for getSharedNodes controller, requires req.user'); + if (!user) throw Error('Middleware not properly setup for ListSharedNodes controller, requires req.user'); const logger = parentLogger.child({ - module: 'PrivateShare::GetSharedNodesController', + module: 'PrivateShare::ListSharedNodesController', body: req.body, userId: user.id, }); diff --git a/desci-server/src/routes/v1/nodes.ts b/desci-server/src/routes/v1/nodes.ts index 00d843939..b3b06ca66 100755 --- a/desci-server/src/routes/v1/nodes.ts +++ b/desci-server/src/routes/v1/nodes.ts @@ -4,6 +4,7 @@ import { addContributor } from '../../controllers/nodes/contributions/create.js' import { deleteContributor } from '../../controllers/nodes/contributions/delete.js'; import { getNodeContributions } from '../../controllers/nodes/contributions/getNodeContributions.js'; import { getUserContributions } from '../../controllers/nodes/contributions/getUserContributions.js'; +import { getUserContributionsAuthed } from '../../controllers/nodes/contributions/getUserContributionsAuthed.js'; import { updateContributor } from '../../controllers/nodes/contributions/update.js'; import { verifyContribution } from '../../controllers/nodes/contributions/verify.js'; import { dispatchDocumentChange, getNodeDocument } from '../../controllers/nodes/documents.js'; @@ -32,12 +33,12 @@ import { } from '../../controllers/nodes/index.js'; import { retrieveTitle } from '../../controllers/nodes/legacyManifestApi.js'; import { prepublish } from '../../controllers/nodes/prepublish.js'; +import { listSharedNodes } from '../../controllers/nodes/sharedNodes.js'; import { thumbnails } from '../../controllers/nodes/thumbnails.js'; import { versionDetails } from '../../controllers/nodes/versionDetails.js'; import { asyncHander, attachUser, validate } from '../../internal.js'; import { ensureNodeAccess, ensureWriteNodeAccess } from '../../middleware/authorisation.js'; import { ensureUser } from '../../middleware/permissions.js'; -import { getUserContributionsAuthed } from '../../controllers/nodes/contributions/getUserContributionsAuthed.js'; const router = Router(); @@ -60,6 +61,7 @@ router.get( ); router.post('/terms', [ensureUser], consent); router.get('/share/verify/:shareId', checkPrivateShareId); +router.get('/share', [ensureUser], listSharedNodes); router.get('/share/:uuid', [ensureUser], getPrivateShare); router.post('/share/:uuid', [ensureUser], createPrivateShare); router.post('/revokeShare/:uuid', [ensureUser], revokePrivateShare);