diff --git a/rag-service/main.py b/rag-service/main.py index e766c80..33a5c0c 100644 --- a/rag-service/main.py +++ b/rag-service/main.py @@ -274,6 +274,8 @@ def update_processing_progress(session_id, stage, progress): "/validate-session-write", "/sessions/lookup", "/demo-query-validation", + "/sessions/flashcards", + "/sessions/flashcards/progress", } PROTECTED_RAG_PREFIXES = ( "/ask/", @@ -4865,7 +4867,7 @@ def generate_flashcards_from_text(indexed_docs, count): return cards -@app.post("/sessions/flashcards/generate") +@app.post("/sessions/flashcards") def generate_flashcards(data: FlashcardGenerateRequest): cleanup_expired_sessions() session_id = str(data.session_id) @@ -4909,7 +4911,7 @@ def generate_flashcards(data: FlashcardGenerateRequest): return {"flashcards": cards} -@app.post("/sessions/flashcards/update-progress") +@app.post("/sessions/flashcards/progress") def update_flashcard_progress(data: FlashcardProgressRequest): cleanup_expired_sessions() session_id = str(data.session_id) diff --git a/server.js b/server.js index 449963b..2fadfe1 100644 --- a/server.js +++ b/server.js @@ -23,6 +23,8 @@ const { generateFlashcardsSchema, updateFlashcardProgressSchema, MAX_QUESTION_LENGTH, + generateFlashcardsSchema, + updateFlashcardProgressSchema, } = require("./validators/schemas"); const { clientIpFromRequest } = require("./security/ip"); const { createRedisClient } = require("./security/redis"); @@ -1754,6 +1756,47 @@ app.post("/sessions/lookup", async (req, res) => { } }); +app.post("/sessions/flashcards", async (req, res) => { + const validation = generateFlashcardsSchema.safeParse(req.body); + + if (!validation.success) { + return res.status(400).json({ + error: "Validation failed", + details: validation.error.flatten(), + }); + } + + try { + const response = await axios.post( + `${RAG_SERVICE_URL}/sessions/flashcards`, + validation.data, + { headers: ragAuthHeaders(), timeout: 60000 }, + ); + return res.json(response.data); + } catch (err) { + return propagateRagError(err, res, "Failed to generate flashcards"); + } +}); + +app.post("/sessions/flashcards/progress", async (req, res) => { + const validation = updateFlashcardProgressSchema.safeParse(req.body); + + if (!validation.success) { + return res.status(400).json({ + error: "Validation failed", + details: validation.error.flatten(), + }); + } + + try { + const response = await axios.post( + `${RAG_SERVICE_URL}/sessions/flashcards/progress`, + validation.data, + { headers: ragAuthHeaders(), timeout: 10000 }, + ); + return res.json(response.data); + } catch (err) { + return propagateRagError(err, res, "Failed to update flashcard progress"); app.get("/processing-status/:session_id", async (req, res) => { const { session_id } = req.params; diff --git a/server.test.js b/server.test.js index 2454893..653488d 100644 --- a/server.test.js +++ b/server.test.js @@ -518,6 +518,7 @@ describe("route error responses", () => { method: "POST", headers: { "Content-Type": "application/json", + Authorization: `Bearer ${jwt.sign({ role: "authenticated" }, process.env.SUPABASE_JWT_SECRET)}`, Authorization: `Bearer ${validToken}`, }, body: JSON.stringify({ @@ -564,6 +565,7 @@ describe("route error responses", () => { method: "POST", headers: { "Content-Type": "application/json", + Authorization: `Bearer ${jwt.sign({ role: "authenticated" }, process.env.SUPABASE_JWT_SECRET)}`, Authorization: `Bearer ${validToken}`, }, body: JSON.stringify({ @@ -850,6 +852,59 @@ describe("route error responses", () => { } }); + test("POST /sessions/flashcards generates flashcards", async () => { + const originalPost = axios.post; + let forwardedHeaders = null; + + axios.post = async (url, body, options) => { + forwardedHeaders = options?.headers; + return { data: { flashcards: [] } }; + }; + + try { + const res = await fetch(`${baseUrl}/sessions/flashcards`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + session_id: "550e8400-e29b-41d4-a716-446655440000", + session_secret: "secret-abc", + count: 5, + }), + }); + assert.equal(res.status, 200); + assert.equal(forwardedHeaders["X-Internal-Token"], process.env.INTERNAL_RAG_TOKEN); + } finally { + axios.post = originalPost; + } + }); + + test("POST /sessions/flashcards/progress updates progress", async () => { + const originalPost = axios.post; + let forwardedHeaders = null; + + axios.post = async (url, body, options) => { + forwardedHeaders = options?.headers; + return { data: { success: true } }; + }; + + try { + const res = await fetch(`${baseUrl}/sessions/flashcards/progress`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + session_id: "550e8400-e29b-41d4-a716-446655440000", + session_secret: "secret-abc", + card_id: "card-1", + rating: "good", + }), + }); + assert.equal(res.status, 200); + assert.equal(forwardedHeaders["X-Internal-Token"], process.env.INTERNAL_RAG_TOKEN); + } finally { + axios.post = originalPost; + } + }); + test("GET unknown route returns 404", async () => { const res = await fetch(`${baseUrl}/nonexistent`, { method: "GET",