Skip to content

Commit

Permalink
Add some better validation for request bodies on some of our main end…
Browse files Browse the repository at this point in the history
…points.
  • Loading branch information
Carifio24 committed Sep 12, 2024
1 parent ac0ccdd commit 17dc97b
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 121 deletions.
97 changes: 56 additions & 41 deletions src/database.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { BaseError, Model, Op, QueryTypes, Sequelize, UniqueConstraintError, WhereOptions } from "sequelize";
import dotenv from "dotenv";

import * as S from "@effect/schema/Schema";

import {
Class,
Educator,
Expand All @@ -20,6 +22,7 @@ import {
encryptPassword,
isNumberArray,
Either,
Mutable,
} from "./utils";


Expand Down Expand Up @@ -192,16 +195,18 @@ async function educatorVerificationCodeExists(code: string): Promise<boolean> {
return result.length > 0;
}

export interface SignUpEducatorOptions {
first_name: string;
last_name: string;
password: string;
email: string;
username: string;
institution?: string;
age?: number;
gender?: string;
}
export const SignUpEducatorSchema = S.struct({
first_name: S.string,
last_name: S.string,
password: S.string,
email: S.string,
username: S.string,
institution: S.optional(S.string),
age: S.optional(S.number),
gender: S.optional(S.string),
});

export type SignUpEducatorOptions = S.Schema.To<typeof SignUpEducatorSchema>;

export async function signUpEducator(options: SignUpEducatorOptions): Promise<SignUpResult> {

Expand All @@ -227,16 +232,17 @@ export async function signUpEducator(options: SignUpEducatorOptions): Promise<Si
return result;
}

export interface SignUpStudentOptions {
username: string;
password: string;
email?: string;
age?: number;
gender?: string;
institution?: string;
classroom_code?: string;
}
export const SignUpStudentSchema = S.struct({
username: S.string,
password: S.string,
email: S.optional(S.string),
age: S.optional(S.number),
gender: S.optional(S.string),
institution: S.optional(S.string),
classroom_code: S.optional(S.string),
});

export type SignUpStudentOptions = S.Schema.To<typeof SignUpStudentSchema>;

export async function signUpStudent(options: SignUpStudentOptions): Promise<SignUpResult> {

Expand Down Expand Up @@ -279,15 +285,18 @@ export async function signUpStudent(options: SignUpStudentOptions): Promise<Sign
return result;
}

export async function createClass(educatorID: number, name: string): Promise<CreateClassResponse> {
export const CreateClassSchema = S.struct({
educator_id: S.number,
name: S.string,
});

export type CreateClassOptions = S.Schema.To<typeof CreateClassSchema>;

export async function createClass(options: CreateClassOptions): Promise<CreateClassResponse> {

let result = CreateClassResult.Ok;
const code = createClassCode(educatorID, name);
const creationInfo = {
educator_id: educatorID,
name: name,
code: code,
};
const code = createClassCode(options);
const creationInfo = { ...options, code };
const cls = await Class.create(creationInfo)
.catch(error => {
result = createClassResultFromError(error);
Expand Down Expand Up @@ -751,24 +760,30 @@ export async function findQuestion(tag: string, version?: number): Promise<Quest
}
}

interface QuestionInfo {
tag: string;
text: string;
shorthand: string;
story_name: string;
answers_text?: string[];
correct_answers?: number[];
neutral_answers?: number[];
version?: number;
}
export const QuestionInfoSchema = S.struct({
tag: S.string,
text: S.string,
shorthand: S.string,
story_name: S.string,
answers_text: S.optional(S.mutable(S.array(S.string))),
correct_answers: S.optional(S.mutable(S.array(S.number))),
neutral_answers: S.optional(S.mutable(S.array(S.number))),
version: S.optional(S.number),
});

export type QuestionInfo = S.Schema.To<typeof QuestionInfoSchema>;

export async function addQuestion(info: QuestionInfo): Promise<Question | null> {
if (!info.version) {
const currentVersion = await currentVersionForQuestion(info.tag);
info.version = currentVersion || 1;

const infoToUse: Mutable<QuestionInfo> = { ...info };

if (!infoToUse.version) {
const currentVersion = await currentVersionForQuestion(infoToUse.tag);
infoToUse.version = currentVersion || 1;
}
return Question.create(info).catch((error) => {
return Question.create(infoToUse).catch((error) => {
logger.error(error);
logger.error(`Question info: ${JSON.stringify(info)}`);
logger.error(`Question info: ${JSON.stringify(infoToUse)}`);
return null;
});
}
Expand Down
6 changes: 3 additions & 3 deletions src/models/educator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export class Educator extends Model<InferAttributes<Educator>, InferCreationAttr
declare first_name: string;
declare last_name: string;
declare password: string;
declare institution: string | null;
declare age: number | null;
declare gender: string | null;
declare institution: CreationOptional<string | null>;
declare age: CreationOptional<number | null>;
declare gender: CreationOptional<string | null>;
declare ip: CreationOptional<string | null>;
declare lat: CreationOptional<string | null>;
declare lon: CreationOptional<string | null>;
Expand Down
110 changes: 35 additions & 75 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
checkStudentLogin,
createClass,
signUpEducator,
SignUpStudentSchema,
signUpStudent,
SignUpEducatorSchema,
verifyEducator,
verifyStudent,
getAllEducators,
Expand Down Expand Up @@ -41,6 +43,9 @@ import {
UserType,
findEducatorByUsername,
findEducatorById,
CreateClassSchema,
QuestionInfoSchema,
QuestionInfo,
} from "./database";

import { getAPIKey, hasPermission } from "./authorization";
Expand All @@ -66,7 +71,9 @@ import cors from "cors";
import jwt from "jsonwebtoken";

import { isStudentOption } from "./models/student_options";
import { isNumberArray, isStringArray } from "./utils";

import * as S from "@effect/schema/Schema";
import * as Either from "effect/Either";

export const app = express();

Expand Down Expand Up @@ -231,20 +238,11 @@ app.post([
"/educator-sign-up", // Old
], 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.username === "string" &&
((typeof data.age === "number") || (data.age == null)) &&
((typeof data.gender === "string") || data.gender == null)
);
const maybe = S.decodeUnknownEither(SignUpEducatorSchema)(data);

let result: SignUpResult;
if (valid) {
result = await signUpEducator(data);
if (Either.isRight(maybe)) {
result = await signUpEducator(maybe.right);
} else {
result = SignUpResult.BadRequest;
res.status(400);
Expand All @@ -262,19 +260,11 @@ app.post([
"/student-sign-up", // Old
], 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))
);
const maybe = S.decodeUnknownEither(SignUpStudentSchema)(data);

let result: SignUpResult;
if (valid) {
result = await signUpStudent(data);
if (Either.isRight(maybe)) {
result = await signUpStudent(maybe.right);
} else {
result = SignUpResult.BadRequest;
res.status(400);
Expand All @@ -288,10 +278,14 @@ app.post([

async function handleLogin(request: GenericRequest, identifierField: string, checker: (identifier: string, pw: string) => Promise<LoginResponse>): Promise<LoginResponse> {
const data = request.body;
const valid = typeof data[identifierField] === "string" && typeof data.password === "string";
const schema = S.struct({
[identifierField]: S.string,
password: S.string,
});
const maybe = S.decodeUnknownEither(schema)(data);
let res: LoginResponse;
if (valid) {
res = await checker(data[identifierField], data.password);
if (Either.isRight(maybe)) {
res = await checker(maybe.right[identifierField], maybe.right.password);
} else {
res = { result: LoginResult.BadRequest, success: false, type: "none" };
}
Expand Down Expand Up @@ -355,29 +349,6 @@ app.put("/educator-login", async (req, res) => {
res.status(status).json(loginResponse);
});

app.post("/create-class", async (req, res) => {
const data = req.body;
const valid = (
typeof data.educatorID === "number" &&
typeof data.name === "string"
);

let result: CreateClassResult;
let cls: object | undefined = undefined;
if (valid) {
const createClassResponse = await createClass(data.educatorID, data.name);
result = createClassResponse.result;
cls = createClassResponse.class;
} else {
result = CreateClassResult.BadRequest;
res.status(400);
}
res.json({
class: cls,
status: result
});
});

async function verify(request: VerificationRequest, verifier: (code: string) => Promise<VerificationResult>): Promise<{ code: string; status: VerificationResult }> {
const params = request.params;
const verificationCode = params.verificationCode;
Expand Down Expand Up @@ -567,15 +538,15 @@ app.post("/classes/join", async (req, res) => {
});

/* Classes */
app.post("/classes/create", async (req, res) => {
app.post([
"/classes/create",
"/create-class",
], async (req, res) => {
const data = req.body;
const valid = (
typeof data.username === "string" &&
typeof data.educator_id === "string"
);
const maybe = S.decodeUnknownEither(CreateClassSchema)(data);
let response: CreateClassResponse;
if (valid) {
response = await createClass(data.educator_id, data.username);
if (Either.isRight(maybe)) {
response = await createClass(maybe.right);
} else {
response = {
result: CreateClassResult.BadRequest,
Expand Down Expand Up @@ -855,32 +826,21 @@ app.get("/question/:tag", async (req, res) => {

app.post("/question/:tag", async (req, res) => {

const tag = req.params.tag;
const text = req.body.text;
const shorthand = req.body.shorthand;
const story_name = req.body.story_name;
const answers_text = req.body.answers_text;
const correct_answers = req.body.correct_answers;
const neutral_answers = req.body.neutral_answers;

const valid = typeof tag === "string" &&
typeof text === "string" &&
typeof shorthand === "string" &&
typeof story_name === "string" &&
(answers_text === undefined || isStringArray(answers_text)) &&
(correct_answers === undefined || isNumberArray(correct_answers)) &&
(neutral_answers === undefined || isNumberArray(neutral_answers));
if (!valid) {
const data = { ...req.body, tag: req.params.tag };
const maybe = S.decodeUnknownEither(QuestionInfoSchema)(data);

if (Either.isLeft(maybe)) {
res.statusCode = 400;
res.json({
error: "One of your fields is missing or of the incorrect type"
});
return;
}

const currentQuestion = await findQuestion(tag);
const currentQuestion = await findQuestion(req.params.tag);
const version = currentQuestion !== null ? currentQuestion.version + 1 : 1;
const addedQuestion = await addQuestion({tag, text, shorthand, story_name, answers_text, correct_answers, neutral_answers, version});
const questionInfo = { ...maybe.right, version };
const addedQuestion = await addQuestion(questionInfo);
if (addedQuestion === null) {
res.statusCode = 500;
res.json({
Expand Down
9 changes: 7 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { enc, SHA256 } from "crypto-js";
import { v5 } from "uuid";

import { Model } from "sequelize";
import { CreateClassOptions } from "./database";

// This type describes objects that we're allowed to pass to a model's `update` method
export type UpdateAttributes<M extends Model> = Parameters<M["update"]>[0];
Expand All @@ -15,6 +16,10 @@ export type Only<T, U> = {

export type Either<T, U> = Only<T,U> | Only<U,T>;

export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
}

export function createVerificationCode(): string {
return nanoid(21);
}
Expand All @@ -29,8 +34,8 @@ function createV5(name: string): string {
return v5(name, cdsNamespace);
}

export function createClassCode(educatorID: number, className: string) {
const nameString = `${educatorID}_${className}`;
export function createClassCode(options: CreateClassOptions) {
const nameString = `${options.educator_id}_${options.name}`;
return createV5(nameString);
}

Expand Down

0 comments on commit 17dc97b

Please sign in to comment.