Skip to content

Commit

Permalink
Mep pictures (#39)
Browse files Browse the repository at this point in the history
* feat: add pictures table + button

* feat: add pictures to db

* feat: add input file with capture prop

* feat: add report pictures

* wip

* feat: display pictures in app and in pdf

* big wip

* alos peut être ?

* types?

* wip: service worker

* alors peut être ?

* feat: background image upload

* feat: cleanup local pictures when uploaded

* fix: use local picture data if it exists

* fix: correctly display pdf images

* feat: add pictures on backend pdf gen
  • Loading branch information
ledouxm authored Oct 3, 2024
1 parent 7309573 commit ec011f2
Show file tree
Hide file tree
Showing 38 changed files with 3,479 additions and 296 deletions.
8 changes: 8 additions & 0 deletions db/migrations/907-add_pictures.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE pictures (
id TEXT PRIMARY KEY,
"reportId" TEXT REFERENCES report(id) ON DELETE CASCADE,
url TEXT,
"createdAt" TIMESTAMP
);

ALTER TABLE pictures ENABLE ELECTRIC;
2 changes: 1 addition & 1 deletion packages/backend/openapi.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"openapi":"3.1.0","info":{"title":"CR VIF API","description":"CR VIF API Documentation","version":"1.0"},"components":{"schemas":{}},"paths":{"/api/create-user":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"udap_id":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"}},"required":["name","udap_id","email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/login":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"}},"required":["email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/refresh-token":{"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"token","required":true},{"schema":{"type":"string"},"in":"query","name":"refreshToken","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/send-reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"}},"required":["email"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"temporaryLink":{"type":"string"},"newPassword":{"type":"string"}},"required":["temporaryLink","newPassword"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/udaps":{"get":{"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}}}}}}}},"/api/pdf/report":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"htmlString":{"type":"string"},"reportId":{"type":"string"},"recipients":{"type":"string"}},"required":["htmlString","reportId","recipients"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"string"}}}}}},"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}}}
{"openapi":"3.1.0","info":{"title":"CR VIF API","description":"CR VIF API Documentation","version":"1.0"},"components":{"schemas":{}},"paths":{"/api/create-user":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"udap_id":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"}},"required":["name","udap_id","email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/login":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"}},"required":["email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/refresh-token":{"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"token","required":true},{"schema":{"type":"string"},"in":"query","name":"refreshToken","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/send-reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"}},"required":["email"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"temporaryLink":{"type":"string"},"newPassword":{"type":"string"}},"required":["temporaryLink","newPassword"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/udaps":{"get":{"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}}}}}}}},"/api/upload/image":{"post":{"responses":{"200":{"description":"Default Response"}}}},"/api/pdf/report":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"htmlString":{"type":"string"},"reportId":{"type":"string"},"recipients":{"type":"string"}},"required":["htmlString","reportId","recipients"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"string"}}}}}},"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}}}
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"drizzle-typebox": "^0.1.1",
"drizzle-zod": "^0.5.1",
"fastify": "^4.26.2",
"fastify-multer": "^2.0.3",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.13",
"pastable": "^2.2.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import fs from "node:fs/promises";
import { makeDebug } from "./features/debug";
import { staticDataPlugin } from "./routes/staticDataRoutes";
import { pdfPlugin } from "./routes/pdfRoutes";
import { uploadPlugin } from "./routes/uploadRoutes";

const debug = makeDebug("fastify");

Expand Down Expand Up @@ -52,6 +53,7 @@ export const initFastify = async () => {

await instance.register(userPlugin);
await instance.register(staticDataPlugin);
await instance.register(uploadPlugin, { prefix: "/upload" });
await instance.register(pdfPlugin, { prefix: "/pdf" });
},
{ prefix: "/api" },
Expand Down
16 changes: 13 additions & 3 deletions packages/backend/src/routes/pdfRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Type, type FastifyPluginAsyncTypebox } from "@fastify/type-provider-typebox";
import { renderToBuffer } from "@react-pdf/renderer";
import { ReportPDFDocument } from "@cr-vif/pdf";
import { Udap } from "@cr-vif/electric-client/frontend";
import { Pictures, Udap } from "@cr-vif/electric-client/frontend";
import { authenticate } from "./authMiddleware";
import { db } from "../db/db";
import { sendReportMail } from "../features/mail";
Expand All @@ -14,7 +14,9 @@ export const pdfPlugin: FastifyPluginAsyncTypebox = async (fastify, _) => {
const { reportId, htmlString } = request.body;
const { udap } = request.user.user;

const pdf = await generatePdf({ htmlString, udap });
const pictures = await db.pictures.findMany({ where: { reportId }, orderBy: { createdAt: "asc" } });

const pdf = await generatePdf({ htmlString, udap, pictures });

const name = getPDFName(reportId);

Expand Down Expand Up @@ -54,7 +56,15 @@ export const pdfPlugin: FastifyPluginAsyncTypebox = async (fastify, _) => {
);
};

const generatePdf = async ({ htmlString, udap }: { htmlString: string; udap: Udap }) => {
const generatePdf = async ({
htmlString,
udap,
pictures,
}: {
htmlString: string;
udap: Udap;
pictures: Pictures[];
}) => {
return renderToBuffer(
<ReportPDFDocument
udap={udap as Udap}
Expand Down
67 changes: 53 additions & 14 deletions packages/backend/src/routes/uploadRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { FastifyPluginAsyncTypebox } from "@fastify/type-provider-typebox";
import { FastifyPluginAsyncTypebox, Type } from "@fastify/type-provider-typebox";
import multipart, { MultipartFile } from "@fastify/multipart";
import { AppError } from "../features/errors";
import util from "node:util";
import { pipeline } from "node:stream";
import { getPictureName } from "../services/uploadService";
import { db } from "../db/db";

const pump = util.promisify(pipeline);

export const uploadPlugin: FastifyPluginAsyncTypebox = async (fastify, _) => {
fastify.register(multipart, {
Expand All @@ -9,25 +15,58 @@ export const uploadPlugin: FastifyPluginAsyncTypebox = async (fastify, _) => {
},
});

fastify.post("/upload-image", async (request) => {
const files = request.files();
fastify.post("/image", async (request, reply) => {
const file = await request.file();
const { reportId, id } = request.query || ({} as any);

if (!file) throw new AppError(400, "No file provided");
if (!reportId || !id) throw new AppError(400, "No reportId or id provided");

const url = await request.services.upload.addPDFToReport({
reportId: (request.query as any).reportId as string,
buffer: await file.toBuffer(),
name: getPictureName(reportId, id),
publicRead: true,
});

await db.pictures.update({ where: { id }, data: { url } });

reply.send();

for await (const file of files) {
const isImage = ["image/png", "image/jpeg", "image/jpg"].includes(file.mimetype);
// for await (const file of files) {
// const isImage = ["image/png", "image/jpeg", "image/jpg"].includes(file.mimetype);

if (!isImage) {
throw new AppError(400, "File is not an image");
}
// if (!isImage) {
// throw new AppError(400, "File is not an image");
// }

// await request.services.upload.addImageToReport({
// reportId: "",
// buffer: await file.toBuffer(),
// name: getFileName(file),
// });
}
// // await request.services.upload.addImageToReport({
// // reportId: "",
// // buffer: await file.toBuffer(),
// // name: getFileName(file),
// // });
// }

console.log("done");

return "ok";
});

fastify.get(
"/picture",
{
schema: {
querystring: Type.Object({ reportId: Type.String(), pictureId: Type.String() }),
response: { 200: Type.Any() },
},
},
async (request) => {
const { reportId, pictureId } = request.query;
const buffer = await request.services.upload.getReportPicture({ reportId, pictureId });

return buffer.toString("base64");
},
);
};

const getFileName = (file: MultipartFile) => {
Expand Down
32 changes: 30 additions & 2 deletions packages/backend/src/services/uploadService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,24 @@ const debug = makeDebug("upload");
export const upload = async () => {};

export class UploadService {
async addPDFToReport({ reportId, buffer, name }: { reportId: string; buffer: Buffer; name: string }) {
async addPDFToReport({
reportId,
buffer,
name,
publicRead,
}: {
reportId: string;
buffer: Buffer;
name: string;
publicRead?: boolean;
}) {
debug("Uploading PDF to S3", reportId);
const command = new PutObjectCommand({ Bucket: ENV.AWS_BUCKET_NAME, Body: buffer, Key: name });
const command = new PutObjectCommand({
Bucket: ENV.AWS_BUCKET_NAME,
Body: buffer,
Key: name,
ACL: publicRead ? "public-read" : undefined,
});
await client.send(command);

const url = `https://${ENV.AWS_BUCKET_NAME}.s3.${ENV.AWS_REGION}.scw.cloud/${name}`;
Expand All @@ -30,6 +45,19 @@ export class UploadService {

return Buffer.from(buffer);
}

async getReportPicture({ reportId, pictureId }: { reportId: string; pictureId: string }) {
const name = getPictureName(reportId, pictureId);

const command = new GetObjectCommand({ Bucket: ENV.AWS_BUCKET_NAME, Key: name });
const response = await client.send(command);

const buffer = await response.Body?.transformToByteArray();
if (!buffer) throw new AppError(404, "Picture not found");

return Buffer.from(buffer);
}
}

export const getPDFName = (reportId: string) => `${reportId}/compte_rendu.pdf`;
export const getPictureName = (reportId: string, pictureId: string) => `${reportId}/pictures/${pictureId}.png`;
27 changes: 23 additions & 4 deletions packages/electric-client/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ model clause {
}

model report {
id String @id
id String @id
title String?
projectDescription String?
redactedBy String?
meetDate DateTime? @db.Timestamp(6)
meetDate DateTime? @db.Timestamp(6)
applicantName String?
applicantAddress String?
projectCadastralRef String?
Expand All @@ -52,14 +52,17 @@ model report {
contacts String?
furtherInformation String?
createdBy String
createdAt DateTime @db.Timestamp(6)
createdAt DateTime @db.Timestamp(6)
serviceInstructeur Int?
pdf String?
disabled Boolean?
udap_id String?
redactedById String?
applicantEmail String?
user user @relation(fields: [createdBy], references: [id], onDelete: SetNull, onUpdate: NoAction)
city String?
zipCode String?
pictures pictures[]
user user @relation(fields: [createdBy], references: [id], onDelete: SetNull, onUpdate: NoAction)
}

model delegation {
Expand Down Expand Up @@ -131,3 +134,19 @@ model clause_v2 {
udap_id String?
text String
}

model pdf_snapshot {
id String @id
report_id String?
html String?
report String?
user_id String?
}

model pictures {
id String @id
reportId String?
url String?
createdAt DateTime? @db.Timestamp(6)
report report? @relation(fields: [reportId], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
Loading

0 comments on commit ec011f2

Please sign in to comment.