diff --git a/confiture-rest-api/prisma/schema.prisma b/confiture-rest-api/prisma/schema.prisma index 1c441a7ca..2427d4811 100644 --- a/confiture-rest-api/prisma/schema.prisma +++ b/confiture-rest-api/prisma/schema.prisma @@ -204,6 +204,7 @@ enum EmailType { EMAIL_UPDATE_VERIFICATION EMAIL_UPDATE_CONFIRMATION PASSWORD_RESET_REQUEST + AUDIT_TRANSFER } model EmailLog { diff --git a/confiture-rest-api/src/audits/audit.service.ts b/confiture-rest-api/src/audits/audit.service.ts index 3b2c958b4..db237a1c7 100644 --- a/confiture-rest-api/src/audits/audit.service.ts +++ b/confiture-rest-api/src/audits/audit.service.ts @@ -1870,4 +1870,44 @@ export class AuditService { return audit; } + + /** + * Transfer an audit ownership to another user and link it to its account (if any) + */ + async transferAudit(uniqueId: string, newEmail: string) { + const updatedAudit = this.prisma.$transaction(async (tx) => { + // Get new owner info + const user = await tx.user.findUnique({ + where: { + username: newEmail + }, + select: { + name: true, + orgName: true + } + }); + + const auditToUpdate = await tx.audit.findUnique({ + where: { editUniqueId: uniqueId }, + select: { auditorName: true } + }); + + // Update audit with new owner info if any + const data: Pick = { + auditorEmail: newEmail, + auditorName: user?.name + ? user.name + : (auditToUpdate.auditorName || ""), + auditorOrganisation: (user && user.orgName) ? user.orgName : "" + }; + + return await tx.audit.update({ + where: { editUniqueId: uniqueId }, + data, + select: AUDIT_PRISMA_SELECT + }); + }); + + return updatedAudit; + } } diff --git a/confiture-rest-api/src/audits/audits.controller.ts b/confiture-rest-api/src/audits/audits.controller.ts index 9b847579e..f3481fcd8 100644 --- a/confiture-rest-api/src/audits/audits.controller.ts +++ b/confiture-rest-api/src/audits/audits.controller.ts @@ -41,6 +41,7 @@ import { GetPageWithResultsDto } from "./dto/get-page-with-results.dto"; import { CreateAuditDto } from "./dto/requests/create-audit.dto"; import { DuplicateAuditDto } from "./dto/requests/duplicate-audit.dto"; import { PatchAuditDto } from "./dto/requests/patch-audit.dto"; +import { TransferAuditDto } from "./dto/requests/transfer-audit.dto"; import { UpdateAuditDto } from "./dto/requests/update-audit.dto"; import { UpdateResultsDto } from "./dto/requests/update-results.dto"; import { UploadImageDto } from "./dto/requests/upload-image.dto"; @@ -407,6 +408,41 @@ export class AuditsController { return file; } + /** + * TODO: add decorator @AuditId if PR is merged + */ + @Put("/:uniqueId/transfer") + @ApiCreatedResponse({ + description: "The audit has been successfully transfered.", + type: AuditDto + }) + @ApiNotFoundResponse({ description: "The audit does not exist." }) + @ApiGoneResponse({ description: "The audit has been previously deleted." }) + async transferAudit( + @Param("uniqueId") uniqueId: string, + @Body() body: TransferAuditDto + ) { + const newAudit = await this.auditService.transferAudit(uniqueId, body.newEmail); + + if (!newAudit) { + await this.sendAuditNotFoundStatus(uniqueId); + } + + this.mailer.sendAuditTransferEmail(body.newEmail, { + editUniqueId: uniqueId, + auditorEmail: body.senderEmail, + auditorName: newAudit.auditorName, + procedureName: newAudit.procedureName + }).catch((err) => { + console.error( + `Failed to send transfer email for audit ${newAudit.editUniqueId}` + ); + console.error(err); + }); + + return newAudit; + } + /** * Send 404 (Not Found) status for audits that never existed * and 410 (Gone) for audits that existed but were deleted. diff --git a/confiture-rest-api/src/audits/dto/requests/transfer-audit.dto.ts b/confiture-rest-api/src/audits/dto/requests/transfer-audit.dto.ts new file mode 100644 index 000000000..07e91a7ae --- /dev/null +++ b/confiture-rest-api/src/audits/dto/requests/transfer-audit.dto.ts @@ -0,0 +1,15 @@ +import { IsEmail } from "class-validator"; + +export class TransferAuditDto { + /** + * @example "name@domain.com" + */ + @IsEmail() + senderEmail: string; + + /** + * @example "name@domaine.com" + */ + @IsEmail() + newEmail: string; +} diff --git a/confiture-rest-api/src/mail/audit-transfer-email.ts b/confiture-rest-api/src/mail/audit-transfer-email.ts new file mode 100644 index 000000000..273f1246e --- /dev/null +++ b/confiture-rest-api/src/mail/audit-transfer-email.ts @@ -0,0 +1,27 @@ +import { renderMailTemplate } from "./render-mjml-template"; + +export interface AuditTransferEmailData { + procedureName: string; + auditUrl: string; + senderEmail: string; + senderName: string; +} + +export function subject(): string { + return "Un audit vous a été transféré"; +} + +export function html(data: AuditTransferEmailData): string { + return renderMailTemplate("audit-transfer", data); +} + +export function plain(data: AuditTransferEmailData): string { + return `Un audit vous a été transféré + + ${data.senderName ? data.senderName + " (" + data.senderEmail + ")" : data.senderEmail} vous a transféré l’audit « ${data.procedureName} ». Si vous possédez un compte Ara, vous retrouverez cet audit dans votre espace. + + ${data.auditUrl} + + Besoin d’aide ? Écrivez-nous : ara@design.numerique.gouv.fr + `; +} diff --git a/confiture-rest-api/src/mail/mail.service.ts b/confiture-rest-api/src/mail/mail.service.ts index 20e0ed0b7..b1c334a5f 100644 --- a/confiture-rest-api/src/mail/mail.service.ts +++ b/confiture-rest-api/src/mail/mail.service.ts @@ -9,6 +9,8 @@ import * as accountConfirmationEmail from "./account-confirmation-email"; import * as accountVerificationEmail from "./account-verification-email"; import * as auditCreationEmail from "./audit-creation-email"; import { AuditCreationEmailData } from "./audit-creation-email"; +import * as auditTransferEmail from "./audit-transfer-email"; +import { AuditTransferEmailData } from "./audit-transfer-email"; import { EmailConfig } from "./email-config.interface"; import * as passwordUpdateConfirmationEmail from "./password-update-confirmation-email"; import * as requestPasswordResetEmail from "./request-password-reset-email"; @@ -22,7 +24,8 @@ const EMAILS: Record = { [EmailType.PASSWORD_UPDATE_CONFIRMATION]: passwordUpdateConfirmationEmail, [EmailType.EMAIL_UPDATE_VERIFICATION]: updateEmailVerificationEmail, [EmailType.EMAIL_UPDATE_CONFIRMATION]: updateEmailConfirmationEmail, - [EmailType.PASSWORD_RESET_REQUEST]: requestPasswordResetEmail + [EmailType.PASSWORD_RESET_REQUEST]: requestPasswordResetEmail, + [EmailType.AUDIT_TRANSFER]: auditTransferEmail }; @Injectable() @@ -161,4 +164,18 @@ export class MailService { verificationLink }); } + + sendAuditTransferEmail(email: string, audit: Pick) { + const baseUrl = this.config.get("FRONT_BASE_URL"); + const auditUrl = `${baseUrl}/audits/${audit.editUniqueId}/synthese`; + + const data: AuditTransferEmailData = { + auditUrl, + senderName: audit.auditorName, + senderEmail: audit.auditorEmail, + procedureName: audit.procedureName + }; + + return this.sendMail(email, EmailType.AUDIT_TRANSFER, data); + } } diff --git a/confiture-rest-api/templates/audit-transfer.mjml b/confiture-rest-api/templates/audit-transfer.mjml new file mode 100644 index 000000000..cf9406f09 --- /dev/null +++ b/confiture-rest-api/templates/audit-transfer.mjml @@ -0,0 +1,37 @@ + + + Un audit vous a été transféré + + Un audit vous a été transféré + + + + + + + + + + + + Un audit vous a été transféré + + + {{#if senderName}}{{senderName}} ({{senderEmail}}){{else}}{{senderEmail}}{{/if}} + vous a transféré l’audit « {{procedureName}} ». Si vous possédez un compte Ara, vous retrouverez cet audit dans votre espace. + + Accéder à l’audit + + + + + + Vous ne possédez pas encore de compte Ara ? + Créez votre compte pour retrouver facilement tous vos audits au même endroit :
Créer un compte Ara
+
+
+ + + +
+
diff --git a/confiture-web-app/README.md b/confiture-web-app/README.md index 460d93222..99916f360 100644 --- a/confiture-web-app/README.md +++ b/confiture-web-app/README.md @@ -130,7 +130,7 @@ Il extiste des variantes des composants de champs fréquement utilisés (` - + \ No newline at end of file diff --git a/confiture-web-app/src/components/account/dashboard/AuditRow.vue b/confiture-web-app/src/components/account/dashboard/AuditRow.vue index c26f028e3..b5bdde878 100644 --- a/confiture-web-app/src/components/account/dashboard/AuditRow.vue +++ b/confiture-web-app/src/components/account/dashboard/AuditRow.vue @@ -25,7 +25,7 @@ const props = defineProps<{ zIndex?: number; }>(); -defineEmits(["delete"]); +defineEmits(["delete", "transfer"]); const notify = useNotifications(); const auditStore = useAuditStore(); @@ -75,6 +75,25 @@ function duplicateAudit(name: string) { }); } +// const transferModalRef = useTemplateRef>("transferModalRef"); + +// async function transferAudit(newEmail: string) { +// try { +// await auditStore.transferAudit(props.audit.editUniqueId, newEmail); + +// transferModalRef.value?.hide(); + +// notify("success", `Audit « ${props.audit.procedureName} » transféré`, `Un lien d’accès a été envoyé à : ${newEmail}`); +// } catch (error) { +// notify( +// "error", +// "Échec du transfert de l'audit", +// DEFAULT_NOTIFICATION_ERROR_DESCRIPTION +// ); +// captureWithPayloads(error); +// } +// } + const csvExportUrl = computed( () => `/api/audits/${props.audit.editUniqueId}/exports/csv` ); @@ -284,6 +303,16 @@ defineExpose({ {{ audit.procedureName }} +