Initial commit

This commit is contained in:
Leo Wen
2025-12-06 16:33:33 -08:00
commit ad5b308873
16 changed files with 1672 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+175
View File
@@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
+4
View File
@@ -0,0 +1,4 @@
{
"schema": "path/to/schema.graphql",
"documents": "src/**/*.{graphql,ts,tsx}"
}
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
+225
View File
@@ -0,0 +1,225 @@
ENG-1A
ENG-1AH
ALR-4
ENG-1B
ENG-1BH
PHI-11
COM-1
COM-1H
COM-6
COM-9
COM-9H
MAT-1A
MAT-5
MAT-25
MAT-1AH
MAT-9
MAT-70A
MAT-1B
MAT-10
MAT-70B
MAT-1C
MAT-11
PSY-48
SOC-48
MAT-2
MAT-12
MAT-12H
MAT-3
AHS-1
AHS-9
GAM-2
AHS-10
MUS-3
MUS-19
MUS-19H
AHS-2
AHS-2H
AHS-12
AHS-13
AHS-14
ARE-35
ARE-36
AHS-4
AHS-5
AHS-6
ART-6
AHS-6H
ART-6H
AHS-7
AHS-8
DAN-6
DAN-6H
MUS-23
MUS-25
MUS-89
MUS-89H
THE-3
THE-9
THE-29
HUM-4
HUM-4H
HUM-5
HUM-5H
HUM-8
ENG-8
HUM-9
HUM-10
HUM-10H
HUM-11
HUM-16
HUM-18
PHI-18
HUM-23
ENG-23
HUM-35
PHI-35
PHI-36
SPA-2
SPA-3
SPA-4
SPA-8
SPA-11
SPA-12
CHI-11
COM-12
ETS-2
ETS-7
ETS-8
ETS-14
FRE-2
FRE-8
HIS-1
HIS-2
HIS-6
HIS-6H
HIS-7
HIS-7H
HIS-14
HIS-25
HIS-26
HIS-31
HIS-34
PHI-10
PHI-10H
PHI-12
PHI-19
PHI-22
PHI-33
PHI-34
ADJ-1
ADJ-3
ADJ-9
ADJ-9H
ANT-1
ANT-1H
ANT-2
ANT-2H
ANT-3
ANT-4
ANT-5
ANT-6
ANT-7
ANT-8
COM-13
COM-20
EAR-20
ECO-4
ECO-7
ECO-7H
ECO-8
ECO-8H
ETS-1
ETS-3
ETS-21
ETS-23
ETS-27
GEG-2
GEG-3
GEG-4
GEG-6
HIS-34
JOU-7
POL-1
POL-1H
POL-2
POL-4
POL-4H
POL-5
POL-11
POL-13
POL-20
POL-21
POL-22
POL-23
PSY-1
PSY-1H
PSY-2
PSY-8
PSY-9
PSY-11
PSY-33
PSY-35
PSY-35H
PSY-50
SJS-110
SOC-1
SOC-1H
SOC-2
SOC-3
SOC-10
SOC-10H
SOC-12
SOC-15
SOC-20
SOC-50
CHE-1A
CHE-1B
CHE-2A
CHE-2B
CHE-3
CHE-10
CHE-12A
CHE-12B
GEG-1
GEG-1H
GEG-1L
GEG-5
PHS-1
PHY-2A
PHY-2B
PHY-4A
PHY-4B
PHY-4C
PHY-10
PHY-11
ANT-1L
BIO-1
BIO-1H
BIO-3
BIO-4
BIO-5
BIO-7
BIO-8
BIO-10
BIO-16
BIO-18
BIO-19
BIO-21
BIO-45
BIO-50A
BIO-50B
BIO-55
BIO-60
BIO-61
CHI-2
ARA-1
ASL-1
FRE-1
SPA-1
ETS-1
ETS-3
ETS-7
ETS-8
ETS-21
ETS-23
+224
View File
@@ -0,0 +1,224 @@
ENG-1A: English Composition
ENG-1AH: Honors English Composition
ALR-4: Intermediate Algebra
ENG-1B: Critical Thinking and Writing
ENG-1BH: Honors Critical Thinking and Writing
PHI-11: Critical Thinking
COM-1: Public Speaking
COM-1H: Honors Public Speaking
COM-6: Dynamics of Small Group Communication
COM-9: Interpersonal Communication
COM-9H: Honors Interpersonal Communication
MAT-1A: Calculus I
MAT-5: Discrete Structures
MAT-25: Mathematics for the Liberal Arts Student
MAT-1AH: Honors Calculus I
MAT-9: Introduction to Statistics
MAT-70A: Intermediate Algebra Part I
MAT-1B: Calculus II
MAT-10: Precalculus
MAT-70B: Intermediate Algebra Part II
MAT-1C: Calculus III
MAT-11: College Algebra
PSY-48: Introduction to Chemical Dependency
SOC-48: Introduction to Chemical Dependency
MAT-2: Differential Equations
MAT-12: Statistics
MAT-12H: Honors Statistics
MAT-3: Linear Algebra
AHS-1: Introduction to Allied Health
AHS-9: Medical Terminology
GAM-2: Introduction to Game Development
AHS-10: Health Education
MUS-3: Introduction to Music
MUS-19: Introduction to Music Theory
MUS-19H: Honors Introduction to Music Theory
AHS-2: Medical Terminology
AHS-2H: Honors Medical Terminology
AHS-12: Healthcare Ethics
AHS-13: Pharmacology
AHS-14: Clinical Practices in Allied Health
ARE-35: Agricultural Economics
ARE-36: Agricultural Business
AHS-4: Patient Care Procedures
AHS-5: Basic Medical Office Procedures
AHS-6: Introduction to Healthcare
ART-6: Art Appreciation
AHS-6H: Honors Introduction to Healthcare
ART-6H: Honors Art Appreciation
AHS-7: Anatomy and Physiology
AHS-8: Medical Assistant Skills
DAN-6: Dance Appreciation
DAN-6H: Honors Dance Appreciation
MUS-23: History of Jazz
MUS-25: History of Rock and Roll
MUS-89: Music Production
MUS-89H: Honors Music Production
THE-3: Introduction to Theatre
THE-9: History of Theatre
THE-29: Technical Theatre
HUM-4: Introduction to Humanities
HUM-4H: Honors Introduction to Humanities
HUM-5: World Religions
HUM-5H: Honors World Religions
HUM-8: Introduction to Philosophy
ENG-8: Introduction to Mythology
HUM-9: Introduction to Ethics
HUM-10: World Civilizations
HUM-10H: Honors World Civilizations
HUM-11: History of Art
HUM-16: Humanities of Latin America
HUM-18: Humanities of Asia
PHI-18: Philosophy of Religion
HUM-23: Women in Western Culture
ENG-23: The Bible as Literature
HUM-35: Cultural History of the United States
PHI-35: Introduction to Ethics
PHI-36: Asian Philosophy
SPA-2: Spanish Language and Culture
SPA-3: Intermediate Spanish
SPA-4: Advanced Spanish
SPA-8: Spanish for Native Speakers
SPA-11: Spanish Conversation
SPA-12: Spanish Composition
CHI-11: Chinese Language and Culture
COM-12: Intercultural Communication
ETS-2: Electronic Tools for Students
ETS-7: Computer Applications for Business
ETS-8: Introduction to Digital Media
ETS-14: Web Design and Development
FRE-2: French Language and Culture
FRE-8: French Literature
HIS-1: World History
HIS-2: United States History
HIS-6: Introduction to Historical Research
HIS-6H: Honors Introduction to Historical Research
HIS-7: History of Western Civilization
HIS-7H: Honors History of Western Civilization
HIS-14: History of California
HIS-25: History of the United States
HIS-26: History of the American West
HIS-31: History of Mexico
HIS-34: History of Latin America
PHI-10: Introduction to Philosophy
PHI-10H: Honors Introduction to Philosophy
PHI-12: Introduction to Ethics
PHI-19: Native American Thought
PHI-22: Philosophy of Science
PHI-33: Introduction to Social and Political Philosophy
PHI-34: Philosophy of Art and Beauty
ADJ-1: Introduction to the Administration of Justice
ADJ-3: Concepts of Criminal Law
ADJ-9: Law in American Society
ADJ-9H: Honors Law in American Society
ANT-1: Introduction to Anthropology
ANT-1H: Honors Introduction to Anthropology
ANT-2: Cultural Anthropology
ANT-2H: Honors Cultural Anthropology
ANT-3: Archaeology
ANT-4: Human Evolution
ANT-5: Physical Anthropology
ANT-6: Native American Studies
ANT-7: Anthropology of Religion
ANT-8: Human Behavior and Society
COM-13: Gender and Communication
COM-20: Introduction to Communication Theory
EAR-20: Introduction to Environmental Science
ECO-4: Microeconomics
ECO-7: Macroeconomics
ECO-7H: Honors Macroeconomics
ECO-8: Principles of Economics
ECO-8H: Honors Principles of Economics
ETS-1: Introduction to Information Systems
ETS-3: Information Technology
ETS-21: Computer Science
ETS-23: Web Development
GEG-2: World Geography
GEG-3: Geography of the United States
GEG-4: Geography of California
GEG-6: Economic Geography
HIS-34: History of Latin America
JOU-7: Introduction to Journalism
POL-1: Introduction to Political Science
POL-1H: Honors Introduction to Political Science
POL-2: Comparative Politics
POL-4: International Relations
POL-4H: Honors International Relations
POL-5: United States Government
POL-11: Political Theory
POL-13: Political Ideologies
POL-20: Introduction to Political Economy
POL-21: California Politics
POL-22: Environmental Politics
POL-23: Politics of the Middle East
PSY-1: General Psychology
PSY-1H: Honors General Psychology
PSY-2: Biological Psychology
PSY-8: Introduction to Social Psychology
PSY-9: Developmental Psychology
PSY-11: Psychology of Gender
PSY-33: Theories of Personality
PSY-35: Abnormal Psychology
PSY-35H: Honors Abnormal Psychology
PSY-50: Research Methods in Psychology
SJS-110: Introduction to Social Justice
SOC-1: Introduction to Sociology
SOC-1H: Honors Introduction to Sociology
SOC-2: American Social Problems
SOC-3: Marriage and Family Relations
SOC-10: Race and Ethnic Relations
SOC-10H: Honors Race and Ethnic Relations
SOC-12: Introduction to Gender
SOC-15: Sociology of Health and Illness
SOC-20: Criminology
SOC-50: Social Research Methods
CHE-1A: General Chemistry 1
CHE-1B: General Chemistry 2
CHE-2A: Organic Chemistry 1
CHE-2B: Organic Chemistry 2
CHE-3: Introduction to Chemistry
CHE-10: Environmental Chemistry
CHE-12A: Biochemistry
CHE-12B: Biochemistry
GEG-1: Introduction to Geography
GEG-1H: Honors Introduction to Geography
GEG-1L: Geography Laboratory
GEG-5: Geography of California
PHS-1: Physical Science
PHY-2A: General Physics
PHY-2B: General Physics
PHY-4A: Physics for Scientists and Engineers
PHY-4B: Physics for Scientists and Engineers
PHY-4C: Physics for Scientists and Engineers
PHY-10: Introduction to Physics
PHY-11: Principles of Physics
ANT-1L: Introduction to Physical Anthropology Lab
BIO-1: General Biology
BIO-1H: Honors General Biology
BIO-3: Introduction to Microbiology
BIO-4: Introduction to Environmental Science
BIO-5: Human Anatomy
BIO-7: Human Physiology
BIO-8: Human Biology
BIO-10: General Botany
BIO-16: Ecology
BIO-18: Marine Biology
BIO-19: Introduction to Biology
BIO-21: General Genetics
BIO-45: Human Genetics
BIO-50A: Principles of Biology
BIO-50B: Principles of Biology
BIO-55: Botany
BIO-60: Molecular Biology
BIO-61: Microbiology
CHI-2: Chinese Language and Culture
ARA-1: Arabic Language and Culture
ASL-1: American Sign Language
FRE-1: French Language and Culture
SPA-1: Introduction to Spanish Language
ETS-1: Introduction to Information Systems
ETS-3: Information Technology
ETS-7: Computer Applications for Business
ETS-8: Introduction to Digital Media
ETS-21: Computer Science
ETS-23: Web Development
+360
View File
@@ -0,0 +1,360 @@
import { PrismaClient, type Syllabus } from "@prisma/client";
import express from "express";
import cors from "cors";
import fileUpload from "express-fileupload";
import http from "node:http";
import fuzzysort from "fuzzysort";
import mime from "mime-types";
import jwt from 'jsonwebtoken';
import argon2 from 'argon2';
import { idText } from "typescript";
import 'dotenv/config';
import { jwtMiddleware } from "./authMiddleware";
const app = express();
const httpServer = http.createServer(app);
const prisma = new PrismaClient();
app.use(cors({
origin: "*", // TODO change this to the actual domain
}))
app.use(express.json());
app.use(fileUpload());
app.use('/files', express.static('syllabi'));
app.post("/create", async (req, res) => {
console.debug(req.body);
if (req.files === null || req.files === undefined)
return res.status(400).json({ msg: "No file uploaded" });
if (req.files.upload === null)
return res.status(400).json({ msg: "No file uploaded" });
if (Array.isArray(req.files.upload))
return res.status(400).json({ msg: "Multiple files uploaded" });
const file = req.files.upload as fileUpload.UploadedFile;
if (![mime.lookup(".pdf"), mime.lookup(".docx")].includes(file.mimetype)) return res.status(400).json({ msg: "Invalid file type" });
if (file.size > 5_000_000) {
return res.status(400).json({ msg: "File too large (max 5MB)" });
}
// Add error handling for missing required fields
if (!req.body.schoolName || !req.body.className || !req.body.professor) {
return res.status(400).json({ msg: "Missing required fields" });
}
// Add proper error handling for file operations
try {
let school = await prisma.school.findUnique({
where: { name: req.body.schoolName },
});
if (!school) {
return res.status(404).json({ msg: "School not found" });
}
let classRecord = await prisma.class.findFirst({ // Change findUnique to findFirst
where: {
className: req.body.className,
schoolId: school.id,
},
});
if (!classRecord) {
return res.status(404).json({ msg: "Class not found" });
}
let professorObject = await prisma.professor.findUnique({
where: { name: req.body.professor },
});
if (!professorObject) {
professorObject = await prisma.professor.create({
data: {
name: req.body.professor,
schoolId: school.id,
},
});
}
let { id } = await prisma.syllabus.create({
data: {
mimeType: file.mimetype,
createdByName: req.body.name || undefined,
createdByEmail: req.body.email || undefined,
fullClassName: req.body.fullClassName || undefined,
classLength: req.body.classLength ? parseInt(req.body.classLength, 10) : undefined,
textbookCost: req.body.textbookCost || undefined,
description: req.body.description || undefined,
content: "", // Add the content property
class: { connect: { id: classRecord.id } },
professorObject: { connect: { id: professorObject.id } },
school: { connect: { id: school.id } },
},
});
await file.mv(`./syllabi/${id}.${mime.extension(file.mimetype)}`);
return res.status(200).json({ id });
} catch (error) {
console.error('Error creating syllabus:', error);
if (error instanceof Error) {
return res.status(500).json({ msg: "Internal server error", error: error.message });
}
return res.status(500).json({ msg: "Internal server error" });
}
});
app.get("/schools", async (req, res) => {
let colleges = await prisma.school.findMany({});
res.json(colleges);
})
app.post("/search/class/", async (req, res) => {
let take = req.body.take || 10;
let skip = req.body.skip || 0;
if (await prisma.school.findUnique({ where: { name: req.query.s as string } }) === null) return res.status(404).json({ msg: "School not found" });
let classes = await prisma.class.findMany({
where: {
school: {
name: req.query.s as string,
},
},
});
let results = classes;
if (req.query.q) {
results = fuzzysort.go(req.query.q as string, classes, { keys: ["className", "fullClassName"] }).map(i => i.obj);
}
res.json(results.slice(skip, skip + take));
});
app.post("/search/professor/", async (req, res) => {
let take = req.body.take || 10;
let skip = req.body.skip || 0;
if (await prisma.school.findUnique({ where: { name: req.query.s as string } }) === null) return res.status(404).json({ msg: "School not found" });
let professors = await prisma.professor.findMany({
where: {
school: {
name: req.query.s as string,
},
},
});
let results = professors;
if (req.query.q) {
results = fuzzysort.go(req.query.q as string, professors, { keys: ["name"] }).map(i => i.obj);
}
res.json(results.slice(skip, skip + take));
});
app.get("/professor/:id", async (req, res) => {
let professor = await prisma.professor.findUnique({
where: { id: req.params.id },
include: {
school: true,
syllabi: {
include: {
class: true,
},
},
},
});
if (professor === null) return res.status(404).json({ msg: "Professor not found" });
return res.json({
name: professor.name,
school: professor.school?.name,
syllabi: professor.syllabi.map(i => {
return {
id: i.id,
className: i.class?.className,
fullClassName: i.fullClassName,
textbookCost: i.textbookCost,
classLength: i.classLength,
description: i.description,
dateCreated: i.dateCreated.toISOString(),
fileName: `${i.id}.${mime.extension(i.mimeType)}`,
};
}),
});
});
app.get("/syllabus/:id", async (req, res) => {
let syllabus = await prisma.syllabus.findUnique({
where: { id: req.params.id},
include: {
class: {
include: {
discipline: true,
},
},
professorObject: true,
},
});
if (syllabus === null) return res.status(404).json({ msg: "Syllabus not found" });
return res.json({
className: syllabus.class?.className,
description: syllabus.description,
fullClassName: syllabus.fullClassName,
textbookCost: syllabus.textbookCost,
content: syllabus.content,
classLength: syllabus.classLength,
professor: syllabus.professorObject?.name,
dateCreated: syllabus.dateCreated.toISOString(),
fileName: `${syllabus.id}.${mime.extension(syllabus.mimeType)}`,
class: {
className: syllabus.class?.className ?? '',
fullClassName: syllabus.class?.fullClassName ?? '',
discipline: syllabus.class?.discipline?.name ?? '',
},
});
});
app.get("/syllabus/:id/download", async (req, res) => {
let syllabus = await prisma.syllabus.findUnique({
where: { id: req.params.id, reviewed: true },
include: {
class: true,
},
});
if (syllabus === null) return res.status(404).json({ msg: "Syllabus not found" });
res.download(`./syllabi/${syllabus.id}.${mime.extension(syllabus.mimeType)}`);
});
app.get("/search/", async (req, res) => {
try {
let syllabi = await prisma.syllabus.findMany({
include: {
class: {
include: {
discipline: true,
},
},
professorObject: true,
school: true,
},
});
let results = syllabi;
if (req.query.s) {
results = results.filter(i => i.school?.name === req.query.s);
}
if (req.query.c) {
results = results.filter(i => i.class?.className === req.query.c);
}
if (req.query.p) {
results = results.filter(i => i.professorObject?.name === req.query.p);
}
if (req.query.q) {
results = fuzzysort.go(req.query.q as string, results, { keys: ["class.className", "professorObject.name", "class.fullClassName"] }).map(i => i.obj);
}
let take = req.query.take as unknown as number || 10;
let skip = req.query.skip as unknown as number || 0;
res.json(results.map(i => {
return {
id: i.id,
className: i.class?.className,
professor: i.professorObject?.name ?? '',
fullClassName: i.fullClassName,
textbookCost: i.textbookCost,
description: i.description,
classLength: i.classLength,
dateCreated: i.dateCreated.toISOString(),
fileName: `${i.id}.${mime.extension(i.mimeType)}`,
class: {
className: i.class?.className ?? '',
fullClassName: i.class?.fullClassName ?? '',
discipline: i.class?.discipline?.name ?? '',
},
professorId: i.professorObject?.id ?? '',
school: {
name: i.school?.name ?? '',
id: i.school?.id ?? '',
fullName: i.school?.fullName ?? '',
}
};
}).slice(skip).slice(0, take));
} catch (e) {
console.error(e);
res.status(500).json({ msg: "Internal server error" });
}
});
app.post("/report/:id", async (req, res) => {
try {
const { reportType, reportTitle, reportBody, reportBy } = req.body;
const syllabusId = req.params.id;
// Check if the syllabus exists
const syllabus = await prisma.syllabus.findUnique({ where: { id: syllabusId } });
if (!syllabus) {
return res.status(404).json({ msg: "Syllabus not found" });
}
// Create the report
const report = await prisma.report.create({
data: {
reportType,
reportTitle,
reportBody,
reportBy,
syllabusId,
},
});
res.json(report);
} catch (e) {
console.error(e);
res.status(500).json({ msg: `Internal Server Error ${e}` });
}
})
app.post("/moderator/login", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ msg: "Missing username or password" });
}
const moderator = await prisma.moderator.findUnique({ where: { username } });
if (!moderator) {
return res.status(401).json({ msg: "Invalid username or password" });
}
const valid = await argon2.verify(moderator.password, password);
if (!valid) {
return res.status(401).json({ msg: "Invalid username or password" });
}
const token = jwt.sign({ id: moderator.id }, process.env.SECRET_KEY as string, { expiresIn: '2h' });
res.json({ token });
return;
});
app.post("/moderator/flag", jwtMiddleware, async (req, res) => {
res.json(req.body);
});
await new Promise<void>((resolve) =>
httpServer.listen({ port: 4000, host: "0.0.0.0" }, resolve)
);
+40
View File
@@ -0,0 +1,40 @@
{
"name": "sms-server",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.7.3"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@graphql-tools/schema": "^10.0.23",
"@graphql-typed-document-node/core": "^3.2.0",
"@prisma/client": "^6.3.1",
"@types/cors": "^2.8.17",
"@types/express-fileupload": "^1.5.1",
"@types/jsonwebtoken": "^9.0.9",
"@types/mime-types": "^2.1.4",
"argon2": "^0.41.1",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-fileupload": "^1.5.1",
"fuzzysort": "^3.1.0",
"g4f": "^1.4.6",
"gpt4free": "^0.0.1",
"graphql": "^16.10.0",
"graphql-tag": "^2.12.6",
"jsonwebtoken": "^9.0.2",
"jwt": "^0.2.0",
"pg": "^8.13.1",
"prisma": "^6.3.1",
"spamscanner": "^5.1.5"
},
"scripts": {
"start": "bun index.ts",
"dev": "ts-node-dev --respawn --transpile-only index.ts"
}
}
+176
View File
@@ -0,0 +1,176 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
//TODO: add functionality for other colleges to add classes
//TODO: make teachers have an object
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum TextbookCost {
free
cheap // 30 dollars and less
moderate // 30-60 dollars
expensive // beyond 60 dollars
}
model Discipline {
id String @id @default(cuid())
name String @unique
classes Class[]
}
model Class {
id String @id @default(cuid())
className String @unique
fullClassName String @default("Full Class Name")
syllabi Syllabus[]
discipline Discipline @relation(fields: [disciplineId], references: [id])
disciplineId String
school School? @relation(fields: [schoolId], references: [id])
schoolId String?
professors Professor[] @relation("ProfessorClasses")
}
model School {
id String @id @default(cuid())
name String @unique
fullName String @default("")
classes Class[]
professors Professor[]
syllabi Syllabus[]
ClassSubmission ClassSubmission[]
}
model Professor {
id String @id @default(cuid())
name String @unique
classes Class[] @relation("ProfessorClasses")
rmp String?
syllabi Syllabus[]
school School? @relation(fields: [schoolId], references: [id])
schoolId String?
}
model Syllabus {
id String @id @default(cuid())
submission ClassSubmission? @relation(fields: [submissionId], references: [id])
submissionId String?
school School? @relation(fields: [schoolId], references: [id])
schoolId String?
// Whether or not the syllabus has been reviewed by a Tier 2 moderator
reviewed Boolean @default(false)
dateCreated DateTime @default(now())
// Class length (weeks)
classLength Int?
// Course description
description String?
// Creator data
createdByEmail String?
createdByName String?
// File type
mimeType String // TODO: remove?
// Markdown content
content String?
professor String
textbookCost TextbookCost @default(free)
fullClassName String @default("Full Class Name") // Full name of class (e.g. "Critical Thinking & Writing")
reports Report[] // User reports
class Class? @relation(fields: [classId], references: [id])
classId String?
changes Change[] // Potential changes made by moderators
flags Flag[]
Professor Professor[]
}
model ClassSubmission {
id String @id @default(cuid())
dateCreated DateTime @default(now())
school School? @relation(fields: [schoolId], references: [id])
schoolId String?
className String
professor String
classLength Int?
description String?
createdByEmail String?
createdByName String?
textbookCost TextbookCost @default(free)
syllabusFileType String
Syllabus Syllabus[]
}
model Moderator {
id String @id @default(cuid())
username String @unique
password String
changes Change[]
flags Flag[]
}
model Flag {
id String @id @default(cuid())
reason String
date DateTime @default(now())
moderator Moderator @relation(fields: [moderatorId], references: [id])
moderatorId String
syllabusId String @unique
syllabus Syllabus @relation(fields: [syllabusId], references: [id])
}
model Change {
id String @id @default(cuid())
reason String
date DateTime @default(now())
moderator Moderator @relation(fields: [moderatorId], references: [id])
moderatorId String
syllabusId String @unique
syllabus Syllabus @relation(fields: [syllabusId], references: [id])
className String?
professor String?
reviewed Boolean @default(false)
dateCreated DateTime @default(now())
rating Int?
ratingNumber Int @default(0)
classLength Int?
description String?
textbookCost TextbookCost @default(free)
fullClassName String @default("Full Class Name")
}
model Report {
id String @id @default(cuid())
reportType String
reportTitle String
reportBody String
reportDate DateTime @default(now())
reportBy String
syllabusId String
syllabus Syllabus @relation(fields: [syllabusId], references: [id])
}
+20
View File
@@ -0,0 +1,20 @@
import { PrismaClient } from "@prisma/client";
import argon2 from "argon2";
const prisma = new PrismaClient();
let username = Bun.argv[2];
let password = Bun.argv[3];
let priviledge = Bun.argv[4];
try {
let newModerator = await prisma.moderator.create({
data: {
username: username,
password: await argon2.hash(password),
}
});
console.log(`Successfully created new moderator with username: ${username} and id: ${newModerator.id}`);
} catch (e) {
console.error(e);
}
+18
View File
@@ -0,0 +1,18 @@
import jwt, { JwtPayload } from 'jsonwebtoken';
import "dotenv/config";
import { Request, Response, NextFunction } from 'express';
export function jwtMiddleware(req: Request, res: Response, next: NextFunction): void {
const token = req.header('Authorization');
if (!token) {
res.status(401).json({ error: 'Access denied' })
return;
};
try {
const decoded = jwt.verify(token.replace("Bearer ", ''), process.env.SECRET_KEY as string) as JwtPayload;
res.locals.userId = decoded.userId;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
+88
View File
@@ -0,0 +1,88 @@
import { Router } from "express";
import fileUpload from "express-fileupload";
import mime from "mime";
import { prisma } from "..";
const router = Router();
router.get("/", async (req, res) => {
if (req.files === null || req.files === undefined)
return res.status(400).json({ msg: "No file uploaded" });
if (req.files.upload === null)
return res.status(400).json({ msg: "No file uploaded" });
if (Array.isArray(req.files.upload))
return res.status(400).json({ msg: "Multiple files uploaded" });
const file = req.files.upload as fileUpload.UploadedFile;
if (![mime.lookup(".pdf"), mime.lookup(".docx")].includes(file.mimetype)) return res.status(400).json({ msg: "Invalid file type" });
if (file.size > 5_000_000) {
return res.status(400).json({ msg: "File too large (max 5MB)" });
}
// Add error handling for missing required fields
if (!req.body.schoolName || !req.body.className || !req.body.professor) {
return res.status(400).json({ msg: "Missing required fields" });
}
// Add proper error handling for file operations
try {
let school = await prisma.school.findUnique({
where: { name: req.body.schoolName },
});
if (!school) {
return res.status(404).json({ msg: "School not found" });
}
let classRecord = await prisma.class.findFirst({ // Change findUnique to findFirst
where: {
className: req.body.className,
schoolId: school.id,
},
});
if (!classRecord) {
return res.status(404).json({ msg: "Class not found" });
}
let professorObject = await prisma.professor.findUnique({
where: { name: req.body.professor },
});
if (!professorObject) {
professorObject = await prisma.professor.create({
data: {
name: req.body.professor,
schoolId: school.id,
},
});
}
let { id } = await prisma.syllabus.create({
data: {
mimeType: file.mimetype,
createdByName: req.body.name || undefined,
createdByEmail: req.body.email || undefined,
fullClassName: req.body.fullClassName || undefined,
classLength: req.body.classLength ? parseInt(req.body.classLength, 10) : undefined,
textbookCost: req.body.textbookCost || undefined,
description: req.body.description || undefined,
content: "", // Add the content property
class: { connect: { id: classRecord.id } },
professorObject: { connect: { id: professorObject.id } },
school: { connect: { id: school.id } },
},
});
await file.mv(`./syllabi/${id}.${mime.extension(file.mimetype)}`);
return res.status(200).json({ id });
} catch (error) {
console.error('Error creating syllabus:', error);
if (error instanceof Error) {
return res.status(500).json({ msg: "Internal server error", error: error.message });
}
return res.status(500).json({ msg: "Internal server error" });
}
});
export default router;
+294
View File
@@ -0,0 +1,294 @@
import { PrismaClient, type Syllabus } from "@prisma/client";
import express from "express";
import cors from "cors";
import fileUpload from "express-fileupload";
import http from "node:http";
import fuzzysort from "fuzzysort";
import mime from "mime-types";
import jwt from 'jsonwebtoken';
import argon2 from 'argon2';
import { idText } from "typescript";
import 'dotenv/config';
import { jwtMiddleware } from "./authMiddleware";
import uploadSyllabus from "./entities/uploadSyllabus";
import gql from "graphql-tag";
const schema = gql`
{
type Amazing {
}
}
`
const app = express();
const httpServer = http.createServer(app);
export const prisma = new PrismaClient();
app.use(cors({
origin: "*", // TODO change this to the actual domain
}))
app.use(express.json());
app.use(fileUpload());
app.use('/files', express.static('syllabi'));
app.use('/create', uploadSyllabus)
app.get("/schools", async (req, res) => {
let colleges = await prisma.school.findMany({});
res.json(colleges);
})
app.post("/search/class/", async (req, res) => {
let take = req.body.take || 10;
let skip = req.body.skip || 0;
if (await prisma.school.findUnique({ where: { name: req.query.s as string } }) === null) return res.status(404).json({ msg: "School not found" });
let classes = await prisma.class.findMany({
where: {
school: {
name: req.query.s as string,
},
},
});
let results = classes;
if (req.query.q) {
results = fuzzysort.go(req.query.q as string, classes, { keys: ["className", "fullClassName"] }).map(i => i.obj);
}
res.json(results.slice(skip, skip + take));
});
app.post("/search/professor/", async (req, res) => {
let take = req.body.take || 10;
let skip = req.body.skip || 0;
if (await prisma.school.findUnique({ where: { name: req.query.s as string } }) === null) return res.status(404).json({ msg: "School not found" });
let professors = await prisma.professor.findMany({
where: {
school: {
name: req.query.s as string,
},
},
});
let results = professors;
if (req.query.q) {
results = fuzzysort.go(req.query.q as string, professors, { keys: ["name"] }).map(i => i.obj);
}
res.json(results.slice(skip, skip + take));
});
app.get("/professor/:id", async (req, res) => {
let professor = await prisma.professor.findUnique({
where: { id: req.params.id },
include: {
school: true,
syllabi: {
include: {
class: true,
},
},
},
});
if (professor === null) return res.status(404).json({ msg: "Professor not found" });
return res.json({
name: professor.name,
school: professor.school?.name,
syllabi: professor.syllabi.map(i => {
return {
id: i.id,
className: i.class?.className,
fullClassName: i.fullClassName,
textbookCost: i.textbookCost,
classLength: i.classLength,
description: i.description,
dateCreated: i.dateCreated.toISOString(),
fileName: `${i.id}.${mime.extension(i.mimeType)}`,
};
}),
});
});
app.get("/syllabus/:id", async (req, res) => {
let syllabus = await prisma.syllabus.findUnique({
where: { id: req.params.id, reviewed: true },
include: {
class: {
include: {
discipline: true,
},
},
professorObject: true,
},
});
if (syllabus === null) return res.status(404).json({ msg: "Syllabus not found" });
return res.json({
className: syllabus.class?.className,
description: syllabus.description,
fullClassName: syllabus.fullClassName,
textbookCost: syllabus.textbookCost,
content: syllabus.content,
classLength: syllabus.classLength,
professor: syllabus.professorObject?.name,
dateCreated: syllabus.dateCreated.toISOString(),
fileName: `${syllabus.id}.${mime.extension(syllabus.mimeType)}`,
class: {
className: syllabus.class?.className ?? '',
fullClassName: syllabus.class?.fullClassName ?? '',
discipline: syllabus.class?.discipline?.name ?? '',
},
});
});
app.get("/syllabus/:id/download", async (req, res) => {
let syllabus = await prisma.syllabus.findUnique({
where: { id: req.params.id, reviewed: true },
include: {
class: true,
},
});
if (syllabus === null) return res.status(404).json({ msg: "Syllabus not found" });
res.download(`./syllabi/${syllabus.id}.${mime.extension(syllabus.mimeType)}`);
});
app.get("/search/", async (req, res) => {
try {
let syllabi = await prisma.syllabus.findMany({
include: {
class: {
include: {
discipline: true,
},
},
professorObject: true,
school: true,
},
});
let results = syllabi;
if (req.query.s) {
results = results.filter(i => i.school?.name === req.query.s);
}
if (req.query.c) {
results = results.filter(i => i.class?.className === req.query.c);
}
if (req.query.p) {
results = results.filter(i => i.professorObject?.name === req.query.p);
}
if (req.query.q) {
results = fuzzysort.go(req.query.q as string, results, { keys: ["class.className", "professorObject.name", "class.fullClassName"] }).map(i => i.obj);
}
let take = req.query.take as unknown as number || 10;
let skip = req.query.skip as unknown as number || 0;
res.json(results.map(i => {
return {
id: i.id,
className: i.class?.className,
professor: i.professorObject?.name ?? '',
fullClassName: i.fullClassName,
textbookCost: i.textbookCost,
description: i.description,
classLength: i.classLength,
dateCreated: i.dateCreated.toISOString(),
fileName: `${i.id}.${mime.extension(i.mimeType)}`,
class: {
className: i.class?.className ?? '',
fullClassName: i.class?.fullClassName ?? '',
discipline: i.class?.discipline?.name ?? '',
},
professorId: i.professorObject?.id ?? '',
school: {
name: i.school?.name ?? '',
id: i.school?.id ?? '',
fullName: i.school?.fullName ?? '',
}
};
}).slice(skip).slice(0, take));
} catch (e) {
console.error(e);
res.status(500).json({ msg: "Internal server error" });
}
});
app.post("/report/:id", async (req, res) => {
try {
const { reportType, reportTitle, reportBody, reportBy } = req.body;
const syllabusId = req.params.id;
// Check if the syllabus exists
const syllabus = await prisma.syllabus.findUnique({ where: { id: syllabusId } });
if (!syllabus) {
return res.status(404).json({ msg: "Syllabus not found" });
}
// Create the report
const report = await prisma.report.create({
data: {
reportType,
reportTitle,
reportBody,
reportBy,
syllabusId,
},
});
res.json(report);
} catch (e) {
console.error(e);
res.status(500).json({ msg: `Internal Server Error ${e}` });
}
})
app.post("/moderator/login", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ msg: "Missing username or password" });
}
const moderator = await prisma.moderator.findUnique({ where: { username } });
if (!moderator) {
return res.status(401).json({ msg: "Invalid username or password" });
}
const valid = await argon2.verify(moderator.password, password);
if (!valid) {
return res.status(401).json({ msg: "Invalid username or password" });
}
const token = jwt.sign({ id: moderator.id }, process.env.SECRET_KEY as string, { expiresIn: '2h' });
res.json({ token });
return;
});
app.post("/moderator/check", jwtMiddleware, async (req, res) => {
res.json({ id: req.params.userId});
});
app.post("/moderator/flag", jwtMiddleware, async (req, res) => {
res.json(req.body);
});
await new Promise<void>((resolve) =>
httpServer.listen({ port: 4000, host: "0.0.0.0" }, resolve)
);
+34
View File
@@ -0,0 +1,34 @@
import { PrismaClient } from '@prisma/client';
import * as fs from 'fs';
import * as readline from 'readline';
const prisma = new PrismaClient();
async function updateFullClassNames(filePath: string) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
for await (const line of rl) {
const [className, fullClassName] = line.split(':').map(part => part.trim());
if (className && fullClassName) {
try {
await prisma.class.update({
where: { className },
data: { fullClassName },
});
console.log(`Updated ${className} with fullClassName: ${fullClassName}`);
} catch (error) {
console.error(`Error updating ${className}:`, error);
}
}
}
await prisma.$disconnect();
}
// Run the function
updateFullClassNames('fullClassNames.txt');
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"module": "ESNext",
"target": "es2017",
"strict": true,
"noImplicitAny": false,
"types": ["graphql-tag"],
"skipLibCheck": true
}
}