Base URL: http://localhost:3333 (by default)
Swagger API Docs: /swagger/index.html (Available internally when running in IsDebug mode)
Note: All [Protected] routes assume you have authenticated via /user/login and that you are sending the session cookie with your requests.
- URL:
/user/login - Method:
POST - Access: Public
- Body:
{ "email": "user@example.com", "password": "mysecurepassword" } - Response:
200 OK{ "message": "Logged in", "token": "jwt_token..." }
- URL:
/user/register - Method:
POST - Access: Public
- Body:
{ "name": "Jane Doe", "username": "janedoe", "email": "jane@example.com", "password": "SecurePassword1" // Min 8 chars, 1 letter, 1 number } - Response:
201 Created
- URL:
/profile - Method:
GET - Access:
[Protected] - Response:
200 OK{ "id": 1, "name": "Jane Doe", "username": "janedoe", "email": "jane@example.com" }
- URL:
/user/challenges/list - Method:
GET - Access:
[Protected] - Response:
200 OK{ "challenges": [ { "id": 1, "title": "Easy Web Challenge", "description": "Find the flag.", "difficulty": "Easy", "type": "Web", "points": 100, "solved": false } ] }
- URL:
/user/challenges/submit - Method:
POST - Access:
[Protected] - Body:
{ "challenge_id": 1, "flag": "GDSC{fake_flag}" } - Response:
200 OKon Correct,400 Bad Requeston Incorrect.
This section defines the target LMS structure to implement.
- Store YouTube embeds as iframe HTML (no extra media service required).
- Support questions in two placements:
- Lesson-level (applies to whole lesson)
- Video-segment-level (applies to a specific segment)
- Support all practical question types via one extensible schema.
- Grade answers case-insensitively.
- Migrate all existing LMS data without losing progress.
id(pk)titledescriptionorder
id(pk)module_id(fk -> modules.id)titlebody(rename from oldcontent, still Markdown/HTML)video_iframe(text, nullable)order
id(pk)lesson_id(fk -> lessons.id)titledescription(nullable)start_seconds(int, >= 0)end_seconds(int, > start_seconds)order
id(pk)lesson_id(fk -> lessons.id)video_segment_id(fk -> video_segments.id, nullable)placement(enum:lesson,segment)prompt(text)type(enum:single_choice,multi_choice,true_false,short_text,long_text,numeric,code)options(jsonb/text JSON, nullable)answer_key(jsonb/text JSON)points(int)order
- keep existing table
- keep relation to
question_id,user_id - recommended additions:
normalized_answer(text/json, nullable)attempt_no(int, default 1)
{
"type": "single_choice",
"options": ["A", "B", "C"],
"answer_key": {"correct": "b"}
}{
"type": "multi_choice",
"options": ["A", "B", "C", "D"],
"answer_key": {"correct": ["a", "c"]}
}{
"type": "true_false",
"answer_key": {"correct": true}
}{
"type": "short_text",
"answer_key": {
"accepted": ["sql injection", "sqli"]
}
}{
"type": "numeric",
"answer_key": {
"value": 3.14159,
"tolerance": 0.01
}
}{
"type": "code",
"answer_key": {
"accepted": ["nmap -sV target", "nmap -sv target"]
}
}Apply normalization before comparison:
- convert to lowercase
- trim leading/trailing spaces
- collapse repeated internal whitespace to a single space
For each type:
single_choice: compare normalized submitted value with normalized correct valuemulti_choice: normalize each choice, deduplicate, sort, compare exact set equalitytrue_false: parse to boolean (true/false,1/0,yes/no) then compareshort_text/long_text/code: normalize and compare against each accepted answernumeric: parse float and validateabs(input - value) <= tolerance
- URL:
/user/lms/modules - Method:
GET - Access:
[Protected] - Response:
200 OK(modules with lesson summaries)
- URL:
/user/lms/lessons/{id} - Method:
GET - Access:
[Protected] - Response:
200 OK{ "status": "success", "lesson": { "id": 10, "title": "Intro to Web Security", "body": "# Markdown body", "video_iframe": "<iframe src=\"https://www.youtube.com/embed/VIDEO_ID\" allowfullscreen></iframe>", "segments": [ { "id": 1, "title": "SQLi Demo", "start_seconds": 35, "end_seconds": 140 } ], "questions": [ { "id": 100, "placement": "lesson", "video_segment_id": null, "type": "single_choice", "prompt": "What does SQL stand for?", "options": ["Structured Query Language", "Secure Query Layer"], "points": 75 }, { "id": 101, "placement": "segment", "video_segment_id": 1, "type": "short_text", "prompt": "Name this attack type", "options": null, "points": 125 } ] } }
- URL:
/user/lms/questions/{id}/submit - Method:
POST - Access:
[Protected] - Body:
{ "answer": "Structured query language" } - Response:
200 OK{ "status": "success", "correct": true, "awarded_points": 75, "normalized_answer": "structured query language" }
- URL:
/user/lms/progress - Method:
GET - Access:
[Protected] - Response:
200 OK(all attempts and correctness status, grouped by lesson/question in service layer)
Keep existing module/lesson/question CRUD and add segment CRUD:
/admin/lms/modules/admin/lms/lessons/admin/lms/video-segments/admin/lms/questions
Recommended extra admin operation:
POST /admin/lms/migrations/v2(idempotent migration runner)
Run once in a transactional/idempotent migration:
-
Schema additions
- add
lessons.body,lessons.video_iframe - create
video_segments - add
questions.video_segment_id,questions.placement,questions.prompt,questions.answer_key,questions.order
- add
-
Backfill lesson text
- set
lessons.body = lessons.contentfor all existing rows
- set
-
Backfill question fields
questions.prompt = questions.contentquestions.placement = 'lesson'questions.video_segment_id = NULL- convert old
correct_answerandoptionstoanswer_key/JSON form
-
Preserve solves
- keep all existing
question_solvesrows unchanged - old solves remain valid because question IDs are preserved
- keep all existing
-
Compatibility period
- for one release, keep reading legacy fields (
content,correct_answer) if new fields are empty - then remove legacy fields in a later cleanup migration
- for one release, keep reading legacy fields (
- Reject
video_iframeif not an<iframe ...>with a YouTube embed URL. - If
placement = segment, require non-nullvideo_segment_idbelonging to same lesson. - If
placement = lesson, enforcevideo_segment_id = null. - Enforce
points >= 0. - Enforce
start_seconds < end_secondsfor segments.
These routes are protected by AdminMiddleware meaning the request session must be attached to a User with IsAdmin = true.
-
[GET] /admin/challenges/list- Fetch all challenges -
[POST] /admin/challenges/create- Create a new puzzle{ "Title": "SQL Injection Basics", "Description": "Find the flaw in the login form", "Difficulty": "Easy", "Type": "Web", "Points": 100, "Flag": "GDSC{sql_inj3ct}", "Hidden": false, "Docker": false, "DockerImage": "" }(Note that fields must be Capitalized depending on the model struct bindings, though case-insensitive forms like "title" usually work)
-
[PUT] /admin/challenges/{id}- Modify an existing puzzle (send similarly mapped payload as above).
LMS v2 endpoints support full RESTful CRUD operations (GET, POST, PUT, DELETE). Base paths:
-
/admin/lms/modules -
/admin/lms/lessons -
/admin/lms/video-segments -
/admin/lms/questions -
[GET] /admin/lms/<resource>- List all instances of<resource> -
[GET] /admin/lms/<resource>/{id}- Fetch single instance -
[POST] /admin/lms/<resource>- Create a new instance (JSON payload)- For
lessons, include optionalvideo_iframe. - For
video-segments, includelesson_id,start_seconds,end_seconds, and metadata. - For
questions, includeplacement(lesson/segment) and questiontype(single_choice,multi_choice,true_false,short_text,long_text,numeric,code).
- For
-
[PUT] /admin/lms/<resource>/{id}- Update an instance (JSON payload) -
[DELETE] /admin/lms/<resource>/{id}- Soft-delete an instance -
[POST] /admin/lms/migrations/v2- Run idempotent migration from legacy LMS schema
These endpoints aggregate all correctly solved tasks across the user base and return an ordered array of user scores, automatically filtering out admin accounts.
- URL:
/scoreboard/ctf - Method:
GET - Access: Public
- Response:
200 OK{ "status": "success", "scoreboard": [ { "user_id": 2, "username": "hacker123", "name": "Jane Doe", "score": 1400 } ] }
- URL:
/scoreboard/lms - Method:
GET - Access: Public
- Response:
200 OK(Same schema as above, calculating from the theoretical LMS Question solves instead)