WebFund 2024F: Assignment 4

From Soma-notes

Please submit the answers to the following questions via Brightspace by December 4, 2024. There are 20 points in 9 questions.

Submit your answers as a plain text file following the standard format for filename and file contents. Please make sure to use your correct student ID number otherwise your grades may not be properly recorded.

Please solve each question rather than look up their answers. Solutions where the answer was found through an online search or answered by a friend or AI will likely get partial or no marks. The questions can only be fully answered by documenting your journey to find the answer rather than the answer itself. Outside resources and other individuals should be used to help with specific technical problems (e.g., JavaScript syntax and semantics), not to solve the problem itself. Your solution should be your own, and the journey by which you reach it should be your own.

Having said this, for each question, please include what outside resources you used to complete each of your answers, including other students, man pages, and web resources (including AI services). You do not need to list help from the instructor, TA, or information covered in lecture or tutorial.

Getting Started

You'll need to be running the latest deno (deno 2.X) for this code to run. So first, if you haven't already, upgrade your installation:

 deno upgrade

Then, you can get the app running as follows:

 wget https://homeostasis.scs.carleton.ca/~soma/webfund-2024f/code/authdemo2.zip
 unzip authedemo2.zip
 cd authdemo2
 deno task start

Questions

All of these questions are in reference to authdemo2, the code of which is included below.

For all questions be sure to explain 1) how you arrived at the answer and 2) any issues you had in developing your answer. Failure to include this information can result in partial or no marks at all on a question.

  1. [2] Where in authdemo2 is the number of submission questions hardcoded? How does this compare to previous versions of this application?
  2. [2] What is the format of the submission data uploaded to the server? Be precise, and explain how you confirmed your answer was correct.
  3. [2] What happens when a user uploads more than one submission? What new code (relative to past versions) is responsible for this behavior?
  4. [2] Does authdemo2 allow users to upload invalid submissions? If so, will such invalid submissions be stored in the server's database? Explain briefly.
  5. [2] How could you change the analysis page of authdemo2 so it reports the number of empty answers for each question, and do so with minimal code changes?
  6. [2] By looking at the accounts table, can you easily tell if two users chose the same password in authdemo2? How do you know?
  7. [3] Tutorial 8's authdemo stored the logged in user's username in a cookie. What does authdemo2 store in a cookie to indicate that a user is logged in? In what way is this solution better? How is it worse?
  8. [2] How long are sessions valid for in authdemo2? How could you change this so sessions last for 10 minutes?
  9. [3] How could you change authdemo2 so it stored the uploading filename and file contents in its database? Be sure to specify what functionality was already present and what you had to add.

Code

Download authdemo2 here.

deno.json

{
  "tasks": {
    "start": "deno run --allow-read --allow-write --allow-net --allow-ffi --allow-env authdemo2.js",
  },
}

authdemo2.js

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// authdemo2.js, part of authdemo2
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 23, 2024
//
// run with "deno task start"
//

// From https://github.com/felix-schindler/deno-argon2
import { hash, verify } from "jsr:@felix/argon2";

import * as db from "./authdb.js";
import * as template from "./templates.js";
import { expectedQuestionList,
         checkSubmission } from "./static/validator-core.js";

const status_OK = 200;
const status_SEE_OTHER = 303;
const status_BAD_REQUEST = 400;
const status_UNAUTHORIZED = 401
const status_FORBIDDEN = 403;
const status_NOT_FOUND = 404;
const status_INTERNAL_SERVER_ERROR = 500;
const status_NOT_IMPLEMENTED = 501;

function MIMEtype(filename) {

    const MIME_TYPES = {
        'css': 'text/css',
        'gif': 'image/gif',
        'htm': 'text/html',
        'html': 'text/html',
        'ico': 'image/x-icon',
        'jpeg': 'image/jpeg',
        'jpg': 'image/jpeg',
        'js': 'text/javascript',
        'json': 'application/json',
        'pdf': 'application/pdf',
        'png': 'image/png',
        'txt': 'text/text'
    };

    var extension = "";
    
    if (filename) {
        extension = filename.slice(filename.lastIndexOf('.')+1).toLowerCase();
    }

    return MIME_TYPES[extension] || "application/octet-stream";
};


function listSubmissions(req) {
    const session = getSession(req);
    var response;

    if (!session) {
        response = {
            contentType: "text/plain",
            status: status_UNAUTHORIZED,
            contents: "Unauthorized: User not logged in",
        };
    } else {
        
        const account = db.getAccount(session.username);
        
        if (account && account.access === "admin") {
            var state = db.getAllSubmissions();
            
            response = { contentType: "application/JSON",
                         status: status_OK,
                         contents: JSON.stringify(state),
                       };
        } else {
            response = {
                contentType: "text/plain",
                status: status_FORBIDDEN   ,
                contents: "Forbidden: User doesn't have admin access",
            };              
        }
    }
    
    return response;
}

async function routeGet(req) {
    const path = new URL(req.url).pathname;
    if (path === "/list") {
        return listSubmissions(req);
    } else if (path === "/admin/analyze") {
        return await showAnalysis();
    }  else {
        return null;
    }
}

async function showAnalysis() {
    var analysis = db.analyzeSubmissions();
    var studentIDList =
        '<li>' + analysis.studentIDList.join('</li> <li>') + '</li>';
    
    var analysisBody = `  <body>
  <body>
    <h1>Submissions analysis</h1>
    <p># Records: ${analysis.count}</p>
    <p>Student IDs:
      <ol>
       ${studentIDList}
      </ol>
    </p>

    <form method="get" action="/admin/index.html">
      <button type="submit">Home</button>
    </form>
  </body>
</html>`

    var contents =
        template.header("Submission analysis") + analysisBody;

    var response = { contentType: "text/html",
                     status: status_OK,
                     contents: contents,
                   };
    
    return response;
}

// getCookie is from claude.ai, but pretty standard code
function getCookie(cookieStr, name) {
    if (!cookieStr) return null;
    const cookies = cookieStr.split(';');
    for (const cookie of cookies) {
        const [cookieName, cookieValue] = cookie.trim().split('=');
        if (cookieName === name) {
            return cookieValue;
        }
    }
    return null;
}

function getSession(req) {
    const cookieStr = req.headers.get('cookie');
    const sessionid = getCookie(cookieStr, "sessionid");
    return db.getSession(sessionid);
}

async function addSubmission(req) {
    const session = getSession(req);
    var response;

    if (!session) {
        return {
            contentType: "text/plain",
            status: status_UNAUTHORIZED,
            contents: "Unauthorized: User not logged in",
        };
    }
    
    const account = db.getAccount(session.username);
    const submission = await req.json();

    if (!submission || !submission.filename || !submission.filecontents) {
        return {
            contentType: "text/plain",
            status: status_BAD_REQUEST,
            contents: "Bad Request: Invalid raw submission upload",
        };
    }

    if (account && account.access === "student") {
        const q = checkSubmission(submission.filename, submission.filecontents);

        if (!q) {
            return {
                contentType: "text/plain",
                status: status_BAD_REQUEST,
                contents: "Bad Request: Invalid parsed submission upload",
            };
        }
        
        let result = db.addSubmission(q);
        
        if (result) {
            response = {
                contentType: "text/plain",
                status: status_OK,
                contents: "Got the data",
            };
        } else {
            response = {
                contentType: "text/plain",
                status: status_INTERNAL_SERVER_ERROR,
                contents: "Internal server error"
            };
        }       
    } else {
        response = {
            contentType: "text/plain",
            status: status_FORBIDDEN,
            contents:
            "FORBIDDEN: submission studentID doesn't match account studentID"
        };
    }
    
    return response;
}

function redirect(url) {
    var response = {
        status: status_SEE_OTHER,
        contentType: "text/html",
        location: url,
        contents: "Redirect to " + url
    }

    return response;
}

async function login(req) {
    const body = await req.formData();
    const username = body.get("username");
    const password = body.get("password");
    var sessionid;
    var response;
    
    var account = db.getAccount(username);

    if (account && (await verify(account.password, password))) {
        sessionid = db.newSession(username);
        if (account.access === "student") {
            response = redirect("/student/index.html")
        } else if (account.access === "admin") {
            response = redirect("/admin/index.html")
        } else {
            sessionid = "UNAUTHORIZED";
            response = {
                status: status_INTERNAL_SERVER_ERROR,
                contentType: "text/plain",
                contents: "Internal server error, unknown access.",
            }
        }
    } else {
        sessionid = "UNAUTHORIZED";
        response = redirect("/loginFailed.html");
    }
    
    response.sessionid = sessionid;
    
    return response;
}

async function createAcct(req) {
    const body = await req.formData();
    const username = body.get("username");
    const pwhash = await hash(body.get("password"));
    const access = body.get("access");
    const studentID = body.get("studentID");
    const name = body.get("name");
    const result = db.addAccount(username, pwhash, access, studentID, name);
    var response;
    
    if (result) {
        response = {
            status: status_OK,
            contentType: "text/plain",
            contents: "Account created",
        }
    } else {
        response = {
            status: status_INTERNAL_SERVER_ERROR,
            contentType: "text/plain",
            contents: "Internal server error, could not create account.",
        }
    }

    return response;
}

async function routePost(req) {
    const path = new URL(req.url).pathname;    

    if (path === "/uploadSubmission") {
        return await addSubmission(req);
    } else if (path === "/login") {
        return await login(req);
    } else if (path === "/admin/createAcct") {
        return await createAcct(req);
    } else {
        return null;
    }
}


async function route(req) {

    if (req.method === "GET") {
        return await routeGet(req);
    } else if (req.method === "POST") {
        return await routePost(req);
    } else {
        return {
            contents: "Method not implemented.",
            status: status_NOT_IMPLEMENTED,
            contentType: "text/plain"
        };
    }
}


async function fileData(path) {
    var contents, status, contentType;
    
    try {
        contents = await Deno.readFile("./static" + path);
        status = status_OK;
        contentType = MIMEtype(path);
    } catch (e) {
        contents = template.notFound(path);
        status = status_NOT_FOUND;
        contentType = "text/html";
    }
    
    return { contents, status, contentType };
}


async function handler(req) {

    var origpath = new URL(req.url).pathname;
    var path = origpath;
    var r =  await route(req);
    
    if (!r) {
        if (path === "/") {
            path = "/index.html";
        }
        r = await fileData(path);
    }

    console.log(`${r.status} ${req.method} ${r.contentType} ${origpath}`); 
    
    var responseInit = {
        status: r.status,
        headers: {
            "Content-Type": r.contentType,
        }
    };
    
    if (r.sessionid) {
        responseInit.headers["Set-Cookie"] = "sessionid=" + r.sessionid;
    }

    if (r.location) {
        responseInit.headers["Location"] = r.location;
    }
    
    return new Response(r.contents, responseInit);
}

const ac = new AbortController();

const server = Deno.serve(
    {
        signal: ac.signal,
        port: 8000,
        hostname: "0.0.0.0"
    },
    handler);

Deno.addSignalListener("SIGINT", () => {
    console.log("SIGINT received, terminating...");
    ac.abort();
});

server.finished.then(() => {
    console.log("Server terminating, closing database.")
    db.close();
});

authdb.js

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// authdb.js, part of authdemo2
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 23, 2024
//

import { DB } from "https://deno.land/x/sqlite/mod.ts";
import { expectedQuestionList,
         numQuestions,
         submissionShortName
       } from "./static/validator-core.js";

const dbFile = "submissions.db";
const submissionTable = submissionShortName;
const authTable = "accounts";
const sessionTable = "sessions";
const sessionLength = 3600 * 72 * 1000 // length of sessions in milliseconds

const db = new DB(dbFile);

const expectedQarray = expectedQuestionList.split(",");
const createQFields = expectedQarray.map((q) => "q" + q + " TEXT").join(", ");
const queryQFields = expectedQarray.map((q) => "q" + q).join(", ");

function queryBlanks(n) {
    return "(" + Array(n).fill("?").join(", ") + ")";
}

db.execute(`
  CREATE TABLE IF NOT EXISTS ${submissionTable} (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    studentID TEXT UNIQUE,
    ${createQFields}
  )
`);

db.execute(`
  CREATE TABLE IF NOT EXISTS ${authTable} (
    username TEXT PRIMARY KEY,
    password TEXT,
    access TEXT,
    studentID TEXT,
    name TEXT
  )
`);

db.execute(`
  CREATE TABLE IF NOT EXISTS ${sessionTable} (
    sessionid TEXT PRIMARY KEY,
    username TEXT,
    expiration INTEGER
  )
`);

export function addSubmission(r) {
    const qa = expectedQuestionList.split(",");
    const answers = qa.map((q) => r[q]);
    
    return db.query(`INSERT OR REPLACE INTO ${submissionTable} ` +
                    "(studentID, " + queryQFields + ")" +
                    "VALUES " + queryBlanks(numQuestions + 1), 
                    [r.studentID].concat(answers));
}

export function getAllSubmissions() {
    var state = [];
    const query =
          db.prepareQuery(
              "SELECT id, studentID, " + queryQFields + " FROM " +
                  submissionTable + " ORDER BY studentID ASC LIMIT 50");

    for (var s of query.iter()) {
        let id = s.shift();
        let studentID = s.shift();
        let name = db.query("SELECT name FROM " + authTable +
                            " WHERE studentID = ?", studentID);
        let r = {id, studentID, name};
        for (let i in expectedQarray) {
            let qname = "q" + expectedQarray[i];
            let qval = s[i];
            r[qname] = qval;
        }
        state.push(r);
    }
    
    query.finalize();
    
    return state;
}

function calcBlankQuestionStats() {

    // From https://stackoverflow.com/questions/154059/how-do-i-check-for-an-empty-undefined-null-string-in-javascript
    
    function isBlank(str) {
        return (!str || /^\s*$/.test(str));
    }

    var state = getAllSubmissions();
    const counts = Array(numQuestions).fill(0);
    var f, s;

    for (s of state) {  
        for (f=0; f < counts.length; f++) {
            let qindex = f + 1;
            let question = "q" + qindex;

            if (isBlank(s[question])) {
                counts[f]++;
            }
        }
    }

    return counts;
}

export function analyzeSubmissions() {
    var analysis = {};

    analysis.count = db.query("SELECT COUNT(*) FROM " + submissionTable);
    analysis.studentIDList =
        db.query("SELECT DISTINCT studentID FROM " + submissionTable);

    analysis.blankQuestions = calcBlankQuestionStats();
    
    return analysis;
}

export function addAccount(username, password, access, studentID, name) {
    return db.query(`INSERT INTO ${authTable} ` +
                    "(username, password, access, studentID, name) " +
                    "VALUES (?, ?, ?, ?, ?)",
                    [username, password, access, studentID, name]);
}

export function getAccount(username) {
    var result =
        db.query(`SELECT * FROM ${authTable} WHERE username = '${username}'`);

    if (result[0]) {
        let a = result[0];
        let username = a[0];
        let password = a[1];
        let access = a[2];
        let studentID = a[3];
        let name = a[4];

        return {username, password, access, studentID, name};
    } else {
        return null;
    }
}

export function getName(studentID) {
    return db.query(`SELECT name FROM ${authTable} ` +
                    `WHERE studentID = '${studentID}'`);
}

export function newSession(username) {
    const sessionid = crypto.randomUUID();
    const expiration = Date.now() + sessionLength;
    
    db.query(`INSERT INTO ${sessionTable} ` +
             "(sessionid, username, expiration) " +
             "VALUES (?, ?, ?)",
             [sessionid, username, expiration]);

    return sessionid;
}

export function getSession(sessionid) {
    var result;

    if (!sessionid) {
        return null;
    }
    
    result = db.query("SELECT username, expiration " +
                      `FROM ${sessionTable} WHERE sessionid = ?`,
                      [sessionid]);

    if (result && result.length > 0) {
        let username = result[0][0];
        let expiration = result[0][1];

        if (expiration > Date.now()) {
            return { sessionid, username, expiration };
        } else {
            return null;
        }
    } else {
        return null;
    }
}

export function close() {
    db.close();
}

templates.js

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// templates.js, part of authdemo2
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 23, 2024
//

const appTitle = "COMP 2406 Authorization Demo";

export function header(title) {
    const fullTitle = appTitle + ": " + title;
    
    return `<!DOCTYPE html>
<html>
  <head>
    <title>${fullTitle}</title>
    <link rel="stylesheet" href="/style.css">
  </head>
`
}

export function notFound(path) {
    return header("Page not found") +
        `<body>
<h1>Page not found</h1>

<p>Sorry, the requested page was not found.</p>
</body>
</html>
`
}


static/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>COMP 2406 Authorization Demo</title>
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <h1>COMP 2406 Authorization Demo</h1>

    <p>Please login:</p>
    
    <form method="post" action="/login">
      <div>
        <label>Username: </label>
        <input id="username" type="text" name="username">
      </div>
      <div>
        <label>Password: </label>
        <input id="password" type="password" name="password">
      </div>
      <button type="submit">Login</button>
    </form>

    <p><a href="/admin/create.html">Create an account</a></p>
    
  </body>
</html>

static/loginFailed.html

<!DOCTYPE html>
<html>
  <head>
    <title>COMP 2406 Authorization Demo: Login Failed</title>
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <h1>COMP 2406 Authorization Demo: Login Failed</h1>

    <p>Login failed.</p>
    
    <form method="get" action="/">
      <button type="submit">Home</button>
    </form>

  </body>
</html>

static/validator-core.js

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// static/validator-core.js, part of authdemo2
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 22, 2024
//

export const submissionShortName = "assign4";
const filePrefix = "comp2406-" + submissionShortName;
const submissionName = "COMP 2406 2024F Assignment 4"
export const expectedQuestionList = "1,2,3,4,5,6,7";

export const numQuestions = expectedQuestionList.split(",").length;

function updateTag(id, val) {
    if (typeof document === 'object') {
        document.getElementById(id).innerHTML = val;
    }
}

function lineEncoding(lines) {
    var n, i, s;
    
    for (i = 0; i < lines.length; i++) {
        s = lines[i];
        n = s.length;
        if (s[n-1] === '\r') {
            return "DOS/Windows Text (CR/LF)";
        }
    }
    
    return "UNIX";
}

export function checkSubmission(fn, f) {
    var lines = f.split('\n')
    var c = 0;
    var questionList = [];
    var questionString;
    var q = {};
    var i;
    var lastQuestion = null;
    const fnPattern = filePrefix + "-[a-zA-Z0-9]+\.txt";
    const fnRexp = new RegExp(fnPattern);

    q.filename = fn;
    q.filecontents = f;
    
    if (!fnRexp.test(fn)) {
        updateTag("status", "ERROR " + fn +
                  " doesn't follow the pattern " + fnRexp);
        return;
    }

    if (fn === filePrefix + "-template.txt") {
        updateTag("status", "ERROR " + fn +
                  " has the default name, please change template to your mycarletonone username");
        return;
    }
    
    updateTag("filename", fn);
    
    let encoding = lineEncoding(lines);
    if (encoding !== "UNIX") {
        updateTag("status", "ERROR " + fn +
                  " is not a UNIX textfile, it is a "
                  + encoding + " file.");
        return;
    }
    
    if (submissionName !== lines[0]) {
        updateTag("status", "ERROR " + fn +
                  " doesn't start with \"" + submissionName + "\"");
        return;
    }
    
    try {
        q.name = lines[1].match(/^Name:(.+)/m)[1].trim();
        q.studentID = lines[2].match(/^Student ID:(.+)/m)[1].trim();
    } catch (error) {
        updateTag("status", "ERROR " + fn +
                  " has bad Name or Student ID field");
        return;
    }

    updateTag("name", q.name);
    updateTag("studentID", q.studentID);
    
    var questionRE = /^([0-9a-g]+)\.(.*)/;
    
    for (i = 4; i < lines.length; i++) {
        if (typeof(lines[i]) === 'string') {
            lines[i] = lines[i].replace('\r','');
        }
        
        let m = lines[i].match(questionRE);
        if (m) {
            c++;
            questionList.push(m[1]);
            q[m[1]] = m[2];
            lastQuestion = m[1];
        } else {
            if (lastQuestion !== null) {
                if ((q[lastQuestion] === '') || (q[lastQuestion] === ' ')) {
                    q[lastQuestion] = lines[i];
                } else {
                    q[lastQuestion] = q[lastQuestion] + "\n" + lines[i];
                }
            }
        }
    }

    questionString = questionList.toString();
    if (questionString !== expectedQuestionList) {
        updateTag("status", "ERROR expected questions " +
                  expectedQuestionList + " but got questions " +
                  questionString);
        return;
    } else {
        updateTag("status", "PASSED " +
                  fn + ": " + q.name + " (" + q.studentID + ")");
    }
    
    return q;
}

static/style.css

body {
  padding: 50px;
  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}

a {
  color: #00B7FF;
}

static/admin/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>COMP 2406 Authorization Demo: Admin Menu</title>
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <h1>COMP 2406 Authorization Demo: Admin Menu</h1>

    <p>Please select what you would like to do:
      <ol>
        <li><a href="/admin/list.html">List submissions.</a></li>
        <li><a href="/admin/analyze">Analyze submissions.</a></li>        
      </ol>
      </p>
  </body>
</html>

static/admin/create.html

<!DOCTYPE html>
<html>
  <head>
    <title>COMP 2406 Authorization Demo: Create an Account</title>
    <link rel="stylesheet" href="/style.css">
  </head>
  <body>
    <h1>Create an Account</h1>
    <div>
      <p>Fill out the info below on the account to be created:</p>
      <form method="post" action="/admin/createAcct">
        <div>
          <input id="username" type="text" name="username">
          <label>Username</label>
        </div>
        <div>
          <input id="password" type="password" name="password">
          <label>Password</label>
        </div>
        <div>
          <input id="studentID" type="text" name="studentID">
          <label>Student ID</label>
        </div>
        <div>
          <input id="name" type="text" name="name">
          <label>Name</label>
        </div>
        <div>
          <label>Access: </label>
          <select id="access" name="access">
            <option value="student">Student</option>
            <option value="admin">Admin</option>
          </select>
        </div>
        <button type="submit">Submit</button>
      </form>
    </div>
  </body>
</html>

static/admin/list.html

<!DOCTYPE html>
<html>
  <head>
    <title>COMP 2406 Authorization Demo: Submission Listing</title>
    <link rel="stylesheet" href="/style.css">
    <script type="module" src="/admin/list.js" defer></script>
  </head>
  <body>
    <h1>Submission Listing</h1>
    <div>
      <div></div>
      <table>
        <thead>
          <tr id="headings">
            <th>DB ID</th>
            <th>Student ID</th>
            <th>Name</th>
          </tr>
        </thead>
        <tbody id="table"/>
      </table>
    </div>
    <form method="get" action="/admin/index.html">
      <button type="submit">Home</button>
    </form>
  </body>
</html>

static/admin/list.js

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// list.js, part of authdemo2
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 23, 2024
//

import { expectedQuestionList } from "/validator-core.js";

function insertTableData(tableData) {

    function rowMarkup(s) {
        return "<td>" + s + "</td>";
    }

    var t = document.querySelector("#table");
    var row = [];
    var questionList = expectedQuestionList.split(",");
    var headingsDiv = document.getElementById("headings");
    
    var questionHeadings = questionList.map((i) => {
        return "          <th>Question " + i + "</th>"
    }).join("\n");

    var oldHeadings = headingsDiv.innerHTML;
    headingsDiv.innerHTML = oldHeadings + questionHeadings;

    var questionList = expectedQuestionList.split(',').map((q) => "q" + q);
    var fieldList = ["id", "studentID", "name"].concat(questionList);
    
    for (let r of tableData) {
        row.push("<tr>")
        for (let f of fieldList) {
            row.push(rowMarkup(r[f]));
        }
        row.push("</tr>")
    }

    t.innerHTML = row.join("\n");
}

async function updateTable() {
    console.log("Updating Table...");
    
    try {
        const response = await fetch("/list");
        if (response.ok) {
            const tableData = await response.json();
            insertTableData(tableData);
        } else {
            console.error("Table loading error response: " + response.status);
        }
    } catch (error) {
        console.error("Table loading fetch error: " + error.message);
    }
}

updateTable().then(() => {console.log("Table updated!");});

static/student/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>COMP 2406 Authorization Demo: Upload</title>
    <link rel="stylesheet" href="/style.css">
    <script type="module" src="/student/validator.js"></script>
  </head>
  <body>
    <h1>Upload Submission</h1>

    <form>
      <input type="file" id="assignmentFile"/>
      <button type="button" id="uploadButton">Upload</button>
    </form>

    <div id="analysis">
      <p><b>Status:</b> <span id="status">UNKNOWN</span></p>
      <p>Filename: <span id="filename"></span></p>
      <p>Name: <span id="name"></span><br>
        Student ID: <span id="studentID"></span> <i>(Please check that your ID number is correct!)</i></p>
      <hr>
      <div id="questions"><p><b>Questions:</b> <i>(Please check that your answers are numbered correctly!)</i></p></div>
    </div>
  </body>
</html>

static/student/validator.js

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// validator.js, part of authdemo2
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 23, 2024
//

import { expectedQuestionList,
         numQuestions,
         submissionShortName,
         checkSubmission
       } from "/validator-core.js";

var analysisSection;

export function loadAssignment(fileInput, upload) {
    analysisSection.hidden = false;

    if(fileInput.files[0] == undefined) {
        updateTag("status", "ERROR: No files to examine.")
        return;
    }
    
    var reader = new FileReader();
    reader.onload = function(ev) {
        var content = ev.target.result;
        var fn = fileInput.files[0].name
        var q = validateSubmission(fn, content);
        if (upload) {
            doUploadSubmission(fn, content);
        }
    };
    reader.onerror = function(err) {
        updateTag("status", "ERROR: Failed to read file");
    }
    reader.readAsText(fileInput.files[0]);

}

function validateSubmission(fn, f) {
    var q = checkSubmission(fn, f);

    if (!q) {
        return;
    }

    var questionList = expectedQuestionList.split(",");
    
    var newP, newText, newPre, newPreText;
    let questionDiv = document.getElementById("questions");
    for (let qName of questionList) {
        let qText = q[qName];
        newP = document.createElement("p");
        newText = document.createTextNode(qName + ":");
        newP.appendChild(newText);
        questionDiv.appendChild(newP);
        newPre = document.createElement("pre");
        newPreText = document.createTextNode(qText);
        newPre.appendChild(newPreText);
        newP.appendChild(newPre);
    }
    
    return q;
}

export function hideAnalysis() {
    analysisSection = document.getElementById("analysis");
    analysisSection.hidden = true;
}

export function uploadSubmission() {
    var assignmentFile = document.getElementById("assignmentFile");
    loadAssignment(assignmentFile, true);
}

function updateTag(id, val) {
    document.getElementById(id).innerHTML = val;
}

function doUploadSubmission(filename, filecontents) {
    if (filecontents) {
        const request = new Request("/uploadSubmission", {
            method: "POST",
            body: JSON.stringify({filename, filecontents}),
            headers: {
                'Content-Type': 'application/json'
            }
        });

        fetch(request).then((response) => {
            if (response.status === 200) {
                updateTag("status", "UPLOAD COMPLETE of " + filename);
            } else {
                updateTag("status", "UPLOAD ERROR of " + filename +
                          ", failed with status " +
                          response.status);
            }
        });
    } else {
        updateTag("status", "ERROR No data to upload!");
    }
}

window.addEventListener("load", (event) => {
    hideAnalysis();
    
    let fileInput = document.getElementById("assignmentFile");
    fileInput.addEventListener("change", (event) => {
        loadAssignment(fileInput, false);
    });

    let uploadButton = document.getElementById("uploadButton");
    uploadButton.addEventListener("click", (event) => {
        uploadSubmission();
    });
});