WebFund 2024F: Tutorial 8: Difference between revisions

From Soma-notes
 
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
'''This tutorial is not yet finalized.'''
In this tutorial you will be playing with [https://homeostasis.scs.carleton.ca/~soma/webfund-2024f/code/authdemo.zip Authorization Demo], which is an expansion of the code from [[WebFund 2024F: Tutorial 7|Tutorial 7]]. Notably, this demo supports accounts and splits up the functionality of submitdemo between those for students and those for admin.
 
==Tasks==
 
# Download and run authdemo. Attempt to login. Can you do so? Are there any accounts?
# Create two student accounts and an admin account. Verify that you can login as both. Make sure the student accounts have different student IDs. (Note the student ID for the admin account doesn't matter.) Note that you get a blank page after creating an account; this is normal.
# Login as each student user and upload an assignment. Note that the student ID on the assignment must match the student ID of the current account. Once you are done, login as the admin user and check the listing and analysis pages: are your uploads reflected there?
# Try to create an account with the same username as one that already exists. What happens?
# Create a file that has the wrong student ID for the current user. What happens? Why?
# Where is the database schema specified? How many tables have been defined? What fields do they each have?
# Change the validator so it properly reports when the current user's student ID doesn't match that of the student ID of the submission.
# Fix the create account functionality so it displays a summary page of the account created and then has a button that takes you back to the login screen.


==Code==
==Code==


[https://homeostasis.scs.carleton.ca/~soma/webfund-2024f/code/authdemo.zip Download authdemo.zip]
[https://homeostasis.scs.carleton.ca/~soma/webfund-2024f/code/authdemo.zip Code for authdemo.zip]


===authdemo.js===
===authdemo.js===
Line 168: Line 179:
         contentType: "text/html",
         contentType: "text/html",
         location: url,
         location: url,
         content: "Redirect to " + url
         contents: "Redirect to " + url
     }
     }


Line 194: Line 205:
                 status: status_INTERNAL_SERVER_ERROR,
                 status: status_INTERNAL_SERVER_ERROR,
                 contentType: "text/plain",
                 contentType: "text/plain",
                 content: "Internal server error, unknown access.",
                 contents: "Internal server error, unknown access.",
             }
             }
         }
         }
Line 221: Line 232:
             status: status_OK,
             status: status_OK,
             contentType: "text/plain",
             contentType: "text/plain",
             content: "Account created",
             contents: "Account created",
         }
         }
     } else {
     } else {
Line 227: Line 238:
             status: status_INTERNAL_SERVER_ERROR,
             status: status_INTERNAL_SERVER_ERROR,
             contentType: "text/plain",
             contentType: "text/plain",
             content: "Internal server error, could not create account.",
             contents: "Internal server error, could not create account.",
         }
         }
     }
     }
Line 666: Line 677:


<syntaxhighlight lang="javascript" line>
<syntaxhighlight lang="javascript" line>
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// list.js, part of authdemo
// for COMP 2406 (Fall 2024), Carleton University
//
// Initial version: November 6, 2024
//
function insertTableData(tableData) {
    var t = document.querySelector("#table");
    var row = [];
 
    function rowMarkup(s) {
        return "<td>" + s + "</td>";
    }
    for (let r of tableData) {
        row.push("<tr>")
        row.push(rowMarkup(r.id));
        row.push(rowMarkup(r.studentID));
        row.push(rowMarkup(r.name));
        row.push(rowMarkup(r.q1));
        row.push(rowMarkup(r.q2));
        row.push(rowMarkup(r.q3));
        row.push(rowMarkup(r.q4));
        row.push(rowMarkup(r.q5));
        row.push("</tr>")
    }
    t.innerHTML = row.join("\n");
}


<syntaxhighlight>
 
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!");});
</syntaxhighlight>


===static/student/index.html===
===static/student/index.html===


<syntaxhighlight lang="html" line>
<syntaxhighlight lang="html" line>
<!DOCTYPE html>
<html>
  <head>
    <title>COMP 2406 Authorization Demo: Upload</title>
    <link rel="stylesheet" href="/style.css">
    <script type="text/javascript" src="/student/validator.js"></script>
  </head>
  <body onload="hideAnalysis()">
    <h1>Upload Submission</h1>


<syntaxhighlight>
    <form>
      <input type="file" id="assignmentFile"
            onchange="loadAssignment(this, false)" />
      <button type="button" onclick="uploadSubmission()">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>
</syntaxhighlight>


===static/student/validator.js===
===static/student/validator.js===


<syntaxhighlight lang="javascript" line>
<syntaxhighlight lang="javascript" line>
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// validator.js, part of authdemo
// for COMP 2406 (Fall 2024), Carleton University
//
// Initial version: November 6, 2024
//
const filePrefix = "comp2406-tutorial8";
const submissionName = "COMP 2406 2024F Tutorial 8"
const expectedQuestionList = "1,2,3,4,5";
var analysisSection;


<syntaxhighlight>
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 = checkSubmission(fn, content);
        if (upload) {
            doUploadSubmission(fn, q);
        }
    };
    reader.onerror = function(err) {
        updateTag("status", "ERROR: Failed to read file");
    }
    reader.readAsText(fileInput.files[0]);
 
}
 
var numQuestions = expectedQuestionList.split(",").length;
 
function updateTag(id, val) {
    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";
}
 
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);
   
    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];
                }
            }
        }
    }
 
    console.log(JSON.stringify(q, null, '  '));
 
    questionString = questionList.toString();
    if (questionString !== expectedQuestionList) {
        updateTag("status", "ERROR expected questions " +
                  expectedQuestionList + " but got questions " +
                  questionString);
    } else {
        updateTag("status", "PASSED " +
                  fn + ": " + q.name + " (" + q.studentID + ")");
    }
   
    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;
}
 
function hideAnalysis() {
    analysisSection = document.getElementById("analysis");
    analysisSection.hidden = true;
}
 
function uploadSubmission() {
    var assignmentFile = document.getElementById("assignmentFile");
    loadAssignment(assignmentFile, true);
}
 
function doUploadSubmission(fn, q) {
    console.log("sending data to server...");
    if (q) {
        const request = new Request("/uploadSubmission", {
            method: "POST",
            body: JSON.stringify(q),
            headers: {
                'Content-Type': 'application/json'
            }
        });
 
        fetch(request).then((response) => {
            if (response.status === 200) {
                updateTag("status", "UPLOAD COMPLETE of " + fn);
            } else {
                updateTag("status", "UPLOAD ERROR of " + fn +
                          ", failed with status " +
                          response.status);
            }
        });
    } else {
        updateTag("status", "ERROR No valid data to upload!");
    }
}
</syntaxhighlight>


===static/style.css===
===static/style.css===


<syntaxhighlight lang="css" line>
<syntaxhighlight lang="css" line>
body {
  padding: 50px;
  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}


<syntaxhighlight>
a {
  color: #00B7FF;
}
</syntaxhighlight>

Latest revision as of 22:34, 16 November 2024

In this tutorial you will be playing with Authorization Demo, which is an expansion of the code from Tutorial 7. Notably, this demo supports accounts and splits up the functionality of submitdemo between those for students and those for admin.

Tasks

  1. Download and run authdemo. Attempt to login. Can you do so? Are there any accounts?
  2. Create two student accounts and an admin account. Verify that you can login as both. Make sure the student accounts have different student IDs. (Note the student ID for the admin account doesn't matter.) Note that you get a blank page after creating an account; this is normal.
  3. Login as each student user and upload an assignment. Note that the student ID on the assignment must match the student ID of the current account. Once you are done, login as the admin user and check the listing and analysis pages: are your uploads reflected there?
  4. Try to create an account with the same username as one that already exists. What happens?
  5. Create a file that has the wrong student ID for the current user. What happens? Why?
  6. Where is the database schema specified? How many tables have been defined? What fields do they each have?
  7. Change the validator so it properly reports when the current user's student ID doesn't match that of the student ID of the submission.
  8. Fix the create account functionality so it displays a summary page of the account created and then has a button that takes you back to the login screen.

Code

Code for authdemo.zip

authdemo.js

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// authdemo.js, part of authdemo
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 13, 2024
//
// run with the following command:
//    deno run --allow-net --allow-read --allow-write authdemo.js
//

import * as db from "./authdb.js";
import * as template from "./templates.js";

const status_OK = 200;
const status_SEE_OTHER = 303;
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() {
    var state = db.getAllSubmissions();
    
    var response = { contentType: "application/JSON",
                     status: status_OK,
                     contents: JSON.stringify(state),
                   };

    return response;
}

async function routeGet(req) {
    const path = new URL(req.url).pathname;
    if (path === "/list") {
        return listSubmissions();
    } 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 requestAuthUser(req) {
    const cookieStr = req.headers.get('cookie');

    return getCookie(cookieStr, "authuser");
}

async function addSubmission(req) {
    const submission = await req.json();
    const authuser = requestAuthUser(req);
    const account = db.getAccount(authuser);
    var response;

    if (account.studentID === submission.studentID) {
        let result = db.addSubmission(submission);
        
        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 authuser;
    var response;
    
    var account = db.getAccount(username);

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

async function createAcct(req) {
    const body = await req.formData();
    const username = body.get("username");
    const password = body.get("password");
    const access = body.get("access");
    const studentID = body.get("studentID");
    const name = body.get("name");
    const result = db.addAccount(username, password, 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.authuser) {
        responseInit.headers["Set-Cookie"] = "authuser=" + r.authuser;
    }

    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 authdemo
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 13, 2024
//

import { DB } from "https://deno.land/x/sqlite/mod.ts";

const dbFile = "submissions.db";
const submissionTable = "tutorial8";
const authTable = "accounts";

const db = new DB(dbFile);

db.execute(`
  CREATE TABLE IF NOT EXISTS ${submissionTable} (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    studentID TEXT,
    q1 TEXT,
    q2 TEXT,
    q3 TEXT,
    q4 TEXT,
    q5 TEXT
  )
`);

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

export function addSubmission(r) {
    return db.query(`INSERT INTO ${submissionTable} ` +
                    "(studentID, q1, q2, q3, q4, q5) " +
                    "VALUES (?, ?, ?, ?, ?, ?)",
                    [r.studentID,
                     r["1"], r["2"], r["3"], r["4"], r["5"]]);
}

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

    for (const [id, studentID, q1, q2, q3, q4, q5]
         of query.iter()) {
        let name = db.query("SELECT name FROM " + authTable +
                           " WHERE studentID = '" + studentID + "'")
        state.push({id, studentID, name, q1, q2, q3, q4, q5});
    }
    
    query.finalize();
    
    return state;
}

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

    analysis.count = db.query("SELECT COUNT(*) FROM " + submissionTable);
    analysis.studentIDList =
        db.query("SELECT DISTINCT studentID FROM " + submissionTable);
    
    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 close() {
    db.close();
}

templates.js

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Anil Somayaji
//
// templates.js, part of authdemo
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 13, 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>
`
}

export function addRecord(obj) {
    return header("Submission just added") +
        `<body>
  <body>
    <h1>Submission just added</h1>
    <p>Student ID: ${obj.studentID}</p>
    <p>Name: ${obj.name}</p>
    <p>Q1: ${obj.q1}</p>
    <p>Q2: ${obj.q2}</p>
    <p>Q3: ${obj.q3}</p>
    <p>Q4: ${obj.q4}</p>
    <p>Q5: ${obj.q5}</p>
    <form method="get" action="/">
      <button type="submit">Home</button>
    </form>
  </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/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/list.html

<!DOCTYPE html>
<html>
  <head>
    <title>COMP 2406 Authorization Demo: Submission Listing</title>
    <link rel="stylesheet" href="/style.css">
    <script src="/admin/list.js" defer></script>
  </head>
  <body>
    <h1>Submission Listing</h1>
    <div>
      <div></div>
      <table>
        <thead>
          <th>DB ID</th>
          <th>Student ID</th>
          <th>Name</th>
          <th>Question 1</th>
          <th>Question 2</th>
          <th>Question 3</th>
          <th>Question 4</th>
          <th>Question 5</th>
        </thead>
        <tbody id="table"/>
      </table>
    </div>
    <form method="get" action="/admin/index.html">
      <button type="submit">Home</button>
    </form>
  </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.js

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

function insertTableData(tableData) {
    var t = document.querySelector("#table");
    var row = [];
   
    function rowMarkup(s) {
        return "<td>" + s + "</td>";
    }

    for (let r of tableData) {
        row.push("<tr>")
        row.push(rowMarkup(r.id));
        row.push(rowMarkup(r.studentID));
        row.push(rowMarkup(r.name));
        row.push(rowMarkup(r.q1));
        row.push(rowMarkup(r.q2));
        row.push(rowMarkup(r.q3));
        row.push(rowMarkup(r.q4));
        row.push(rowMarkup(r.q5));
        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="text/javascript" src="/student/validator.js"></script>
  </head>
  <body onload="hideAnalysis()">
    <h1>Upload Submission</h1>

    <form>
      <input type="file" id="assignmentFile"
             onchange="loadAssignment(this, false)" />
      <button type="button" onclick="uploadSubmission()">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 authdemo
// for COMP 2406 (Fall 2024), Carleton University
// 
// Initial version: November 6, 2024
//

const filePrefix = "comp2406-tutorial8";
const submissionName = "COMP 2406 2024F Tutorial 8"
const expectedQuestionList = "1,2,3,4,5";
var analysisSection;

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 = checkSubmission(fn, content);
        if (upload) {
            doUploadSubmission(fn, q);
        }
    };
    reader.onerror = function(err) {
        updateTag("status", "ERROR: Failed to read file");
    }
    reader.readAsText(fileInput.files[0]);

}

var numQuestions = expectedQuestionList.split(",").length;

function updateTag(id, val) {
    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";
}

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);
    
    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];
                }
            }
        }
    }

    console.log(JSON.stringify(q, null, '   '));

    questionString = questionList.toString();
    if (questionString !== expectedQuestionList) {
        updateTag("status", "ERROR expected questions " +
                  expectedQuestionList + " but got questions " +
                  questionString);
    } else {
        updateTag("status", "PASSED " +
                  fn + ": " + q.name + " (" + q.studentID + ")");
    }
    
    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;
}

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

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

function doUploadSubmission(fn, q) {
    console.log("sending data to server...");
    if (q) {
        const request = new Request("/uploadSubmission", {
            method: "POST",
            body: JSON.stringify(q),
            headers: {
                'Content-Type': 'application/json'
            }
        });

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

static/style.css

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

a {
  color: #00B7FF;
}