Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate request body structure #132

Merged
merged 3 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
10 changes: 9 additions & 1 deletion src/request_results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,15 @@ export enum SignUpResult {

export namespace SignUpResult {
export function statusCode(result: SignUpResult): number {
return result === SignUpResult.BadRequest ? 400 : 200;
switch (result) {
case SignUpResult.Ok:
return 200;
case SignUpResult.EmailExists:
return 409;
case SignUpResult.BadRequest:
case SignUpResult.Error:
return 400;
}
}

export function success(result: SignUpResult): boolean {
Expand Down
117 changes: 38 additions & 79 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,8 @@ import {
UserType,
findEducatorByUsername,
findEducatorById,
CreateClassSchema,
QuestionInfoSchema,
} from "./database";

import { getAPIKey, hasPermission } from "./authorization";
Expand All @@ -66,7 +70,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,25 +237,16 @@ 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);
}
res.json({
const statusCode = SignUpResult.statusCode(result);
res.status(statusCode).json({
educator_info: data,
status: result,
success: SignUpResult.success(result)
Expand All @@ -262,24 +259,16 @@ 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);
}
res.json({
const statusCode = SignUpResult.statusCode(result);
res.status(statusCode).json({
student_info: data,
status: result,
success: SignUpResult.success(result)
Expand All @@ -288,10 +277,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 +348,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 +537,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 +825,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
Loading
Loading