Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ assets/*
!assets/mail_onboarding.txt
!assets/template_onboarding.pdf
!assets/school_logo.png
!assets/Logo_GM.*
20 changes: 20 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ tasks:
requires:
vars: [STUDENT_ID]

create-onboarding-pdf-with-custom-png-logo:
desc: Gets the onboarding document as PDF
cmds:
- 'echo -n ''{"schoolLogo": "{{.LOGO_BASE64}}"}'' | http -b POST localhost:8099/students/{{ .STUDENT_ID }}/onboarding Accept:application/pdf ''X-API-KEY: This_is_a_test_APIKEY_with_30_chars+'' > {{ .STUDENT_ID }}_onboarding.pdf'
requires:
vars: [STUDENT_ID]
vars:
LOGO_BASE64:
sh: 'cat assets/Logo_GM.png | {{if eq OS "darwin"}}base64{{else}}base64 -w 0{{end}}'

create-onboarding-pdf-with-custom-jpg-logo:
desc: Gets the onboarding document as PDF
cmds:
- 'echo -n ''{"schoolLogo": "{{.LOGO_BASE64}}"}'' | http -b POST localhost:8099/students/{{ .STUDENT_ID }}/onboarding Accept:application/pdf ''X-API-KEY: This_is_a_test_APIKEY_with_30_chars+'' > {{ .STUDENT_ID }}_onboarding.pdf'
requires:
vars: [STUDENT_ID]
vars:
LOGO_BASE64:
sh: 'cat assets/Logo_GM.jpg | {{if eq OS "darwin"}}base64{{else}}base64 -w 0{{end}}'

get-onboarding-png:
desc: Gets the onboarding document as PNG
cmds:
Expand Down
Binary file added assets/Logo_GM.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Logo_GM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 48 additions & 14 deletions src/StudentsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export class StudentsController {
return entries;
}

public async getOnboardingDataForStudent(student: Student): Promise<Result<{ pdf: Buffer; png: Buffer; link: string }>> {
public async getOnboardingDataForStudent(student: Student, schoolLogo?: string): Promise<Result<{ pdf: Buffer; png: Buffer; link: string }>> {
if (!student.correspondingRelationshipTemplateId || !student.givenname || !student.surname) {
throw new ApplicationError("error.schoolModule.studentAlreadyDeleted", "The student seems to be already deleted.");
}
Expand All @@ -260,7 +260,8 @@ export class StudentsController {
{
organizationDisplayName: (this.displayName.content.value as DisplayNameJSON).value,
givenname: student.givenname,
surname: student.surname
surname: student.surname,
schoolLogo: schoolLogo
},
pngAsBuffer
);
Expand All @@ -273,6 +274,7 @@ export class StudentsController {
organizationDisplayName: string;
givenname: string;
surname: string;
schoolLogo?: string;
},
pngAsBuffer: Buffer
) {
Expand All @@ -298,17 +300,7 @@ export class StudentsController {
form.getTextField("Ort_Datum").setText("");
form.getTextField("QR_Code_Schueler").setImage(qrImage);

const schoolLogoPNGPath = path.join(this.assetsLocation, "school_logo.png");
const schoolLogoJPGPath = path.join(this.assetsLocation, "school_logo.jpg");
if (fs.existsSync(schoolLogoPNGPath)) {
const schoolLogoBytes = await fs.promises.readFile(schoolLogoPNGPath);
const schoolLogoImage = await pdfDoc.embedPng(schoolLogoBytes);
this.embedImage(pdfDoc, schoolLogoImage);
} else if (fs.existsSync(schoolLogoJPGPath)) {
const schoolLogoBytes = await fs.promises.readFile(schoolLogoJPGPath);
const schoolLogoImage = await pdfDoc.embedJpg(schoolLogoBytes);
this.embedImage(pdfDoc, schoolLogoImage);
}
await this.embedImage(pdfDoc, data.schoolLogo);

try {
form.flatten();
Expand All @@ -327,7 +319,10 @@ export class StudentsController {
return Buffer.from(pdfBytes);
}

private embedImage(pdfDoc: PDFDocument, image: PDFImage) {
private async embedImage(pdfDoc: PDFDocument, schoolLogoBase64?: string) {
const image = await this.getImage(pdfDoc, schoolLogoBase64);
if (!image) return;

const page = pdfDoc.getPage(0);
const maxWidth = ((page.getWidth() - 42 - 42) / 5) * 2;
const maxHeight = 80;
Expand All @@ -341,6 +336,45 @@ export class StudentsController {
});
}

private async getImage(pdfDoc: PDFDocument, schoolLogoBase64?: string) {
if (schoolLogoBase64 === undefined) return await this.getAssetImage(pdfDoc);

const bytes = Buffer.from(schoolLogoBase64, "base64");
const fistBytesAsHex = bytes.toString("hex", 0, 4);

const pdfMagicBytes = "89504e47";
if (fistBytesAsHex === pdfMagicBytes) {
const schoolLogoImage = await pdfDoc.embedPng(bytes);
return schoolLogoImage;
}

const jpgMagicBytes = "ffd8";
if (fistBytesAsHex.startsWith(jpgMagicBytes)) {
const schoolLogoImage = await pdfDoc.embedJpg(bytes);
return schoolLogoImage;
}

throw new ApplicationError("error.schoolModule.onboardingInvalidLogo", "The logo is not a valid PNG or JPG file. Please check the logo and try again.");
}

private async getAssetImage(pdfDoc: PDFDocument): Promise<PDFImage | undefined> {
const schoolLogoPNGPath = path.join(this.assetsLocation, "school_logo.png");
if (fs.existsSync(schoolLogoPNGPath)) {
const schoolLogoBytes = await fs.promises.readFile(schoolLogoPNGPath);
const schoolLogoImage = await pdfDoc.embedPng(schoolLogoBytes);
return schoolLogoImage;
}

const schoolLogoJPGPath = path.join(this.assetsLocation, "school_logo.jpg");
if (fs.existsSync(schoolLogoJPGPath)) {
const schoolLogoBytes = await fs.promises.readFile(schoolLogoJPGPath);
const schoolLogoImage = await pdfDoc.embedJpg(schoolLogoBytes);
return schoolLogoImage;
}

return undefined;
}

public async getStudents(): Promise<Student[]> {
const docs = await this.#studentsCollection.find({});

Expand Down
24 changes: 23 additions & 1 deletion src/controllers/StudentsRESTController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import express from "express";
import { fromError } from "zod-validation-error";
import { StudentsController } from "../StudentsController";
import { Student, StudentAuditLog, StudentOnboardingDTO } from "../types";
import { createStudentRequestSchema, sendAbiturzeugnisRequestSchema, sendFileRequestSchema, sendMailRequestSchema } from "./schemas";
import { createStudentOnboardingPDFSchema, createStudentRequestSchema, sendAbiturzeugnisRequestSchema, sendFileRequestSchema, sendMailRequestSchema } from "./schemas";

@Path("/students")
export class StudentsRESTController extends BaseController {
Expand Down Expand Up @@ -78,6 +78,28 @@ export class StudentsRESTController extends BaseController {
return this.noContent(Result.ok<unknown, ApplicationError>(undefined));
}

@POST
@Path(":id/onboarding")
@Accept("application/pdf")
public async createStudentOnboardingPDF(@PathParam("id") id: string, @ContextResponse response: express.Response, body: any): Promise<Envelope | void> {
const validationResult = createStudentOnboardingPDFSchema.safeParse(body);
if (!validationResult.success) throw new ApplicationError("error.schoolModule.invalidRequest", `The request is invalid: ${fromError(validationResult.error)}`);
const data = validationResult.data;

const student = await this.studentsController.getStudent(id);
if (!student) throw RuntimeErrors.general.recordNotFound(Student);

const result = await this.studentsController.getOnboardingDataForStudent(student, data.schoolLogo);
return this.file(
result,
(r) => r.value.pdf,
() => `${id}_onboarding.pdf`,
() => Mimetype.pdf(),
response,
200
);
}

@GET
@Path(":id/onboarding")
@Accept("application/json", "application/pdf", "image/png")
Expand Down
4 changes: 4 additions & 0 deletions src/controllers/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const createStudentRequestSchema = z.object({
.default([])
});

export const createStudentOnboardingPDFSchema = z.object({
schoolLogo: z.string().base64()
});

export const sendMailRequestSchema = z.object({
subject: z.string().min(3).max(255),
body: z.string().min(5).max(4000)
Expand Down