WebFund 2024F: Tutorial 8
This tutorial is not yet finalized.
Code
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>