Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
37f90f7
feat: initial website design
Ronaldo93 Jun 11, 2026
959ee2c
feat: Python code execution, and backend for website.
Ronaldo93 Jun 11, 2026
af8c5d4
refactor: splitting editor into smaller components
Ronaldo93 Jun 11, 2026
ec5b681
feat: improved design for the IDE with terminal.
Ronaldo93 Jun 12, 2026
1251e0c
feat: implement student problem view layout
Ronaldo93 Jun 12, 2026
3591eba
feat(web): update default configuration example
Ronaldo93 Jun 12, 2026
afa4e97
feat: removed autosaved pill
Ronaldo93 Jun 13, 2026
e6d1b83
feat(web): changed problem location to left and collapse chat ui by d…
Ronaldo93 Jun 13, 2026
97e43a2
test(web): add api check for code execution testing
Ronaldo93 Jun 14, 2026
8ce1c4b
refactor: moved coding page to course endpoint
Ronaldo93 Jun 15, 2026
8b0c888
feat: a new homepage with user info and question list throughout the …
Ronaldo93 Jun 15, 2026
70b2ea8
feat: integrate Convex for backend data management and add loading st…
Ronaldo93 Jun 16, 2026
20b9547
chore: Replace custom markdown parser to react-markdown.
Ronaldo93 Jun 16, 2026
5dd5df2
chore: using git-cliff for version bumping
Ronaldo93 Jun 17, 2026
42b1b85
feat: implement authentication UI components and route structure
Ronaldo93 Jun 17, 2026
88366b2
fix: css styles
Ronaldo93 Jun 16, 2026
e9b2a9d
fix: authentication button not working
Ronaldo93 Jun 17, 2026
42667fa
feat(auth): magic link with better-auth
Ronaldo93 Jun 18, 2026
fe4b808
fix(ci): not running.
Ronaldo93 Jun 19, 2026
1232a3c
chore: additional variable for better auth configuration
Ronaldo93 Jun 19, 2026
ed0489d
feat: invitation code system for the application
Ronaldo93 Jun 19, 2026
caee750
chore: update environment variable configuration instruction
Ronaldo93 Jun 19, 2026
34daefb
ci: fix broken workflow
Ronaldo93 Jun 19, 2026
102f1c9
fix: tailwind styling
Ronaldo93 Jun 19, 2026
26312c4
fix: problem id not present.
Ronaldo93 Jun 19, 2026
23ccabd
ci: disable version preview to prevent spammy
Ronaldo93 Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/pr-preview.yaml.disabled
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Release Preview

on:
pull_request:
branches:
- main # Runs whenever a PR is opened or updated against main

jobs:
preview:
name: Preview Next Version
runs-on: ubuntu-latest
permissions:
pull-requests: write # Required so the bot can post a comment on the PR

steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history so git-cliff can calculate the bump correctly

# 1. Run git-cliff to see what version it calculates based on the PR commits
- name: Calculate Next Version
id: git_cliff_dry
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --bumped-version
continue-on-error: true

# 2. Extract just the version string from the output
- name: Parse Version
id: version_parse
run: |
# git-cliff outputs text; we grab the version line
VERSION=$(echo "${{ steps.git_cliff_dry.outputs.content }}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
echo "calculated_version=v$VERSION" >> $GITHUB_OUTPUT

# 3. Post a comment on the Pull Request with the preview data
- name: Comment PR
uses: thollander/actions-comment-pull-request@v2
with:
message: |
### 🚀 Release Preview
If you merge this PR right now, `git-cliff` will bump the project to:
**`${{ steps.version_parse.outputs.calculated_version }}`**

_Based on the conventional commits found in this branch._
54 changes: 54 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Automated Release

on:
push:
branches:
- main # Triggers when code is merged into main

jobs:
release:
name: Generate Changelog & Release
runs-on: ubuntu-latest
permissions:
contents: write # Crucial: Allows the bot to push changes and create releases

steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetches all history so git-cliff can read past commits
Comment on lines +16 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all workflow action references are SHA-pinned and checkout disables credential persistence.
fd -t f '.ya?ml$' .github/workflows -x sh -c '
  f="$1"
  echo "== $f =="
  rg -n "uses:\s*[^@]+@([A-Za-z0-9._/-]+)$" "$f"
  rg -n "uses:\s*actions/checkout@" "$f" -A4 -B1
' sh {}

Repository: RMIT-BDSL/ExemplAI

Length of output: 471


🏁 Script executed:

cat -n .github/workflows/release.yaml

Repository: RMIT-BDSL/ExemplAI

Length of output: 2117


Pin action SHAs and disable checkout credential persistence.

Lines 17, 29, 37, and 46 use mutable action tags (@v4, @v2), and line 19 omits persist-credentials: false. In a release workflow with contents: write permission, this creates a supply-chain attack surface and increases the blast radius of a potential token compromise.

Suggested hardening diff
       - name: Checkout Code
-        uses: actions/checkout@v4
+        uses: actions/checkout@<PINNED_SHA>
         with:
           fetch-depth: 0 # Fetches all history so git-cliff can read past commits
+          persist-credentials: false

       - name: Determine Next Version
         id: next_version
-        uses: orhun/git-cliff-action@v4
+        uses: orhun/git-cliff-action@<PINNED_SHA>
         with:
           config: cliff.toml
           args: --bump --bump-deps

       - name: Generate Changelog
         id: git_cliff
-        uses: orhun/git-cliff-action@v4
+        uses: orhun/git-cliff-action@<PINNED_SHA>
         with:
           config: cliff.toml
           args: --verbose --bump --strip all

       - name: Create GitHub Release
-        uses: softprops/action-gh-release@v2
+        uses: softprops/action-gh-release@<PINNED_SHA>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 16-19: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yaml around lines 16 - 19, Replace all mutable
action version tags with pinned commit SHAs in the release workflow.
Specifically, update the actions/checkout@v4 on line 17 and any other actions on
lines 29, 37, and 46 that use mutable tags like `@v4` or `@v2` with their full
commit SHA references instead. Additionally, add persist-credentials: false to
the checkout action's with block on line 19 to prevent credential persistence
and reduce the attack surface in this release workflow that has contents: write
permission.

Source: Linters/SAST tools


- name: Setup Git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# 1. Use git-cliff to calculate the next version number based on semantic commits
- name: Determine Next Version
id: next_version
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --bump --bump-deps

# 2. Generate the actual changelog text for this specific release
- name: Generate Changelog
id: git_cliff
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --verbose --bump --strip all
env:
OUTPUT: NEW_CHANGELOG.md

# 3. Create the GitHub Release and paste the changelog into it
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.next_version.outputs.version }}
name: Release ${{ steps.next_version.outputs.version }}
body: ${{ steps.git_cliff.outputs.content }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
data/csedm-2019/
**/.venv/
**/.env

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ignore all environment-file variants, not only .env.

Line 3 only ignores the exact .env filename. Files like .env.local / .env.production can still be committed with secrets.

Suggested fix
 **/.env
+**/.env.*
+!**/.env.example
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**/.env
**/.env
**/.env.*
!**/.env.example
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore at line 3, The pattern on line 3 of .gitignore uses **/.env which
only matches the exact filename .env, allowing variants like .env.local and
.env.production to be committed accidentally. Change the pattern from **/.env to
**/.env* to match all environment files that start with .env prefix, ensuring
all environment file variants are properly ignored from version control.

**/__pycache__/
94 changes: 94 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration


[changelog]
# A Tera template to be rendered for each release in the changelog.
# See https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}
"""
# Remove leading and trailing whitespaces from the changelog's body.
trim = true
# Render body even when there are no releases to process.
render_always = true
# An array of regex based postprocessors to modify the changelog.
postprocessors = [
# Replace the placeholder <REPO> with a URL.
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
]
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"

[git]
# Parse commits according to the conventional commits specification.
# See https://www.conventionalcommits.org
conventional_commits = true
# Exclude commits that do not match the conventional commits specification.
filter_unconventional = true
# Require all commits to be conventional.
# Takes precedence over filter_unconventional.
require_conventional = false
# Split commits on newlines, treating each line as an individual commit.
split_commits = false
# An array of regex based parsers to modify commit messages prior to further processing.
commit_preprocessors = [
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit message using https://github.com/crate-ci/typos.
# If the spelling is incorrect, it will be fixed automatically.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# Prevent commits that are breaking from being excluded by commit parsers.
protect_breaking_commits = false
# An array of regex based parsers for extracting data from the commit message.
# Assigns commits to groups.
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = ".*", group = "<!-- 10 -->💼 Other" },
]
# Exclude commits that are not matched by any commit parser.
filter_commits = false
# Fail on a commit that is not matched by any commit parser.
fail_on_unmatched_commit = false
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
link_parsers = []
# Include only the tags that belong to the current branch.
use_branch_tags = false
# Order releases topologically instead of chronologically.
topo_order = false
# Order commits topologically instead of chronologically.
topo_order_commits = true
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "oldest"
# Process submodules commits
recurse_submodules = false
2 changes: 2 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
JUDGE0_ENDPOINT=
JUDGE0_AUTH_KEY=
1 change: 1 addition & 0 deletions server/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
9 changes: 9 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
> [!warning]
> This is currently under development

Hosts backend for ExemplAI.

# Tech stack
- FastAPI
- Python 3.13
- uvicorn
82 changes: 82 additions & 0 deletions server/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import requests
import os
from model.student_code import StudentCode
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
Comment on lines +3 to +5
from dotenv import load_dotenv

# logging with rich
import logging
from rich.logging import RichHandler

log = logging.getLogger("rich")
log.setLevel(logging.INFO)
log.handlers.clear()
handler = RichHandler(rich_tracebacks=True)
handler.setFormatter(logging.Formatter("%(message)s", datefmt="[%X]"))
log.addHandler(handler)
log.propagate = False


# load .env file
load_dotenv()

app = FastAPI()

# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, replace "*" with your frontend origin (e.g., ["http://localhost:5173"])
allow_credentials=True,
allow_methods=["*"], # Allows all methods, including POST and OPTIONS
allow_headers=["*"], # Allows all headers
)
Comment on lines +26 to +33



@app.get("/")
def read_root():
return {"Hello": "World"}


# send to url
# todo: prob need question id to do this, testing
# code execution for now
@app.post('/execute')
def judge0_execution(student_code: StudentCode):
# print student code

# send the code to judge0
# make new request to configured judge0 endpoint - current would block until done
exec_url = os.getenv('JUDGE0_ENDPOINT') + '/submissions?base64_encoded=false&wait=true'

Comment on lines +49 to +52
# beautifully format the request payload
payload = {
'source_code': student_code.code,
'language_id': 71, # python
}

# setup headers if auth key is provided
headers = {}
auth_key = os.getenv('JUDGE0_AUTH_KEY')
if auth_key:
headers['X-Auth-Token'] = auth_key

# make request
response = requests.post(exec_url, json=payload, headers=headers)

# get the output
output = response.json()

Comment on lines +65 to +70
# print the output using rich
log.info(output)

# return the output
return output
Comment on lines +45 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing timeout, error handling, and env validation on Judge0 proxy.

Several issues in this endpoint:

  1. No timeoutrequests.post can hang indefinitely if Judge0 is unresponsive, blocking the request thread.
  2. No validation of JUDGE0_ENDPOINT — If the env var is missing, None + '/submissions...' raises TypeError.
  3. No error handling — Network failures, non-2xx responses, or non-JSON bodies will crash the endpoint.
🛠️ Proposed fix
+from fastapi import FastAPI, HTTPException
+
+JUDGE0_ENDPOINT = os.getenv('JUDGE0_ENDPOINT')
+if not JUDGE0_ENDPOINT:
+    raise RuntimeError("JUDGE0_ENDPOINT environment variable is required")
+
 `@app.post`('/execute')
 def judge0_execution(student_code: StudentCode):
-    exec_url = os.getenv('JUDGE0_ENDPOINT') + '/submissions?base64_encoded=false&wait=true'
+    exec_url = JUDGE0_ENDPOINT + '/submissions?base64_encoded=false&wait=true'
     
     payload = {
         'source_code': student_code.code,
         'language_id': 71,
     }
 
     headers = {}
     auth_key = os.getenv('JUDGE0_AUTH_KEY')
     if auth_key:
         headers['X-Auth-Token'] = auth_key
 
-    response = requests.post(exec_url, json=payload, headers=headers)
-    
-    output = response.json()
+    try:
+        response = requests.post(exec_url, json=payload, headers=headers, timeout=30)
+        response.raise_for_status()
+        output = response.json()
+    except requests.exceptions.Timeout:
+        raise HTTPException(status_code=504, detail="Judge0 request timed out")
+    except requests.exceptions.RequestException as e:
+        log.error(f"Judge0 request failed: {e}")
+        raise HTTPException(status_code=502, detail="Failed to reach code execution service")
+    except ValueError:
+        raise HTTPException(status_code=502, detail="Invalid response from code execution service")
 
     log.info(output)
     return output
🧰 Tools
🪛 ast-grep (0.43.0)

[info] 65-65: no timeout was given on call to external resource
Context: requests.post(exec_url, json=payload, headers=headers)
Note: [CWE-1088].

(requests-timeout)

🪛 Ruff (0.15.17)

[error] 66-66: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/main.py` around lines 45 - 75, In the judge0_execution function, add
validation to ensure JUDGE0_ENDPOINT is not None before concatenating it with
the path string. Add a timeout parameter to the requests.post call to prevent
indefinite blocking. Wrap the requests.post call and response.json() parsing in
a try-except block to handle network errors, HTTP errors, and JSON decoding
failures, returning appropriate error responses with meaningful error messages
for each failure case.

Source: Linters/SAST tools





@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
11 changes: 11 additions & 0 deletions server/model/student_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic import BaseModel

class StudentCode(BaseModel):
code: str


# data model for sending to judge0
class CodeSubmission(StudentCode):
language_id: int
source_code: str

12 changes: 12 additions & 0 deletions server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "server"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"fastapi[standard]>=0.136.3",
"python-dotenv>=1.2.2",
"requests>=2.34.2",
"rich>=15.0.0",
]
Loading