WebFund 2024F: Tutorial 8

From Soma-notes

This tutorial is not yet finalized.

Code

Download 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,
        content: "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",
                content: "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",
            content: "Account created",
        }
    } else {
        response = {
            status: status_INTERNAL_SERVER_ERROR,
            contentType: "text/plain",
            content: "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

<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;

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

<syntaxhighlight lang="css" line>

<syntaxhighlight>