Simple backend providing connection between database and clients.
- rock solid and fast (thanks to Rust)
- compile-time-safe SQL queries
- using pool of database connections, enhancing performance by reusing connections across multiple requests
- asynchronous and capable of handling high concurrency
Prerequisites:
- Rust
- PostgreSQL (17+) - table creation script is located in
/dbdirectory
Building:
1. open project directory in terminal
2. execute build command `cargo build --release` for optimized version, omit `--release` otherwise
3. enjoy executable from `/target` directory
Usage:
Usage: lightweight-fgpe-server [OPTIONS] --connection-str <CONNECTION_STR>
Options:
--connection-str <CONNECTION_STR>
Database connection string (e.g., "postgres://user:password@host:port/database") Can also be set using the DATABASE_URL environment variable [env: DATABASE_URL=]
--db-pool-max-size <DB_POOL_MAX_SIZE>
Database connection pool size Can also be set using the DB_POOL_MAX_SIZE environment variable. Default value: 10 [env: DB_POOL_MAX_SIZE=] [default: 10]
--server-address <SERVER_ADDRESS>
Server listen address and port (e.g., "127.0.0.1:3000") Can also be set using the SERVER_ADDRESS environment variable. Default value: 127.0.0.1:3000 [env: SERVER_ADDRESS=] [default: 127.0.0.1:3000]
--keycloak-server-url <KEYCLOAK_SERVER_URL>
Keycloak server address and port (e.g., "127.0.0.1:8443") Can also be set using the KEYCLOAK_SERVER_URL environment variable. Default value: https://127.0.0.1:8443 [env: KEYCLOAK_SERVER_URL=] [default: https://127.0.0.1:8443]
--keycloak-realm <KEYCLOAK_REALM>
Keycloak realm name Can also be set using the KEYCLOAK_REALM environment variable. Default value: fgpe [env: KEYCLOAK_REALM=] [default: fgpe]
--keycloak-audiences <KEYCLOAK_AUDIENCES>
Keycloak allowed audiences (e.g., "account") Can also be set using the KEYCLOAK_AUDIENCES environment variable. Default value: fgpe-backend [env: KEYCLOAK_AUDIENCES=] [default: fgpe-backend]
--log-level <LOG_LEVEL>
Log level (e.g., "info") Can also be set using the RUST_LOG environment variable. Default value: info [env: RUST_LOG=] [default: info]
-h, --help
Print help
-V, --version
Print version
Testing:
1. open project directory in terminal
2. execute command `cargo test -- --test-threads=1`
All endpoints require authentication via a Keycloak-issued JWT Bearer token provided in the Authorization header. The token must be valid, unexpired, and contain the audience specified in the server configuration (--keycloak-audiences / KEYCLOAK_AUDIENCES).
Instructor that with id = 0 is treated as an admin.
All API responses follow a standard JSON structure:
Success:
{
"status_code": 200,
"status_message": "OK",
"data": { "something": {} }
}Error:
{
"status_code": 404,
"status_message": "Error description",
"data": null
}- 400 - Bad Request: Invalid request format or parameters.
- 401 - Unauthorized: Missing or invalid authentication token.
- 403 - Forbidden: Authenticated user lacks permission for the action/resource.
- 404 - Not Found: The requested resource (game, player, course, etc.) does not exist.
- 409 - Conflict: The request conflicts with the current state (e.g., unique constraint violation).
- 422 - Unprocessable Entity: The request was well-formed but semantically incorrect (e.g., invalid language choice).
- 500 - Internal Server Error: An unexpected error occurred on the server.
- submissions are considered correct when
result > 50
All endpoints require authentication.
GET /get_available_games- Description: Retrieves a list of public and active game IDs.
- Request Body: None
- Success Response Body (
datafield):[101, 105, 210]
POST /join_game- Description: Registers the authenticated player into a specific game.
- Request Body:
{ "player_id": 123, "game_id": 456, "language": "en" } - Success Response Body (
datafield):789 - Errors: 404 (Player/Game not found), 409 (Already registered)
POST /save_game- Description: Saves the player's current game state for a specific registration.
- Request Body:
{ "player_registrations_id": 789, "game_state": {} } - Success Response Body (
datafield):true - Errors: 404 (Registration not found)
POST /load_game- Description: Loads the player's previously saved game state for a specific registration.
- Request Body:
{ "player_registrations_id": 789 } - Success Response Body (
datafield):{} - Errors: 404 (Registration not found)
POST /leave_game- Description: Marks the player's registration in a game as inactive.
- Request Body:
{ "player_id": 123, "game_id": 456 } - Success Response Body (
datafield):null - Errors: 404 (Active registration not found)
POST /set_game_lang- Description: Sets the preferred language for the player within a specific game registration, if allowed by the course.
- Request Body:
{ "player_id": 123, "game_id": 456, "language": "fr" } - Success Response Body (
datafield):true - Errors: 404 (Registration not found), 422 (Language not allowed)
GET /get_player_games- Description: Retrieves the player registration IDs for the authenticated player.
- Query Params:
player_id(i64, required),active(bool, required) - Request Body: None
- Success Response Body (
datafield):[789, 801, 805]
- Errors: 404 (Player not found)
GET /get_game_metadata/{registration_id}- Description: Retrieves detailed metadata about a specific game registration and the associated game.
- Path Params:
registration_id(i64) - Request Body: None
- Success Response Body (
datafield):{ "registration_id": 789, "progress": 5, "joined_at": "2024-07-27T10:00:00Z", "left_at": null, "language": "en", "game_id": 456, "game_title": "Adventure Quest", "game_active": true, "game_description": "Explore the world!", "game_programming_language": "py", "game_total_exercises": 10, "game_start_date": "2024-07-01T00:00:00Z", "game_end_date": "2024-12-31T23:59:59Z" } - Errors: 404 (Registration not found)
GET /get_course_data- Description: Retrieves course-level data (gamification rules, module IDs) relevant to a specific game and language.
- Query Params:
game_id(i64, required),language(string, required) - Request Body: None
- Success Response Body (
datafield):{ "gamification_rule_conditions": "<some rules>", "gamification_complex_rules": "<some rules>", "gamification_rule_results": "<some rewards>", "module_ids": [11, 12, 15] } - Errors: 404 (Game or associated course not found)
GET /get_module_data- Description: Retrieves module details and relevant exercise IDs based on language filters.
- Query Params:
module_id(i64, required),language(string, required),programming_language(string, required) - Request Body: None
- Success Response Body (
datafield):{ "order": 1, "title": "Introduction", "description": "Getting started basics.", "start_date": "2024-07-01T00:00:00Z", "end_date": "2024-07-15T23:59:59Z", "exercise_ids": [101, 102, 103] } - Errors: 404 (Module not found)
GET /get_exercise_data- Description: Retrieves detailed data for a specific exercise, calculating context-dependent hidden/locked status based on game rules and player progress/unlocks.
- Query Params:
exercise_id(i64, required),game_id(i64, required),player_id(i64, required) - Request Body: None
- Success Response Body (
datafield):{ "order": 1, "title": "Hello World", "description": "Print 'Hello, World!'", "init_code": "", "pre_code": "", "post_code": "", "test_code": "assert output == 'Hello, World!'", "check_source": "", "mode": "code", "mode_parameters": {}, "difficulty": "easy", "hidden": false, "locked": false } - Errors: 404 (Exercise or Game not found)
POST /submit_solution- Description: Submits a solution attempt for an exercise, updates progress, and potentially grants rewards.
- Request Body:
{ "player_id": 123, "exercise_id": 101, "game_id": 456, "client": "web-ide", "submitted_code": "print('Hello, World!')", "metrics": {}, "result": 100.0, "result_description": {}, "feedback": "", "entered_at": "2024-07-27T11:00:00Z", "earned_rewards": [51, 52] } - Success Response Body (
datafield):(truetrueif first correct submission,falseotherwise) - Errors: 404 (Registration, Game, Exercise, or Reward ID not found)
POST /unlock- Description: Explicitly unlocks (makes visible/accessible) a specific exercise for the player.
- Request Body:
{ "player_id": 123, "exercise_id": 102 } - Success Response Body (
datafield):null - Errors: 404 (Player or Exercise not found)
GET /get_last_solution- Description: Retrieves the most recent relevant submission for an exercise (prioritizes last correct, falls back to last overall).
- Query Params:
player_id(i64, required),exercise_id(i64, required) - Request Body: None
- Success Response Body (
datafield) (nullif solution doesnt exist):{ "submitted_code": "print('Hello, World!')", "metrics": {}, "result": 100.0, "result_description": {}, "feedback": "", "submitted_at": "2024-07-27T11:05:00Z" } - Errors: 404 (Player or Exercise not found)
All endpoints require authentication.
GET /get_instructor_games- Description: Retrieves game IDs associated with the authenticated instructor.
- Query Params:
instructor_id(i64, required) - Request Body: None
- Success Response Body (
datafield):[456, 457, 459]
- Errors: 404 (Instructor not found)
GET /get_instructor_game_metadata- Description: Retrieves detailed metadata for a specific game if the instructor has access.
- Query Params:
instructor_id(i64, required),game_id(i64, required) - Request Body: None
- Success Response Body (
datafield):{ "title": "Adventure Quest", "description": "Explore the world!", "active": true, "public": false, "total_exercises": 10, "start_date": "2024-07-01T00:00:00Z", "end_date": "2024-12-31T23:59:59Z", "is_owner": true, "player_count": 25 } - Errors: 403 (Permission denied), 404 (Game not found)
GET /list_students- Description: Lists student IDs participating in a specific game, with optional filters.
- Query Params:
instructor_id(i64, required),game_id(i64, required),group_id(i64, optional),only_active(bool, optional, default=false) - Request Body: None
- Success Response Body (
datafield):[123, 124, 125, 127]
- Errors: 403 (Permission denied), 404 (Game or filter group not found)
GET /get_student_progress- Description: Retrieves progress metrics (attempts, solved, percentage) for a specific student in a game.
- Query Params:
instructor_id(i64, required),game_id(i64, required),player_id(i64, required) - Request Body: None
- Success Response Body (
datafield):{ "attempts": 15, "solved_exercises": 8, "progress": 80.0 } - Errors: 403 (Permission denied), 404 (Game/Player not found, or player not registered)
GET /get_student_exercises- Description: Retrieves lists of attempted and solved exercise IDs for a student in a game.
- Query Params:
instructor_id(i64, required),game_id(i64, required),player_id(i64, required) - Request Body: None
- Success Response Body (
datafield):{ "attempted_exercises": [101, 102, 103, 104, 105], "solved_exercises": [101, 102, 104] } - Errors: 403 (Permission denied), 404 (Game/Player not found, or player not registered)
GET /get_student_submissions- Description: Retrieves submission IDs for a student in a game, optionally filtering for success.
- Query Params:
instructor_id(i64, required),game_id(i64, required),player_id(i64, required),success_only(bool, optional, default=false) - Request Body: None
- Success Response Body (
datafield):[5001, 5005, 5008, 5010]
- Errors: 403 (Permission denied), 404 (Game/Player not found, or player not registered)
GET /get_submission_data- Description: Retrieves the full data for a specific submission.
- Query Params:
instructor_id(i64, required),submission_id(i64, required) - Request Body: None
- Success Response Body (
datafield):{ "id": 5005, "exercise_id": 102, "game_id": 456, "player_id": 123, "client": "web-ide", "submitted_code": "def func():\n pass", "metrics": {}, "result": 100.0, "result_description": {}, "first_solution": true, "feedback": "", "earned_rewards": [], "entered_at": "2024-07-27T12:00:00Z", "submitted_at": "2024-07-27T12:00:05Z" } - Errors: 403 (Permission denied for associated game), 404 (Submission or associated game not found)
GET /get_exercise_stats- Description: Retrieves statistics (attempts, success rate, difficulty) for an exercise within a game.
- Query Params:
instructor_id(i64, required),game_id(i64, required),exercise_id(i64, required) - Request Body: None
- Success Response Body (
datafield):{ "attempts": 50, "successful_attempts": 35, "difficulty": 30.0, "solved_percentage": 70.0 } - Errors: 403 (Permission denied), 404 (Game or Exercise not found)
GET /get_exercise_submissions- Description: Retrieves submission IDs for a specific exercise within a game, optionally filtering for success.
- Query Params:
instructor_id(i64, required),game_id(i64, required),exercise_id(i64, required),success_only(bool, optional, default=false) - Request Body: None
- Success Response Body (
datafield):[5001, 5003, 5005, 5006, 5008]
- Errors: 403 (Permission denied), 404 (Game or Exercise not found)
POST /create_game- Description: Creates a new game based on a course and assigns ownership to the requesting instructor.
- Request Body:
{ "instructor_id": 201, "title": "New Python Course Game", "public": false, "active": true, "description": "A game for the Python course.", "course_id": 33, "programming_language": "py", "module_lock": 0.5, "exercise_lock": true } - Success Response Body (
datafield):460 - Errors: 404 (Instructor or Course not found), 422 (Programming language not allowed for course)
POST /modify_game- Description: Modifies settings of an existing game. Only include fields to be changed.
- Request Body:
{ "instructor_id": 201, "game_id": 460, "title": "Updated Python Game Title", "active": false, "module_lock": 0.8 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Game not found)
POST /add_game_instructor- Description: Adds another instructor to a game, potentially granting ownership. Requires owner permission.
- Request Body:
{ "requesting_instructor_id": 201, "game_id": 460, "instructor_to_add_id": 205, "is_owner": false } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Game or instructor_to_add not found)
POST /remove_game_instructor- Description: Removes an instructor's association from a game. Requires owner permission.
- Request Body:
{ "requesting_instructor_id": 201, "game_id": 460, "instructor_to_remove_id": 205 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Game not found, or instructor not associated)
POST /activate_game- Description: Sets a game's status to active.
- Request Body:
{ "instructor_id": 201, "game_id": 460 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Game not found)
POST /stop_game- Description: Sets a game's status to inactive.
- Request Body:
{ "instructor_id": 201, "game_id": 460 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Game not found)
POST /remove_game_student- Description: Removes a student's registration from a game.
- Request Body:
{ "instructor_id": 201, "game_id": 460, "student_id": 123 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Game not found, or student not registered)
GET /translate_email_to_player_id- Description: Finds the player ID associated with a given email address.
- Query Params:
email(string, required) - Request Body: None
- Success Response Body (
datafield):123 - Errors: 404 (Email not found)
POST /create_group- Description: Creates a new group, assigns ownership, and optionally adds initial members.
- Request Body:
{ "instructor_id": 201, "display_name": "Study Group Alpha", "display_avatar": null, "member_list": [123, 124] } - Success Response Body (
datafield):55 - Errors: 404 (Instructor or member player not found), 409 (Group name conflict)
POST /dissolve_group- Description: Deletes a group and removes all members and ownership. Requires owner permission.
- Request Body:
{ "instructor_id": 201, "group_id": 55 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Group not found)
POST /add_group_member- Description: Adds a student (player) to a group. Requires owner permission.
- Request Body:
{ "instructor_id": 201, "group_id": 55, "player_id": 125 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Group or Player not found)
POST /remove_group_member- Description: Removes a student (player) from a group. Requires owner permission.
- Request Body:
{ "instructor_id": 201, "group_id": 55, "player_id": 124 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Group not found, or player not a member)
POST /create_player- Description: Creates a new player account, optionally adding them to a game and/or group. Requires admin or relevant game/group permission.
- Request Body:
{ "instructor_id": 201, "email": "[email protected]", "display_name": "New Student", "display_avatar": null, "game_id": 460, "group_id": 55, "language": "en" } - Success Response Body (
datafield):130 - Errors: 403 (Permission denied), 404 (Game or Group not found), 409 (Email conflict)
POST /disable_player- Description: Disables a player account. Requires admin permission.
- Request Body:
{ "instructor_id": 0, "player_id": 130 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Player not found)
POST /delete_player- Description: Permanently deletes a player account and all associated data. Requires admin permission.
- Request Body:
{ "instructor_id": 0, "player_id": 130 } - Success Response Body (
datafield):true - Errors: 403 (Permission denied), 404 (Player not found)
POST /generate_invite_link- Description: Generates a unique invite link (UUID), optionally associated with a game and/or group. Requires admin or group permission.
- Request Body:
{ "instructor_id": 201, "game_id": 460, "group_id": 55 } - Success Response Body (
datafield):{ "invite_uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } - Errors: 403/404 (Permission denied or Instructor/Game/Group not found)
POST /process_invite_link- Description: Processes an invite link for a player, adding them to the associated game/group if applicable.
- Request Body:
{ "player_id": 135, "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } - Success Response Body (
datafield):true - Errors: 404 (Invite, Player, Game, or Group not found)
All endpoints require authentication.
-
POST /import_course- Description: Imports a complete course structure (modules, exercises) from JSON data and assigns ownership to the specified instructor.
- Request Body: (See
ImportCoursePayloadstructure - complex, example below){ "instructor_id": 301, "public": false, "course_data": { "title": "Imported Advanced Course", "description": "Details about the imported course.", "languages": "en", "programming_languages": "rust", "gamification_rule_conditions": "{}", "gamification_complex_rules": "{}", "gamification_rule_results": "{}", "modules": [ { "order": 1, "title": "Module A", "description": "First module.", "language": "en", "start_date": null, "end_date": null, "exercises": [ { "version": 1.0, "order": 1, "title": "Exercise A.1", "description": "First exercise.", "language": "en", "programming_language": "rust", "init_code": "", "pre_code": "", "post_code": "", "test_code": "", "check_source": "", "hidden": false, "locked": false, "mode": "code", "mode_parameters": {}, "difficulty": "medium" } ] } ] } } - Success Response Body (
datafield):true - Errors: 404 (Instructor specified in payload not found)
-
GET /export_course- Description: Exports the full structure of a course (details, modules, exercises) as JSON. Requires course ownership or admin permission.
- Query Params:
instructor_id(i64, required),course_id(i64, required) - Request Body: None
- Success Response Body (
datafield): (SeeExportCourseResponsestructure - complex, example below){ "title": "Exported Course Title", "description": "Description of the course.", "languages": "en,fr", "programming_languages": "py,rust", "gamification_rule_conditions": "", "gamification_complex_rules": "", "gamification_rule_results": "", "modules": [ { "order": 1, "title": "Exported Module 1", "description": "First module.", "language": "en", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-06-30T23:59:59Z", "exercises": [ { "order": 1, "title": "Exported Exercise 1.1", "description": "First exercise.", "language": "en", "programming_language": "py", "init_code": "...", "pre_code": "...", "post_code": "...", "test_code": "...", "check_source": "...", "hidden": false, "locked": false, "mode": "code", "mode_parameters": {}, "difficulty": "easy" } ] } ] } - Errors: 403 (Permission denied), 404 (Course not found)
-
Acknowledgments
|
This software has been developed as a part of the FGPE++ Gamified Programming Learning at Scale (https://fgpeplus2.usz.edu.pl/) project which was co-funded by the European Union. |

