Merge pull request #460 from OHTU-German-learning-website/459-add-check-for-exercises-that-only-admin-can-create-them

459 add check for exercises that only admin can create them
This commit is contained in:
Filippa Högnäsbacka
2025-12-08 09:25:42 +02:00
committed by GitHub
4 changed files with 222 additions and 196 deletions
@@ -1,53 +1,60 @@
import { DB } from "@/backend/db";
import { withAuth } from "@/backend/middleware/withAuth";
export async function POST(request) {
const json = await request.json();
const { title, targetCategory, targetWords, allWords } = json;
export const POST = withAuth(
async (request) => {
const json = await request.json();
const { title, targetCategory, targetWords, allWords } = json;
if (!title || !targetCategory || !targetWords || !allWords) {
return Response.json(
{ error: "Alle Felder sind erforderlich." },
{ status: 400 }
if (!title || !targetCategory || !targetWords || !allWords) {
return Response.json(
{ error: "Alle Felder sind erforderlich." },
{ status: 400 }
);
}
if (title.length < 3 || title.length > 50) {
return Response.json(
{ error: "Der Titel muss zwischen 3 und 50 Zeichen lang sein." },
{ status: 422 }
);
}
if (targetCategory.length < 3 || targetCategory.length > 30) {
return Response.json(
{ error: "Die Kategorie muss zwischen 3 und 30 Zeichen lang sein." },
{ status: 422 }
);
}
if (targetWords.length < 1 || targetWords.length > 1000) {
return Response.json(
{ error: "Es müssen zwischen 1 und 1000 Zielwörter vorhanden sein." },
{ status: 422 }
);
}
if (allWords.length < 1 || allWords.length > 1000) {
return Response.json(
{ error: "Es müssen zwischen 1 und 1000 Wörter vorhanden sein." },
{ status: 422 }
);
}
const existingExercise = await DB.pool(
"SELECT * FROM click_exercises WHERE title = $1",
[title]
);
}
if (title.length < 3 || title.length > 50) {
return Response.json(
{ error: "Der Titel muss zwischen 3 und 50 Zeichen lang sein." },
{ status: 422 }
);
}
if (targetCategory.length < 3 || targetCategory.length > 30) {
return Response.json(
{ error: "Die Kategorie muss zwischen 3 und 30 Zeichen lang sein." },
{ status: 422 }
);
}
if (targetWords.length < 1 || targetWords.length > 1000) {
return Response.json(
{ error: "Es müssen zwischen 1 und 1000 Zielwörter vorhanden sein." },
{ status: 422 }
);
}
if (allWords.length < 1 || allWords.length > 1000) {
return Response.json(
{ error: "Es müssen zwischen 1 und 1000 Wörter vorhanden sein." },
{ status: 422 }
);
}
const existingExercise = await DB.pool(
"SELECT * FROM click_exercises WHERE title = $1",
[title]
);
if (existingExercise.rows.length > 0) {
return Response.json(
{ error: "Eine Übung mit diesem Titel existiert bereits." },
{ status: 409 }
);
}
const id = await DB.pool(
`INSERT INTO click_exercises (title, category, target_words, all_words)
if (existingExercise.rows.length > 0) {
return Response.json(
{ error: "Eine Übung mit diesem Titel existiert bereits." },
{ status: 409 }
);
}
const id = await DB.pool(
`INSERT INTO click_exercises (title, category, target_words, all_words)
VALUES ($1, $2, $3, $4) returning id`,
[title, targetCategory, targetWords, allWords]
);
return Response.json({ id: id.rows[0].id }, { status: 201 });
}
[title, targetCategory, targetWords, allWords]
);
return Response.json({ id: id.rows[0].id }, { status: 201 });
},
{
requireAdmin: true,
requireAuth: true,
}
);
@@ -9,8 +9,8 @@ describe("create click_exercises API", () => {
useTestDatabase();
it("should create a new click_exercise with valid details", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -34,8 +34,8 @@ describe("create click_exercises API", () => {
expect(exercise.rows.length).toBe(1);
});
it("should return 400 if required fields are missing", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -51,8 +51,8 @@ describe("create click_exercises API", () => {
expect(json.error).toBe("Alle Felder sind erforderlich.");
});
it("should return 422 if title is too short", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -71,8 +71,8 @@ describe("create click_exercises API", () => {
);
});
it("should return 422 if title is too long", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -91,8 +91,8 @@ describe("create click_exercises API", () => {
);
});
it("should return 422 if targetCategory is too short", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -111,8 +111,8 @@ describe("create click_exercises API", () => {
);
});
it("should return 422 if targetCategory is too long", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -131,8 +131,8 @@ describe("create click_exercises API", () => {
);
});
it("should return 422 if targetWords is empty", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -151,8 +151,8 @@ describe("create click_exercises API", () => {
);
});
it("should return 422 if targetWords is too long", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -170,8 +170,8 @@ describe("create click_exercises API", () => {
);
});
it("should return 422 if allWords is empty", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -190,8 +190,8 @@ describe("create click_exercises API", () => {
);
});
it("should return 422 if allWords is too long", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
const response = await POST(
mockPost("api/exercises/create/click", {
@@ -210,8 +210,8 @@ describe("create click_exercises API", () => {
);
});
it("should return 409 if exercise with the same title already exists", async () => {
const user = await TestFactory.user();
const { mockPost } = useTestRequest(user);
const admin = await TestFactory.user({ is_admin: true });
const { mockPost } = useTestRequest(admin);
// Create the first exercise
await POST(
@@ -1,84 +1,91 @@
import { DB } from "@/backend/db";
import { withAuth } from "@/backend/middleware/withAuth";
export async function POST(request) {
try {
const rawBody = await request.json();
const body = rawBody.body ?? rawBody;
const title = body.title;
const fields = body.fields;
export const POST = withAuth(
async (request) => {
try {
const rawBody = await request.json();
const body = rawBody.body ?? rawBody;
const title = body.title;
const fields = body.fields;
if (!fields || !Array.isArray(fields) || fields.length === 0) {
return Response.json({ error: "No fields provided" }, { status: 400 });
}
if (!fields || !Array.isArray(fields) || fields.length === 0) {
return Response.json({ error: "No fields provided" }, { status: 400 });
}
const created_by = 1;
const exerciseCategory = "dnd";
const created_by = 1;
const exerciseCategory = "dnd";
// 1. Insert into exercises
const exRes = await DB.pool(
`INSERT INTO exercises (created_by, category)
// 1. Insert into exercises
const exRes = await DB.pool(
`INSERT INTO exercises (created_by, category)
VALUES ($1, $2)
RETURNING id`,
[created_by, exerciseCategory]
);
const exercise_id = exRes.rows[0].id;
[created_by, exerciseCategory]
);
const exercise_id = exRes.rows[0].id;
// 2. Insert into dnd_exercises
const dndRes = await DB.pool(
`INSERT INTO dnd_exercises (created_by, exercise_id, title)
// 2. Insert into dnd_exercises
const dndRes = await DB.pool(
`INSERT INTO dnd_exercises (created_by, exercise_id, title)
VALUES ($1, $2, $3)
RETURNING id`,
[created_by, exercise_id, title]
);
const dnd_id = dndRes.rows[0].id;
// Process fields
for (const field of fields) {
// Insert category
const catRes = await DB.pool(
`INSERT INTO dnd_categories (category, color)
[created_by, exercise_id, title]
);
const dnd_id = dndRes.rows[0].id;
// Process fields
for (const field of fields) {
// Insert category
const catRes = await DB.pool(
`INSERT INTO dnd_categories (category, color)
VALUES ($1, $2)
ON CONFLICT (category, color) DO NOTHING
RETURNING id`,
[field.category, field.color]
);
[field.category, field.color]
);
const category_id =
catRes.rows[0]?.id ||
(
await DB.pool(
`SELECT id FROM dnd_categories WHERE category = $1 AND color = $2`,
[field.category, field.color]
)
).rows[0].id;
const category_id =
catRes.rows[0]?.id ||
(
await DB.pool(
`SELECT id FROM dnd_categories WHERE category = $1 AND color = $2`,
[field.category, field.color]
)
).rows[0].id;
// Insert words
const words = field.content.split(",").map((word) => word.trim());
for (const word of words) {
const wordRes = await DB.pool(
`INSERT INTO draggable_words (word)
// Insert words
const words = field.content.split(",").map((word) => word.trim());
for (const word of words) {
const wordRes = await DB.pool(
`INSERT INTO draggable_words (word)
VALUES ($1)
ON CONFLICT (word) DO UPDATE SET word = EXCLUDED.word
RETURNING id`,
[word]
);
[word]
);
const word_id = wordRes.rows[0]?.id;
const word_id = wordRes.rows[0]?.id;
// Insert mapping
await DB.pool(
`INSERT INTO word_category_mappings (word_id, category_id, exercise_id)
// Insert mapping
await DB.pool(
`INSERT INTO word_category_mappings (word_id, category_id, exercise_id)
VALUES ($1, $2, $3)`,
[word_id, category_id, dnd_id]
);
[word_id, category_id, dnd_id]
);
}
}
}
return Response.json({ success: true, exerciseId: exercise_id });
} catch (err) {
console.error("Error creating dragdrop exercise:", err);
return Response.json(
{ error: "Fehler beim Erstellen der Übung." },
{ status: 500 }
);
return Response.json({ success: true, exerciseId: exercise_id });
} catch (err) {
console.error("Error creating dragdrop exercise:", err);
return Response.json(
{ error: "Fehler beim Erstellen der Übung." },
{ status: 500 }
);
}
},
{
requireAdmin: true,
requireAuth: true,
}
}
);
@@ -1,87 +1,99 @@
import { DB } from "@/backend/db";
import { withAuth } from "@/backend/middleware/withAuth";
export async function POST(request) {
try {
const json = await request.json();
const { title, description, content } = json;
export const POST = withAuth(
async (request) => {
try {
const json = await request.json();
const { title, description, content } = json;
// Validation
if (!title || !content || !Array.isArray(content) || content.length === 0) {
return Response.json(
{ error: "Titel und Inhalt erforderlich." },
{ status: 400 }
// Validation
if (
!title ||
!content ||
!Array.isArray(content) ||
content.length === 0
) {
return Response.json(
{ error: "Titel und Inhalt erforderlich." },
{ status: 400 }
);
}
// Prevent duplicates
const exists = await DB.pool(
"SELECT id FROM multichoice_exercises WHERE title = $1",
[title]
);
}
if (exists.rows.length > 0) {
return Response.json(
{ error: "Eine Übung mit diesem Titel existiert bereits." },
{ status: 409 }
);
}
// Prevent duplicates
const exists = await DB.pool(
"SELECT id FROM multichoice_exercises WHERE title = $1",
[title]
);
if (exists.rows.length > 0) {
return Response.json(
{ error: "Eine Übung mit diesem Titel existiert bereits." },
{ status: 409 }
// Insert into exercises table
const exResult = await DB.pool(
`INSERT INTO exercises (category) VALUES ('multichoice') RETURNING id`
);
}
const exercise_id = exResult.rows[0].id;
// Insert into exercises table
const exResult = await DB.pool(
`INSERT INTO exercises (category) VALUES ('multichoice') RETURNING id`
);
const exercise_id = exResult.rows[0].id;
// Insert into multichoice_exercises metadata table
const mcRes = await DB.pool(
`INSERT INTO multichoice_exercises (exercise_id, title, exercise_description)
// Insert into multichoice_exercises metadata table
const mcRes = await DB.pool(
`INSERT INTO multichoice_exercises (exercise_id, title, exercise_description)
VALUES ($1, $2, $3)
RETURNING id`,
[exercise_id, title, description ?? null]
);
const multichoice_id = mcRes.rows[0].id;
[exercise_id, title, description ?? null]
);
const multichoice_id = mcRes.rows[0].id;
// Insert content rows
let contentOrder = 1;
for (const item of content) {
const contentRes = await DB.pool(
`INSERT INTO multichoice_content
// Insert content rows
let contentOrder = 1;
for (const item of content) {
const contentRes = await DB.pool(
`INSERT INTO multichoice_content
(multichoice_exercise_id, content_type, content_value, content_order, correct_answer)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
[
multichoice_id,
item.type,
item.value ?? "",
contentOrder,
item.type === "multichoice" ? item.correct : null,
]
);
[
multichoice_id,
item.type,
item.value ?? "",
contentOrder,
item.type === "multichoice" ? item.correct : null,
]
);
const content_id = contentRes.rows[0].id;
const content_id = contentRes.rows[0].id;
// Insert options if it is a selection field
if (item.type === "multichoice" && item.options?.length > 0) {
for (const option of item.options) {
await DB.pool(
`INSERT INTO multichoice_options (multichoice_content_id, option_value)
// Insert options if it is a selection field
if (item.type === "multichoice" && item.options?.length > 0) {
for (const option of item.options) {
await DB.pool(
`INSERT INTO multichoice_options (multichoice_content_id, option_value)
VALUES ($1, $2)`,
[content_id, option]
);
[content_id, option]
);
}
}
contentOrder++;
}
contentOrder++;
return Response.json(
{ success: true, exerciseId: exercise_id },
{ status: 201 }
);
} catch (err) {
console.error("Error saving multichoice exercise:", err);
return Response.json(
{ error: "Fehler beim Speichern der Übung." },
{ status: 500 }
);
}
return Response.json(
{ success: true, exerciseId: exercise_id },
{ status: 201 }
);
} catch (err) {
console.error("Error saving multichoice exercise:", err);
return Response.json(
{ error: "Fehler beim Speichern der Übung." },
{ status: 500 }
);
},
{
requireAdmin: true,
requireAuth: true,
}
}
);