Update backend for editing communications pages

This commit is contained in:
Miika Holmlund
2025-11-12 13:40:40 +02:00
parent 4298cc8c12
commit 8425f59938
6 changed files with 50 additions and 44 deletions
+1 -2
View File
@@ -51,7 +51,6 @@ services:
command: > command: >
sh -c "apt update && apt install -y wget postgresql-client && sh -c "apt update && apt install -y wget postgresql-client &&
wget https://github.com/peterldowns/pgmigrate/releases/download/v0.0.6%2Bcommit.4f90829/pgmigrate-linux-amd64 -O /usr/local/bin/pgmigrate && wget https://github.com/peterldowns/pgmigrate/releases/download/v0.0.6%2Bcommit.4f90829/pgmigrate-linux-amd64 -O /usr/local/bin/pgmigrate &&
chmod +x /usr/local/bin/pgmigrate && chmod +x /usr/local/bin/pgmigrate && sleep inf"
/usr/local/bin/docker-entrypoint.sh sleep inf"
volumes: volumes:
pg-data: pg-data:
@@ -18,26 +18,26 @@ import { useTestDatabase } from "@/backend/test/testdb";
import { useTestRequest } from "@/backend/test/mock-request"; import { useTestRequest } from "@/backend/test/mock-request";
import { getHTMLContent } from "@/backend/html-services"; import { getHTMLContent } from "@/backend/html-services";
describe("GET /api/html-content/[id]", () => { describe("GET /api/html-content/[type]/[id]", () => {
useTestDatabase(); useTestDatabase();
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
const getRequest = async (id, type = "resources") => { const getRequest = async (type, id) => {
const { mockGet } = useTestRequest(); const { mockGet } = useTestRequest();
const req = mockGet(`/api/html-content/${id}?type=${type}`); const req = mockGet(`/api/html-content/${type}/${id}`);
return GET(req); return GET(req, { params: { type, id } });
}; };
it("should return HTML content as JSON", async () => { it("should return HTML content as JSON", async () => {
getHTMLContent.mockResolvedValueOnce("<p>Mocked HTML</p>"); getHTMLContent.mockResolvedValueOnce("<p>Mocked HTML</p>");
const response = await getRequest(1, "resources"); const response = await getRequest("resources", 1);
const body = await response.json(); const body = await response.json();
expect(getHTMLContent).toHaveBeenCalledWith("1", "learning_pages_html"); expect(getHTMLContent).toHaveBeenCalledWith("learning_pages_html", 1);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(body).toEqual({ content: "<p>Mocked HTML</p>" }); expect(body).toEqual({ content: "<p>Mocked HTML</p>" });
}); });
@@ -45,21 +45,18 @@ describe("GET /api/html-content/[id]", () => {
it("should call correct table for communications type", async () => { it("should call correct table for communications type", async () => {
getHTMLContent.mockResolvedValueOnce("<p>Mocked HTML</p>"); getHTMLContent.mockResolvedValueOnce("<p>Mocked HTML</p>");
const response = await getRequest(5, "communications"); const response = await getRequest("communications", 5);
await response.json(); await response.json();
expect(getHTMLContent).toHaveBeenCalledWith( expect(getHTMLContent).toHaveBeenCalledWith("communications_pages_html", 5);
"5",
"communications_pages_html"
);
}); });
it("should return empty string table if type is invalid", async () => { it("should return empty string table if type is invalid", async () => {
getHTMLContent.mockResolvedValueOnce("<p>Mocked HTML</p>"); getHTMLContent.mockResolvedValueOnce("<p>Mocked HTML</p>");
const response = await getRequest(3, "unknown"); const response = await getRequest("unknown", 3);
await response.json(); await response.json();
expect(getHTMLContent).toHaveBeenCalledWith("3", ""); expect(getHTMLContent).toHaveBeenCalledWith("", 3);
}); });
}); });
@@ -6,19 +6,8 @@ import DOMPurify from "dompurify";
/* /*
This route fetches html content from db according to id and page type This route fetches html content from db according to id and page type
*/ */
export async function GET(req) { export async function GET(req, { params }) {
// create URL-object from request const { type, id } = await params;
const url = new URL(req.url);
// split path to segments
const pathSegments = url.pathname.split("/");
// get page type from url parameters
const type = url.searchParams.get("type");
// get last segment, which is the correct id of the page
const id = pathSegments[pathSegments.length - 1];
// returns correct db table based on url. Prevents malicious code // returns correct db table based on url. Prevents malicious code
const getTableType = (type) => { const getTableType = (type) => {
if (type == "resources") { if (type == "resources") {
@@ -30,7 +19,7 @@ export async function GET(req) {
} }
}; };
const content = await getHTMLContent(id, getTableType(type)); const content = await getHTMLContent(getTableType(type), id);
// returns the fetched html as json // returns the fetched html as json
return new Response(JSON.stringify({ content }), { return new Response(JSON.stringify({ content }), {
@@ -41,13 +30,23 @@ export async function GET(req) {
export const PUT = withAuth( export const PUT = withAuth(
async (req, { params }) => { async (req, { params }) => {
const data = await req.json(); const data = await req.json();
const { id } = await params; const { type, id } = await params;
const getTableType = (type) => {
if (type == "resources") {
return "learning_pages_html";
} else if (type == "communications") {
return "communications_pages_html";
} else {
return "";
}
};
const window = new JSDOM("").window; const window = new JSDOM("").window;
const purify = DOMPurify(window); const purify = DOMPurify(window);
const cleaned = purify.sanitize(data.content, { ADD_ATTR: ["target"] }); const cleaned = purify.sanitize(data.content, { ADD_ATTR: ["target"] });
if (!(await updateHTMLContent(id, cleaned))) { if (!(await updateHTMLContent(getTableType(type), id, cleaned))) {
return new Response("Error updating HTML content", { return new Response("Error updating HTML content", {
status: 400, status: 400,
}); });
+1 -1
View File
@@ -22,7 +22,7 @@ export default function RenderHTML({ contentId, type }) {
// function to fetch HTML content using html-services, and set it to state html // function to fetch HTML content using html-services, and set it to state html
async function fetchHTML() { async function fetchHTML() {
// fetch response through next.js route using getHTMLContent function // fetch response through next.js route using getHTMLContent function
const res = await fetch(`/api/html-content/${contentId}?type=${type}`); const res = await fetch(`/api/html-content/${type}/${contentId}`);
// chnages the response to proper json // chnages the response to proper json
const data = await res.json(); const data = await res.json();
+12 -9
View File
@@ -22,7 +22,7 @@ export default function Chapters() {
useEffect(() => { useEffect(() => {
async function fetchHTML() { async function fetchHTML() {
const res = await fetch( const res = await fetch(
`/api/html-content/${parseInt(chapter)}?type=resources` `/api/html-content/resources/${parseInt(chapter)}`
); );
const data = await res.json(); const data = await res.json();
setEditorContent(data.content); setEditorContent(data.content);
@@ -35,14 +35,17 @@ export default function Chapters() {
const jsonData = JSON.stringify({ const jsonData = JSON.stringify({
content: editorContent.replace(/&nbsp;/g, " "), content: editorContent.replace(/&nbsp;/g, " "),
}); });
const res = await fetch(`/api/html-content/${parseInt(chapter)}`, { const res = await fetch(
headers: { `/api/html-content/resources/${parseInt(chapter)}`,
Accept: "application/json", {
"Content-Type": "application/json", headers: {
}, Accept: "application/json",
method: "PUT", "Content-Type": "application/json",
body: jsonData, },
}); method: "PUT",
body: jsonData,
}
);
if (res.status == 200) { if (res.status == 200) {
setEditorMessage({ error: false, msg: "Updated successfully" }); setEditorMessage({ error: false, msg: "Updated successfully" });
} else { } else {
+11 -3
View File
@@ -1,5 +1,7 @@
import { DB } from "@/backend/db"; import { DB } from "@/backend/db";
const allowedTables = ["learning_pages_html", "communications_pages_html"];
/* /*
This function fetches html content from database This function fetches html content from database
@@ -15,9 +17,12 @@ NOTE: This function could be used to fetch html content to all pages, not just
other identifier as parameter for this function. other identifier as parameter for this function.
*/ */
export async function getHTMLContent(id, type) { export async function getHTMLContent(type, id) {
try { try {
console.log("Table type: ", type); console.log("Table type: ", type);
if (!allowedTables.includes(type)) {
return "<p>Error loading page</p>";
}
// get content from db using parameterized query // get content from db using parameterized query
const result = await DB.pool( const result = await DB.pool(
"SELECT content FROM " + type + " WHERE id = $1", "SELECT content FROM " + type + " WHERE id = $1",
@@ -49,10 +54,13 @@ NOTE: This function also might need to be updated if the functions in these fi
should be used to fetch and update any html content, and not just the chapter pages. should be used to fetch and update any html content, and not just the chapter pages.
In this case, same kind of modifications apply as seen above in function getHTMLContent(id) In this case, same kind of modifications apply as seen above in function getHTMLContent(id)
*/ */
export async function updateHTMLContent(id, content) { export async function updateHTMLContent(type, id, content) {
if (!allowedTables.includes(type)) {
return false;
}
try { try {
const result = await DB.pool( const result = await DB.pool(
"UPDATE learning_pages_html SET content = $1 WHERE id = $2", "UPDATE " + type + " SET content = $1 WHERE id = $2",
[content, id] [content, id]
); );
if (result.rowCount == 1) { if (result.rowCount == 1) {