diff --git a/backend/src/controllers/search.controller.ts b/backend/src/controllers/search.controller.ts index a60cdb02..da9aa770 100644 --- a/backend/src/controllers/search.controller.ts +++ b/backend/src/controllers/search.controller.ts @@ -5,11 +5,11 @@ import express from 'express'; import { writeFile, access, mkdir, createReadStream } from 'fs'; import { promisify } from 'util'; import { resultsHeader, jsonPath, prettyString } from '../processStream'; -import { runRequest } from '../runRequest'; +import { runRequest, runBulkRequest } from '../runRequest'; import { buildRequest } from '../buildRequest'; import { RequestInput, RequestBody } from '../models/requestInput'; -import { StrAndNumber, UpdateFields } from '../models/entities'; -import { buildResult, Result, ErrorResponse } from '../models/result'; +import { StrAndNumber, Modification, UpdateRequest, UpdateUserRequest, Review, ReviewsStringified, statusAuthMap } from '../models/entities'; +import { buildResult, buildResultSingle, Result, ErrorResponse } from '../models/result'; import { format } from '@fast-csv/format'; import { updatedFields } from '../updatedIds'; import { sendUpdateConfirmation } from '../mail'; @@ -248,7 +248,7 @@ export class SearchController extends Controller { @Post('/id/{id}') public async updateId( @Path() id: string, - @Body() updateFields: UpdateFields, + @Body() updateRequest: UpdateRequest, @Request() request: express.Request ): Promise { // get user & rights from Security @@ -260,19 +260,20 @@ export class SearchController extends Controller { const result = await runRequest(requestBuild, requestInput.scroll); const builtResult = buildResult(result.data, requestInput) if (builtResult.response.persons.length > 0) { - let proof const date = new Date(Date.now()).toISOString() const bytes = forge.random.getBytesSync(24); const randomId = forge.util.bytesToHex(bytes); if (!isAdmin) { - updateFields = {...await request.body}; + updateRequest = {...await request.body} as UpdateUserRequest; + let proof; + const message = updateRequest.message; + delete updateRequest.message; if (request.files && request.files.length > 0) { const [ file ]: any = request.files proof = file.path - } else if (updateFields.proof) { - ({ proof } = updateFields); - delete updateFields.proof - if (Object.keys(updateFields).length === 0) { + } else if (updateRequest.proof) { + ({ proof } = updateRequest); + if (Object.keys(updateRequest).length === 0) { this.setStatus(400); return { msg: 'A field at least must be provided' } } @@ -280,14 +281,18 @@ export class SearchController extends Controller { this.setStatus(400); return { msg: 'Proof must be provided' } } - const correctionData = { + delete updateRequest.proof; + const correctionData: Modification = { id: randomId, date, proof, auth: 0, author, - fields: updateFields + fields: updateRequest }; + if (message) { + correctionData.message = message; + } try { await accessAsync(`./data/proofs/${id}`); } catch(err) { @@ -302,30 +307,31 @@ export class SearchController extends Controller { this.setStatus(406); return { msg: "Id exists but no update to validate" } } else { - const checkedIds = {...await request.body}; + const checkedIds = {...await request.body} as ReviewsStringified; const checks = Object.keys(checkedIds).length; - let validated = 0; - let rejected = 0; - let noChange = 0; + const count:any = { + rejected: 0, + validated: 0, + closed: 0, + noChange: 0 + }; await Promise.all(updatedFields[id].map(async (update: any) => { - if (checkedIds[update.id] === 'true') { - if (update.auth !== 1) { - update.auth = 1; - validated++; + const review: Review = JSON.parse(checkedIds[update.id]); + review.date = date; + if (review.status) { + const auth = statusAuthMap[review.status]; + const reviewChange = update.review && + ['proofQuality','proofScript','proofType','silent','message'].some(k => (review as any)[k] !== update.review[k]) + if ((update.auth !== auth) || reviewChange) { + update.auth = auth; + count[review.status]++; + update.review = review; await writeFileAsync(`./data/proofs/${id}/${update.date as string}_${id}.json`, JSON.stringify(update)); - await sendUpdateConfirmation(update.author, true, undefined, id); + if (!review.silent) { + await sendUpdateConfirmation(update.author, review.status, review.message, id); + } } else { - noChange++; - } - delete checkedIds[update.id]; - } else if (checkedIds[update.id] !== undefined) { - if (update.auth !== -1) { - update.auth = -1; - rejected++; - await writeFileAsync(`./data/proofs/${id}/${update.date as string}_${id}.json`, JSON.stringify(update)); - await sendUpdateConfirmation(update.author, false, checkedIds[update.id] || undefined, id); - } else { - noChange++; + count.noChange++; } delete checkedIds[update.id]; } @@ -336,17 +342,13 @@ export class SearchController extends Controller { } else if (Object.keys(checkedIds).length > 0) { return { msg: "Partial validation could be achieved", - validated, - rejected, - noChange, + ...count, invalidIds: Object.keys(checkedIds) } } else { return { msg: "All validations processed", - validated, - rejected, - noChange + ...count } } } @@ -367,8 +369,45 @@ export class SearchController extends Controller { @Security('jwt',['user']) @Tags('Simple') @Get('/updated') - public updateList(): any { - return updatedFields + public async updateList(@Request() request: express.Request): Promise { + const author = (request as any).user && (request as any).user.user + const isAdmin = (request as any).user && (request as any).user.scopes && (request as any).user.scopes.includes('admin'); + let updates:any = {}; + if (isAdmin) { + updates = {...updatedFields}; + } else { + Object.keys(updatedFields).forEach((id:any) => { + let filter = false; + const modifications = updatedFields[id].map((m:any) => { + const modif:any = {...m} + if (modif.author !== author) { + modif.author = modif.author.substring(0,2) + + '...' + modif.author.replace(/@.*/,'').substring(modif.author.replace(/@.*/,'').length-2) + + '@' + modif.author.replace(/.*@/,''); + modif.message = undefined; + modif.review = undefined; + } else { + filter=true + } + return modif; + }); + if (filter) { + updates[id] = modifications; + } + }) + } + const bulkRequest = Object.keys(updates).map((id: any) => + [JSON.stringify({index: "deces"}), JSON.stringify(buildRequest(new RequestInput({id})))] + ); + const msearchRequest = bulkRequest.map((x: any) => x.join('\n\r')).join('\n\r') + '\n'; + const result = await runBulkRequest(msearchRequest); + return result.data.responses.map((r:any) => buildResultSingle(r.hits.hits[0])) + .map((r:any) => { + delete r.score; + delete r.scores; + r.modifications = updates[r.id]; + return r; + }); } private async handleFile(request: express.Request): Promise { @@ -441,7 +480,7 @@ export class SearchController extends Controller { end }); stream.pipe(request.res); - await new Promise((resolve, reject) => { + await new Promise((resolve) => { stream.on('end', () => { request.res.end(); resolve(true); diff --git a/backend/src/mail.ts b/backend/src/mail.ts index a8b4bb9f..b01f4f63 100644 --- a/backend/src/mail.ts +++ b/backend/src/mail.ts @@ -1,4 +1,5 @@ import { SMTPClient } from 'emailjs'; +import { ReviewStatus } from './models/entities'; const client = new SMTPClient({ host: process.env.SMTP_HOST, @@ -40,29 +41,32 @@ export const validateOTP = (email:string,otp:string): boolean => { return false; } -export const sendUpdateConfirmation = async (email:string, validation: boolean, rejectMsg: string, id: string): Promise => { +export const sendUpdateConfirmation = async (email:string, status: ReviewStatus, rejectMsg: string, id: string): Promise => { try { const message: any = { from: process.env.API_EMAIL, to: `${email}`, } - if (validation) { + if (status === 'validated') { message.subject = `Suggestion validée ! - ${process.env.APP_DNS}`; message.attachment = { data: `

Merci de votre contibution !

Votre proposition de correction a été acceptée.
Retrouvez la fiche modifiée .

+ Vous pouvez à tout moment revenir sur vos contributions.
+
l'équipe matchID `, alternative: true}; - } else { - message.subject = `Suggestion non retenue - ${process.env.APP_DNS}`; + } else if (status === 'rejected') { + message.subject = `Suggestion incomplète - ${process.env.APP_DNS}`; message.attachment = { data: ` Nous vous remercions de votre contribution,

- Néanmoins les éléments fournis ne nous ont pas permis de retenir votre proposition
- ${rejectMsg ? rejectMsg : ''}
+ Néanmoins les éléments fournis ne nous ont pas permis de retenir votre proposition à ce stade.
+ ${rejectMsg ? '
' + rejectMsg + '
' : ''}
+ Vous pourrez de nouveau soumettre une nouvelle proposition sur la fiche: .

l'équipe matchID diff --git a/backend/src/models/entities.ts b/backend/src/models/entities.ts index ca1a6729..a077e01e 100644 --- a/backend/src/models/entities.ts +++ b/backend/src/models/entities.ts @@ -59,6 +59,78 @@ export interface Location { longitude?: number; }; +/** + * Identity modification + * @tsoaModel + * @example + * { + * "firstName": "Paul" + * } + */ +export interface UpdateFields { + firstName?: string; + lastName?: string; + birthDate?: string; + birthCity?: string; + birthCountry?: string; + birthLocationCode?: string; + deathAge?: number; + deathDate?: string; + deathCity?: string; + deathCountry?: string; + deathLocationCode?: string; +} + +export interface UpdateUserRequest extends UpdateFields { + proof: string; + message?: string; +}; + +export type ReviewStatus = "rejected"|"validated"|"closed"; + +export const statusAuthMap = { + rejected: -1, + validated: 1, + closed: -2, +}; + +export type ProofType = "french death certificate"|"french birth certificate"|"other french document"|"foreign document"|"grave"|"other"; + +export type ProofScript = "manuscript"|"typed"|"numerical"; + +export type ProofQuality = "poor"|"good"; + +export interface Review { + status: ReviewStatus; + date?: string; + message?: string; + silent?: boolean; + proofType?: ProofType; + proofScrupt?: ProofScript; + proofQuality?: ProofQuality; +}; + +export interface Reviews { + [key: string]: Review; +}; + +export interface ReviewsStringified { + [key: string]: string; +}; + +export type UpdateRequest = UpdateUserRequest | ReviewsStringified ; + +export interface Modification { + id: string; + date: string; + author: string; + fields: UpdateFields; + proof: string; + auth: number; + message?: string; + review?: Review; +}; + export interface Person { score: number; source: string; @@ -83,14 +155,7 @@ export interface Person { wikidata?: string; wikimedia?: string; }; - modifications?: { - id: string; - date: string; - auth: number; - proof: string; - author: string; - fields: UpdateFields; - }; + modifications?: Modification[]; }; export interface ScoreParams { @@ -99,27 +164,4 @@ export interface ScoreParams { candidateNumber?: number; }; -/** - * Identity modification - * @tsoaModel - * @example - * { - * "firstName": "Paul" - * } - */ -export interface UpdateFields { - firstName?: string; - lastName?: string; - birthDate?: string; - birthCity?: string; - birthCountry?: string; - birthLocationCode?: string; - deathAge?: number; - deathDate?: string; - deathCity?: string; - deathCountry?: string; - deathLocationCode?: string; - proof?: string; -} - export type StrAndNumber = string | number;