diff --git a/src/database.ts b/src/database.ts index cf7578d..6f408ff 100644 --- a/src/database.ts +++ b/src/database.ts @@ -42,7 +42,9 @@ import { Stage } from "./models/stage"; type SequelizeError = { parent: { code: string } }; export type LoginResponse = { + type: "none" | "student" | "educator" | "admin", result: LoginResult; + user?: User; id?: number; success: boolean; }; @@ -52,6 +54,13 @@ export type CreateClassResponse = { class?: object | undefined; } +export enum UserType { + None = 0, // Not logged in + Student, + Educator, + Admin +} + // Grab any environment variables // Currently, just the DB password dotenv.config(); @@ -107,7 +116,7 @@ async function findEducatorByEmail(email: string): Promise { }); } -async function findStudentByEmail(email: string): Promise { +async function _findStudentByEmail(email: string): Promise { return Student.findOne({ where: { email: { [Op.like] : email } } }); @@ -115,19 +124,25 @@ async function findStudentByEmail(email: string): Promise { export async function findStudentByUsername(username: string): Promise { return Student.findOne({ - where: { username: username } + where: { username } }); } export async function findStudentById(id: number): Promise { return Student.findOne({ - where: { id : id } + where: { id } }); } export async function findEducatorById(id: number): Promise { return Educator.findOne({ - where: { id: id } + where: { id } + }); +} + +export async function findEducatorByUsername(username: string): Promise { + return Educator.findOne({ + where: { username }, }); } @@ -183,11 +198,20 @@ async function educatorVerificationCodeExists(code: string): Promise { return result.length > 0; } -export async function signUpEducator(firstName: string, lastName: string, - password: string, institution: string | null, - email: string, age: number | null, gender: string): Promise { +export interface SignUpEducatorOptions { + first_name: string; + last_name: string; + password: string; + email: string; + username: string; + institution?: string; + age?: number; + gender?: string; +} + +export async function signUpEducator(options: SignUpEducatorOptions): Promise { - const encryptedPassword = encryptPassword(password); + const encryptedPassword = encryptPassword(options.password); let validCode; let verificationCode: string; @@ -198,15 +222,10 @@ export async function signUpEducator(firstName: string, lastName: string, let result = SignUpResult.Ok; await Educator.create({ - first_name: firstName, - last_name: lastName, + ...options, verified: 0, verification_code: verificationCode, password: encryptedPassword, - institution: institution, - email: email, - age: age, - gender: gender, }) .catch(error => { result = signupResultFromError(error); @@ -214,12 +233,20 @@ export async function signUpEducator(firstName: string, lastName: string, return result; } -export async function signUpStudent(username: string, - password: string, institution: string | null, - email: string, age: number, gender: string, - classroomCode: string | null): Promise { +export interface SignUpStudentOptions { + username: string; + password: string; + email?: string; + age?: number; + gender?: string; + institution?: string; + classroom_code?: string; +} + + +export async function signUpStudent(options: SignUpStudentOptions): Promise { - const encryptedPassword = encryptPassword(password); + const encryptedPassword = encryptPassword(options.password); let validCode; let verificationCode: string; @@ -234,10 +261,10 @@ export async function signUpStudent(username: string, verified: 0, verification_code: verificationCode, password: encryptedPassword, - institution: institution, - email: email, - age: age, - gender: gender, + institution: options.institution, + email: options.email, + age: options.age, + gender: options.gender, }) .catch(error => { result = signupResultFromError(error); @@ -245,8 +272,8 @@ export async function signUpStudent(username: string, // If the student has a valid classroom code, // add them to the class - if (student && classroomCode) { - const cls = await findClassByCode(classroomCode); + if (student && options.classroom_code) { + const cls = await findClassByCode(options.classroom_code); if (cls !== null) { StudentsClasses.create({ student_id: student.id, @@ -293,11 +320,11 @@ export async function addStudentToClass(studentID: number, classID: number): Pro }); } -async function checkLogin(email: string, password: string, emailFinder: (email: string) +async function checkLogin(identifier: string, password: string, identifierFinder: (identifier: string) => Promise): Promise { const encryptedPassword = encryptPassword(password); - const user = await emailFinder(email); + const user = await identifierFinder(identifier); let result: LoginResult; if (user === null) { result = LoginResult.EmailNotExist; @@ -314,15 +341,28 @@ async function checkLogin(email: string, password: strin where: { id: user.id } }); } - return { + + let type: LoginResponse["type"] = "none"; + if (user instanceof Student) { + type = "student"; + } else if (user instanceof Educator) { + type = "educator"; + } + + const response: LoginResponse = { result: result, + success: LoginResult.success(result), + type, id: user?.id ?? 0, - success: LoginResult.success(result) }; + if (user) { + response.user = user; + } + return response; } -export async function checkStudentLogin(email: string, password: string): Promise { - return checkLogin(email, password, findStudentByEmail); +export async function checkStudentLogin(username: string, password: string): Promise { + return checkLogin(username, password, findStudentByUsername); } export async function checkEducatorLogin(email: string, password: string): Promise { diff --git a/src/models/educator.ts b/src/models/educator.ts index 9934ac2..8d57ccd 100644 --- a/src/models/educator.ts +++ b/src/models/educator.ts @@ -6,6 +6,7 @@ export class Educator extends Model, InferCreationAttr declare verified: number; declare verification_code: string; declare email: string; + declare username: string; declare first_name: string; declare last_name: string; declare password: string; @@ -45,6 +46,11 @@ export function initializeEducatorModel(sequelize: Sequelize) { allowNull: false, unique: true }, + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, first_name: { type: DataTypes.STRING, allowNull: false, diff --git a/src/models/student.ts b/src/models/student.ts index 2e3e8ee..c3a5ac4 100644 --- a/src/models/student.ts +++ b/src/models/student.ts @@ -5,12 +5,12 @@ export class Student extends Model, InferCreationAttrib declare id: CreationOptional; declare verified: number; declare verification_code: string; - declare email: string; + declare email: CreationOptional; declare username: string; declare password: string; - declare institution: string | null; - declare age: number | null; - declare gender: string | null; + declare institution: CreationOptional; + declare age: CreationOptional; + declare gender: CreationOptional; declare ip: CreationOptional; declare lat: CreationOptional; declare lon: CreationOptional; @@ -50,19 +50,23 @@ export function initializeStudentModel(sequelize: Sequelize) { username: { type: DataTypes.STRING, allowNull: false, + unique: true }, password: { type: DataTypes.STRING, allowNull: false, }, institution: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: null, }, age: { type: DataTypes.TINYINT, + defaultValue: null, }, gender: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: null, }, ip: { type: DataTypes.STRING diff --git a/src/request_results.ts b/src/request_results.ts index 7c288ea..28f4aae 100644 --- a/src/request_results.ts +++ b/src/request_results.ts @@ -22,6 +22,10 @@ export namespace CreateClassResult { return 200; } } + + export function success(result: CreateClassResult): boolean { + return result === CreateClassResult.Ok; + } } export enum LoginResult { diff --git a/src/server.ts b/src/server.ts index 4628c03..948e56b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -37,6 +37,10 @@ import { getStory, getStageStates, StageStateQuery, + CreateClassResponse, + UserType, + findEducatorByUsername, + findEducatorById, } from "./database"; import { getAPIKey, hasPermission } from "./authorization"; @@ -48,7 +52,7 @@ import { VerificationResult, } from "./request_results"; -import { CosmicDSSession } from "./models"; +import { CosmicDSSession, StudentsClasses } from "./models"; import { ParsedQs } from "qs"; import express, { Request, Response as ExpressResponse, NextFunction } from "express"; @@ -78,12 +82,7 @@ type VerificationRequest = Request<{verificationCode: string}, any, any, ParsedQ type CDSSession = session.Session & Partial & CosmicDSSession; -export enum UserType { - None = 0, // Not logged in - Student, - Educator, - Admin -} + const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",") : []; @@ -221,7 +220,7 @@ function _sendLoginCookie(userId: number, res: ExpressResponse): void { } // set port, listen for requests -const PORT = process.env.PORT || 8080; +const PORT = process.env.PORT || 8081; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}.`); }); @@ -235,13 +234,14 @@ app.post("/educator-sign-up", async (req, res) => { typeof data.password === "string" && ((typeof data.institution === "string") || (data.institution == null)) && typeof data.email === "string" && + typeof data.username === "string" && ((typeof data.age === "number") || (data.age == null)) && ((typeof data.gender === "string") || data.gender == null) ); let result: SignUpResult; if (valid) { - result = await signUpEducator(data.firstName, data.lastName, data.password, data.institution, data.email, data.age, data.gender); + result = await signUpEducator(data); } else { result = SignUpResult.BadRequest; res.status(400); @@ -263,12 +263,12 @@ app.post("/student-sign-up", async (req, res) => { typeof data.email === "string" && ((typeof data.age === "number") || (data.age == null)) && ((typeof data.gender === "string") || (data.gender == null)) && - ((typeof data.classroomCode === "string") || (data.classroomCode == null)) + ((typeof data.classroom_code === "string") || (data.classroom_code == null)) ); let result: SignUpResult; if (valid) { - result = await signUpStudent(data.username, data.password, data.institution, data.email, data.age, data.gender, data.classroomCode); + result = await signUpStudent(data); } else { result = SignUpResult.BadRequest; res.status(400); @@ -280,38 +280,58 @@ app.post("/student-sign-up", async (req, res) => { }); }); -async function handleLogin(request: GenericRequest, checker: (email: string, pw: string) => Promise): Promise { +async function handleLogin(request: GenericRequest, identifierField: string, checker: (identifier: string, pw: string) => Promise): Promise { const data = request.body; - const valid = typeof data.email === "string" && typeof data.password === "string"; + const valid = typeof data[identifierField] === "string" && typeof data.password === "string"; let res: LoginResponse; if (valid) { - res = await checker(data.email, data.password); + res = await checker(data[identifierField], data.password); } else { - res = { result: LoginResult.BadRequest, success: false }; + res = { result: LoginResult.BadRequest, success: false, type: "none" }; } return res; } +// app.put("/login", async (req, res) => { +// const sess = req.session as CDSSession; +// let result = LoginResult.BadSession; +// res.status(401); +// if (sess.user_id && sess.user_type) { +// result = LoginResult.Ok; +// res.status(200); +// } +// res.json({ +// result: result, +// id: sess.user_id, +// success: LoginResult.success(result) +// }); +// }); + app.put("/login", async (req, res) => { - const sess = req.session as CDSSession; - let result = LoginResult.BadSession; - res.status(401); - if (sess.user_id && sess.user_type) { - result = LoginResult.Ok; - res.status(200); + let response = await handleLogin(req, "username", checkStudentLogin); + let type = UserType.Student; + if (!(response.success && response.user)) { + response = await handleLogin(req, "username", checkEducatorLogin); + type = UserType.Educator; } - res.json({ - result: result, - id: sess.user_id, - success: LoginResult.success(result) - }); + + if (response.success && response.user) { + const sess = req.session as CDSSession; + if (sess) { + sess.user_id = response.user.id; + sess.user_type = type; + } + } + + const status = response.success ? 200 : 401; + res.status(status).json(response); }); app.put("/student-login", async (req, res) => { - const loginResponse = await handleLogin(req, checkStudentLogin); - if (loginResponse.success && loginResponse.id) { + const loginResponse = await handleLogin(req, "username", checkStudentLogin); + if (loginResponse.success && loginResponse.user) { const sess = req.session as CDSSession; - sess.user_id = loginResponse.id; + sess.user_id = loginResponse.user.id; sess.user_type = UserType.Student; } const status = loginResponse.success ? 200 : 401; @@ -319,10 +339,10 @@ app.put("/student-login", async (req, res) => { }); app.put("/educator-login", async (req, res) => { - const loginResponse = await handleLogin(req, checkEducatorLogin); - if (loginResponse.success && loginResponse.id) { + const loginResponse = await handleLogin(req, "email", checkEducatorLogin); + if (loginResponse.success && loginResponse.user) { const sess = req.session as CDSSession; - sess.user_id = loginResponse.id; + sess.user_id = loginResponse.user.id; sess.user_type = UserType.Educator; } const status = loginResponse.success ? 200 : 401; @@ -412,6 +432,8 @@ app.get("/validate-classroom-code/:code", async (req, res) => { }); +/* Users (students and educators) */ + app.get("/students", async (_req, res) => { const queryResponse = await getAllStudents(); res.json(queryResponse); @@ -422,6 +444,213 @@ app.get("/educators", async (_req, res) => { res.json(queryResponse); }); +app.get("/users", async (_req, res) => { + const students = await getAllStudents(); + const educators = await getAllEducators(); + res.json({ students, educators }); +}); + +app.get([ + "/students/:identifier", + "/student/:identifier", // Backwards compatibility +], async (req, res) => { + const params = req.params; + const id = Number(params.identifier); + + let student; + if (isNaN(id)) { + student = await findStudentByUsername(params.identifier); + } else { + student = await findStudentById(id); + } + if (student == null) { + res.statusCode = 404; + } + res.json({ + student, + }); +}); + +app.get("/students/:identifier/classes", async (req, res) => { + const id = Number(req.params.identifier); + + let student; + if (isNaN(id)) { + student = await findStudentByUsername(req.params.identifier); + } else { + student = await findStudentById(id); + } + + if (student === null) { + res.statusCode = 404; + res.json({ + student_id: null, + classes: [] + }); + return; + } + + const classes = await getClassesForStudent(student.id); + res.json({ + student_id: student.id, + classes: classes + }); + +}); + +app.get("/educators/:identifier", async (req, res) => { + const params = req.params; + const id = Number(params.identifier); + + let educator; + if (isNaN(id)) { + educator = await findEducatorByUsername(params.identifier); + } else { + educator = await findEducatorById(id); + } + if (educator == null) { + res.statusCode = 404; + } + res.json({ + educator, + }); +}); + +app.post("/classes/join", async (req, res) => { + const username = req.body.username as string; + const classCode = req.body.class_code as string; + const student = await findStudentByUsername(username); + const cls = await findClassByCode(classCode); + const isStudent = student !== null; + const isClass = cls !== null; + + if (!(isStudent && isClass)) { + let message = "The following were invalid:"; + const invalid: string[] = []; + if (!isStudent) { + invalid.push("username"); + } + if (!isClass) { + invalid.push("class_code"); + } + message += invalid.join(", "); + res.statusCode = 404; + res.json({ + success: false, + message: message + }); + return; + } + + const [join, created] = await StudentsClasses.upsert({ + class_id: cls.id, + student_id: student.id + }); + const success = join !== null; + res.statusCode = success ? 200 : 404; + let message: string; + if (!success) { + message = "Error adding student to class"; + } else if (!created) { + message = "Student was already enrolled in class"; + } else { + message = "Student added to class successfully"; + } + + res.json({ success, message }); +}); + +app.post("/educators/create", async (req, res) => { + const data = req.body; + const valid = ( + typeof data.first_name === "string" && + typeof data.last_name === "string" && + typeof data.password === "string" && + ((typeof data.institution === "string") || (data.institution == null)) && + typeof data.email === "string" && + ((typeof data.age === "number") || (data.age == null)) && + ((typeof data.gender === "string") || data.gender == null) + ); + + let result: SignUpResult; + if (valid) { + result = await signUpEducator(data); + } else { + result = SignUpResult.BadRequest; + res.status(400); + } + res.json({ + educator_info: data, + status: result, + success: SignUpResult.success(result) + }); +}); + +app.post("/students/create", async (req, res) => { + const data = req.body; + const valid = ( + typeof data.username === "string" && + typeof data.password === "string" && + ((typeof data.institution === "string") || (data.institution == null)) && + ((typeof data.email === "string") || (data.email == null)) && + ((typeof data.age === "number") || (data.age == null)) && + ((typeof data.gender === "string") || (data.gender == null)) && + ((typeof data.classroom_code === "string") || (data.classroom_code == null)) + ); + + let result: SignUpResult; + if (valid) { + result = await signUpStudent(data); + } else { + result = SignUpResult.BadRequest; + res.status(400); + } + res.json({ + student_info: data, + status: result, + success: SignUpResult.success(result) + }); +}); + +/* Classes */ +app.post("/classes/create", async (req, res) => { + const data = req.body; + const valid = ( + typeof data.username === "string" && + typeof data.educator_id === "string" + ); + let response: CreateClassResponse; + if (valid) { + response = await createClass(data.educator_id, data.username); + } else { + response = { + result: CreateClassResult.BadRequest, + }; + res.status(400); + } + res.json({ + class_info: response.class, + status: response.result, + success: CreateClassResult.success(response.result) + }); +}); + +app.delete("/classes/:code", async (req, res) => { + const cls = await findClassByCode(req.params.code); + const success = cls !== null; + if (!success) { + res.status(400); + } + cls?.destroy(); + const message = success ? + "Class deleted" : + "No class with the given code exists"; + res.json({ + success, + message + }); +}); + app.get("/classes/size/:classID", async (req, res) => { const classID = Number(req.params.classID); const cls = await findClassById(classID); @@ -431,7 +660,6 @@ app.get("/classes/size/:classID", async (req, res) => { }); return; } - const size = classSize(classID); res.json({ class_id: classID, @@ -633,23 +861,7 @@ app.get("/logout", (req, res) => { }); }); -app.get("/student/:identifier", async (req, res) => { - const params = req.params; - const id = Number(params.identifier); - let student; - if (isNaN(id)) { - student = await findStudentByUsername(params.identifier); - } else { - student = await findStudentById(id); - } - if (student == null) { - res.statusCode = 404; - } - res.json({ - student: student - }); -}); // Question information app.get("/question/:tag", async (req, res) => { diff --git a/src/sql/create_educator_table.sql b/src/sql/create_educator_table.sql index b5f4c4e..da4e2f7 100644 --- a/src/sql/create_educator_table.sql +++ b/src/sql/create_educator_table.sql @@ -16,8 +16,8 @@ CREATE TABLE Educators ( visits int(11) NOT NULL DEFAULT 0, last_visit datetime NOT NULL, last_visit_ip varchar(50) COLLATE utf8_unicode_ci, + username varchar(64) COLLATE utf8_unicode_ci NOT NULL UNIQUE, PRIMARY KEY(id), - INDEX(email), INDEX(last_name) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci PACK_KEYS=0; diff --git a/src/sql/create_student_table.sql b/src/sql/create_student_table.sql index f778370..a6e5c86 100644 --- a/src/sql/create_student_table.sql +++ b/src/sql/create_student_table.sql @@ -20,6 +20,4 @@ CREATE TABLE Students ( dummy tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY(id), - INDEX(username), - INDEX(email) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci PACK_KEYS=0; diff --git a/src/user.ts b/src/user.ts index 892a644..72f03d4 100644 --- a/src/user.ts +++ b/src/user.ts @@ -1,6 +1,6 @@ export interface User { id: number; - email: string; + email: string | null; password: string; verified: number; verification_code: string;