Skip to content

Commit

Permalink
Merge pull request #683 from desci-labs/feat/auto-mint-doi
Browse files Browse the repository at this point in the history
feat: Attestation Privileges and Automated DOI Workflow (publish and claim verification)
  • Loading branch information
shadrach-tayo authored Dec 2, 2024
2 parents d2b1a0c + 814b0c0 commit ca8f110
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Attestation" ADD COLUMN "canMintDoi" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "canUpdateOrcid" BOOLEAN NOT NULL DEFAULT false;
2 changes: 2 additions & 0 deletions desci-server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,8 @@ model Attestation {
protected Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
canMintDoi Boolean @default(false)
canUpdateOrcid Boolean @default(false)
community DesciCommunity @relation(fields: [communityId], references: [id])
template AttestationTemplate? @relation(fields: [templateId], references: [id])
AttestationVersion AttestationVersion[]
Expand Down
8 changes: 8 additions & 0 deletions desci-server/src/controllers/admin/communities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,16 @@ export const createAttestation = async (req: Request, res: Response, _next: Next
if (!image_url) throw new BadRequestError('No attestation logo uploaded');

const isProtected = body.protected.toString() === 'true' ? true : false;
const doiPrivilege = body.canMintDoi.toString() === 'true' ? true : false;
const orcidPrivilege = body.canUpdateOrcid.toString() === 'true' ? true : false;
const attestation = await attestationService.create({
...body,
image_url,
verified_image_url,
communityId: community.id,
protected: isProtected,
canMintDoi: doiPrivilege,
canUpdateOrcid: orcidPrivilege,
});
// logger.trace({ attestation }, 'created');
const AttestationVersion = await attestationService.getAttestationVersions(attestation.id);
Expand Down Expand Up @@ -311,12 +315,16 @@ export const updateAttestation = async (req: Request, res: Response, _next: Next
if (!image_url) throw new BadRequestError('No attestation image uploaded');

const isProtected = body.protected.toString() === 'true' ? true : false;
const doiPrivilege = body.canMintDoi.toString() === 'true' ? true : false;
const orcidPrivilege = body.canUpdateOrcid.toString() === 'true' ? true : false;
const attestation = await attestationService.updateAttestation(exists.id, {
...body,
image_url,
verified_image_url,
communityId: exists.communityId,
protected: isProtected,
canMintDoi: doiPrivilege,
canUpdateOrcid: orcidPrivilege,
});
new SuccessResponse(attestation).send(res);
};
Expand Down
43 changes: 43 additions & 0 deletions desci-server/src/controllers/admin/doi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextFunction, Response } from 'express';
import { Request } from 'express';
import _ from 'lodash';

import { BadRequestError } from '../../../core/ApiError.js';
import { SuccessResponse } from '../../../core/ApiResponse.js';
import { MintError } from '../../../core/doi/error.js';
import { logger as parentLogger } from '../../../logger.js';
import { RequestWithUser } from '../../../middleware/authorisation.js';
import { getTargetDpidUrl } from '../../../services/fixDpid.js';
import { doiService } from '../../../services/index.js';
import { DiscordChannel, discordNotify, DiscordNotifyType } from '../../../utils/discordUtils.js';
import { ensureUuidEndsWithDot } from '../../../utils.js';

const logger = parentLogger.child({ module: 'ADMIN::DOI' });

export const listDoiRecords = async (_req: RequestWithUser, res: Response, _next: NextFunction) => {
const data = await doiService.listDoi();
logger.info({ data }, 'List DOIs');
new SuccessResponse(data).send(res);
};

export const mintDoi = async (req: Request, res: Response, _next: NextFunction) => {
const { uuid } = req.params;
if (!uuid) throw new BadRequestError();
const sanitizedUuid = ensureUuidEndsWithDot(uuid);
const isPending = await doiService.hasPendingSubmission(sanitizedUuid);
if (isPending) {
throw new MintError('You have a pending submission');
} else {
const submission = await doiService.mintDoi(sanitizedUuid);
const data = _.pick(submission, ['id', 'status']);
new SuccessResponse(data).send(res);

const targetDpidUrl = getTargetDpidUrl();
discordNotify({
channel: DiscordChannel.DoiMinting,
type: DiscordNotifyType.INFO,
title: 'Mint DOI',
message: `${targetDpidUrl}/${submission.dpid} sent a request to mint: ${submission.uniqueDoi}`,
});
}
};
31 changes: 27 additions & 4 deletions desci-server/src/controllers/attestations/verification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionType } from '@prisma/client';
import { ActionType, Prisma, User } from '@prisma/client';
import { NextFunction, Request, Response } from 'express';
// import { Attestation, NodeAttestation } from '@prisma/client';
import _ from 'lodash';
Expand All @@ -8,9 +8,12 @@ import { ForbiddenError } from '../../core/ApiError.js';
import { SuccessMessageResponse, SuccessResponse } from '../../core/ApiResponse.js';
import { logger as parentLogger } from '../../logger.js';
import { attestationService } from '../../services/Attestation.js';
import { getTargetDpidUrl } from '../../services/fixDpid.js';
import { doiService } from '../../services/index.js';
import { saveInteraction, saveInteractionWithoutReq } from '../../services/interactionLog.js';
import { emitNotificationOnAttestationValidation } from '../../services/NotificationService.js';
import orcidApiService from '../../services/orcid.js';
import { DiscordChannel, discordNotify, DiscordNotifyType } from '../../utils/discordUtils.js';
import { ensureUuidEndsWithDot } from '../../utils.js';

type RemoveVerificationBody = {
Expand Down Expand Up @@ -105,8 +108,12 @@ export const addVerification = async (
/**
* Update ORCID Profile
*/
const node = await prisma.node.findFirst({ where: { uuid: ensureUuidEndsWithDot(claim.nodeUuid) } });
const owner = await prisma.user.findFirst({ where: { id: node.ownerId } });
const node = await prisma.node.findFirst({
where: { uuid: ensureUuidEndsWithDot(claim.nodeUuid) },
include: { owner: { select: { id: true, orcid: true } } },
});

const owner = node.owner as User; // await prisma.user.findFirst({ where: { id: node.ownerId } });
if (owner.orcid) await orcidApiService.postWorkRecord(node.uuid, owner.orcid, node.dpidAlias.toString());
await saveInteractionWithoutReq(ActionType.UPDATE_ORCID_RECORD, {
ownerId: owner.id,
Expand All @@ -115,10 +122,26 @@ export const addVerification = async (
claimId,
});

if (attestation.canMintDoi) {
// trigger doi minting workflow
try {
const submission = await doiService.autoMintTrigger(node.uuid);
const targetDpidUrl = getTargetDpidUrl();
discordNotify({
channel: DiscordChannel.DoiMinting,
type: DiscordNotifyType.INFO,
title: 'Mint DOI',
message: `${targetDpidUrl}/${submission.dpid} sent a request to mint: ${submission.uniqueDoi}`,
});
} catch (err) {
logger.error({ err }, 'Error: Mint DOI on Publish');
}
}

/**
* Fire off notification
*/
await emitNotificationOnAttestationValidation({ node, user, claimId: parseInt(claimId) });
await emitNotificationOnAttestationValidation({ node, user: owner, claimId: parseInt(claimId) });
}
};

Expand Down
13 changes: 0 additions & 13 deletions desci-server/src/controllers/doi/admin.ts

This file was deleted.

25 changes: 0 additions & 25 deletions desci-server/src/controllers/doi/mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,12 @@ import { Request, Response, NextFunction } from 'express';
import _ from 'lodash';

import { prisma } from '../../client.js';
import { BadRequestError } from '../../core/ApiError.js';
import { SuccessMessageResponse, SuccessResponse } from '../../core/ApiResponse.js';
import { MintError } from '../../core/doi/error.js';
import { logger as parentLogger } from '../../logger.js';
import { EmailTypes, sendEmail } from '../../services/email.js';
import { getTargetDpidUrl } from '../../services/fixDpid.js';
import { crossRefClient, doiService } from '../../services/index.js';
import { DiscordChannel, discordNotify, DiscordNotifyType } from '../../utils/discordUtils.js';
import { ensureUuidEndsWithDot } from '../../utils.js';

export const mintDoi = async (req: Request, res: Response, _next: NextFunction) => {
const { uuid } = req.params;
if (!uuid) throw new BadRequestError();
const sanitizedUuid = ensureUuidEndsWithDot(uuid);
const isPending = await doiService.hasPendingSubmission(sanitizedUuid);
if (isPending) {
throw new MintError('You have a pending submission');
} else {
const submission = await doiService.mintDoi(sanitizedUuid);
const data = _.pick(submission, ['id', 'status']);
new SuccessResponse(data).send(res);

const targetDpidUrl = getTargetDpidUrl();
discordNotify({
channel: DiscordChannel.DoiMinting,
type: DiscordNotifyType.INFO,
title: 'Mint DOI',
message: `${targetDpidUrl}/${submission.dpid} sent a request to mint: ${submission.uniqueDoi}`,
});
}
};

export interface RequestWithCrossRefPayload extends Request {
payload: {
Expand Down
17 changes: 16 additions & 1 deletion desci-server/src/controllers/nodes/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { attestationService } from '../../services/Attestation.js';
import { directStreamLookup } from '../../services/ceramic.js';
import { getManifestByCid } from '../../services/data/processing.js';
import { getTargetDpidUrl } from '../../services/fixDpid.js';
import { doiService } from '../../services/index.js';
import { saveInteraction, saveInteractionWithoutReq } from '../../services/interactionLog.js';
import {
cacheNodeMetadata,
Expand All @@ -21,7 +22,7 @@ import {
import { emitNotificationOnPublish } from '../../services/NotificationService.js';
import { publishServices } from '../../services/PublishServices.js';
import { _getIndexedResearchObjects, getIndexedResearchObjects } from '../../theGraph.js';
import { discordNotify } from '../../utils/discordUtils.js';
import { DiscordChannel, discordNotify, DiscordNotifyType } from '../../utils/discordUtils.js';
import { ensureUuidEndsWithDot } from '../../utils.js';

import { getOrCreateDpid, upgradeDpid } from './createDpid.js';
Expand Down Expand Up @@ -168,6 +169,20 @@ export const publish = async (req: PublishRequest, res: Response<PublishResBody>
owner.id,
);

// trigger doi minting workflow
try {
const submission = await doiService.autoMintTrigger(node.uuid);
const targetDpidUrl = getTargetDpidUrl();
discordNotify({
channel: DiscordChannel.DoiMinting,
type: DiscordNotifyType.INFO,
title: 'Mint DOI',
message: `${targetDpidUrl}/${submission.dpid} sent a request to mint: ${submission.uniqueDoi}`,
});
} catch (err) {
logger.error({ err }, 'Error: Mint DOI on Publish');
}

return res.send({
ok: true,
dpid: dpidAlias ?? parseInt(manifest.dpid?.id),
Expand Down
8 changes: 8 additions & 0 deletions desci-server/src/routes/v1/admin/communities/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export const addAttestationSchema = z.object({
.boolean()
.transform((value) => (value.toString() === 'true' ? true : false))
.default(false),
canMintDoi: z.coerce
.boolean()
.transform((value) => (value.toString() === 'true' ? true : false))
.default(false),
canUpdateOrcid: z.coerce
.boolean()
.transform((value) => (value.toString() === 'true' ? true : false))
.default(false),
}),
});

Expand Down
4 changes: 3 additions & 1 deletion desci-server/src/routes/v1/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Router } from 'express';
import { createCsv, getAnalytics } from '../../../controllers/admin/analytics.js';
import { listAttestations } from '../../../controllers/admin/communities/index.js';
import { debugAllNodesHandler, debugNodeHandler } from '../../../controllers/admin/debug.js';
import { listDoiRecords } from '../../../controllers/doi/admin.js';
import { listDoiRecords, mintDoi } from '../../../controllers/admin/doi/index.js';
import { ensureAdmin } from '../../../middleware/ensureAdmin.js';
import { ensureUser } from '../../../middleware/permissions.js';
import { asyncHandler } from '../../../utils/asyncHandler.js';
Expand All @@ -15,7 +15,9 @@ const router = Router();

router.get('/analytics', [ensureUser, ensureAdmin], getAnalytics);
router.get('/analytics/csv', [ensureUser, ensureAdmin], createCsv);

router.get('/doi/list', [ensureUser, ensureAdmin], listDoiRecords);
router.post('/mint/:uuid', [ensureUser, ensureAdmin], asyncHandler(mintDoi));

router.get('/debug', [ensureUser, ensureAdmin], asyncHandler(debugAllNodesHandler));
router.get('/debug/:uuid', [ensureUser, ensureAdmin], asyncHandler(debugNodeHandler));
Expand Down
2 changes: 0 additions & 2 deletions desci-server/src/routes/v1/doi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Router } from 'express';

import { checkMintability, retrieveDoi } from '../../controllers/doi/check.js';
import { mintDoi } from '../../controllers/doi/mint.js';
import { retrieveDoiSchema } from '../../controllers/doi/schema.js';
import { ensureNodeAccess } from '../../middleware/authorisation.js';
import { ensureUser } from '../../middleware/permissions.js';
Expand All @@ -11,7 +10,6 @@ import { asyncHandler } from '../../utils/asyncHandler.js';
const router = Router();

router.get('/check/:uuid', [ensureUser, ensureNodeAccess], asyncHandler(checkMintability));
router.post('/mint/:uuid', [ensureUser, ensureNodeAccess], asyncHandler(mintDoi));
router.get('/', [validate(retrieveDoiSchema)], asyncHandler(retrieveDoi));

export default router;
23 changes: 19 additions & 4 deletions desci-server/src/services/Attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,12 @@ export class AttestationService {
if (!attestation) throw new AttestationNotFoundError();
await prisma.attestation.update({
where: { id: attestationId },
data: { verified_image_url: data.verified_image_url },
data: {
verified_image_url: data.verified_image_url,
protected: data.protected,
canMintDoi: data.canMintDoi,
canUpdateOrcid: data.canUpdateOrcid,
},
});
await this.#publishVersion({
name: data.name as string,
Expand All @@ -195,8 +200,10 @@ export class AttestationService {
}

async getAttestationVersion(id: number, attestationId: number) {
return prisma.attestationVersion.findFirst({
where: { attestationId, id },
logger.trace({ id, attestationId }, 'getAttestationVersion');

return prisma.attestationVersion.findUnique({
where: { id },
include: { attestation: { select: { communityId: true } } },
});
}
Expand Down Expand Up @@ -290,7 +297,7 @@ export class AttestationService {
where: { nodeUuid, revoked: false },
include: {
community: { select: { name: true } },
attestation: { select: { protected: true } },
attestation: { select: { protected: true, canUpdateOrcid: true, canMintDoi: true } },
attestationVersion: { select: { name: true, description: true, image_url: true } },
_count: {
select: { NodeAttestationVerification: true },
Expand All @@ -309,12 +316,20 @@ export class AttestationService {
community: claim.community.name,
attestationId: claim.attestationId,
nodeVersion: claim.nodeVersion,
privileges: { doiMint: claim.attestation.canMintDoi, orcidUpdate: claim.attestation.canUpdateOrcid },
}))
.value();

return protectedClaims;
}

async getAttestationPrivileges(id: number) {
return await prisma.attestation.findUnique({
where: { id },
select: { canMintDoi: true, canUpdateOrcid: true },
});
}

async getNodeCommunityAttestations(dpid: string, communityId: number) {
return prisma.nodeAttestation.findMany({
where: { nodeDpid10: dpid, desciCommunityId: communityId, revoked: false },
Expand Down
13 changes: 13 additions & 0 deletions desci-server/src/services/Doi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PdfComponent, ResearchObjectComponentType, ResearchObjectV1 } from '@desci-labs/desci-models';
import { DoiStatus, DoiSubmissionQueue, NodeVersion, Prisma, PrismaClient } from '@prisma/client';
// import _ from 'lodash';
import { v4 } from 'uuid';

import {
Expand Down Expand Up @@ -45,6 +46,7 @@ export class DoiService {
async assertHasValidatedAttestations(uuid: string) {
const doiAttestations = await attestationService.getProtectedAttestations({
protected: true,
canMintDoi: true,
// community: { slug: 'desci-foundation' },
});
// logger.info(doiAttestations, 'DOI Requirements');
Expand Down Expand Up @@ -208,6 +210,17 @@ export class DoiService {
return submission;
}

async autoMintTrigger(uuid: string) {
const sanitizedUuid = ensureUuidEndsWithDot(uuid);
const isPending = await this.hasPendingSubmission(sanitizedUuid);
if (isPending) {
throw new MintError('You have a pending submission');
} else {
const submission = await this.mintDoi(sanitizedUuid);
return submission;
}
}

/**
* Query for Doi Record entry for a node using it's
* identifier (dPid, uuid or Doi)
Expand Down
1 change: 0 additions & 1 deletion desci-server/src/services/NotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,6 @@ export const emitNotificationOnAttestationValidation = async ({
const claim = await attestationService.findClaimById(claimId);
const versionedAttestation = await attestationService.getAttestationVersion(claim.attestationVersionId, claimId);
const dpid = await getDpidFromNode(node);

const attestationName = versionedAttestation.name;

const payload: AttestationValidationPayload = {
Expand Down
Loading

0 comments on commit ca8f110

Please sign in to comment.