From b682bca7645263e0ab866915f8e729d95bc6f92e Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Thu, 31 Jul 2025 11:19:25 -0400 Subject: [PATCH 01/53] defined user model --- database/user.js | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/database/user.js b/database/user.js index 755c757..35868f5 100644 --- a/database/user.js +++ b/database/user.js @@ -3,30 +3,18 @@ const db = require("./db"); const bcrypt = require("bcrypt"); const User = db.define("user", { - username: { - type: DataTypes.STRING, - allowNull: false, - unique: true, - validate: { - len: [3, 20], - }, - }, - email: { - type: DataTypes.STRING, - allowNull: true, - unique: true, - validate: { - isEmail: true, - }, + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, }, - auth0Id: { + firstName: { type: DataTypes.STRING, - allowNull: true, - unique: true, + allowNull: false, }, - passwordHash: { + lastName: { type: DataTypes.STRING, - allowNull: true, + allowNull: false, }, }); From 1883fcc07c18b17b93ac2f2d2a378f3fc4b731a8 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 31 Jul 2025 11:33:24 -0400 Subject: [PATCH 02/53] created db tables and associations --- database/Calculator.js | 18 ++++++++++++++++++ database/Reminder.js | 17 +++++++++++++++++ database/Session.js | 15 +++++++++++++++ database/Tasks.js | 25 +++++++++++++++++++++++++ database/db.js | 2 +- database/index.js | 28 +++++++++++++++++++++++++++- package-lock.json | 12 ++++++------ package.json | 2 +- 8 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 database/Calculator.js create mode 100644 database/Reminder.js create mode 100644 database/Session.js create mode 100644 database/Tasks.js diff --git a/database/Calculator.js b/database/Calculator.js new file mode 100644 index 0000000..bce2053 --- /dev/null +++ b/database/Calculator.js @@ -0,0 +1,18 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Calculator = db.define('Calculator', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true }, + assessment: DataTypes.TEXT, + grade: DataTypes.INTEGER, + weight: DataTypes.INTEGER, + user_id: { + type: DataTypes.INTEGER, + allowNull: false } + +}); + +module.exports = Calculator; diff --git a/database/Reminder.js b/database/Reminder.js new file mode 100644 index 0000000..c487941 --- /dev/null +++ b/database/Reminder.js @@ -0,0 +1,17 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Reminder = db.define('Reminder', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true }, + task_id: { + type: DataTypes.INTEGER, + allowNull: false }, + + remind: DataTypes.DATE +}, +); + +module.exports = Reminder; diff --git a/database/Session.js b/database/Session.js new file mode 100644 index 0000000..f1ec7d6 --- /dev/null +++ b/database/Session.js @@ -0,0 +1,15 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Session = db.define('Session', { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + duration: DataTypes.INTEGER, + user_id: { type: DataTypes.INTEGER, allowNull: false }, + started_at: DataTypes.DATE, + created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW } +}, { + tableName: 'session', + timestamps: false +}); + +module.exports = Session; diff --git a/database/Tasks.js b/database/Tasks.js new file mode 100644 index 0000000..bf07486 --- /dev/null +++ b/database/Tasks.js @@ -0,0 +1,25 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Task = db.define("Task", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true }, + class: DataTypes.STRING, + assignment: DataTypes.STRING, + description: DataTypes.TEXT, + status: DataTypes.STRING, + deadline: DataTypes.DATE, + priority: DataTypes.STRING, + user_id: { + type: DataTypes.INTEGER, + allowNull: false }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW }, + }, +); + +module.exports = Task; diff --git a/database/db.js b/database/db.js index b251a9d..5d62e89 100644 --- a/database/db.js +++ b/database/db.js @@ -6,7 +6,7 @@ const pg = require("pg"); const dbName = "capstone-1"; const db = new Sequelize( - process.env.DATABASE_URL || `postgres://localhost:5432/${dbName}`, + process.env.DATABASE_URL || `postgres://localhost:5432/${capstone-2}`, { logging: false, // comment this line to enable SQL logging } diff --git a/database/index.js b/database/index.js index e498df6..c46e2ab 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,33 @@ +const Sequelize = require("sequelize"); const db = require("./db"); -const User = require("./user"); + + +const User = require('./User'); +const Task = require('./Tasks'); +const Calculator = require('./Calculator'); +const Reminder = require('./Reminder'); +const Session = require('./Session'); + +// Define associations +User.hasMany(Task, { foreignKey: 'user_id' }); // One user can have many tasks +Task.belongsTo(User, { foreignKey: 'user_id' }); // Each task belongs to a specific user + +User.hasMany(Calculator, { foreignKey: 'user_id' }); // User can have many grade calculator instances +Calculator.belongsTo(User, { foreignKey: 'user_id' }); // Each calculator belongs to a user + +User.hasMany(Session, { foreignKey: 'user_id' }); // Each user can have many study sessions +Session.belongsTo(User, { foreignKey: 'user_id' }); // Each session belongs to a user + +Task.hasMany(Reminder, { foreignKey: 'task_id' }); // One task can have many reminders +Reminder.belongsTo(Task, { foreignKey: 'task_id' }); // One reminder belongs to a specific task + +// Export everything module.exports = { db, User, + Task, + Calculator, + Reminder, + Session }; diff --git a/package-lock.json b/package-lock.json index af0cf82..4b28357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "license": "ISC", "dependencies": { "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "dotenv": "^16.5.0", + "dotenv": "^16.6.1", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", @@ -353,9 +353,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 7e0a0af..4e36aed 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "dotenv": "^16.5.0", + "dotenv": "^16.6.1", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", From 2d4fdfb3d742c4a71c6ceff5de4dccf95f57593c Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 31 Jul 2025 11:33:24 -0400 Subject: [PATCH 03/53] created db tables and associations --- database/Calculator.js | 18 ++++++++++++++++++ database/Reminder.js | 17 +++++++++++++++++ database/Session.js | 15 +++++++++++++++ database/Tasks.js | 25 +++++++++++++++++++++++++ database/db.js | 2 +- database/index.js | 28 +++++++++++++++++++++++++++- package-lock.json | 12 ++++++------ package.json | 2 +- 8 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 database/Calculator.js create mode 100644 database/Reminder.js create mode 100644 database/Session.js create mode 100644 database/Tasks.js diff --git a/database/Calculator.js b/database/Calculator.js new file mode 100644 index 0000000..bce2053 --- /dev/null +++ b/database/Calculator.js @@ -0,0 +1,18 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Calculator = db.define('Calculator', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true }, + assessment: DataTypes.TEXT, + grade: DataTypes.INTEGER, + weight: DataTypes.INTEGER, + user_id: { + type: DataTypes.INTEGER, + allowNull: false } + +}); + +module.exports = Calculator; diff --git a/database/Reminder.js b/database/Reminder.js new file mode 100644 index 0000000..c487941 --- /dev/null +++ b/database/Reminder.js @@ -0,0 +1,17 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Reminder = db.define('Reminder', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true }, + task_id: { + type: DataTypes.INTEGER, + allowNull: false }, + + remind: DataTypes.DATE +}, +); + +module.exports = Reminder; diff --git a/database/Session.js b/database/Session.js new file mode 100644 index 0000000..f1ec7d6 --- /dev/null +++ b/database/Session.js @@ -0,0 +1,15 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Session = db.define('Session', { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + duration: DataTypes.INTEGER, + user_id: { type: DataTypes.INTEGER, allowNull: false }, + started_at: DataTypes.DATE, + created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW } +}, { + tableName: 'session', + timestamps: false +}); + +module.exports = Session; diff --git a/database/Tasks.js b/database/Tasks.js new file mode 100644 index 0000000..bf07486 --- /dev/null +++ b/database/Tasks.js @@ -0,0 +1,25 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Task = db.define("Task", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true }, + class: DataTypes.STRING, + assignment: DataTypes.STRING, + description: DataTypes.TEXT, + status: DataTypes.STRING, + deadline: DataTypes.DATE, + priority: DataTypes.STRING, + user_id: { + type: DataTypes.INTEGER, + allowNull: false }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW }, + }, +); + +module.exports = Task; diff --git a/database/db.js b/database/db.js index b251a9d..5d62e89 100644 --- a/database/db.js +++ b/database/db.js @@ -6,7 +6,7 @@ const pg = require("pg"); const dbName = "capstone-1"; const db = new Sequelize( - process.env.DATABASE_URL || `postgres://localhost:5432/${dbName}`, + process.env.DATABASE_URL || `postgres://localhost:5432/${capstone-2}`, { logging: false, // comment this line to enable SQL logging } diff --git a/database/index.js b/database/index.js index e498df6..c46e2ab 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,33 @@ +const Sequelize = require("sequelize"); const db = require("./db"); -const User = require("./user"); + + +const User = require('./User'); +const Task = require('./Tasks'); +const Calculator = require('./Calculator'); +const Reminder = require('./Reminder'); +const Session = require('./Session'); + +// Define associations +User.hasMany(Task, { foreignKey: 'user_id' }); // One user can have many tasks +Task.belongsTo(User, { foreignKey: 'user_id' }); // Each task belongs to a specific user + +User.hasMany(Calculator, { foreignKey: 'user_id' }); // User can have many grade calculator instances +Calculator.belongsTo(User, { foreignKey: 'user_id' }); // Each calculator belongs to a user + +User.hasMany(Session, { foreignKey: 'user_id' }); // Each user can have many study sessions +Session.belongsTo(User, { foreignKey: 'user_id' }); // Each session belongs to a user + +Task.hasMany(Reminder, { foreignKey: 'task_id' }); // One task can have many reminders +Reminder.belongsTo(Task, { foreignKey: 'task_id' }); // One reminder belongs to a specific task + +// Export everything module.exports = { db, User, + Task, + Calculator, + Reminder, + Session }; diff --git a/package-lock.json b/package-lock.json index af0cf82..4b28357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "license": "ISC", "dependencies": { "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "dotenv": "^16.5.0", + "dotenv": "^16.6.1", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", @@ -353,9 +353,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 7e0a0af..4e36aed 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "dotenv": "^16.5.0", + "dotenv": "^16.6.1", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", From 48f2663c3260b65ed7936f5f68ce70e683338749 Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Thu, 31 Jul 2025 11:46:01 -0400 Subject: [PATCH 04/53] added validation for email format --- database/user.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/database/user.js b/database/user.js index 35868f5..0c86524 100644 --- a/database/user.js +++ b/database/user.js @@ -16,6 +16,18 @@ const User = db.define("user", { type: DataTypes.STRING, allowNull: false, }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true, + }, + }, + passwordHash: { + type: DataTypes.STRING, + allowNull: false, + }, }); // Instance method to check password @@ -28,7 +40,8 @@ User.prototype.checkPassword = function (password) { // Class method to hash password User.hashPassword = function (password) { - return bcrypt.hashSync(password, 10); + const saltValue = 12; + return bcrypt.hashSync(password, saltValue); }; module.exports = User; From ffcd831988944f85f94a544e05b5bb7679926735 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 1 Aug 2025 10:28:07 -0400 Subject: [PATCH 05/53] Added get and post route for the task manager feature --- api/TaskOrganizer.js | 40 ++++++++++++++++++++++++++++++ api/index.js | 3 +++ database/Tasks.js | 6 ++--- database/index.js | 12 ++++----- database/seed.js | 58 +++++++++++++++++++++++++++++++++++--------- database/user.js | 4 +-- 6 files changed, 100 insertions(+), 23 deletions(-) create mode 100644 api/TaskOrganizer.js diff --git a/api/TaskOrganizer.js b/api/TaskOrganizer.js new file mode 100644 index 0000000..74bd22a --- /dev/null +++ b/api/TaskOrganizer.js @@ -0,0 +1,40 @@ +const express = require("express"); +const router = express.Router(); +const { Tasks } = require("../database"); + +// GET all tasks for a user +router.get("/tasks/:userId", async (req, res) => { + try { + const userId = req.params.userId;// storing the user ID from the URL + const tasks = await Tasks.findAll({ where: { user_id: userId } });//This is storing all the data that .findall is getting form the model in Tasks in this case it is filtering where the user ID equals the one found in the URL + res.json(tasks);//Outputting it as a json + } catch (error) { + console.error("āŒ Error fetching tasks:", error); + res.status(500).json({ error: "Failed to fetch tasks" }); + } +}); + +//POST a new task +//The goal of this is to store the data that the user is sending into the Tasks model +router.post("/tasks", async (req, res) => { + try { + const { title, due_date, priority, user_id } = req.body; // This grabs the data from the body of the request + + const newTask = await Tasks.create({ //Cretaing a new row in the tasks model + title, + due_date, + priority, + user_id + }); + + res.status(201).json(newTask);//The data then gets stored in the new row in the tasks model (newTask) + } catch (error) { + console.error("āŒ Error creating task:", error); + res.status(500).json({ error: "Failed to create task" }); + } +}); + + + + +module.exports = router; diff --git a/api/index.js b/api/index.js index f08162e..65629f3 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,10 @@ const express = require("express"); const router = express.Router(); + const testDbRouter = require("./test-db"); +const taskOrganizerRouter = require("./TaskOrganizer"); router.use("/test-db", testDbRouter); +router.use("/tasks", taskOrganizerRouter); module.exports = router; diff --git a/database/Tasks.js b/database/Tasks.js index bf07486..9e7840d 100644 --- a/database/Tasks.js +++ b/database/Tasks.js @@ -1,7 +1,7 @@ const { DataTypes } = require("sequelize"); const db = require("./db"); -const Task = db.define("Task", +const Tasks = db.define("Tasks", { id: { type: DataTypes.INTEGER, @@ -10,7 +10,7 @@ const Task = db.define("Task", class: DataTypes.STRING, assignment: DataTypes.STRING, description: DataTypes.TEXT, - status: DataTypes.STRING, + status: DataTypes.ENUM, deadline: DataTypes.DATE, priority: DataTypes.STRING, user_id: { @@ -22,4 +22,4 @@ const Task = db.define("Task", }, ); -module.exports = Task; +module.exports = Tasks; diff --git a/database/index.js b/database/index.js index c46e2ab..e3b567c 100644 --- a/database/index.js +++ b/database/index.js @@ -4,14 +4,14 @@ const db = require("./db"); const User = require('./User'); -const Task = require('./Tasks'); +const Tasks = require('./Tasks'); const Calculator = require('./Calculator'); const Reminder = require('./Reminder'); const Session = require('./Session'); // Define associations -User.hasMany(Task, { foreignKey: 'user_id' }); // One user can have many tasks -Task.belongsTo(User, { foreignKey: 'user_id' }); // Each task belongs to a specific user +User.hasMany(Tasks, { foreignKey: 'user_id' }); // One user can have many tasks +Tasks.belongsTo(User, { foreignKey: 'user_id' }); // Each task belongs to a specific user User.hasMany(Calculator, { foreignKey: 'user_id' }); // User can have many grade calculator instances Calculator.belongsTo(User, { foreignKey: 'user_id' }); // Each calculator belongs to a user @@ -19,14 +19,14 @@ Calculator.belongsTo(User, { foreignKey: 'user_id' }); // Each calculator belong User.hasMany(Session, { foreignKey: 'user_id' }); // Each user can have many study sessions Session.belongsTo(User, { foreignKey: 'user_id' }); // Each session belongs to a user -Task.hasMany(Reminder, { foreignKey: 'task_id' }); // One task can have many reminders -Reminder.belongsTo(Task, { foreignKey: 'task_id' }); // One reminder belongs to a specific task +Tasks.hasMany(Reminder, { foreignKey: 'task_id' }); // One task can have many reminders +Reminder.belongsTo(Tasks, { foreignKey: 'task_id' }); // One reminder belongs to a specific task // Export everything module.exports = { db, User, - Task, + Tasks, Calculator, Reminder, Session diff --git a/database/seed.js b/database/seed.js index e58b595..eaeaa2c 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,30 +1,64 @@ const db = require("./db"); -const { User } = require("./index"); +const { User, Tasks, Calculator, Reminder, Session } = require('./index'); + + const seed = async () => { try { db.logging = false; await db.sync({ force: true }); // Drop and recreate tables - const users = await User.bulkCreate([ - { username: "admin", passwordHash: User.hashPassword("admin123") }, - { username: "user1", passwordHash: User.hashPassword("user111") }, - { username: "user2", passwordHash: User.hashPassword("user222") }, - ]); + const user = await User.create({ + username: 'benjamin', + email: 'benjamin@example.com', + password: 'supersecurepassword', + role: 'student' + }); + + // Create a Task + const task = await Tasks.create({ + class: 'Math 101', + assignment: 'Homework 1', + description: 'Complete exercises 1–10 on page 52', + status: 'in progress', + deadline: new Date('2025-08-05'), + priority: 'high', + user_id: user.id + }); + // Add calculator entry + const calculator = await Calculator.create({ + assessment: 'Midterm Exam', + grade: 85, + weight: 25, + user_id: user.id + }); - console.log(`šŸ‘¤ Created ${users.length} users`); + // Add study session + await Session.create({ + duration: 45, + user_id: user.id, + started_at: new Date(), + created_at: new Date() + }); - // Create more seed data here once you've created your models - // Seed files are a great way to test your database schema! + // Add reminder for task + await Reminder.create({ + task_id: task.id, + remind: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000) + }); - console.log("🌱 Seeded the database"); + console.log("🌱 Seeded the database!"); } catch (error) { - console.error("Error seeding database:", error); + console.error("āŒ Error seeding database:", error); + if (error.message.includes("does not exist")) { console.log("\nšŸ¤”šŸ¤”šŸ¤” Have you created your database??? šŸ¤”šŸ¤”šŸ¤”"); } + + process.exit(1); } - db.close(); + + await db.close(); }; seed(); diff --git a/database/user.js b/database/user.js index 35868f5..a49561f 100644 --- a/database/user.js +++ b/database/user.js @@ -10,11 +10,11 @@ const User = db.define("user", { }, firstName: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, lastName: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, }); From 72a17d6241ca0e162ba0cc2002e1fc6de801f825 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 1 Aug 2025 10:50:29 -0400 Subject: [PATCH 06/53] Fixed the seed to correspond with the changes made in the tasks model --- database/index.js | 1 - database/seed.js | 2 +- database/user.js | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/database/index.js b/database/index.js index 8912eea..e3b567c 100644 --- a/database/index.js +++ b/database/index.js @@ -1,5 +1,4 @@ const Sequelize = require("sequelize"); -const Sequelize = require("sequelize"); const db = require("./db"); diff --git a/database/seed.js b/database/seed.js index eaeaa2c..a3f74b9 100644 --- a/database/seed.js +++ b/database/seed.js @@ -20,7 +20,7 @@ const seed = async () => { class: 'Math 101', assignment: 'Homework 1', description: 'Complete exercises 1–10 on page 52', - status: 'in progress', + status: 'in-progress', deadline: new Date('2025-08-05'), priority: 'high', user_id: user.id diff --git a/database/user.js b/database/user.js index 0b0a155..3fcef22 100644 --- a/database/user.js +++ b/database/user.js @@ -26,7 +26,7 @@ const User = db.define("user", { }, passwordHash: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, }); From 0f5253d46c33b41bb67d3b386a2be8f5f83146a1 Mon Sep 17 00:00:00 2001 From: KidCharles Date: Sun, 3 Aug 2025 13:46:06 -0400 Subject: [PATCH 07/53] cleaning up calculator model --- database/Calculator.js | 23 +++++++++++++++++------ database/db.js | 2 +- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/database/Calculator.js b/database/Calculator.js index bce2053..de61f6a 100644 --- a/database/Calculator.js +++ b/database/Calculator.js @@ -5,14 +5,25 @@ const Calculator = db.define('Calculator', { id: { type: DataTypes.INTEGER, primaryKey: true, - autoIncrement: true }, - assessment: DataTypes.TEXT, - grade: DataTypes.INTEGER, - weight: DataTypes.INTEGER, + autoIncrement: true, + }, user_id: { type: DataTypes.INTEGER, - allowNull: false } - + allowNull: false, + }, + assessment: DataTypes.TEXT, + assignment_type: { + type: DataTypes.ENUM('Homework', 'Quiz', 'Midterm', 'Final'), + allowNull: false, + }, + assignment_grades: { + type: DataTypes.INTEGER, + allowNull: false, + }, + assignment_weight: { + type: DataTypes.INTEGER, + allowNull: false, + }, }); module.exports = Calculator; diff --git a/database/db.js b/database/db.js index 5d62e89..fbee587 100644 --- a/database/db.js +++ b/database/db.js @@ -6,7 +6,7 @@ const pg = require("pg"); const dbName = "capstone-1"; const db = new Sequelize( - process.env.DATABASE_URL || `postgres://localhost:5432/${capstone-2}`, + process.env.DATABASE_URL || `postgres://localhost:5432/capstone2`, { logging: false, // comment this line to enable SQL logging } From ddd1630c8cf7bcf23eb2958a2b74d44169ccd87b Mon Sep 17 00:00:00 2001 From: KidCharles Date: Sun, 3 Aug 2025 15:34:54 -0400 Subject: [PATCH 08/53] post method for creating new grade entry. AKA calculating a final grade --- api/Calculator.js | 35 +++++++++++++++++++++++++++++++++++ api/index.js | 2 ++ database/Calculator.js | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 api/Calculator.js diff --git a/api/Calculator.js b/api/Calculator.js new file mode 100644 index 0000000..954527e --- /dev/null +++ b/api/Calculator.js @@ -0,0 +1,35 @@ +const express = require("express"); +const router = express.Router(); +const { Calculator } = require("../database"); + + +// POST: Calculate a final grade. Creates a new grade entry in db +router.post("/grade-entry", async (req, res) => { + const {user_id, assignment_type, assignment_grade, assignment_weight} = req.body; + + try { + // check that required fields are not omitted + if (!assignment_type || weight == null || score == null) { + return res.status(400).json({error: "Missing required fields"}); + } + // Create new grade entry in db + const newGradeEntry = await Calculator.create({ + user_id, + assignment_type, + assignment_grade, + assignment_weight + }); + // Return new created grade entry + res.status(201).json(newGradeEntry); + } catch(error) { + console.error("Error creating new grade entry: ", error); + res.status(500).json({error: "Unable to calculate final grade. Sorry!"}) + } +}); + + + + + + +module.exports = router; \ No newline at end of file diff --git a/api/index.js b/api/index.js index 65629f3..69819f6 100644 --- a/api/index.js +++ b/api/index.js @@ -3,8 +3,10 @@ const router = express.Router(); const testDbRouter = require("./test-db"); const taskOrganizerRouter = require("./TaskOrganizer"); +const CaclulatorRouter = require("./Calculator"); router.use("/test-db", testDbRouter); router.use("/tasks", taskOrganizerRouter); +router.use("/grade-calculator", CaclulatorRouter); module.exports = router; diff --git a/database/Calculator.js b/database/Calculator.js index de61f6a..3e8aae0 100644 --- a/database/Calculator.js +++ b/database/Calculator.js @@ -16,7 +16,7 @@ const Calculator = db.define('Calculator', { type: DataTypes.ENUM('Homework', 'Quiz', 'Midterm', 'Final'), allowNull: false, }, - assignment_grades: { + assignment_grade: { type: DataTypes.INTEGER, allowNull: false, }, From 86c1556120a2f38e6765c70bed4e024e35d72e13 Mon Sep 17 00:00:00 2001 From: KidCharles Date: Mon, 4 Aug 2025 11:18:29 -0400 Subject: [PATCH 09/53] get route for fetching all of user's previous grade entries --- api/Calculator.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/api/Calculator.js b/api/Calculator.js index 954527e..9f61850 100644 --- a/api/Calculator.js +++ b/api/Calculator.js @@ -4,7 +4,7 @@ const { Calculator } = require("../database"); // POST: Calculate a final grade. Creates a new grade entry in db -router.post("/grade-entry", async (req, res) => { +router.post("/new-grade-entry", async (req, res) => { const {user_id, assignment_type, assignment_grade, assignment_weight} = req.body; try { @@ -27,6 +27,20 @@ router.post("/grade-entry", async (req, res) => { } }); +// GET: Fetch all of user's past grade-calculator entries. +router.get("/grade-entries/:userId", async (req, res) => { + try { + const userId = req.params.userId; + const entries = await Calculator.findAll({userId}); + res.status(200).json(entries); + } catch (error) { + console.error("Error fetching previous grade entries for user", error); + res.status(500).json({error: "Unable to return your previous grade entries. Sorry!"}); + } +}); + + + From d6bd3f56ffc45101045f6ff254cbad594934a67d Mon Sep 17 00:00:00 2001 From: KidCharles Date: Mon, 4 Aug 2025 11:33:56 -0400 Subject: [PATCH 10/53] route for fetching a user's specific grade entry --- api/Calculator.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/api/Calculator.js b/api/Calculator.js index 9f61850..40f54a7 100644 --- a/api/Calculator.js +++ b/api/Calculator.js @@ -39,7 +39,20 @@ router.get("/grade-entries/:userId", async (req, res) => { } }); - +// GET: Fetch a user's specific grade-calculator entry +router.get("/grade-entry/:entryId", async (req, res) => { + try { + const entryId = req.params.entryId; + const entry = await Calculator.findByPk(entryId); + if(!entry) { + return res.status(404).json({error:"Entry not found"}); + } + res.json(entry); + } catch (error) { + console.error("Error fetching previous grade entry"); + res.status(500).json({error: "Unable to return previous grade entry. Sorry!"}) + } +}); From 78ad9d0a443a68d8e10513a306491599d8117e02 Mon Sep 17 00:00:00 2001 From: KidCharles Date: Mon, 4 Aug 2025 13:35:04 -0400 Subject: [PATCH 11/53] delete route --- api/Calculator.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/api/Calculator.js b/api/Calculator.js index 40f54a7..9b7886d 100644 --- a/api/Calculator.js +++ b/api/Calculator.js @@ -31,7 +31,7 @@ router.post("/new-grade-entry", async (req, res) => { router.get("/grade-entries/:userId", async (req, res) => { try { const userId = req.params.userId; - const entries = await Calculator.findAll({userId}); + const entries = await Calculator.findAll({ where: {userId} }); res.status(200).json(entries); } catch (error) { console.error("Error fetching previous grade entries for user", error); @@ -55,8 +55,23 @@ router.get("/grade-entry/:entryId", async (req, res) => { }); +// PUT: Update a specific grade entry +// DELETE: Delete a specific grade entry +router.delete("/grade-entry/:entryId", async (req, res) => { + try { + const entryId = req.params.entryId; + const deleted = await Calculator.destroy({ where: {entryId} }); + if (!deleted) { + return res.status(404).json({ error: "Entry not found"}); + } + res.json({message: "Grade entry deleted"}); + } catch (error) { + console.error("Error deleting grade entry: ", error); + res.status(500).json({error: "Unable to delete grade entry"}); + } +}); module.exports = router; \ No newline at end of file From ee0a7c1911ac5a10753c84fd07a0f6efc731a7db Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 4 Aug 2025 15:46:37 -0400 Subject: [PATCH 12/53] Added get route to retreive task of a specefic user, post route to create a task , and then a put route to update tasks --- api/TaskOrganizer.js | 50 +++++++++++++++++++++++++++++++++++++++----- api/index.js | 2 +- database/Tasks.js | 2 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/api/TaskOrganizer.js b/api/TaskOrganizer.js index 74bd22a..a8858ae 100644 --- a/api/TaskOrganizer.js +++ b/api/TaskOrganizer.js @@ -18,22 +18,62 @@ router.get("/tasks/:userId", async (req, res) => { //The goal of this is to store the data that the user is sending into the Tasks model router.post("/tasks", async (req, res) => { try { - const { title, due_date, priority, user_id } = req.body; // This grabs the data from the body of the request + const { className, assignment, description, status, deadline, priority, user_id } = req.body; // Storing all the info found in the body of the request, so basically all of the things that the user inputted in the table - const newTask = await Tasks.create({ //Cretaing a new row in the tasks model - title, - due_date, + const newTask = await Tasks.create({ // With that data we we create a new row in the tasks model and store all the data + className, + assignment, + description, + status, + deadline, priority, user_id }); - res.status(201).json(newTask);//The data then gets stored in the new row in the tasks model (newTask) + res.status(201).json(newTask); } catch (error) { console.error("āŒ Error creating task:", error); res.status(500).json({ error: "Failed to create task" }); } }); +// UPDATE a task by ID +router.put("/tasks/:taskId", async (req, res) => { + try { + const { taskId } = req.params; + + const { + className, + assignment, + description, + status, + deadline, + priority + } = req.body; + + // Find the task + const task = await Tasks.findByPk(taskId); + + if (!task) { + return res.status(404).json({ error: "Task not found" }); + } + + // Update the task + await task.update({ + className, + assignment, + description, + status, + deadline, + priority + }); + + res.json(task); + } catch (error) { + console.error("āŒ Error updating task:", error); + res.status(500).json({ error: "Failed to update task" }); + } +}); diff --git a/api/index.js b/api/index.js index 65629f3..6de90e8 100644 --- a/api/index.js +++ b/api/index.js @@ -5,6 +5,6 @@ const testDbRouter = require("./test-db"); const taskOrganizerRouter = require("./TaskOrganizer"); router.use("/test-db", testDbRouter); -router.use("/tasks", taskOrganizerRouter); +router.use("/", taskOrganizerRouter); module.exports = router; diff --git a/database/Tasks.js b/database/Tasks.js index 0f07e95..6f68a6b 100644 --- a/database/Tasks.js +++ b/database/Tasks.js @@ -7,7 +7,7 @@ const Tasks = db.define("Tasks", type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, - class: DataTypes.STRING, + className: DataTypes.STRING, assignment: DataTypes.STRING, description: DataTypes.TEXT, status: { From ea86ee07b32e63704cb98f4438990d67667d431e Mon Sep 17 00:00:00 2001 From: KidCharles Date: Mon, 4 Aug 2025 22:41:02 -0400 Subject: [PATCH 13/53] put route and small fixes --- api/Calculator.js | 24 ++++++++++++++++++++---- api/index.js | 4 ++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/api/Calculator.js b/api/Calculator.js index 9b7886d..10cdc7b 100644 --- a/api/Calculator.js +++ b/api/Calculator.js @@ -9,7 +9,7 @@ router.post("/new-grade-entry", async (req, res) => { try { // check that required fields are not omitted - if (!assignment_type || weight == null || score == null) { + if (!assignment_type || assignment_weight == null || assignment_grade == null) { return res.status(400).json({error: "Missing required fields"}); } // Create new grade entry in db @@ -54,15 +54,31 @@ router.get("/grade-entry/:entryId", async (req, res) => { } }); - // PUT: Update a specific grade entry - +router.put("/grade-entry/:entryId", async (req, res) => { + try { + const entryId = req.params.entryId; + const { assignment_grade, assignment_weight } = req.body; + const entry = await Calculator.findByPk(entryId); + if(!entry) { + return res.status(404).json({error: "Entry not found"}); + } + // update the assignment grade and weight if provided; otherwise, keep the existing values + entry.assignment_grade = assignment_grade ?? entry.assignment_grade; + entry.assignment_weight = assignment_weight ?? entry.assignment_weight; + await entry.save(); + res.json(entry); + } catch (error) { + console.error("Error updating grade entry: ", error); + res.status(500).json({error: "Unable to update grade entry. Sorry!"}) + } +}); // DELETE: Delete a specific grade entry router.delete("/grade-entry/:entryId", async (req, res) => { try { const entryId = req.params.entryId; - const deleted = await Calculator.destroy({ where: {entryId} }); + const deleted = await Calculator.destroy({ where: {id: entryId} }); if (!deleted) { return res.status(404).json({ error: "Entry not found"}); } diff --git a/api/index.js b/api/index.js index 69819f6..c2829df 100644 --- a/api/index.js +++ b/api/index.js @@ -3,10 +3,10 @@ const router = express.Router(); const testDbRouter = require("./test-db"); const taskOrganizerRouter = require("./TaskOrganizer"); -const CaclulatorRouter = require("./Calculator"); +const CalculatorRouter = require("./Calculator"); router.use("/test-db", testDbRouter); router.use("/tasks", taskOrganizerRouter); -router.use("/grade-calculator", CaclulatorRouter); +router.use("/grade-calculator", CalculatorRouter); module.exports = router; From 663103b505138d8366076340258f385b2b0b31dd Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 5 Aug 2025 15:29:27 -0400 Subject: [PATCH 14/53] Created multiple routes for the task organizer: Update Task By ID , Delete Task by ID, uodate status of specific task --- api/TaskOrganizer.js | 105 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/api/TaskOrganizer.js b/api/TaskOrganizer.js index a8858ae..3500080 100644 --- a/api/TaskOrganizer.js +++ b/api/TaskOrganizer.js @@ -5,9 +5,9 @@ const { Tasks } = require("../database"); // GET all tasks for a user router.get("/tasks/:userId", async (req, res) => { try { - const userId = req.params.userId;// storing the user ID from the URL - const tasks = await Tasks.findAll({ where: { user_id: userId } });//This is storing all the data that .findall is getting form the model in Tasks in this case it is filtering where the user ID equals the one found in the URL - res.json(tasks);//Outputting it as a json + const userId = req.params.userId; // storing the user ID from the URL + const tasks = await Tasks.findAll({ where: { user_id: userId } }); //This is storing all the data that .findall is getting form the model in Tasks in this case it is filtering where the user ID equals the one found in the URL + res.json(tasks); //Outputting it as a json } catch (error) { console.error("āŒ Error fetching tasks:", error); res.status(500).json({ error: "Failed to fetch tasks" }); @@ -15,19 +15,29 @@ router.get("/tasks/:userId", async (req, res) => { }); //POST a new task -//The goal of this is to store the data that the user is sending into the Tasks model -router.post("/tasks", async (req, res) => { +//The goal of this is to store the data that the user is sending into the Tasks model +router.post("/tasks/:userId", async (req, res) => { try { - const { className, assignment, description, status, deadline, priority, user_id } = req.body; // Storing all the info found in the body of the request, so basically all of the things that the user inputted in the table + const { userId } = req.params; - const newTask = await Tasks.create({ // With that data we we create a new row in the tasks model and store all the data + //Gets data from the body of the request and stores it in the variables + const { className, assignment, description, status, deadline, priority, - user_id + } = req.body; + + const newTask = await Tasks.create({ + className, + assignment, + description, + status, + deadline, + priority, + user_id: userId, // }); res.status(201).json(newTask); @@ -37,22 +47,24 @@ router.post("/tasks", async (req, res) => { } }); + // UPDATE a task by ID -router.put("/tasks/:taskId", async (req, res) => { +router.put("/tasks/:userId/:taskId", async (req, res) => { try { - const { taskId } = req.params; + const { taskId, userId } = req.params; - const { - className, - assignment, - description, - status, - deadline, - priority - } = req.body; + //This is storing all of the updated data from the request body(When the user updates an + // "assignment,description" etc we are stroing that new data in these variables + const { className, assignment, description, status, deadline, priority } = + req.body; - // Find the task - const task = await Tasks.findByPk(taskId); + // Finds the task of a specefic user + const task = await Tasks.findOne({ + where: { + id: taskId, + user_id: userId, + } + }); if (!task) { return res.status(404).json({ error: "Task not found" }); @@ -65,7 +77,7 @@ router.put("/tasks/:taskId", async (req, res) => { description, status, deadline, - priority + priority, }); res.json(task); @@ -75,6 +87,57 @@ router.put("/tasks/:taskId", async (req, res) => { } }); +//DELETE +router.delete("/tasks/:userId/:taskId", async (req, res) => { + try { + const { userId, taskId } = req.params; + + // Searches database and finds the task of a specefic user + const task = await Tasks.findOne({ + where: { + id: taskId, + user_id: userId, + }, + }); + + if (!task) { + return res.status(404).json({ error: "Task not found for this user" }); + } + + await task.destroy(); + + res.json({ message: "Task deleted successfully", deletedTask: task }); + } catch (error) { + console.error("āŒ Error deleting task:", error); + res.status(500).json({ error: "Failed to delete task" }); + } +}); + +//PATCH just update the status + +router.patch("/tasks/:userId/:taskId", async (req, res) => { + try { + const { userId, taskId } = req.params; + const { status } = req.body; + + const task = await Tasks.findOne({ + where: { + id: taskId, + user_id: userId, + }, + }); + + if (!task) { + return res.status(404).json({ error: "Task not found" }); + } + await task.update({ status }); + + res.json(task); + } catch (error) { + console.error("āŒ Error updating task status:", error); + res.status(500).json({ error: "Failed to update task status" }); + } +}); module.exports = router; From 875c22639e8fbfc0cd867cb3107651ddc831e1d0 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 5 Aug 2025 23:11:30 -0400 Subject: [PATCH 15/53] Added route where user can filter tasks by their status --- api/TaskOrganizer.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/api/TaskOrganizer.js b/api/TaskOrganizer.js index 3500080..952cefd 100644 --- a/api/TaskOrganizer.js +++ b/api/TaskOrganizer.js @@ -103,7 +103,7 @@ router.delete("/tasks/:userId/:taskId", async (req, res) => { if (!task) { return res.status(404).json({ error: "Task not found for this user" }); } - + //If task exists it deletes it await task.destroy(); res.json({ message: "Task deleted successfully", deletedTask: task }); @@ -114,7 +114,6 @@ router.delete("/tasks/:userId/:taskId", async (req, res) => { }); //PATCH just update the status - router.patch("/tasks/:userId/:taskId", async (req, res) => { try { const { userId, taskId } = req.params; @@ -140,4 +139,23 @@ router.patch("/tasks/:userId/:taskId", async (req, res) => { } }); +//GET +//Filter by status , 'Pending' -- 'Completed' --or 'In-progress' + +router.get("/Tasks/:userId/status/:statusTask", async (req, res) => { + try { + const {userId,statusTask} = req.params; // storing the user ID from the URL + const filteredTasks = await Tasks.findAll({ where: + { user_id: userId, + status: statusTask,} }); //This is storing all the data that .findall is getting form the model in Tasks in this case it is filtering where status equals the one found in the uRL + res.json(filteredTasks); //Outputting it as a json + } catch (error) { + console.error("āŒ Error fetching tasks:", error); + res.status(500).json({ error: "Failed to fetch tasks" }); + } +}); + + + + module.exports = router; From 961d7d63612a262229674fa380583f91fc174d23 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 5 Aug 2025 23:33:29 -0400 Subject: [PATCH 16/53] Updated tasks model, priority now has ENUM --- api/TaskOrganizer.js | 17 +++++++++++++++++ database/Tasks.js | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/api/TaskOrganizer.js b/api/TaskOrganizer.js index 952cefd..4fe6e85 100644 --- a/api/TaskOrganizer.js +++ b/api/TaskOrganizer.js @@ -155,6 +155,23 @@ router.get("/Tasks/:userId/status/:statusTask", async (req, res) => { } }); +router.get("/tasks/:userId/priority/:priority", async (req, res) => { + try { + const { userId, priority } = req.params; + + const prioritizedTasks = await Tasks.findAll({ + where: { + user_id: userId, + priority: priority, + }, + }); + + res.json(prioritizedTasks); + } catch (error) { + console.error("āŒ Error filtering tasks by priority:", error); + res.status(500).json({ error: "Failed to filter tasks by priority" }); + } +}); diff --git a/database/Tasks.js b/database/Tasks.js index 6f68a6b..d58f6d5 100644 --- a/database/Tasks.js +++ b/database/Tasks.js @@ -15,7 +15,10 @@ const Tasks = db.define("Tasks", allowNull: false }, deadline: DataTypes.DATE, - priority: DataTypes.STRING, + priority: { + type: DataTypes.ENUM('high', 'medium', 'low'), + allowNull: false + }, user_id: { type: DataTypes.INTEGER, allowNull: false }, From bee3ea44e6049eced0d1ae0aa5d96b05ec31eb67 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 5 Aug 2025 23:42:16 -0400 Subject: [PATCH 17/53] Created a filter by className route and filter by priority route --- api/TaskOrganizer.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api/TaskOrganizer.js b/api/TaskOrganizer.js index 4fe6e85..ea07d35 100644 --- a/api/TaskOrganizer.js +++ b/api/TaskOrganizer.js @@ -155,6 +155,8 @@ router.get("/Tasks/:userId/status/:statusTask", async (req, res) => { } }); + +//Filter Tasks by priority router.get("/tasks/:userId/priority/:priority", async (req, res) => { try { const { userId, priority } = req.params; @@ -173,6 +175,24 @@ router.get("/tasks/:userId/priority/:priority", async (req, res) => { } }); +//Filter tasks by className +router.get("/tasks/:userId/class/:className", async (req, res) => { + try { + const { userId, className } = req.params; + + const classTasks = await Tasks.findAll({ + where: { + user_id: userId, + className: className, + }, + }); + + res.json(classTasks); + } catch (error) { + console.error("āŒ Error filtering tasks by class name:", error); + res.status(500).json({ error: "Failed to filter tasks by class name" }); + } +}); module.exports = router; From f42e8d1a6aae68d894b65bf828933f645561aed8 Mon Sep 17 00:00:00 2001 From: KidCharles Date: Wed, 6 Aug 2025 10:48:44 -0400 Subject: [PATCH 18/53] fixes failed route tests --- api/Calculator.js | 134 ++++++++++++++++++++++------------------- database/Calculator.js | 1 - database/seed.js | 9 +-- 3 files changed, 77 insertions(+), 67 deletions(-) diff --git a/api/Calculator.js b/api/Calculator.js index 10cdc7b..34b2f9f 100644 --- a/api/Calculator.js +++ b/api/Calculator.js @@ -2,92 +2,102 @@ const express = require("express"); const router = express.Router(); const { Calculator } = require("../database"); - // POST: Calculate a final grade. Creates a new grade entry in db router.post("/new-grade-entry", async (req, res) => { - const {user_id, assignment_type, assignment_grade, assignment_weight} = req.body; + const { user_id, assignment_type, assignment_grade, assignment_weight } = + req.body; - try { - // check that required fields are not omitted - if (!assignment_type || assignment_weight == null || assignment_grade == null) { - return res.status(400).json({error: "Missing required fields"}); - } - // Create new grade entry in db - const newGradeEntry = await Calculator.create({ - user_id, - assignment_type, - assignment_grade, - assignment_weight - }); - // Return new created grade entry - res.status(201).json(newGradeEntry); - } catch(error) { - console.error("Error creating new grade entry: ", error); - res.status(500).json({error: "Unable to calculate final grade. Sorry!"}) + try { + // check that required fields are not omitted + if ( + !assignment_type || + assignment_weight == null || + assignment_grade == null + ) { + return res.status(400).json({ error: "Missing required fields" }); } + console.log(user_id, assignment_type, assignment_grade, assignment_weight); + // Create new grade entry in db + const newGradeEntry = await Calculator.create({ + user_id, + assignment_type, + assignment_grade, + assignment_weight, + }); + newGradeEntry.save() + + // Return new created grade entry + res.status(201).json(newGradeEntry); + } catch (error) { + console.error("Error creating new grade entry: ", error); + res.status(500).json({ error: "Unable to calculate final grade. Sorry!" }); + } }); // GET: Fetch all of user's past grade-calculator entries. router.get("/grade-entries/:userId", async (req, res) => { - try { - const userId = req.params.userId; - const entries = await Calculator.findAll({ where: {userId} }); - res.status(200).json(entries); - } catch (error) { - console.error("Error fetching previous grade entries for user", error); - res.status(500).json({error: "Unable to return your previous grade entries. Sorry!"}); - } + try { + const userId = req.params.userId; + const entries = await Calculator.findAll({ where: { id: userId } }); + res.status(200).json(entries); + } catch (error) { + console.error("Error fetching previous grade entries for user", error); + res + .status(500) + .json({ error: "Unable to return your previous grade entries. Sorry!" }); + } }); // GET: Fetch a user's specific grade-calculator entry router.get("/grade-entry/:entryId", async (req, res) => { - try { - const entryId = req.params.entryId; - const entry = await Calculator.findByPk(entryId); - if(!entry) { - return res.status(404).json({error:"Entry not found"}); - } - res.json(entry); - } catch (error) { - console.error("Error fetching previous grade entry"); - res.status(500).json({error: "Unable to return previous grade entry. Sorry!"}) + try { + const entryId = req.params.entryId; + const entry = await Calculator.findByPk(entryId); + if (!entry) { + return res.status(404).json({ error: "Entry not found" }); } + res.json(entry); + } catch (error) { + console.error("Error fetching previous grade entry"); + res + .status(500) + .json({ error: "Unable to return previous grade entry. Sorry!" }); + } }); // PUT: Update a specific grade entry router.put("/grade-entry/:entryId", async (req, res) => { - try { - const entryId = req.params.entryId; - const { assignment_grade, assignment_weight } = req.body; - const entry = await Calculator.findByPk(entryId); - if(!entry) { - return res.status(404).json({error: "Entry not found"}); - } - // update the assignment grade and weight if provided; otherwise, keep the existing values - entry.assignment_grade = assignment_grade ?? entry.assignment_grade; - entry.assignment_weight = assignment_weight ?? entry.assignment_weight; - await entry.save(); - res.json(entry); - } catch (error) { - console.error("Error updating grade entry: ", error); - res.status(500).json({error: "Unable to update grade entry. Sorry!"}) + try { + const entryId = req.params.entryId; + const { assignment_grade, assignment_weight } = req.body; + const entry = await Calculator.findByPk(entryId); + if (!entry) { + return res.status(404).json({ error: "Entry not found" }); } + // update the assignment grade and weight if provided; otherwise, keep the existing values + entry.assignment_grade = assignment_grade ?? entry.assignment_grade; + entry.assignment_weight = assignment_weight ?? entry.assignment_weight; + await entry.save(); + res.json(entry); + } catch (error) { + console.error("Error updating grade entry: ", error); + res.status(500).json({ error: "Unable to update grade entry. Sorry!" }); + } }); // DELETE: Delete a specific grade entry router.delete("/grade-entry/:entryId", async (req, res) => { - try { - const entryId = req.params.entryId; - const deleted = await Calculator.destroy({ where: {id: entryId} }); + try { + const entryId = req.params.entryId; + const deleted = await Calculator.destroy({ where: { id: entryId } }); if (!deleted) { - return res.status(404).json({ error: "Entry not found"}); - } - res.json({message: "Grade entry deleted"}); - } catch (error) { - console.error("Error deleting grade entry: ", error); - res.status(500).json({error: "Unable to delete grade entry"}); + return res.status(404).json({ error: "Entry not found" }); } + res.json({ message: "Grade entry deleted" }); + } catch (error) { + console.error("Error deleting grade entry: ", error); + res.status(500).json({ error: "Unable to delete grade entry" }); + } }); - module.exports = router; \ No newline at end of file diff --git a/database/Calculator.js b/database/Calculator.js index 3e8aae0..6b7d67e 100644 --- a/database/Calculator.js +++ b/database/Calculator.js @@ -11,7 +11,6 @@ const Calculator = db.define('Calculator', { type: DataTypes.INTEGER, allowNull: false, }, - assessment: DataTypes.TEXT, assignment_type: { type: DataTypes.ENUM('Homework', 'Quiz', 'Midterm', 'Final'), allowNull: false, diff --git a/database/seed.js b/database/seed.js index a3f74b9..1aeeeca 100644 --- a/database/seed.js +++ b/database/seed.js @@ -25,12 +25,13 @@ const seed = async () => { priority: 'high', user_id: user.id }); + // Add calculator entry const calculator = await Calculator.create({ - assessment: 'Midterm Exam', - grade: 85, - weight: 25, - user_id: user.id + user_id: user.id, + assignment_type: "Homework", + assignment_grade: 90, + assignment_weight: 20 }); // Add study session From 3202cdfc2405fefa5e57f2cb10d9a435c682d40e Mon Sep 17 00:00:00 2001 From: KidCharles Date: Wed, 6 Aug 2025 11:03:49 -0400 Subject: [PATCH 19/53] changing db back to it's initial state so it doesn't get merged in my PR and affect team members --- database/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/db.js b/database/db.js index fbee587..5d62e89 100644 --- a/database/db.js +++ b/database/db.js @@ -6,7 +6,7 @@ const pg = require("pg"); const dbName = "capstone-1"; const db = new Sequelize( - process.env.DATABASE_URL || `postgres://localhost:5432/capstone2`, + process.env.DATABASE_URL || `postgres://localhost:5432/${capstone-2}`, { logging: false, // comment this line to enable SQL logging } From bebe1c5cdd356f345d0e97457b526d1b17afe03f Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Wed, 6 Aug 2025 11:19:01 -0400 Subject: [PATCH 20/53] Made the filter by className route not case sensitive --- api/TaskOrganizer.js | 10 ++++++++-- database/seed.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/api/TaskOrganizer.js b/api/TaskOrganizer.js index ea07d35..64d55b9 100644 --- a/api/TaskOrganizer.js +++ b/api/TaskOrganizer.js @@ -1,7 +1,9 @@ const express = require("express"); const router = express.Router(); +const { Op } = require("sequelize"); const { Tasks } = require("../database"); + // GET all tasks for a user router.get("/tasks/:userId", async (req, res) => { try { @@ -142,7 +144,7 @@ router.patch("/tasks/:userId/:taskId", async (req, res) => { //GET //Filter by status , 'Pending' -- 'Completed' --or 'In-progress' -router.get("/Tasks/:userId/status/:statusTask", async (req, res) => { +router.get("/tasks/:userId/status/:statusTask", async (req, res) => { try { const {userId,statusTask} = req.params; // storing the user ID from the URL const filteredTasks = await Tasks.findAll({ where: @@ -176,6 +178,7 @@ router.get("/tasks/:userId/priority/:priority", async (req, res) => { }); //Filter tasks by className + router.get("/tasks/:userId/class/:className", async (req, res) => { try { const { userId, className } = req.params; @@ -183,7 +186,9 @@ router.get("/tasks/:userId/class/:className", async (req, res) => { const classTasks = await Tasks.findAll({ where: { user_id: userId, - className: className, + className: { + [Op.iLike]: `%${className}%`, // makes it so that the user can type the name without case sensetive restrictions + }, }, }); @@ -195,4 +200,5 @@ router.get("/tasks/:userId/class/:className", async (req, res) => { }); + module.exports = router; diff --git a/database/seed.js b/database/seed.js index a3f74b9..dd1eeeb 100644 --- a/database/seed.js +++ b/database/seed.js @@ -17,7 +17,7 @@ const seed = async () => { // Create a Task const task = await Tasks.create({ - class: 'Math 101', + className: 'Math 101', assignment: 'Homework 1', description: 'Complete exercises 1–10 on page 52', status: 'in-progress', From 11f118df432dfddc5ea0c20b404697723a3df08e Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Thu, 7 Aug 2025 11:28:35 -0400 Subject: [PATCH 21/53] tested route works for getting duration --- api/index.js | 4 +++- api/timerData.js | 20 ++++++++++++++++++++ database/Session.js | 32 ++++++++++++++++++++------------ 3 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 api/timerData.js diff --git a/api/index.js b/api/index.js index 6de90e8..c1ea18c 100644 --- a/api/index.js +++ b/api/index.js @@ -3,8 +3,10 @@ const router = express.Router(); const testDbRouter = require("./test-db"); const taskOrganizerRouter = require("./TaskOrganizer"); +const timerData = require("./timerData"); router.use("/test-db", testDbRouter); -router.use("/", taskOrganizerRouter); +router.use("/", taskOrganizerRouter); +router.use("/", timerData); module.exports = router; diff --git a/api/timerData.js b/api/timerData.js new file mode 100644 index 0000000..375b5d0 --- /dev/null +++ b/api/timerData.js @@ -0,0 +1,20 @@ +const express = require("express"); +const router = express.Router(); +const { Session } = require("../database"); + +//getting a study duration by userId(foregin key that references the id in users table) +router.get("/data/:userId", async (req, res) => { + const { userId } = req.params; + try { + const sessions = await Session.findAll({ + where: { + user_id: userId, + }, + }); + res.json(sessions); + } catch (error) { + res.sendStatus(501); + } +}); + +module.exports = router; diff --git a/database/Session.js b/database/Session.js index f1ec7d6..4e13803 100644 --- a/database/Session.js +++ b/database/Session.js @@ -1,15 +1,23 @@ -const { DataTypes } = require('sequelize'); -const db = require('./db'); +const { DataTypes } = require("sequelize"); +const db = require("./db"); -const Session = db.define('Session', { - id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, - duration: DataTypes.INTEGER, - user_id: { type: DataTypes.INTEGER, allowNull: false }, - started_at: DataTypes.DATE, - created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW } -}, { - tableName: 'session', - timestamps: false -}); +const Session = db.define( + "Session", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + duration: DataTypes.TIME, + user_id: { type: DataTypes.INTEGER, allowNull: false }, + started_at: DataTypes.DATE, + created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + }, + { + tableName: "session", + timestamps: false, + } +); module.exports = Session; From cb96d39ad14fc2e5198f44970728dcb7a5380169 Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Thu, 7 Aug 2025 11:36:41 -0400 Subject: [PATCH 22/53] reworked route to only get duration rows --- api/timerData.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/timerData.js b/api/timerData.js index 375b5d0..e511428 100644 --- a/api/timerData.js +++ b/api/timerData.js @@ -10,7 +10,9 @@ router.get("/data/:userId", async (req, res) => { where: { user_id: userId, }, + attributes: ["duration"], }); + res.json(sessions); } catch (error) { res.sendStatus(501); From 974e3ba2d3152463568a46da9a98b5cfd410009c Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Fri, 8 Aug 2025 10:28:56 -0400 Subject: [PATCH 23/53] =?UTF-8?q?set=20API=20for=20Claude=20Ai=20=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 98 +++++++++++++++++++++++++++++++++++++++++++++-- database/db.js | 2 +- package-lock.json | 56 +++++++++++++++++++++------ package.json | 2 + 4 files changed, 142 insertions(+), 16 deletions(-) diff --git a/app.js b/app.js index 5857036..ba4c629 100644 --- a/app.js +++ b/app.js @@ -9,20 +9,112 @@ const apiRouter = require("./api"); const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); - +const { OpenAI } = require("openai"); +const { Model } = require("sequelize"); +const Anthropic = require("@anthropic-ai/sdk"); const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; // body parser middleware app.use(express.json()); - app.use( cors({ - origin: FRONTEND_URL, + origin: "http://localhost:3000" || FRONTEND_URL, credentials: true, }) ); +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +app.post("/chat", async (req, res) => { + const { user_request } = req.body; + try { + const response = await anthropic.messages.create({ + model: "claude-3-5-haiku-20241022", + max_tokens: 1000, + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: `${user_request} +You are an AI study assistant designed to help students review topics and prepare for exams. Your task is to create either flashcards or a quiz based on the user's request. The user's request will be provided within tags. + +Here is the user's request: + +${user_request} + + +Instructions: + +1. Analyze the user's request to determine whether they want flashcards or a quiz. + +2. If the user requests flashcards: + a. Create a set of at least 5 flashcards related to the topic. + b. Each flashcard should have a question on one side and the answer on the other. + c. Format the flashcards in markdown using the following structure: + \`\`\`markdown + ## Flashcard 1 + **Q:** [Question] + **A:** [Answer] + + ## Flashcard 2 + **Q:** [Question] + **A:** [Answer] + \`\`\` + +3. If the user requests a quiz: + a. Generate a multiple-choice quiz with at least 5 questions related to the topic. + b. Each question should have 4 options, with only one correct answer. + c. Format the quiz in markdown using the following structure: + \`\`\`markdown + ## Question 1 + [Question text] + A) [Option A] + B) [Option B] + C) [Option C] + D) [Option D] + + Correct Answer: [Letter of correct option] + + ## Question 2 + [Question text] + A) [Option A] + B) [Option B] + C) [Option C] + D) [Option D] + + Correct Answer: [Letter of correct option] + \`\`\` + +4. Before providing your final output, plan and structure your response within tags in your thinking block. Consider the following: + - Clearly state whether the user wants flashcards or a quiz. + - Identify the main concepts or subtopics within the user's request. + - For flashcards: List potential question-answer pairs. + - For quiz: List potential multiple-choice questions with options. + - Review and refine your initial ideas to ensure diversity and challenge. + - Ensure the content is accurate and relevant to the topic. + +5. After your planning process, provide the flashcards or quiz in markdown format as specified above. + +Remember to always format your output in markdown, and ensure that quizzes are multiple-choice with four options per question. Your final output should consist only of the formatted flashcards or quiz and should not duplicate or rehash any of the work you did in the study material planning section.`, + }, + ], + }, + ], + }); + const replyContent = response?.content?.find((c) => c.type === "text"); + const replyText = replyContent?.text || "Sorry, no response."; + res.status(200).send({ reply: replyText }); + } catch (error) { + console.error(error); + res.status(404).send("Bad Request āŒ"); + } +}); + // cookie parser middleware app.use(cookieParser()); diff --git a/database/db.js b/database/db.js index b251a9d..3b580f1 100644 --- a/database/db.js +++ b/database/db.js @@ -3,7 +3,7 @@ const { Sequelize } = require("sequelize"); const pg = require("pg"); // Feel free to rename the database to whatever you want! -const dbName = "capstone-1"; +const dbName = "capstone-2"; const db = new Sequelize( process.env.DATABASE_URL || `postgres://localhost:5432/${dbName}`, diff --git a/package-lock.json b/package-lock.json index af0cf82..525e90b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "license": "ISC", "dependencies": { + "@anthropic-ai/sdk": "^0.57.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -16,6 +17,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7" }, @@ -26,6 +28,15 @@ "win-node-env": "^0.6.1" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.57.0.tgz", + "integrity": "sha512-z5LMy0MWu0+w2hflUgj4RlJr1R+0BxKXL7ldXTO8FasU8fu599STghO+QKwId2dAD0d464aHtU+ChWuRHw4FNw==", + "license": "MIT", + "bin": { + "anthropic-ai-sdk": "bin/cli" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -156,9 +167,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -951,16 +962,16 @@ } }, "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", - "on-headers": "~1.0.2" + "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" @@ -1101,9 +1112,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -1118,6 +1129,27 @@ "wrappy": "1" } }, + "node_modules/openai": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.11.0.tgz", + "integrity": "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", diff --git a/package.json b/package.json index 7e0a0af..a6c613c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "license": "ISC", "description": "", "dependencies": { + "@anthropic-ai/sdk": "^0.57.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -20,6 +21,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7" }, From cf28258f96d4122015c86b0d35af83f37b6dac12 Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Fri, 8 Aug 2025 14:35:54 -0400 Subject: [PATCH 24/53] rout now returns created_at as well for plotting purposes in the front end --- api/timerData.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/timerData.js b/api/timerData.js index e511428..f9cea9a 100644 --- a/api/timerData.js +++ b/api/timerData.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const { Session } = require("../database"); -//getting a study duration by userId(foregin key that references the id in users table) +//getting a study durations by userId(foregin key that references the id in users table) router.get("/data/:userId", async (req, res) => { const { userId } = req.params; try { @@ -10,7 +10,7 @@ router.get("/data/:userId", async (req, res) => { where: { user_id: userId, }, - attributes: ["duration"], + attributes: ["duration", "created_at"], }); res.json(sessions); From 6a34fc8af1b914b6db2c7705649b5f013077dd96 Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Sun, 10 Aug 2025 21:58:42 -0400 Subject: [PATCH 25/53] Added users to the seed --- database/seed.js | 68 ++++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/database/seed.js b/database/seed.js index dd1eeeb..b7ae88b 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,50 +1,68 @@ const db = require("./db"); -const { User, Tasks, Calculator, Reminder, Session } = require('./index'); - - +const { User, Tasks, Calculator, Reminder, Session } = require("./index"); const seed = async () => { try { db.logging = false; await db.sync({ force: true }); // Drop and recreate tables - const user = await User.create({ - username: 'benjamin', - email: 'benjamin@example.com', - password: 'supersecurepassword', - role: 'student' - }); + const user = await User.bulkCreate([{ + username: "benjamin", + email: "benjamin@example.com", + password: "supersecurepassword", + role: "student", + }, + { + username: "David", + email: "David@example.com", + password: "supersecurepassword2", + role: "student", + + }, + + ]); // Create a Task - const task = await Tasks.create({ - className: 'Math 101', - assignment: 'Homework 1', - description: 'Complete exercises 1–10 on page 52', - status: 'in-progress', - deadline: new Date('2025-08-05'), - priority: 'high', - user_id: user.id - }); + const tasks = await Tasks.bulkCreate([ + { + className: "Math 101", + assignment: "Homework 1", + description: "Complete exercises 1–10 on page 52", + status: "in-progress", + deadline: new Date("2025-08-05"), + priority: "high", + user_id: user[0].id, + }, + { + className: "Math 201", + assignment: "Homework 2", + description: "Complete exercises 1–10 on page 52", + status: "in-progress", + deadline: new Date("2025-08-05"), + priority: "high", + user_id: user[1].id, + }, + ]); // Add calculator entry const calculator = await Calculator.create({ - assessment: 'Midterm Exam', + assessment: "Midterm Exam", grade: 85, weight: 25, - user_id: user.id + user_id: user[0].id, }); // Add study session await Session.create({ duration: 45, - user_id: user.id, + user_id: user[0].id, started_at: new Date(), - created_at: new Date() + created_at: new Date(), }); - // Add reminder for task + // Add reminder for the first task await Reminder.create({ - task_id: task.id, - remind: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000) + task_id: tasks[0].id, // use the first task's id + remind: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }); console.log("🌱 Seeded the database!"); From 34b80734d9e147bf119d31777c861ec99a3b8054 Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Mon, 11 Aug 2025 12:01:57 -0400 Subject: [PATCH 26/53] updated seed --- database/Tasks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/Tasks.js b/database/Tasks.js index d58f6d5..c73518f 100644 --- a/database/Tasks.js +++ b/database/Tasks.js @@ -11,7 +11,7 @@ const Tasks = db.define("Tasks", assignment: DataTypes.STRING, description: DataTypes.TEXT, status: { - type: DataTypes.ENUM('pending', 'completed', 'in-progress'), + type: DataTypes.ENUM('Not started', 'Submitted', 'In-progress'), allowNull: false }, deadline: DataTypes.DATE, From 6fe57807b23792905f3950f9e91c5ba608c27b13 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Mon, 11 Aug 2025 14:30:12 -0400 Subject: [PATCH 27/53] =?UTF-8?q?Fix=20Ai=20Route=20to=20parse=20the=20Ai?= =?UTF-8?q?=20quiz=20json=20=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/aichathistory.js | 464 ++++++++++++++++++++++++++++++++++++++ api/index.js | 2 + app.js | 95 +------- database/aichathistory.js | 39 ++++ database/index.js | 5 + package-lock.json | 19 ++ package.json | 1 + 7 files changed, 531 insertions(+), 94 deletions(-) create mode 100644 api/aichathistory.js create mode 100644 database/aichathistory.js diff --git a/api/aichathistory.js b/api/aichathistory.js new file mode 100644 index 0000000..bbed38a --- /dev/null +++ b/api/aichathistory.js @@ -0,0 +1,464 @@ +const express = require("express"); +const router = express.Router(); +const { AiChatHistory } = require("../database"); +const Anthropic = require("@anthropic-ai/sdk"); +const { nanoid } = require("nanoid"); + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +// Safely sanitize JSON string for database storage +function sanitizeJsonString(jsonString) { + try { + // First try to parse and re-stringify to ensure valid JSON + const parsed = JSON.parse(jsonString); + return JSON.stringify(parsed); + } catch (error) { + // If parsing fails, try to clean it up + let cleaned = jsonString + .replace(/\n/g, "\\n") // Escape newlines + .replace(/\r/g, "\\r") // Escape carriage returns + .replace(/\t/g, "\\t") // Escape tabs + .replace(/\\/g, "\\\\") // Escape backslashes + .replace(/"/g, '\\"') // Escape quotes + .replace(/\f/g, "\\f") // Escape form feeds + .replace(/\b/g, "\\b"); // Escape backspaces + + try { + // Try to parse the cleaned string + JSON.parse(cleaned); + return cleaned; + } catch (secondError) { + // If still fails, return a safe fallback + console.error("Failed to sanitize JSON:", secondError); + return JSON.stringify({ + error: "Malformed response", + original: jsonString.substring(0, 100) + "...", + }); + } + } +} + +// More robust JSON extraction that can handle malformed JSON +function extractQuizDataRobust(text) { + if (!text || typeof text !== "string") return null; + + console.log( + "Attempting to extract JSON from:", + text.substring(0, 200) + "..." + ); + + // First try: direct JSON parse + try { + const parsed = JSON.parse(text); + if (Array.isArray(parsed)) { + console.log( + "Direct JSON parse successful, found array with", + parsed.length, + "items" + ); + return parsed; + } + + // Look for array fields in object + if (parsed && typeof parsed === "object") { + const possibleArrays = [ + "items", + "quiz", + "flashcards", + "data", + "questions", + ]; + for (const key of possibleArrays) { + if (Array.isArray(parsed[key])) { + console.log( + "Found array in field:", + key, + "with", + parsed[key].length, + "items" + ); + return parsed[key]; + } + } + } + } catch (error) { + console.log("Direct JSON parse failed:", error.message); + } + + // Second try: extract array with bracket counting + try { + const start = text.indexOf("["); + if (start === -1) { + console.log("No opening bracket found"); + return null; + } + + let bracketCount = 0; + let end = -1; + let inString = false; + let escapeNext = false; + + for (let i = start; i < text.length; i++) { + const char = text[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === "\\") { + escapeNext = true; + continue; + } + + if (char === '"' && !escapeNext) { + inString = !inString; + continue; + } + + if (!inString) { + if (char === "[") { + bracketCount++; + } else if (char === "]") { + bracketCount--; + if (bracketCount === 0) { + end = i; + break; + } + } + } + } + + if (end === -1) { + console.log("Bracket counting failed - unbalanced brackets"); + return null; + } + + const jsonString = text.substring(start, end + 1); + console.log("Extracted JSON string:", jsonString.substring(0, 100) + "..."); + + const parsed = JSON.parse(jsonString); + if (Array.isArray(parsed)) { + console.log( + "Bracket extraction successful, found array with", + parsed.length, + "items" + ); + return parsed; + } + } catch (error) { + console.log("Bracket extraction failed:", error.message); + } + + // Third try: regex-based extraction + try { + const arrayRegex = /\[[\s\S]*?\]/g; + const matches = text.match(arrayRegex); + + if (matches) { + for (const match of matches) { + try { + const parsed = JSON.parse(match); + if (Array.isArray(parsed) && parsed.length > 0) { + console.log( + "Regex extraction successful, found array with", + parsed.length, + "items" + ); + return parsed; + } + } catch (error) { + console.log("Regex match parse failed:", error.message); + } + } + } + } catch (error) { + console.log("Regex extraction failed:", error.message); + } + + console.log("All extraction methods failed"); + return null; +} + +router.post("/", async (req, res) => { + const { user_request } = req.body; + try { + const response = await anthropic.messages.create({ + model: "claude-3-5-haiku-20241022", + max_tokens: 1000, + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: `${user_request} +You are an advanced AI study assistant that creates either flashcards or a multiple-choice quiz based on the user's request to help with exam preparation. + +Instructions: +- Decide whether to create flashcards or a quiz based on the user's request context. +- Determine the appropriate number of items based on the user's request (aim for 5-15 items unless specifically requested otherwise). +- Respond with ONLY a valid JSON array (no prose, no code fences, no surrounding text). The array must contain at least 10 items. +- Use one of the following object shapes for each item: + +Flashcard item example (as plain JSON object, not code-fenced): +{ + "front": "Question or prompt goes here", + "back": "Answer or explanation goes here", + "difficulty": "easy|medium|hard", + "cognitive_skill": "recall|comprehension|application|analysis" +} + +Quiz question item example (as plain JSON object, not code-fenced): +{ + "question": "The question text goes here", + "options": [ + "A) First option", + "B) Second option", + "C) Third option", + "D) Fourth option" + ], + "correct": "A|B|C|D", + "difficulty": "easy|medium|hard", + "cognitive_skill": "recall|comprehension|application|analysis" +} + +Requirements: +- Ensure valid JSON syntax that can be parsed directly. +- Vary difficulty levels and cognitive skills across items. +- Ensure content is accurate and aligned with typical exam expectations. +- Do not include any commentary, explanations, tags, or code fences. +`, + }, + ], + }, + ], + }); + const replyContent = response?.content?.find((c) => c.type === "text"); + const replyText = replyContent?.text || "Sorry, no response."; + + // const isQuiz = replyText.includes("## Question 1"); + // const quizId = isQuiz ? nanoid(8) : null; + + // const responseType = isQuiz ? "quiz" : "flashcard"; + + // await AiChatHistory.create({ + // user_id: req.user?.id || null, + // user_request, + // ai_response: replyText, + // quiz_id: quizId, + // response_type: responseType, + // status: "success", + // }); + + // if (isQuiz && quizId) { + // const link = `http://localhost:3000/quiz/${quizId}`; + // return res.status(200).send({ + // reply: `Your quiz is ready! [Click here to take it](${link})`, + // }); + // } + + let parsed = extractQuizDataRobust(replyText); + + // Determine response type by inspecting the parsed JSON + let responseType = "flashcard"; + if (Array.isArray(parsed) && parsed.length > 0) { + const firstItem = parsed[0]; + const looksLikeQuiz = + firstItem && + typeof firstItem === "object" && + "question" in firstItem && + Array.isArray(firstItem.options) && + "correct" in firstItem; + const looksLikeFlashcard = + firstItem && + typeof firstItem === "object" && + "front" in firstItem && + "back" in firstItem; + if (looksLikeQuiz) responseType = "quiz"; + else if (looksLikeFlashcard) responseType = "flashcard"; + } + // Fallback detection if parsing failed + if (!Array.isArray(parsed)) { + const lower = (replyText || "").toLowerCase(); + const hasQuestion = /"question"\s*:/.test(lower); + const hasOptions = /"options"\s*:\s*\[/.test(lower); + const hasCorrect = /"correct"\s*:/.test(lower); + if (hasQuestion && hasOptions && hasCorrect) { + responseType = "quiz"; + } + } + + const quizId = responseType === "quiz" ? nanoid(8) : null; + + try { + await AiChatHistory.create({ + user_id: req.user?.id || null, + user_request, + ai_response: Array.isArray(parsed) + ? sanitizeJsonString(JSON.stringify(parsed)) + : sanitizeJsonString(replyText), + quiz_id: quizId, + response_type: responseType, + status: "success", + }); + } catch (dbErr) { + console.error("Error saving AI chat history āŒ", dbErr.message || dbErr); + } + + // Generate quiz link if it's a quiz + let quizLink = null; + let userMessage = replyText; + + if (responseType === "quiz" && quizId) { + const baseUrl = process.env.FRONTEND_URL || "http://localhost:3000"; + quizLink = `${baseUrl}/quiz/${quizId}`; + userMessage = `Your quiz is ready! Click here to take it: ${quizLink}`; + } + + res.status(200).send({ + reply: userMessage, + data: parsed, + quiz_id: quizId, + response_type: responseType, + quiz_link: quizLink, + }); + } catch (error) { + console.error( + "Error getting a response āŒ", + error.response?.data || error.message || error + ); + res.status(500).send("Error getting a response."); + } +}); + +// Utility route to clean up malformed JSON data (run once if needed) +router.post("/cleanup-json", async (req, res) => { + try { + const allRecords = await AiChatHistory.findAll(); + let cleanedCount = 0; + + for (const record of allRecords) { + try { + // Try to parse the stored JSON + JSON.parse(record.ai_response); + } catch (parseError) { + // If parsing fails, try to clean it up + try { + const cleaned = sanitizeJsonString(record.ai_response); + await record.update({ ai_response: cleaned }); + cleanedCount++; + } catch (cleanupError) { + console.error(`Failed to clean record ${record.id}:`, cleanupError); + } + } + } + + res.json({ + message: `Cleaned up ${cleanedCount} malformed JSON records`, + total_records: allRecords.length, + }); + } catch (error) { + console.error("Cleanup error:", error); + res.status(500).json({ error: "Cleanup failed" }); + } +}); + +router.get("/quiz/:quizId", async (req, res) => { + const { quizId } = req.params; + + try { + const history = await AiChatHistory.findOne({ where: { quiz_id: quizId } }); + console.log("Fetched AI Response:", history?.ai_response); + + if (!history) { + return res.status(404).json({ error: "Quiz not found" }); + } + + let parsed = null; + try { + parsed = JSON.parse(history.ai_response); + } catch (_) { + // try to extract array if the stored string isn't pure JSON + try { + parsed = extractQuizDataRobust(history.ai_response); + } catch (extractError) { + console.error("Failed to extract JSON from ai_response:", extractError); + // Return error response instead of crashing + return res.status(500).json({ + error: "Failed to parse quiz data", + details: "The stored quiz data is malformed", + ai_response: history.ai_response.substring(0, 200) + "...", + debug_info: { + response_type: history.response_type, + quiz_id: history.quiz_id, + ai_response_length: history.ai_response.length, + }, + }); + } + } + + if (!parsed) { + return res.status(500).json({ + error: "No quiz data could be extracted", + details: "The response format is not supported", + ai_response: history.ai_response.substring(0, 200) + "...", + debug_info: { + response_type: history.response_type, + quiz_id: history.quiz_id, + }, + }); + } + + return res.status(200).json({ + ai_response: history.ai_response, + data: parsed, + response_type: history.response_type, + quiz_id: history.quiz_id, + }); + } catch (err) { + console.error("Error fetching quiz by ID āŒ", err.message || err); + res.status(500).json({ error: "Server error" }); + } +}); + +// Debug route to inspect stored data +router.get("/debug/:quizId", async (req, res) => { + const { quizId } = req.params; + + try { + const history = await AiChatHistory.findOne({ where: { quiz_id: quizId } }); + + if (!history) { + return res.status(404).json({ error: "Quiz not found" }); + } + + // Try to parse the stored response + let parsed = null; + let parseError = null; + + try { + parsed = JSON.parse(history.ai_response); + } catch (error) { + parseError = error.message; + } + + res.json({ + quiz_id: history.quiz_id, + response_type: history.response_type, + user_request: history.user_request, + ai_response_length: history.ai_response.length, + ai_response_preview: history.ai_response.substring(0, 500) + "...", + parse_success: parsed !== null, + parse_error: parseError, + parsed_data: parsed, + raw_ai_response: history.ai_response, + }); + } catch (error) { + res.status(500).json({ error: "Debug failed", message: error.message }); + } +}); + +module.exports = router; diff --git a/api/index.js b/api/index.js index f08162e..fd5fade 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,9 @@ const express = require("express"); const router = express.Router(); const testDbRouter = require("./test-db"); +const chatRouter = require("./aichathistory"); router.use("/test-db", testDbRouter); +router.use("/chat", chatRouter); module.exports = router; diff --git a/app.js b/app.js index ba4c629..8da27e1 100644 --- a/app.js +++ b/app.js @@ -9,9 +9,7 @@ const apiRouter = require("./api"); const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); -const { OpenAI } = require("openai"); const { Model } = require("sequelize"); -const Anthropic = require("@anthropic-ai/sdk"); const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; @@ -19,102 +17,11 @@ const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; app.use(express.json()); app.use( cors({ - origin: "http://localhost:3000" || FRONTEND_URL, + origin: [FRONTEND_URL, "http://localhost:3000"], credentials: true, }) ); -const anthropic = new Anthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, -}); - -app.post("/chat", async (req, res) => { - const { user_request } = req.body; - try { - const response = await anthropic.messages.create({ - model: "claude-3-5-haiku-20241022", - max_tokens: 1000, - messages: [ - { - role: "user", - content: [ - { - type: "text", - text: `${user_request} -You are an AI study assistant designed to help students review topics and prepare for exams. Your task is to create either flashcards or a quiz based on the user's request. The user's request will be provided within tags. - -Here is the user's request: - -${user_request} - - -Instructions: - -1. Analyze the user's request to determine whether they want flashcards or a quiz. - -2. If the user requests flashcards: - a. Create a set of at least 5 flashcards related to the topic. - b. Each flashcard should have a question on one side and the answer on the other. - c. Format the flashcards in markdown using the following structure: - \`\`\`markdown - ## Flashcard 1 - **Q:** [Question] - **A:** [Answer] - - ## Flashcard 2 - **Q:** [Question] - **A:** [Answer] - \`\`\` - -3. If the user requests a quiz: - a. Generate a multiple-choice quiz with at least 5 questions related to the topic. - b. Each question should have 4 options, with only one correct answer. - c. Format the quiz in markdown using the following structure: - \`\`\`markdown - ## Question 1 - [Question text] - A) [Option A] - B) [Option B] - C) [Option C] - D) [Option D] - - Correct Answer: [Letter of correct option] - - ## Question 2 - [Question text] - A) [Option A] - B) [Option B] - C) [Option C] - D) [Option D] - - Correct Answer: [Letter of correct option] - \`\`\` - -4. Before providing your final output, plan and structure your response within tags in your thinking block. Consider the following: - - Clearly state whether the user wants flashcards or a quiz. - - Identify the main concepts or subtopics within the user's request. - - For flashcards: List potential question-answer pairs. - - For quiz: List potential multiple-choice questions with options. - - Review and refine your initial ideas to ensure diversity and challenge. - - Ensure the content is accurate and relevant to the topic. - -5. After your planning process, provide the flashcards or quiz in markdown format as specified above. - -Remember to always format your output in markdown, and ensure that quizzes are multiple-choice with four options per question. Your final output should consist only of the formatted flashcards or quiz and should not duplicate or rehash any of the work you did in the study material planning section.`, - }, - ], - }, - ], - }); - const replyContent = response?.content?.find((c) => c.type === "text"); - const replyText = replyContent?.text || "Sorry, no response."; - res.status(200).send({ reply: replyText }); - } catch (error) { - console.error(error); - res.status(404).send("Bad Request āŒ"); - } -}); - // cookie parser middleware app.use(cookieParser()); diff --git a/database/aichathistory.js b/database/aichathistory.js new file mode 100644 index 0000000..06422e8 --- /dev/null +++ b/database/aichathistory.js @@ -0,0 +1,39 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const AiChatHistory = db.define("AiChatHistory", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: true, + }, + user_request: { + type: DataTypes.TEXT, + allowNull: false, + }, + ai_response: { + type: DataTypes.TEXT, + allowNull: false, + }, + quiz_id: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + response_type: { + type: DataTypes.ENUM("flashcard", "quiz"), + allowNull: false, + defaultValue: "flashcard", + }, + status: { + type: DataTypes.STRING(20), + allowNull: false, + defaultValue: "success", + }, +}); + +module.exports = AiChatHistory; diff --git a/database/index.js b/database/index.js index e498df6..a180c2f 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,12 @@ const db = require("./db"); const User = require("./user"); +const AiChatHistory = require("./aichathistory"); + +User.hasMany(AiChatHistory, { foreignKey: "user_id" }); +AiChatHistory.belongsTo(User, { foreignKey: "user_id" }); module.exports = { db, User, + AiChatHistory, }; diff --git a/package-lock.json b/package-lock.json index 525e90b..f9861f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "nanoid": "^5.1.5", "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7" @@ -1010,6 +1011,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", diff --git a/package.json b/package.json index a6c613c..0e0172b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "nanoid": "^5.1.5", "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7" From 42b147a9151ebb887876620b11366061dfb33291 Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Mon, 11 Aug 2025 18:11:10 -0400 Subject: [PATCH 28/53] added username column for signup config issues --- database/user.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/database/user.js b/database/user.js index 0c86524..31e7353 100644 --- a/database/user.js +++ b/database/user.js @@ -16,6 +16,10 @@ const User = db.define("user", { type: DataTypes.STRING, allowNull: false, }, + username: { + type: DataTypes.STRING, + allowNull: false, + }, email: { type: DataTypes.STRING, allowNull: false, From 20701036018115c36a4c261d31b8395694ca85a1 Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Tue, 12 Aug 2025 10:43:24 -0400 Subject: [PATCH 29/53] edited routes and mounted so the signup can work --- api/index.js | 6 ++++-- auth/index.js | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/api/index.js b/api/index.js index 6e64996..4d4be4f 100644 --- a/api/index.js +++ b/api/index.js @@ -5,10 +5,12 @@ const testDbRouter = require("./test-db"); const taskOrganizerRouter = require("./TaskOrganizer"); const timerData = require("./timerData"); const CalculatorRouter = require("./Calculator"); +const signUp = require("../auth/index"); router.use("/test-db", testDbRouter); -router.use("/", taskOrganizerRouter); +router.use("/", taskOrganizerRouter); router.use("/grade-calculator", CalculatorRouter); router.use("/", timerData); +router.use("/signup", signUp.router); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/auth/index.js b/auth/index.js index 07968c5..d4d9496 100644 --- a/auth/index.js +++ b/auth/index.js @@ -104,7 +104,7 @@ router.post("/auth0", async (req, res) => { // Signup route router.post("/signup", async (req, res) => { try { - const { username, password } = req.body; + const { username, firstName, lastName, password, email } = req.body; if (!username || !password) { return res @@ -126,7 +126,13 @@ router.post("/signup", async (req, res) => { // Create new user const passwordHash = User.hashPassword(password); - const user = await User.create({ username, passwordHash }); + const user = await User.create({ + firstName, + lastName, + username, + email, + passwordHash, + }); // Generate JWT token const token = jwt.sign( From 02cab2598660dbcd6a1f5802ae1852cb04f7c7b8 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Wed, 13 Aug 2025 11:31:17 -0400 Subject: [PATCH 30/53] Updated the system prompt behavior to avoid suggesting flashcards or quizzes unless prompted, while ensuring a study plan is provided upon user request. --- api/aichathistory.js | 243 ++++++++++++++++++++++++++++++++------ database/aichathistory.js | 2 +- 2 files changed, 205 insertions(+), 40 deletions(-) diff --git a/api/aichathistory.js b/api/aichathistory.js index bbed38a..0aae2c6 100644 --- a/api/aichathistory.js +++ b/api/aichathistory.js @@ -195,41 +195,70 @@ router.post("/", async (req, res) => { { type: "text", text: `${user_request} -You are an advanced AI study assistant that creates either flashcards or a multiple-choice quiz based on the user's request to help with exam preparation. +You are an expert AI study assistant designed to help students excel in their academic pursuits. You can create comprehensive study materials including flashcards, quizzes, and detailed study plans. -Instructions: -- Decide whether to create flashcards or a quiz based on the user's request context. -- Determine the appropriate number of items based on the user's request (aim for 5-15 items unless specifically requested otherwise). -- Respond with ONLY a valid JSON array (no prose, no code fences, no surrounding text). The array must contain at least 10 items. -- Use one of the following object shapes for each item: +RESPONSE TYPES: +1. FLASHCARDS: When user requests flashcards or memorization help +2. QUIZ: When user requests practice questions or assessments +3. STUDY_PLAN: When user asks for study plans, strategies, or learning guidance -Flashcard item example (as plain JSON object, not code-fenced): +INSTRUCTIONS: +- Analyze the user's request to determine the most appropriate response type +- For flashcards/quiz: Create 5-15 high-quality items and respond with ONLY valid JSON array +- For study plans: Provide comprehensive, actionable study strategies as regular text (NOT JSON) +- Ensure all content is academically rigorous, accurate, and exam-focused +- Use evidence-based learning techniques and cognitive science principles + +RESPONSE FORMATS: + +For FLASHCARDS (JSON array): { - "front": "Question or prompt goes here", - "back": "Answer or explanation goes here", + "front": "Clear, specific question or concept", + "back": "Comprehensive, accurate answer with key details", "difficulty": "easy|medium|hard", - "cognitive_skill": "recall|comprehension|application|analysis" + "cognitive_skill": "recall|comprehension|application|analysis|synthesis|evaluation", + "topic": "specific subtopic this covers" } -Quiz question item example (as plain JSON object, not code-fenced): +For QUIZ (JSON array): { - "question": "The question text goes here", - "options": [ - "A) First option", - "B) Second option", - "C) Third option", - "D) Fourth option" - ], + "question": "Well-crafted multiple choice question", + "options": ["A) Option", "B) Option", "C) Option", "D) Option"], "correct": "A|B|C|D", + "explanation": "Brief explanation of why this is correct", "difficulty": "easy|medium|hard", - "cognitive_skill": "recall|comprehension|application|analysis" + "cognitive_skill": "recall|comprehension|application|analysis|synthesis|evaluation", + "topic": "specific subtopic this covers" } -Requirements: -- Ensure valid JSON syntax that can be parsed directly. -- Vary difficulty levels and cognitive skills across items. -- Ensure content is accurate and aligned with typical exam expectations. -- Do not include any commentary, explanations, tags, or code fences. +For STUDY_PLAN (Regular text format): +Provide a comprehensive, well-structured study plan in clear, readable text format. Include: + +1. Overview and Learning Objectives +2. Detailed Study Schedule (day-by-day breakdown) +3. Active Learning Strategies +4. Recommended Resources and Materials +5. Assessment and Practice Methods +6. Common Pitfalls to Avoid +7. Success Indicators and Progress Tracking +8. Time Management Tips + +Format the response as clear, organized text with headings, bullet points, and structured sections. Do NOT use JSON format for study plans. + +QUALITY STANDARDS: +- Academic accuracy: All content must be factually correct and up-to-date +- Cognitive depth: Include higher-order thinking skills (analysis, synthesis, evaluation) +- Exam alignment: Focus on concepts commonly tested in academic assessments +- Learning science: Incorporate spaced repetition, active recall, and interleaving principles +- Accessibility: Clear, concise language appropriate for the academic level +- Comprehensive coverage: Address key concepts, common misconceptions, and advanced topics + +RESPONSE RULES: +- For flashcards/quiz: Respond with ONLY valid JSON array (no prose, code fences, or commentary) +- For study plans: Respond with clear, structured text (NOT JSON format) +- Ensure all JSON is properly formatted and parseable +- Include difficulty progression and varied cognitive skills +- Focus on mastery learning and deep understanding `, }, ], @@ -262,9 +291,21 @@ Requirements: let parsed = extractQuizDataRobust(replyText); - // Determine response type by inspecting the parsed JSON + // Determine response type by inspecting the parsed JSON and user request let responseType = "flashcard"; - if (Array.isArray(parsed) && parsed.length > 0) { + + // First, check if the user specifically requested a study plan + const userRequestLower = (user_request || "").toLowerCase(); + const isStudyPlanRequest = + userRequestLower.includes("study plan") || + userRequestLower.includes("study strategy") || + userRequestLower.includes("learning plan") || + userRequestLower.includes("how to study") || + userRequestLower.includes("study guide"); + + if (isStudyPlanRequest) { + responseType = "study_plan"; + } else if (Array.isArray(parsed) && parsed.length > 0) { const firstItem = parsed[0]; const looksLikeQuiz = firstItem && @@ -279,28 +320,48 @@ Requirements: "back" in firstItem; if (looksLikeQuiz) responseType = "quiz"; else if (looksLikeFlashcard) responseType = "flashcard"; + } else if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + // Check if it's a study plan JSON object + const looksLikeStudyPlan = + parsed && + typeof parsed === "object" && + ("title" in parsed || + "study_schedule" in parsed || + "learning_objectives" in parsed); + if (looksLikeStudyPlan) responseType = "study_plan"; } // Fallback detection if parsing failed - if (!Array.isArray(parsed)) { + if (!Array.isArray(parsed) && typeof parsed !== "object") { const lower = (replyText || "").toLowerCase(); const hasQuestion = /"question"\s*:/.test(lower); const hasOptions = /"options"\s*:\s*\[/.test(lower); const hasCorrect = /"correct"\s*:/.test(lower); + const hasStudyPlan = + /"study_schedule"\s*:/.test(lower) || + /"learning_objectives"\s*:/.test(lower); if (hasQuestion && hasOptions && hasCorrect) { responseType = "quiz"; + } else if (hasStudyPlan) { + responseType = "study_plan"; } } const quizId = responseType === "quiz" ? nanoid(8) : null; + // Generate ID for both quiz and flashcard + const contentId = nanoid(8); // Always generate an ID + try { await AiChatHistory.create({ user_id: req.user?.id || null, user_request, - ai_response: Array.isArray(parsed) - ? sanitizeJsonString(JSON.stringify(parsed)) - : sanitizeJsonString(replyText), - quiz_id: quizId, + ai_response: + responseType === "study_plan" + ? replyText // Store study plan as plain text + : Array.isArray(parsed) + ? sanitizeJsonString(JSON.stringify(parsed)) + : sanitizeJsonString(replyText), + quiz_id: contentId, // Use the same ID for both types response_type: responseType, status: "success", }); @@ -308,22 +369,32 @@ Requirements: console.error("Error saving AI chat history āŒ", dbErr.message || dbErr); } - // Generate quiz link if it's a quiz - let quizLink = null; + // Generate link for quiz, flashcard, and study plan + let contentLink = null; let userMessage = replyText; - if (responseType === "quiz" && quizId) { + if (responseType === "quiz") { + const baseUrl = process.env.FRONTEND_URL || "http://localhost:3000"; + contentLink = `${baseUrl}/quiz/${contentId}`; + userMessage = `Your quiz is ready! Click here to take it: ${contentLink}`; + } else if (responseType === "flashcard") { const baseUrl = process.env.FRONTEND_URL || "http://localhost:3000"; - quizLink = `${baseUrl}/quiz/${quizId}`; - userMessage = `Your quiz is ready! Click here to take it: ${quizLink}`; + contentLink = `${baseUrl}/flashcards/${contentId}`; + userMessage = `Your flashcards are ready! Click here to study: ${contentLink}`; + } else if (responseType === "study_plan") { + // For study plans, return the text directly without a link + userMessage = replyText; // Return the study plan as text + contentLink = null; // No link needed for study plans } res.status(200).send({ reply: userMessage, - data: parsed, - quiz_id: quizId, + data: responseType === "study_plan" ? null : parsed, // No parsed data for study plans + quiz_id: contentId, // Keep the field name for backward compatibility response_type: responseType, - quiz_link: quizLink, + quiz_link: contentLink, // Keep the field name for backward compatibility + content_id: contentId, // Add new field for clarity + content_link: contentLink, // Add new field for clarity }); } catch (error) { console.error( @@ -461,4 +532,98 @@ router.get("/debug/:quizId", async (req, res) => { } }); +// New endpoint for retrieving flashcards by ID +router.get("/flashcards/:flashcardId", async (req, res) => { + const { flashcardId } = req.params; + + try { + const history = await AiChatHistory.findOne({ + where: { + quiz_id: flashcardId, // Using the same field for both types + response_type: "flashcard", + }, + }); + + if (!history) { + return res.status(404).json({ error: "Flashcards not found" }); + } + + let parsed = null; + try { + parsed = JSON.parse(history.ai_response); + } catch (_) { + parsed = extractQuizDataRobust(history.ai_response); + } + + if (!parsed) { + return res.status(500).json({ + error: "No flashcard data could be extracted", + details: "The response format is not supported", + }); + } + + return res.status(200).json({ + ai_response: history.ai_response, + data: parsed, + response_type: history.response_type, + flashcard_id: history.quiz_id, // Using the stored ID + }); + } catch (err) { + console.error("Error fetching flashcards by ID āŒ", err.message || err); + res.status(500).json({ error: "Server error" }); + } +}); + +// New endpoint for retrieving study plans by ID +router.get("/study-plan/:studyPlanId", async (req, res) => { + const { studyPlanId } = req.params; + + try { + const history = await AiChatHistory.findOne({ + where: { + quiz_id: studyPlanId, // Using the same field for all types + response_type: "study_plan", + }, + }); + + if (!history) { + return res.status(404).json({ error: "Study plan not found" }); + } + + let parsed = null; + try { + parsed = JSON.parse(history.ai_response); + } catch (_) { + // For study plans, we expect a JSON object, not an array + try { + const cleaned = sanitizeJsonString(history.ai_response); + parsed = JSON.parse(cleaned); + } catch (extractError) { + console.error("Failed to parse study plan JSON:", extractError); + return res.status(500).json({ + error: "Failed to parse study plan data", + details: "The stored study plan data is malformed", + }); + } + } + + if (!parsed || typeof parsed !== "object") { + return res.status(500).json({ + error: "No study plan data could be extracted", + details: "The response format is not supported", + }); + } + + return res.status(200).json({ + ai_response: history.ai_response, + data: parsed, + response_type: history.response_type, + study_plan_id: history.quiz_id, // Using the stored ID + }); + } catch (err) { + console.error("Error fetching study plan by ID āŒ", err.message || err); + res.status(500).json({ error: "Server error" }); + } +}); + module.exports = router; diff --git a/database/aichathistory.js b/database/aichathistory.js index 06422e8..8eaacd9 100644 --- a/database/aichathistory.js +++ b/database/aichathistory.js @@ -25,7 +25,7 @@ const AiChatHistory = db.define("AiChatHistory", { unique: true, }, response_type: { - type: DataTypes.ENUM("flashcard", "quiz"), + type: DataTypes.ENUM("flashcard", "quiz", "study_plan"), allowNull: false, defaultValue: "flashcard", }, From 95b55798d026de4060140271c7c8fd5136aaa425 Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Wed, 13 Aug 2025 15:02:30 -0400 Subject: [PATCH 31/53] Edited seed --- database/Tasks.js | 2 +- database/seed.js | 33 ++++++++++++++++----------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/database/Tasks.js b/database/Tasks.js index c73518f..8fbfcc1 100644 --- a/database/Tasks.js +++ b/database/Tasks.js @@ -11,7 +11,7 @@ const Tasks = db.define("Tasks", assignment: DataTypes.STRING, description: DataTypes.TEXT, status: { - type: DataTypes.ENUM('Not started', 'Submitted', 'In-progress'), + type: DataTypes.ENUM('Pending', 'Submitted', 'In-progress'), allowNull: false }, deadline: DataTypes.DATE, diff --git a/database/seed.js b/database/seed.js index b7ae88b..495e3bd 100644 --- a/database/seed.js +++ b/database/seed.js @@ -6,21 +6,20 @@ const seed = async () => { db.logging = false; await db.sync({ force: true }); // Drop and recreate tables - const user = await User.bulkCreate([{ - username: "benjamin", - email: "benjamin@example.com", - password: "supersecurepassword", - role: "student", - }, - { - username: "David", - email: "David@example.com", - password: "supersecurepassword2", - role: "student", - - }, - - ]); + const user = await User.bulkCreate([ + { + username: "benjamin", + email: "benjamin@example.com", + password: "supersecurepassword", + role: "student", + }, + { + username: "David", + email: "David@example.com", + password: "supersecurepassword2", + role: "student", + }, + ]); // Create a Task const tasks = await Tasks.bulkCreate([ @@ -28,7 +27,7 @@ const seed = async () => { className: "Math 101", assignment: "Homework 1", description: "Complete exercises 1–10 on page 52", - status: "in-progress", + status: "In-progress", deadline: new Date("2025-08-05"), priority: "high", user_id: user[0].id, @@ -37,7 +36,7 @@ const seed = async () => { className: "Math 201", assignment: "Homework 2", description: "Complete exercises 1–10 on page 52", - status: "in-progress", + status: "In-progress", deadline: new Date("2025-08-05"), priority: "high", user_id: user[1].id, From d28f2fd80186bae510832424f9e76947264e6c50 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Wed, 13 Aug 2025 20:44:31 -0400 Subject: [PATCH 32/53] =?UTF-8?q?Updated=20aichat=20api,=20fix=20some=20is?= =?UTF-8?q?sues=20for=20the=20quiz=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/aichathistory.js | 194 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 163 insertions(+), 31 deletions(-) diff --git a/api/aichathistory.js b/api/aichathistory.js index 0aae2c6..8b555be 100644 --- a/api/aichathistory.js +++ b/api/aichathistory.js @@ -87,6 +87,46 @@ function extractQuizDataRobust(text) { console.log("Direct JSON parse failed:", error.message); } + // Try to clean the text and parse again + try { + // Remove common prefixes/suffixes that AI might add + let cleanedText = text.trim(); + + // Remove markdown code blocks if present + cleanedText = cleanedText + .replace(/```json\s*/g, "") + .replace(/```\s*$/g, ""); + + // Remove any text before the first [ + const firstBracket = cleanedText.indexOf("["); + if (firstBracket > 0) { + cleanedText = cleanedText.substring(firstBracket); + } + + // Remove any text after the last ] + const lastBracket = cleanedText.lastIndexOf("]"); + if (lastBracket > 0 && lastBracket < cleanedText.length - 1) { + cleanedText = cleanedText.substring(0, lastBracket + 1); + } + + console.log( + "Attempting to parse cleaned text:", + cleanedText.substring(0, 200) + "..." + ); + + const parsed = JSON.parse(cleanedText); + if (Array.isArray(parsed)) { + console.log( + "Cleaned text parse successful, found array with", + parsed.length, + "items" + ); + return parsed; + } + } catch (error) { + console.log("Cleaned text parse failed:", error.message); + } + // Second try: extract array with bracket counting try { const start = text.indexOf("["); @@ -187,7 +227,7 @@ router.post("/", async (req, res) => { try { const response = await anthropic.messages.create({ model: "claude-3-5-haiku-20241022", - max_tokens: 1000, + max_tokens: 4000, messages: [ { role: "user", @@ -204,32 +244,39 @@ RESPONSE TYPES: INSTRUCTIONS: - Analyze the user's request to determine the most appropriate response type -- For flashcards/quiz: Create 5-15 high-quality items and respond with ONLY valid JSON array +- For flashcards/quiz: Create exactly 10 high-quality items and respond with valid JSON array - For study plans: Provide comprehensive, actionable study strategies as regular text (NOT JSON) - Ensure all content is academically rigorous, accurate, and exam-focused - Use evidence-based learning techniques and cognitive science principles RESPONSE FORMATS: +For flashcards and quizzes, respond with a valid JSON array. + For FLASHCARDS (JSON array): -{ - "front": "Clear, specific question or concept", - "back": "Comprehensive, accurate answer with key details", - "difficulty": "easy|medium|hard", - "cognitive_skill": "recall|comprehension|application|analysis|synthesis|evaluation", - "topic": "specific subtopic this covers" -} +[ + { + "front": "Clear, specific question or concept", + "back": "Comprehensive, accurate answer with key details", + "difficulty": "easy|medium|hard", + "cognitive_skill": "recall|comprehension|application|analysis|synthesis|evaluation", + "topic": "specific subtopic this covers" + } +] For QUIZ (JSON array): -{ - "question": "Well-crafted multiple choice question", - "options": ["A) Option", "B) Option", "C) Option", "D) Option"], - "correct": "A|B|C|D", - "explanation": "Brief explanation of why this is correct", - "difficulty": "easy|medium|hard", - "cognitive_skill": "recall|comprehension|application|analysis|synthesis|evaluation", - "topic": "specific subtopic this covers" -} +Create exactly 10 questions in this format: +[ + { + "question": "What is the first stage of the water cycle?", + "options": ["A) Evaporation", "B) Condensation", "C) Precipitation", "D) Collection"], + "correct": "A", + "explanation": "Evaporation is the first stage where water turns from liquid to vapor due to heat from the sun.", + "difficulty": "easy", + "cognitive_skill": "recall", + "topic": "water cycle stages" + } +] For STUDY_PLAN (Regular text format): Provide a comprehensive, well-structured study plan in clear, readable text format. Include: @@ -254,11 +301,14 @@ QUALITY STANDARDS: - Comprehensive coverage: Address key concepts, common misconceptions, and advanced topics RESPONSE RULES: -- For flashcards/quiz: Respond with ONLY valid JSON array (no prose, code fences, or commentary) +- For flashcards/quiz: Respond with valid JSON array +- For quizzes: Create exactly 10 questions - For study plans: Respond with clear, structured text (NOT JSON format) - Ensure all JSON is properly formatted and parseable - Include difficulty progression and varied cognitive skills - Focus on mastery learning and deep understanding + +IMPORTANT: When creating quizzes, respond with ONLY a JSON array containing exactly 10 question objects. Do not add any text before or after the JSON array. `, }, ], @@ -268,6 +318,15 @@ RESPONSE RULES: const replyContent = response?.content?.find((c) => c.type === "text"); const replyText = replyContent?.text || "Sorry, no response."; + // Log the raw AI response for debugging + console.log("šŸ¤– RAW AI RESPONSE:"); + console.log("Response length:", replyText.length); + console.log("Response preview:", replyText.substring(0, 1000)); + console.log( + "Contains JSON array markers:", + replyText.includes("[") && replyText.includes("]") + ); + // const isQuiz = replyText.includes("## Question 1"); // const quizId = isQuiz ? nanoid(8) : null; @@ -291,8 +350,23 @@ RESPONSE RULES: let parsed = extractQuizDataRobust(replyText); + // Debug logging to see what we're getting + console.log("šŸ” DEBUG: Raw AI Response:"); + console.log("Length:", replyText.length); + console.log("First 500 chars:", replyText.substring(0, 500)); + console.log("Last 500 chars:", replyText.substring(replyText.length - 500)); + console.log("Contains '[':", replyText.includes("[")); + console.log("Contains ']':", replyText.includes("]")); + console.log("Contains 'question':", replyText.includes("question")); + console.log("Parsed result:", parsed); + console.log("Parsed type:", typeof parsed); + console.log( + "Parsed length:", + Array.isArray(parsed) ? parsed.length : "N/A" + ); + // Determine response type by inspecting the parsed JSON and user request - let responseType = "flashcard"; + let responseType = "unknown"; // Start neutral instead of defaulting to flashcard // First, check if the user specifically requested a study plan const userRequestLower = (user_request || "").toLowerCase(); @@ -303,23 +377,43 @@ RESPONSE RULES: userRequestLower.includes("how to study") || userRequestLower.includes("study guide"); + // Check if user specifically requested a quiz + const isQuizRequest = + userRequestLower.includes("quiz") || + userRequestLower.includes("test") || + userRequestLower.includes("practice questions") || + userRequestLower.includes("assessment") || + userRequestLower.includes("multiple choice") || + userRequestLower.includes("mcq"); + + // Check if user specifically requested flashcards + const isFlashcardRequest = + userRequestLower.includes("flashcard") || + userRequestLower.includes("memorization") || + userRequestLower.includes("memory") || + userRequestLower.includes("recall"); + if (isStudyPlanRequest) { responseType = "study_plan"; } else if (Array.isArray(parsed) && parsed.length > 0) { const firstItem = parsed[0]; + + // More flexible quiz detection - just needs question field const looksLikeQuiz = - firstItem && - typeof firstItem === "object" && - "question" in firstItem && - Array.isArray(firstItem.options) && - "correct" in firstItem; + firstItem && typeof firstItem === "object" && "question" in firstItem; + + // More flexible flashcard detection - just needs front/back fields const looksLikeFlashcard = firstItem && typeof firstItem === "object" && "front" in firstItem && "back" in firstItem; - if (looksLikeQuiz) responseType = "quiz"; - else if (looksLikeFlashcard) responseType = "flashcard"; + + if (looksLikeQuiz) { + responseType = "quiz"; + } else if (looksLikeFlashcard) { + responseType = "flashcard"; + } } else if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { // Check if it's a study plan JSON object const looksLikeStudyPlan = @@ -330,22 +424,60 @@ RESPONSE RULES: "learning_objectives" in parsed); if (looksLikeStudyPlan) responseType = "study_plan"; } - // Fallback detection if parsing failed - if (!Array.isArray(parsed) && typeof parsed !== "object") { - const lower = (replyText || "").toLowerCase(); + + // Enhanced fallback detection with user request consideration + if (responseType === "unknown") { + const lower = replyText.toLowerCase(); const hasQuestion = /"question"\s*:/.test(lower); const hasOptions = /"options"\s*:\s*\[/.test(lower); const hasCorrect = /"correct"\s*:/.test(lower); + const hasFront = /"front"\s*:/.test(lower); + const hasBack = /"back"\s*:/.test(lower); const hasStudyPlan = /"study_schedule"\s*:/.test(lower) || /"learning_objectives"\s*:/.test(lower); - if (hasQuestion && hasOptions && hasCorrect) { + + // Prioritize user request over content analysis + if (isQuizRequest && (hasQuestion || hasOptions)) { responseType = "quiz"; + } else if (isFlashcardRequest && (hasFront || hasBack)) { + responseType = "flashcard"; + } else if (hasQuestion && hasOptions) { + responseType = "quiz"; + } else if (hasFront && hasBack) { + responseType = "flashcard"; } else if (hasStudyPlan) { responseType = "study_plan"; + } else if (isQuizRequest) { + // If user asked for quiz but content doesn't match, still treat as quiz + responseType = "quiz"; + } else if (isFlashcardRequest) { + // If user asked for flashcards but content doesn't match, still treat as flashcard + responseType = "flashcard"; + } else { + // Final fallback - default to flashcard only if we have no other clues + responseType = "flashcard"; } } + // Debug logging to help troubleshoot response type detection + console.log("šŸ” Response Type Detection Debug:"); + console.log(" User Request:", user_request); + console.log(" User Request Lower:", userRequestLower); + console.log(" Is Quiz Request:", isQuizRequest); + console.log(" Is Flashcard Request:", isFlashcardRequest); + console.log(" Is Study Plan Request:", isStudyPlanRequest); + console.log( + " Parsed Data Type:", + Array.isArray(parsed) ? "Array" : typeof parsed + ); + console.log( + " Parsed Data Length:", + Array.isArray(parsed) ? parsed.length : "N/A" + ); + console.log(" Final Response Type:", responseType); + console.log(" Content Preview:", replyText.substring(0, 200) + "..."); + const quizId = responseType === "quiz" ? nanoid(8) : null; // Generate ID for both quiz and flashcard From 6944aa1597acdab35b4522e619c6502fe89d9c69 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Fri, 15 Aug 2025 22:05:02 -0400 Subject: [PATCH 33/53] Added a new route to keep track of User study sessions when the user finish a quiz or flashcards --- api/StreakSession.js | 43 +++++++++++++++++++++++++++++++++++++++ database/StreakSession.js | 26 +++++++++++++++++++++++ database/index.js | 34 +++++++++++++++---------------- database/seed.js | 36 ++++++++++++++++---------------- 4 files changed, 103 insertions(+), 36 deletions(-) create mode 100644 api/StreakSession.js create mode 100644 database/StreakSession.js diff --git a/api/StreakSession.js b/api/StreakSession.js new file mode 100644 index 0000000..010bb5a --- /dev/null +++ b/api/StreakSession.js @@ -0,0 +1,43 @@ +const express = require("express"); +const router = express.Router(); +const { StreakSession } = require("../database"); + +router.post("/start", async (req, res) => { + try { + const { userId } = req.body; + + const session = await Session.create({ + userId, + startTime: new Date(), + }); + + res.status(201).json({ message: "Session started", session }); + } catch (error) { + res.status(500).json({ error: "Failed to start session" }); + } +}); + +router.post("/end", async (req, res) => { + try { + const { userId } = req.body; + + // find the most recent open session + const session = await Session.findOne({ + where: { userId, endTime: null }, + order: [["startTime", "DESC"]], + }); + + if (!session) { + return res.status(404).json({ error: "No active session found" }); + } + + session.endTime = new Date(); + await session.save(); + + res.json({ message: "Session ended", session }); + } catch (error) { + res.status(500).json({ error: "Failed to end session" }); + } +}); + +module.exports = router; diff --git a/database/StreakSession.js b/database/StreakSession.js new file mode 100644 index 0000000..c90746c --- /dev/null +++ b/database/StreakSession.js @@ -0,0 +1,26 @@ +// models/Session.js +const { DataTypes } = require("sequelize"); +const sequelize = require("./db"); // your Sequelize instance + +const StreakSession = sequelize.define( + "Session", + { + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + startTime: { + type: DataTypes.DATE, + allowNull: false, + }, + endTime: { + type: DataTypes.DATE, + allowNull: true, + }, + }, + { + timestamps: true, + } +); + +module.exports = StreakSession; diff --git a/database/index.js b/database/index.js index 5509c2a..ccfc929 100644 --- a/database/index.js +++ b/database/index.js @@ -1,32 +1,32 @@ const Sequelize = require("sequelize"); const db = require("./db"); - - - -const User = require('./User'); -const Tasks = require('./Tasks'); -const Calculator = require('./Calculator'); -const Reminder = require('./Reminder'); -const Session = require('./Session'); +const User = require("./User"); +const Tasks = require("./Tasks"); +const Calculator = require("./Calculator"); +const Reminder = require("./Reminder"); +const Session = require("./Session"); const AiChatHistory = require("./aichathistory"); +const StreakSession = require("./StreakSession"); // Define associations -User.hasMany(Tasks, { foreignKey: 'user_id' }); // One user can have many tasks -Tasks.belongsTo(User, { foreignKey: 'user_id' }); // Each task belongs to a specific user +User.hasMany(Tasks, { foreignKey: "user_id" }); // One user can have many tasks +Tasks.belongsTo(User, { foreignKey: "user_id" }); // Each task belongs to a specific user -User.hasMany(Calculator, { foreignKey: 'user_id' }); // User can have many grade calculator instances -Calculator.belongsTo(User, { foreignKey: 'user_id' }); // Each calculator belongs to a user +User.hasMany(Calculator, { foreignKey: "user_id" }); // User can have many grade calculator instances +Calculator.belongsTo(User, { foreignKey: "user_id" }); // Each calculator belongs to a user -User.hasMany(Session, { foreignKey: 'user_id' }); // Each user can have many study sessions -Session.belongsTo(User, { foreignKey: 'user_id' }); // Each session belongs to a user +User.hasMany(Session, { foreignKey: "user_id" }); // Each user can have many study sessions +Session.belongsTo(User, { foreignKey: "user_id" }); // Each session belongs to a user -Tasks.hasMany(Reminder, { foreignKey: 'task_id' }); // One task can have many reminders -Reminder.belongsTo(Tasks, { foreignKey: 'task_id' }); // One reminder belongs to a specific task +Tasks.hasMany(Reminder, { foreignKey: "task_id" }); // One task can have many reminders +Reminder.belongsTo(Tasks, { foreignKey: "task_id" }); // One reminder belongs to a specific task User.hasMany(AiChatHistory, { foreignKey: "user_id" }); AiChatHistory.belongsTo(User, { foreignKey: "user_id" }); +User.hasMany(StreakSession, { foreignKey: "user_id" }); +StreakSession.belongsTo(User, { foreignKey: "user_id" }); // Export everything module.exports = { db, @@ -35,5 +35,5 @@ module.exports = { Tasks, Calculator, Reminder, - Session + Session, }; diff --git a/database/seed.js b/database/seed.js index 8831ecb..ad3c406 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,7 +1,5 @@ const db = require("./db"); -const { User, Tasks, Calculator, Reminder, Session } = require('./index'); - - +const { User, Tasks, Calculator, Reminder, Session } = require("./index"); const seed = async () => { try { @@ -9,43 +7,43 @@ const seed = async () => { await db.sync({ force: true }); // Drop and recreate tables const user = await User.create({ - username: 'benjamin', - email: 'benjamin@example.com', - password: 'supersecurepassword', - role: 'student' + username: "benjamin", + email: "benjamin@example.com", + password: "supersecurepassword", + role: "student", }); // Create a Task const task = await Tasks.create({ - className: 'Math 101', - assignment: 'Homework 1', - description: 'Complete exercises 1–10 on page 52', - status: 'in-progress', - deadline: new Date('2025-08-05'), - priority: 'high', - user_id: user.id + className: "Math 101", + assignment: "Homework 1", + description: "Complete exercises 1–10 on page 52", + status: "in-progress", + deadline: new Date("2025-08-05"), + priority: "high", + user_id: user.id, }); - + // Add calculator entry const calculator = await Calculator.create({ user_id: user.id, assignment_type: "Homework", assignment_grade: 90, - assignment_weight: 20 + assignment_weight: 20, }); // Add study session await Session.create({ - duration: 45, + duration: "00:45:00", user_id: user.id, started_at: new Date(), - created_at: new Date() + created_at: new Date(), }); // Add reminder for task await Reminder.create({ task_id: task.id, - remind: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000) + remind: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }); console.log("🌱 Seeded the database!"); From be789f3c6b696f5e2168501aaf1539d558bffca6 Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Sat, 16 Aug 2025 16:04:35 -0400 Subject: [PATCH 34/53] added route to post the user study session to the db --- api/timerData.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/timerData.js b/api/timerData.js index f9cea9a..d541504 100644 --- a/api/timerData.js +++ b/api/timerData.js @@ -19,4 +19,16 @@ router.get("/data/:userId", async (req, res) => { } }); +router.post("/data/:userId", async (req, res) => { + const { userId } = req.params; + const { duration } = req.body; + + const newSession = await Session.create({ + duration: duration, + user_id: userId, + }); + + res.json(newSession); +}); + module.exports = router; From 4e599b4db14c8728b7e3d27625c57687289e2ea0 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Sat, 16 Aug 2025 17:36:52 -0400 Subject: [PATCH 35/53] Updated Streak Session route Fix the streak session endpoint so it accurately records and returns a user's current streak. --- api/StreakSession.js | 45 +++++++++++++++++++++++++++------------ api/index.js | 3 ++- database/StreakSession.js | 27 +++++++---------------- database/index.js | 1 + database/seed.js | 35 ------------------------------ 5 files changed, 42 insertions(+), 69 deletions(-) diff --git a/api/StreakSession.js b/api/StreakSession.js index 010bb5a..98bdb2b 100644 --- a/api/StreakSession.js +++ b/api/StreakSession.js @@ -2,40 +2,57 @@ const express = require("express"); const router = express.Router(); const { StreakSession } = require("../database"); +router.get("/streak/:userId", async (req, res) => { + const userId = req.params.userId; + + const sessions = await StreakSession.findAll({ + where: { userId }, + order: [["startTime", "ASC"]], + }); + + if (sessions.length === 0) return res.json({ streak: 0 }); + + let streak = 1; + + for (let i = 1; i < sessions.length; i++) { + const lastDate = new Date(sessions[i - 1].startTime).setHours(0, 0, 0, 0); + const currentDate = new Date(sessions[i].startTime).setHours(0, 0, 0, 0); + const diffDays = (currentDate - lastDate) / (1000 * 60 * 60 * 24); + + if (diffDays === 1) streak++; + else if (diffDays > 1) streak = 1; + } + + res.json({ streak }); +}); + router.post("/start", async (req, res) => { + const { userId } = req.body; try { - const { userId } = req.body; - - const session = await Session.create({ + const session = await StreakSession.create({ userId, startTime: new Date(), }); - res.status(201).json({ message: "Session started", session }); - } catch (error) { + } catch (err) { res.status(500).json({ error: "Failed to start session" }); } }); router.post("/end", async (req, res) => { + const { userId } = req.body; try { - const { userId } = req.body; - - // find the most recent open session - const session = await Session.findOne({ + const session = await StreakSession.findOne({ where: { userId, endTime: null }, order: [["startTime", "DESC"]], }); - - if (!session) { + if (!session) return res.status(404).json({ error: "No active session found" }); - } session.endTime = new Date(); await session.save(); - res.json({ message: "Session ended", session }); - } catch (error) { + } catch (err) { res.status(500).json({ error: "Failed to end session" }); } }); diff --git a/api/index.js b/api/index.js index f264d37..c176952 100644 --- a/api/index.js +++ b/api/index.js @@ -2,12 +2,12 @@ const express = require("express"); const router = express.Router(); const testDbRouter = require("./test-db"); - const taskOrganizerRouter = require("./TaskOrganizer"); const timerData = require("./timerData"); const CalculatorRouter = require("./Calculator"); const signUp = require("../auth/index"); const chatRouter = require("./aichathistory"); +const streakSessionRouter = require("./StreakSession"); router.use("/test-db", testDbRouter); router.use("/", taskOrganizerRouter); @@ -15,5 +15,6 @@ router.use("/grade-calculator", CalculatorRouter); router.use("/", timerData); router.use("/signup", signUp.router); router.use("/chat", chatRouter); +router.use("/sessions", streakSessionRouter); module.exports = router; diff --git a/database/StreakSession.js b/database/StreakSession.js index c90746c..d74e11d 100644 --- a/database/StreakSession.js +++ b/database/StreakSession.js @@ -1,26 +1,15 @@ -// models/Session.js const { DataTypes } = require("sequelize"); -const sequelize = require("./db"); // your Sequelize instance +const db = require("./db"); // your Sequelize instance -const StreakSession = sequelize.define( - "Session", +const StreakSession = db.define( + "StreakSession", { - userId: { - type: DataTypes.INTEGER, - allowNull: false, - }, - startTime: { - type: DataTypes.DATE, - allowNull: false, - }, - endTime: { - type: DataTypes.DATE, - allowNull: true, - }, + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + userId: { type: DataTypes.INTEGER, allowNull: false }, + startTime: { type: DataTypes.DATE, allowNull: false }, + endTime: { type: DataTypes.DATE, allowNull: true }, }, - { - timestamps: true, - } + { timestamps: true } ); module.exports = StreakSession; diff --git a/database/index.js b/database/index.js index ccfc929..138aa27 100644 --- a/database/index.js +++ b/database/index.js @@ -36,4 +36,5 @@ module.exports = { Calculator, Reminder, Session, + StreakSession, }; diff --git a/database/seed.js b/database/seed.js index 5916578..8e5cef6 100644 --- a/database/seed.js +++ b/database/seed.js @@ -6,32 +6,6 @@ const seed = async () => { db.logging = false; await db.sync({ force: true }); // Drop and recreate tables -<<<<<<< HEAD - const user = await User.create({ - username: "benjamin", - email: "benjamin@example.com", - password: "supersecurepassword", - role: "student", - }); - - // Create a Task - const task = await Tasks.create({ - className: "Math 101", - assignment: "Homework 1", - description: "Complete exercises 1–10 on page 52", - status: "in-progress", - deadline: new Date("2025-08-05"), - priority: "high", - user_id: user.id, - }); - - // Add calculator entry - const calculator = await Calculator.create({ - user_id: user.id, - assignment_type: "Homework", - assignment_grade: 90, - assignment_weight: 20, -======= const user = await User.bulkCreate([ { username: "benjamin", @@ -74,28 +48,19 @@ const seed = async () => { grade: 85, weight: 25, user_id: user[0].id, ->>>>>>> c81baeaed3cfaea98a6582b12a40e283f1c89844 }); // Add study session await Session.create({ duration: "00:45:00", -<<<<<<< HEAD - user_id: user.id, -======= user_id: user[0].id, ->>>>>>> c81baeaed3cfaea98a6582b12a40e283f1c89844 started_at: new Date(), created_at: new Date(), }); // Add reminder for the first task await Reminder.create({ -<<<<<<< HEAD - task_id: task.id, -======= task_id: tasks[0].id, // use the first task's id ->>>>>>> c81baeaed3cfaea98a6582b12a40e283f1c89844 remind: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }); From b1fa3d73b32c9f58ee270a02e8bf24bcb78e2758 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Mon, 18 Aug 2025 18:32:52 -0400 Subject: [PATCH 36/53] New Routes to track user progress New introduce routes to track user progress for the frontend User Dashboard --- api/StreakSession.js | 398 +++++++++++++++++++++++-- api/UserProgress.js | 613 ++++++++++++++++++++++++++++++++++++++ api/index.js | 2 + app.js | 5 + database/StreakSession.js | 8 +- database/UserProgress.js | 17 ++ database/index.js | 10 +- database/seed.js | 87 +++++- 8 files changed, 1111 insertions(+), 29 deletions(-) create mode 100644 api/UserProgress.js create mode 100644 database/UserProgress.js diff --git a/api/StreakSession.js b/api/StreakSession.js index 98bdb2b..eab9824 100644 --- a/api/StreakSession.js +++ b/api/StreakSession.js @@ -1,60 +1,408 @@ const express = require("express"); const router = express.Router(); -const { StreakSession } = require("../database"); +const { StreakSession, UserProgress } = require("../database"); +const { Op } = require("sequelize"); -router.get("/streak/:userId", async (req, res) => { - const userId = req.params.userId; +// Calculate streak based on study activity (not just session starts) +const calculateStreak = async (userId) => { + try { + // Get all study activities for the user + const studyActivities = await UserProgress.findAll({ + where: { user_id: userId }, + attributes: ["studied_at"], + order: [["studied_at", "DESC"]], + }); + + if (studyActivities.length === 0) { + return { currentStreak: 0, longestStreak: 0, lastStudyDate: null }; + } + + // Get unique study dates (converted to local date) + const studyDates = [ + ...new Set( + studyActivities.map((activity) => { + const date = new Date(activity.studied_at); + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); + }) + ), + ].sort((a, b) => b - a); // Sort descending (most recent first) + + let currentStreak = 0; + let longestStreak = 0; + let tempStreak = 0; + const today = new Date(); + const yesterday = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() - 1 + ); + + // Check if user studied today or yesterday to start current streak + const todayDate = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + ); + const yesterdayDate = new Date( + yesterday.getFullYear(), + yesterday.getMonth(), + yesterday.getDate() + ); + + const studiedToday = studyDates.some( + (date) => date.getTime() === todayDate.getTime() + ); + const studiedYesterday = studyDates.some( + (date) => date.getTime() === yesterdayDate.getTime() + ); - const sessions = await StreakSession.findAll({ - where: { userId }, - order: [["startTime", "ASC"]], - }); + // Calculate current streak + if (studiedToday) { + currentStreak = 1; + let checkDate = yesterdayDate; - if (sessions.length === 0) return res.json({ streak: 0 }); + for (let i = 1; i < studyDates.length; i++) { + const expectedDate = new Date( + todayDate.getTime() - i * 24 * 60 * 60 * 1000 + ); + const foundDate = studyDates.find( + (date) => date.getTime() === expectedDate.getTime() + ); - let streak = 1; + if (foundDate) { + currentStreak++; + } else { + break; + } + } + } else if (studiedYesterday) { + currentStreak = 1; + let checkDate = new Date(yesterdayDate.getTime() - 24 * 60 * 60 * 1000); - for (let i = 1; i < sessions.length; i++) { - const lastDate = new Date(sessions[i - 1].startTime).setHours(0, 0, 0, 0); - const currentDate = new Date(sessions[i].startTime).setHours(0, 0, 0, 0); - const diffDays = (currentDate - lastDate) / (1000 * 60 * 60 * 24); + for (let i = 2; i < studyDates.length; i++) { + const expectedDate = new Date( + yesterdayDate.getTime() - (i - 1) * 24 * 60 * 60 * 1000 + ); + const foundDate = studyDates.find( + (date) => date.getTime() === expectedDate.getTime() + ); - if (diffDays === 1) streak++; - else if (diffDays > 1) streak = 1; + if (foundDate) { + currentStreak++; + } else { + break; + } + } + } + + // Calculate longest streak + for (let i = 0; i < studyDates.length - 1; i++) { + const currentDate = studyDates[i]; + const nextDate = studyDates[i + 1]; + const diffDays = Math.floor( + (currentDate - nextDate) / (24 * 60 * 60 * 1000) + ); + + if (diffDays === 1) { + tempStreak++; + } else { + longestStreak = Math.max(longestStreak, tempStreak + 1); + tempStreak = 0; + } + } + longestStreak = Math.max(longestStreak, tempStreak + 1); + + return { + currentStreak, + longestStreak, + lastStudyDate: studyDates[0], + totalStudyDays: studyDates.length, + }; + } catch (error) { + console.error("Error calculating streak:", error); + return { currentStreak: 0, longestStreak: 0, lastStudyDate: null }; } +}; + +// Get comprehensive streak information +router.get("/streak/:userId", async (req, res) => { + try { + const userId = req.params.userId; + const streakInfo = await calculateStreak(userId); + + // Get recent study activity for context + const recentActivity = await UserProgress.findAll({ + where: { user_id: userId }, + include: [ + { + model: require("../database").AiChatHistory, + attributes: ["response_type"], + }, + ], + order: [["studied_at", "DESC"]], + limit: 10, + }); + + // Calculate streak milestones + const milestones = [1, 3, 7, 14, 30, 60, 100, 365]; + const achievedMilestones = milestones.filter( + (milestone) => streakInfo.longestStreak >= milestone + ); - res.json({ streak }); + res.json({ + ...streakInfo, + recentActivity: recentActivity.map((activity) => ({ + id: activity.id, + type: activity.AiChatHistory?.response_type, + studiedAt: activity.studied_at, + isCorrect: activity.is_correct, + score: activity.score, + })), + achievedMilestones, + nextMilestone: + milestones.find((milestone) => milestone > streakInfo.currentStreak) || + null, + }); + } catch (error) { + console.error("Streak calculation error:", error); + res.status(500).json({ error: "Failed to calculate streak" }); + } }); +// Start a study session router.post("/start", async (req, res) => { - const { userId } = req.body; try { + const { userId } = req.body; + + if (!userId) { + return res.status(400).json({ error: "userId is required" }); + } + + // Check for existing active session + const existingSession = await StreakSession.findOne({ + where: { + user_id: userId, + endTime: null, + }, + attributes: [ + "id", + "user_id", + "startTime", + "endTime", + "createdAt", + "updatedAt", + ], // Only select existing columns + order: [["startTime", "DESC"]], + }); + + if (existingSession) { + return res.status(409).json({ + error: "Active session already exists", + session: existingSession, + }); + } + const session = await StreakSession.create({ - userId, + user_id: userId, startTime: new Date(), }); - res.status(201).json({ message: "Session started", session }); - } catch (err) { + + res.status(201).json({ + message: "Session started", + session, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error("Session start error:", error); res.status(500).json({ error: "Failed to start session" }); } }); +// End a study session router.post("/end", async (req, res) => { - const { userId } = req.body; try { + const { userId } = req.body; + + if (!userId) { + return res.status(400).json({ error: "userId is required" }); + } + const session = await StreakSession.findOne({ - where: { userId, endTime: null }, + where: { + user_id: userId, + endTime: null, + }, + attributes: [ + "id", + "user_id", + "startTime", + "endTime", + "createdAt", + "updatedAt", + ], // Only select existing columns order: [["startTime", "DESC"]], }); - if (!session) + + if (!session) { return res.status(404).json({ error: "No active session found" }); + } session.endTime = new Date(); await session.save(); - res.json({ message: "Session ended", session }); - } catch (err) { + + // Calculate session duration + const duration = session.endTime - session.startTime; + const durationMinutes = Math.floor(duration / (1000 * 60)); + + res.json({ + message: "Session ended", + session, + duration: { + milliseconds: duration, + minutes: durationMinutes, + formatted: `${Math.floor(durationMinutes / 60)}h ${ + durationMinutes % 60 + }m`, + }, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error("Session end error:", error); res.status(500).json({ error: "Failed to end session" }); } }); +// Get session history +router.get("/history/:userId", async (req, res) => { + try { + const userId = req.params.userId; + const { limit = 20, offset = 0 } = req.query; + + const sessions = await StreakSession.findAll({ + where: { user_id: userId }, + attributes: [ + "id", + "user_id", + "startTime", + "endTime", + "createdAt", + "updatedAt", + ], // Only select existing columns + order: [["startTime", "DESC"]], + limit: parseInt(limit), + offset: parseInt(offset), + }); + + const sessionsWithDuration = sessions.map((session) => { + const duration = session.endTime + ? session.endTime - session.startTime + : new Date() - session.startTime; + + return { + id: session.id, + startTime: session.startTime, + endTime: session.endTime, + duration: { + milliseconds: duration, + minutes: Math.floor(duration / (1000 * 60)), + formatted: session.endTime + ? `${Math.floor(duration / (1000 * 60 * 60))}h ${Math.floor( + (duration % (1000 * 60 * 60)) / (1000 * 60) + )}m` + : "Active", + }, + isActive: !session.endTime, + }; + }); + + res.json({ + sessions: sessionsWithDuration, + total: sessions.length, + hasMore: sessions.length === parseInt(limit), + }); + } catch (error) { + console.error("Session history error:", error); + res.status(500).json({ error: "Failed to fetch session history" }); + } +}); + +// Get streak statistics +router.get("/stats/:userId", async (req, res) => { + try { + const userId = req.params.userId; + + // Get all sessions + const allSessions = await StreakSession.findAll({ + where: { user_id: userId }, + attributes: [ + "id", + "user_id", + "startTime", + "endTime", + "createdAt", + "updatedAt", + ], // Only select existing columns + order: [["startTime", "ASC"]], + }); + + // Get all study activities + const allActivities = await UserProgress.findAll({ + where: { user_id: userId }, + order: [["studied_at", "ASC"]], + }); + + // Calculate statistics + const totalSessions = allSessions.length; + const totalStudyTime = allSessions + .filter((s) => s.endTime) + .reduce( + (total, session) => total + (session.endTime - session.startTime), + 0 + ); + + const avgSessionLength = + totalSessions > 0 + ? Math.floor(totalStudyTime / totalSessions / (1000 * 60)) + : 0; + + const studyDays = [ + ...new Set( + allActivities.map((activity) => { + const date = new Date(activity.studied_at); + return new Date(date.getFullYear(), date.getMonth(), date.getDate()) + .toISOString() + .split("T")[0]; + }) + ), + ].length; + + const streakInfo = await calculateStreak(userId); + + res.json({ + totalSessions, + totalStudyTime: { + milliseconds: totalStudyTime, + minutes: Math.floor(totalStudyTime / (1000 * 60)), + hours: Math.floor(totalStudyTime / (1000 * 60 * 60)), + formatted: `${Math.floor( + totalStudyTime / (1000 * 60 * 60) + )}h ${Math.floor((totalStudyTime % (1000 * 60 * 60)) / (1000 * 60))}m`, + }, + avgSessionLength: { + minutes: avgSessionLength, + formatted: `${Math.floor(avgSessionLength / 60)}h ${ + avgSessionLength % 60 + }m`, + }, + studyDays, + currentStreak: streakInfo.currentStreak, + longestStreak: streakInfo.longestStreak, + lastStudyDate: streakInfo.lastStudyDate, + }); + } catch (error) { + console.error("Stats calculation error:", error); + res.status(500).json({ error: "Failed to calculate statistics" }); + } +}); + module.exports = router; diff --git a/api/UserProgress.js b/api/UserProgress.js new file mode 100644 index 0000000..910263a --- /dev/null +++ b/api/UserProgress.js @@ -0,0 +1,613 @@ +const express = require("express"); +const router = express.Router(); +const { Op } = require("sequelize"); +const { UserProgress, AiChatHistory } = require("../database"); + +router.get("/daily/:userId", async (req, res) => { + const { userId } = req.params; + + try { + // Get today's date in local timezone + const today = new Date(); + const start = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + 0, + 0, + 0, + 0 + ); + const end = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + 23, + 59, + 59, + 999 + ); + + const rows = await UserProgress.findAll({ + where: { + user_id: userId, + studied_at: { [Op.between]: [start, end] }, + }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], // "flashcard" | "quiz" + }, + ], + order: [["studied_at", "ASC"]], + }); + + const flashAttempts = rows.filter( + (r) => r.AiChatHistory?.response_type === "flashcard" + ); + const quizRows = rows.filter( + (r) => r.AiChatHistory?.response_type === "quiz" + ); + + const flashStudied = flashAttempts.length; + const flashCorrect = flashAttempts.filter( + (r) => r.is_correct === true + ).length; + const flashAccuracy = flashStudied + ? Math.round((flashCorrect / flashStudied) * 100) + : 0; + + const quizAttempts = quizRows.filter((r) => r.score != null).length; + const quizAvgScore = quizAttempts + ? Math.round( + quizRows + .filter((r) => r.score != null) + .reduce((s, r) => s + r.score, 0) / quizAttempts + ) + : 0; + + res.json({ + date_range: { start, end }, + flashcards: { + studied_today: flashStudied, + correct_today: flashCorrect, + accuracy_today: flashAccuracy, + }, + quizzes: { + attempts_today: quizAttempts, + avg_score_today: quizAvgScore, + }, + // If you want raw rows for debugging: + // rows + }); + } catch (e) { + console.error("Daily progress error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Get weekly progress for a user +router.get("/weekly/:userId", async (req, res) => { + const { userId } = req.params; + + try { + // Get the start of the current week (Sunday) + const today = new Date(); + const dayOfWeek = today.getDay(); + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - dayOfWeek); + startOfWeek.setHours(0, 0, 0, 0); + + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); + endOfWeek.setHours(23, 59, 59, 999); + + const rows = await UserProgress.findAll({ + where: { + user_id: userId, + studied_at: { [Op.between]: [startOfWeek, endOfWeek] }, + }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + order: [["studied_at", "ASC"]], + }); + + const flashAttempts = rows.filter( + (r) => r.AiChatHistory?.response_type === "flashcard" + ); + const quizRows = rows.filter( + (r) => r.AiChatHistory?.response_type === "quiz" + ); + + const flashStudied = flashAttempts.length; + const flashCorrect = flashAttempts.filter( + (r) => r.is_correct === true + ).length; + const flashAccuracy = flashStudied + ? Math.round((flashCorrect / flashStudied) * 100) + : 0; + + const quizAttempts = quizRows.filter((r) => r.score != null).length; + const quizAvgScore = quizAttempts + ? Math.round( + quizRows + .filter((r) => r.score != null) + .reduce((s, r) => s + r.score, 0) / quizAttempts + ) + : 0; + + res.json({ + date_range: { start: startOfWeek, end: endOfWeek }, + flashcards: { + studied_this_week: flashStudied, + correct_this_week: flashCorrect, + accuracy_this_week: flashAccuracy, + }, + quizzes: { + attempts_this_week: quizAttempts, + avg_score_this_week: quizAvgScore, + }, + }); + } catch (e) { + console.error("Weekly progress error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Get all progress for a user (with optional date range) +router.get("/all/:userId", async (req, res) => { + const { userId } = req.params; + const { start_date, end_date } = req.query; + + try { + let whereClause = { user_id: userId }; + + if (start_date && end_date) { + const start = new Date(start_date); + const end = new Date(end_date); + end.setHours(23, 59, 59, 999); + whereClause.studied_at = { [Op.between]: [start, end] }; + } + + const rows = await UserProgress.findAll({ + where: whereClause, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + order: [["studied_at", "DESC"]], + }); + + const flashAttempts = rows.filter( + (r) => r.AiChatHistory?.response_type === "flashcard" + ); + const quizRows = rows.filter( + (r) => r.AiChatHistory?.response_type === "quiz" + ); + + const flashStudied = flashAttempts.length; + const flashCorrect = flashAttempts.filter( + (r) => r.is_correct === true + ).length; + const flashAccuracy = flashStudied + ? Math.round((flashCorrect / flashStudied) * 100) + : 0; + + const quizAttempts = quizRows.filter((r) => r.score != null).length; + const quizAvgScore = quizAttempts + ? Math.round( + quizRows + .filter((r) => r.score != null) + .reduce((s, r) => s + r.score, 0) / quizAttempts + ) + : 0; + + res.json({ + total_flashcards: { + studied_total: flashStudied, + correct_total: flashCorrect, + accuracy_total: flashAccuracy, + }, + total_quizzes: { + attempts_total: quizAttempts, + avg_score_total: quizAvgScore, + }, + total_sessions: rows.length, + recent_activity: rows.slice(0, 10), // Last 10 activities + }); + } catch (e) { + console.error("All progress error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Record new progress entry +router.post("/record", async (req, res) => { + const { + user_id, + ai_chat_history_id, + card_index, + is_correct, + score, + duration_ms, + session_id, + } = req.body; + + try { + // Validate required fields + if (!user_id || !ai_chat_history_id) { + return res.status(400).json({ + error: + "Missing required fields: user_id and ai_chat_history_id are required", + }); + } + + // Create new progress entry + const progressEntry = await UserProgress.create({ + user_id, + ai_chat_history_id, + card_index, + is_correct, + score, + duration_ms, + session_id, + studied_at: new Date(), + }); + + res.status(201).json({ + message: "Progress recorded successfully", + progress: progressEntry, + }); + } catch (e) { + console.error("Record progress error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Record flashcard progress when user studies +router.post("/flashcard-progress", async (req, res) => { + const { + user_id, + ai_chat_history_id, + card_index, + is_correct, + duration_ms, + session_id, + } = req.body; + + try { + // Validate required fields + if ( + !user_id || + !ai_chat_history_id || + card_index === undefined || + is_correct === undefined + ) { + return res.status(400).json({ + error: + "Missing required fields: user_id, ai_chat_history_id, card_index, and is_correct are required", + }); + } + + // Create new progress entry + const progressEntry = await UserProgress.create({ + user_id, + ai_chat_history_id, + card_index, + is_correct, + score: null, // null for flashcards + duration_ms, + session_id, + studied_at: new Date(), + }); + + res.status(201).json({ + message: "Flashcard progress recorded successfully", + progress: progressEntry, + }); + } catch (e) { + console.error("Record flashcard progress error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Record quiz progress when user takes quiz +router.post("/quiz-progress", async (req, res) => { + const { user_id, ai_chat_history_id, score, duration_ms, session_id } = + req.body; + + try { + // Validate required fields + if (!user_id || !ai_chat_history_id || score === undefined) { + return res.status(400).json({ + error: + "Missing required fields: user_id, ai_chat_history_id, and score are required", + }); + } + + // Create new progress entry + const progressEntry = await UserProgress.create({ + user_id, + ai_chat_history_id, + card_index: null, // null for quizzes + is_correct: null, // null for quizzes + score, + duration_ms, + session_id, + studied_at: new Date(), + }); + + res.status(201).json({ + message: "Quiz progress recorded successfully", + progress: progressEntry, + }); + } catch (e) { + console.error("Record quiz progress error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Get real-time progress for a specific flashcard set +router.get("/flashcard-set/:aiChatHistoryId/:userId", async (req, res) => { + const { aiChatHistoryId, userId } = req.params; + + try { + const progress = await UserProgress.findAll({ + where: { + user_id: userId, + ai_chat_history_id: aiChatHistoryId, + }, + order: [["studied_at", "ASC"]], + }); + + const totalAttempts = progress.length; + const correctAttempts = progress.filter( + (p) => p.is_correct === true + ).length; + const accuracy = + totalAttempts > 0 + ? Math.round((correctAttempts / totalAttempts) * 100) + : 0; + + res.json({ + flashcard_set_id: aiChatHistoryId, + total_attempts: totalAttempts, + correct_attempts: correctAttempts, + accuracy: accuracy, + progress_entries: progress, + }); + } catch (e) { + console.error("Get flashcard set progress error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Get progress summary for dashboard +router.get("/summary/:userId", async (req, res) => { + const { userId } = req.params; + + try { + // Get today's date + const today = new Date(); + const startOfToday = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + 0, + 0, + 0, + 0 + ); + const endOfToday = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + 23, + 59, + 59, + 999 + ); + + // Get this week's date range + const dayOfWeek = today.getDay(); + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - dayOfWeek); + startOfWeek.setHours(0, 0, 0, 0); + + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); + endOfWeek.setHours(23, 59, 59, 999); + + // Get all time progress + const allTimeProgress = await UserProgress.findAll({ + where: { user_id: userId }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + }); + + // Get today's progress + const todayProgress = await UserProgress.findAll({ + where: { + user_id: userId, + studied_at: { [Op.between]: [startOfToday, endOfToday] }, + }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + }); + + // Get this week's progress + const weekProgress = await UserProgress.findAll({ + where: { + user_id: userId, + studied_at: { [Op.between]: [startOfWeek, endOfWeek] }, + }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + }); + + // Calculate statistics + const calculateStats = (progressData) => { + const flashAttempts = progressData.filter( + (r) => r.AiChatHistory?.response_type === "flashcard" + ); + const quizRows = progressData.filter( + (r) => r.AiChatHistory?.response_type === "quiz" + ); + + const flashStudied = flashAttempts.length; + const flashCorrect = flashAttempts.filter( + (r) => r.is_correct === true + ).length; + const flashAccuracy = flashStudied + ? Math.round((flashCorrect / flashStudied) * 100) + : 0; + + const quizAttempts = quizRows.filter((r) => r.score != null).length; + const quizAvgScore = quizAttempts + ? Math.round( + quizRows + .filter((r) => r.score != null) + .reduce((s, r) => s + r.score, 0) / quizAttempts + ) + : 0; + + return { + flashStudied, + flashCorrect, + flashAccuracy, + quizAttempts, + quizAvgScore, + totalSessions: progressData.length, + }; + }; + + const allTimeStats = calculateStats(allTimeProgress); + const todayStats = calculateStats(todayProgress); + const weekStats = calculateStats(weekProgress); + + res.json({ + today: todayStats, + this_week: weekStats, + all_time: allTimeStats, + streak_days: weekProgress.length > 0 ? 1 : 0, // Simple streak calculation + }); + } catch (e) { + console.error("Progress summary error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Get daily progress for the last 7 days for charts +router.get("/daily-chart/:userId", async (req, res) => { + const { userId } = req.params; + + try { + const chartData = []; + const today = new Date(); + + // Get data for the last 7 days + for (let i = 6; i >= 0; i--) { + const date = new Date(today); + date.setDate(today.getDate() - i); + + const start = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + 0, + 0, + 0, + 0 + ); + const end = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + 23, + 59, + 59, + 999 + ); + + const rows = await UserProgress.findAll({ + where: { + user_id: userId, + studied_at: { [Op.between]: [start, end] }, + }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + order: [["studied_at", "ASC"]], + }); + + const flashAttempts = rows.filter( + (r) => r.AiChatHistory?.response_type === "flashcard" + ); + const quizRows = rows.filter( + (r) => r.AiChatHistory?.response_type === "quiz" + ); + + const flashStudied = flashAttempts.length; + const flashCorrect = flashAttempts.filter( + (r) => r.is_correct === true + ).length; + const flashAccuracy = flashStudied + ? Math.round((flashCorrect / flashStudied) * 100) + : 0; + + const quizAttempts = quizRows.filter((r) => r.score != null).length; + const quizAvgScore = quizAttempts + ? Math.round( + quizRows + .filter((r) => r.score != null) + .reduce((s, r) => s + r.score, 0) / quizAttempts + ) + : 0; + + chartData.push({ + date: date.toISOString().split("T")[0], + day: date.toLocaleDateString("en-US", { weekday: "short" }), + flashcardAccuracy: flashAccuracy, + quizScore: quizAvgScore, + flashcardCount: flashStudied, + quizCount: quizAttempts, + }); + } + + res.json({ + chartData, + dateRange: { + start: new Date(today.getTime() - 6 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + end: today.toISOString().split("T")[0], + }, + }); + } catch (e) { + console.error("Daily chart data error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +module.exports = router; diff --git a/api/index.js b/api/index.js index c176952..9fe18d8 100644 --- a/api/index.js +++ b/api/index.js @@ -8,6 +8,7 @@ const CalculatorRouter = require("./Calculator"); const signUp = require("../auth/index"); const chatRouter = require("./aichathistory"); const streakSessionRouter = require("./StreakSession"); +const UserProgressRouter = require("./UserProgress"); router.use("/test-db", testDbRouter); router.use("/", taskOrganizerRouter); @@ -16,5 +17,6 @@ router.use("/", timerData); router.use("/signup", signUp.router); router.use("/chat", chatRouter); router.use("/sessions", streakSessionRouter); +router.use("/progress", UserProgressRouter); module.exports = router; diff --git a/app.js b/app.js index 8da27e1..0c764a4 100644 --- a/app.js +++ b/app.js @@ -30,6 +30,11 @@ app.use(express.static(path.join(__dirname, "public"))); // serve static files f app.use("/api", apiRouter); // mount api router app.use("/auth", authRouter); // mount auth router +// Route to serve the chart page +app.get("/chart", (req, res) => { + res.sendFile(path.join(__dirname, "public", "chart.html")); +}); + // error handling middleware app.use((err, req, res, next) => { console.error(err.stack); diff --git a/database/StreakSession.js b/database/StreakSession.js index d74e11d..69dddf9 100644 --- a/database/StreakSession.js +++ b/database/StreakSession.js @@ -5,9 +5,15 @@ const StreakSession = db.define( "StreakSession", { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, - userId: { type: DataTypes.INTEGER, allowNull: false }, + user_id: { type: DataTypes.INTEGER, allowNull: false }, startTime: { type: DataTypes.DATE, allowNull: false }, endTime: { type: DataTypes.DATE, allowNull: true }, + session_type: { + type: DataTypes.ENUM("study", "break", "review"), + allowNull: false, + defaultValue: "study", + }, + notes: { type: DataTypes.TEXT, allowNull: true }, }, { timestamps: true } ); diff --git a/database/UserProgress.js b/database/UserProgress.js new file mode 100644 index 0000000..cc77d1f --- /dev/null +++ b/database/UserProgress.js @@ -0,0 +1,17 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const UserProgress = db.define("UserProgress", { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, // primary key + user_id: { type: DataTypes.INTEGER, allowNull: false }, // foreign key to User.id + ai_chat_history_id: { type: DataTypes.INTEGER, allowNull: false }, // foreign key to AiChatHistory.id + card_index: { type: DataTypes.INTEGER, allowNull: true }, // for flashcards only + is_correct: { type: DataTypes.BOOLEAN, allowNull: true }, // for flashcards only + score: { type: DataTypes.INTEGER, allowNull: true }, // for quizzes only + duration_ms: { type: DataTypes.INTEGER, allowNull: true }, // for flashcards only + session_id: { type: DataTypes.STRING, allowNull: true }, // for flashcards only + + studied_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, +}); + +module.exports = UserProgress; diff --git a/database/index.js b/database/index.js index 138aa27..44702f9 100644 --- a/database/index.js +++ b/database/index.js @@ -8,6 +8,7 @@ const Reminder = require("./Reminder"); const Session = require("./Session"); const AiChatHistory = require("./aichathistory"); const StreakSession = require("./StreakSession"); +const UserProgress = require("./UserProgress"); // Define associations User.hasMany(Tasks, { foreignKey: "user_id" }); // One user can have many tasks @@ -27,7 +28,13 @@ AiChatHistory.belongsTo(User, { foreignKey: "user_id" }); User.hasMany(StreakSession, { foreignKey: "user_id" }); StreakSession.belongsTo(User, { foreignKey: "user_id" }); -// Export everything + +UserProgress.belongsTo(User, { foreignKey: "user_id" }); +UserProgress.belongsTo(AiChatHistory, { foreignKey: "ai_chat_history_id" }); + +User.hasMany(UserProgress, { foreignKey: "user_id" }); +AiChatHistory.hasMany(UserProgress, { foreignKey: "ai_chat_history_id" }); + module.exports = { db, User, @@ -37,4 +44,5 @@ module.exports = { Reminder, Session, StreakSession, + UserProgress, }; diff --git a/database/seed.js b/database/seed.js index 8e5cef6..1d54e2e 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,13 @@ const db = require("./db"); -const { User, Tasks, Calculator, Reminder, Session } = require("./index"); +const { + User, + Tasks, + Calculator, + Reminder, + Session, + AiChatHistory, + UserProgress, +} = require("./index"); const seed = async () => { try { @@ -21,6 +29,81 @@ const seed = async () => { }, ]); + // Create multiple AI chat histories for different days + const aiChatHistories = await AiChatHistory.bulkCreate([ + { + user_id: 1, + user_request: "Make me flashcards", + ai_response: "JSON flashcards...", + response_type: "flashcard", + status: "success", + }, + { + user_id: 1, + user_request: "Create a quiz", + ai_response: "JSON quiz...", + response_type: "quiz", + status: "success", + }, + { + user_id: 1, + user_request: "More flashcards", + ai_response: "JSON flashcards...", + response_type: "flashcard", + status: "success", + }, + { + user_id: 1, + user_request: "Another quiz", + ai_response: "JSON quiz...", + response_type: "quiz", + status: "success", + }, + ]); + + // Create UserProgress entries for the last 7 days with realistic data + const today = new Date(); + const userProgressData = []; + + // Generate data for the last 7 days + for (let i = 6; i >= 0; i--) { + const date = new Date(today); + date.setDate(today.getDate() - i); + + // Each day has some flashcard attempts and quiz attempts + const flashcardAttempts = Math.floor(Math.random() * 5) + 3; // 3-7 attempts + const quizAttempts = Math.floor(Math.random() * 3) + 1; // 1-3 attempts + + // Create flashcard attempts for this day + for (let j = 0; j < flashcardAttempts; j++) { + const isCorrect = Math.random() > 0.3; // 70% accuracy on average + userProgressData.push({ + user_id: 1, + ai_chat_history_id: 1, // flashcard + studied_at: new Date(date.getTime() + j * 60000), // spread throughout the day + is_correct: isCorrect, + score: null, + card_index: j, + }); + } + + // Create quiz attempts for this day + for (let j = 0; j < quizAttempts; j++) { + const score = Math.floor(Math.random() * 30) + 70; // 70-100 score + userProgressData.push({ + user_id: 1, + ai_chat_history_id: 2, // quiz + studied_at: new Date( + date.getTime() + (j + flashcardAttempts) * 60000 + ), + is_correct: null, + score: score, + }); + } + } + + await UserProgress.bulkCreate(userProgressData); + // Create a Task const tasks = await Tasks.bulkCreate([ { @@ -64,7 +147,7 @@ const seed = async () => { remind: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }); - console.log("🌱 Seeded the database!"); + console.log("🌱 Seeded the database"); } catch (error) { console.error("āŒ Error seeding database:", error); From 19a87825c7f3dc52898ae45faa5fcc50f2cd82ca Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Mon, 18 Aug 2025 19:00:12 -0400 Subject: [PATCH 37/53] edited get route to return cleaner date format --- api/timerData.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/api/timerData.js b/api/timerData.js index d541504..dfbdbc8 100644 --- a/api/timerData.js +++ b/api/timerData.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); const { Session } = require("../database"); - +const { Sequelize } = require("sequelize"); //getting a study durations by userId(foregin key that references the id in users table) router.get("/data/:userId", async (req, res) => { const { userId } = req.params; @@ -10,11 +10,17 @@ router.get("/data/:userId", async (req, res) => { where: { user_id: userId, }, - attributes: ["duration", "created_at"], + attributes: [ + "duration", + [ + Sequelize.literal(`to_char("created_at",'MM-DD-YYYY')`), + "formattedDate", + ], + ], }); - res.json(sessions); } catch (error) { + console.log("ERR:", error); res.sendStatus(501); } }); From 3f56235dd09520ac66cd8f06449dad379223ad67 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Mon, 18 Aug 2025 22:46:36 -0400 Subject: [PATCH 38/53] Minor route fixing and new badge route Fix some minor route issue and introduce a new route for user --- api/UserProgress.js | 33 ++- api/badges.js | 313 +++++++++++++++++++++ api/index.js | 2 + components/UserAreaChart/AreaChart.jsx | 125 ++++++++ components/UserBarChart/BarChart.jsx | 96 +++++++ components/UserRadialChart/RadialChart.jsx | 124 ++++++++ database/Badge.js | 33 +++ database/UserBadge.js | 13 + database/index.js | 11 + database/seed.js | 81 ++++++ 10 files changed, 830 insertions(+), 1 deletion(-) create mode 100644 api/badges.js create mode 100644 components/UserAreaChart/AreaChart.jsx create mode 100644 components/UserBarChart/BarChart.jsx create mode 100644 components/UserRadialChart/RadialChart.jsx create mode 100644 database/Badge.js create mode 100644 database/UserBadge.js diff --git a/api/UserProgress.js b/api/UserProgress.js index 910263a..40a0d15 100644 --- a/api/UserProgress.js +++ b/api/UserProgress.js @@ -3,6 +3,9 @@ const router = express.Router(); const { Op } = require("sequelize"); const { UserProgress, AiChatHistory } = require("../database"); +// Import badge checking function +const { checkAndAwardBadges } = require("./badges"); + router.get("/daily/:userId", async (req, res) => { const { userId } = req.params; @@ -307,9 +310,13 @@ router.post("/flashcard-progress", async (req, res) => { studied_at: new Date(), }); + // āœ… Check for newly earned badges + const newlyEarnedBadges = await checkAndAwardBadges(user_id); + res.status(201).json({ message: "Flashcard progress recorded successfully", progress: progressEntry, + newlyEarnedBadges: newlyEarnedBadges, }); } catch (e) { console.error("Record flashcard progress error āŒ", e); @@ -343,9 +350,13 @@ router.post("/quiz-progress", async (req, res) => { studied_at: new Date(), }); + // āœ… Check for newly earned badges + const newlyEarnedBadges = await checkAndAwardBadges(user_id); + res.status(201).json({ message: "Quiz progress recorded successfully", progress: progressEntry, + newlyEarnedBadges: newlyEarnedBadges, }); } catch (e) { console.error("Record quiz progress error āŒ", e); @@ -489,6 +500,18 @@ router.get("/summary/:userId", async (req, res) => { ) : 0; + // āœ… Calculate total study time from duration_ms + const totalStudyTime = progressData.reduce((total, record) => { + return total + (record.duration_ms || 0); + }, 0); + + // āœ… Format study time + const hours = Math.floor(totalStudyTime / (1000 * 60 * 60)); + const minutes = Math.floor( + (totalStudyTime % (1000 * 60 * 60)) / (1000 * 60) + ); + const formattedStudyTime = `${hours}h ${minutes}m`; + return { flashStudied, flashCorrect, @@ -496,6 +519,8 @@ router.get("/summary/:userId", async (req, res) => { quizAttempts, quizAvgScore, totalSessions: progressData.length, + totalStudyTime: formattedStudyTime, // āœ… Add study time + totalStudyTimeMs: totalStudyTime, // āœ… Raw milliseconds for calculations }; }; @@ -507,7 +532,7 @@ router.get("/summary/:userId", async (req, res) => { today: todayStats, this_week: weekStats, all_time: allTimeStats, - streak_days: weekProgress.length > 0 ? 1 : 0, // Simple streak calculation + // āœ… Removed inaccurate streak_days - use /api/sessions/streak/:userId for accurate streak data }); } catch (e) { console.error("Progress summary error āŒ", e); @@ -585,6 +610,11 @@ router.get("/daily-chart/:userId", async (req, res) => { ) : 0; + // āœ… Calculate daily study time + const dailyStudyTime = rows.reduce((total, record) => { + return total + (record.duration_ms || 0); + }, 0); + chartData.push({ date: date.toISOString().split("T")[0], day: date.toLocaleDateString("en-US", { weekday: "short" }), @@ -592,6 +622,7 @@ router.get("/daily-chart/:userId", async (req, res) => { quizScore: quizAvgScore, flashcardCount: flashStudied, quizCount: quizAttempts, + duration_ms: dailyStudyTime, // āœ… Add daily study time }); } diff --git a/api/badges.js b/api/badges.js new file mode 100644 index 0000000..57ea3ef --- /dev/null +++ b/api/badges.js @@ -0,0 +1,313 @@ +const express = require("express"); +const router = express.Router(); +const { Op } = require("sequelize"); +const { + Badge, + UserBadge, + UserProgress, + AiChatHistory, +} = require("../database"); + +// Get all available badges +router.get("/", async (req, res) => { + try { + const badges = await Badge.findAll({ + order: [ + ["category", "ASC"], + ["requirement_value", "ASC"], + ], + }); + res.json(badges); + } catch (error) { + console.error("Error fetching badges:", error); + res.status(500).json({ error: "Failed to fetch badges" }); + } +}); + +// Get user's earned badges +router.get("/user/:userId", async (req, res) => { + const { userId } = req.params; + + try { + const userBadges = await UserBadge.findAll({ + where: { user_id: userId }, + include: [ + { + model: Badge, + attributes: [ + "id", + "name", + "description", + "icon", + "category", + "rarity", + "points", + ], + }, + ], + order: [["earned_at", "DESC"]], + }); + + res.json(userBadges); + } catch (error) { + console.error("Error fetching user badges:", error); + res.status(500).json({ error: "Failed to fetch user badges" }); + } +}); + +// Get user's badge progress (earned + progress towards unearned) +router.get("/progress/:userId", async (req, res) => { + const { userId } = req.params; + + try { + // Get all badges + const allBadges = await Badge.findAll({ + order: [ + ["category", "ASC"], + ["requirement_value", "ASC"], + ], + }); + + // Get user's earned badges + const earnedBadges = await UserBadge.findAll({ + where: { user_id: userId }, + include: [ + { + model: Badge, + attributes: [ + "id", + "name", + "description", + "icon", + "category", + "rarity", + "points", + ], + }, + ], + }); + + const earnedBadgeIds = earnedBadges.map((ub) => ub.badge_id); + + // Calculate user's current stats + const userStats = await calculateUserStats(userId); + + // Build progress array + const badgeProgress = allBadges.map((badge) => { + const earned = earnedBadges.find((ub) => ub.badge_id === badge.id); + const currentValue = getCurrentValue(badge.requirement_type, userStats); + const isEarned = earned !== undefined; + const progress = Math.min( + (currentValue / badge.requirement_value) * 100, + 100 + ); + + return { + badge: { + id: badge.id, + name: badge.name, + description: badge.description, + icon: badge.icon, + category: badge.category, + rarity: badge.rarity, + points: badge.points, + requirement_type: badge.requirement_type, + requirement_value: badge.requirement_value, + }, + earned: isEarned, + earned_at: earned?.earned_at, + current_value: currentValue, + progress_percentage: progress, + is_new: earned?.is_new || false, + }; + }); + + res.json({ + badgeProgress, + totalEarned: earnedBadges.length, + totalBadges: allBadges.length, + userStats, + }); + } catch (error) { + console.error("Error calculating badge progress:", error); + res.status(500).json({ error: "Failed to calculate badge progress" }); + } +}); + +// Mark badge as viewed (remove "NEW" status) +router.put("/view/:userId/:badgeId", async (req, res) => { + const { userId, badgeId } = req.params; + + try { + await UserBadge.update( + { is_new: false }, + { where: { user_id: userId, badge_id: badgeId } } + ); + + res.json({ message: "Badge marked as viewed" }); + } catch (error) { + console.error("Error marking badge as viewed:", error); + res.status(500).json({ error: "Failed to mark badge as viewed" }); + } +}); + +// Check and award badges (called after user activity) +router.post("/check/:userId", async (req, res) => { + const { userId } = req.params; + + try { + const newlyEarnedBadges = await checkAndAwardBadges(userId); + + res.json({ + message: "Badge check completed", + newlyEarned: newlyEarnedBadges, + count: newlyEarnedBadges.length, + }); + } catch (error) { + console.error("Error checking badges:", error); + res.status(500).json({ error: "Failed to check badges" }); + } +}); + +// Helper function to calculate user stats +async function calculateUserStats(userId) { + // Get all user progress + const allProgress = await UserProgress.findAll({ + where: { user_id: userId }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + }); + + // Calculate streak (simplified - you might want to use the streak API) + const today = new Date(); + const startOfToday = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + ); + const todayProgress = allProgress.filter( + (p) => new Date(p.studied_at) >= startOfToday + ); + const currentStreak = todayProgress.length > 0 ? 7 : 0; // Simplified for now + + // Calculate quiz count + const quizProgress = allProgress.filter( + (p) => p.AiChatHistory?.response_type === "quiz" + ); + const quizCount = quizProgress.length; + + // Calculate accuracy + const flashcardProgress = allProgress.filter( + (p) => p.AiChatHistory?.response_type === "flashcard" + ); + const correctFlashcards = flashcardProgress.filter( + (p) => p.is_correct === true + ).length; + const totalFlashcards = flashcardProgress.length; + const accuracy = + totalFlashcards > 0 + ? Math.round((correctFlashcards / totalFlashcards) * 100) + : 0; + + // Calculate average completion time + const validDurations = allProgress.filter( + (p) => p.duration_ms && p.duration_ms > 0 + ); + const avgCompletionTime = + validDurations.length > 0 + ? Math.round( + validDurations.reduce((sum, p) => sum + p.duration_ms, 0) / + validDurations.length + ) + : 0; + + // Calculate total study days + const uniqueDays = [ + ...new Set( + allProgress.map((p) => new Date(p.studied_at).toISOString().split("T")[0]) + ), + ]; + const totalDays = uniqueDays.length; + + return { + currentStreak, + quizCount, + accuracy, + avgCompletionTime, + totalDays, + }; +} + +// Helper function to get current value for a requirement type +function getCurrentValue(requirementType, userStats) { + switch (requirementType) { + case "streak_days": + return userStats.currentStreak; + case "quiz_count": + return userStats.quizCount; + case "accuracy_percentage": + return userStats.accuracy; + case "completion_time": + return userStats.avgCompletionTime; + case "total_days": + return userStats.totalDays; + default: + return 0; + } +} + +// Helper function to check and award badges +async function checkAndAwardBadges(userId) { + const userStats = await calculateUserStats(userId); + const allBadges = await Badge.findAll(); + const earnedBadges = await UserBadge.findAll({ + where: { user_id: userId }, + attributes: ["badge_id"], + }); + + const earnedBadgeIds = earnedBadges.map((ub) => ub.badge_id); + const newlyEarnedBadges = []; + + for (const badge of allBadges) { + // Skip if already earned + if (earnedBadgeIds.includes(badge.id)) { + continue; + } + + const currentValue = getCurrentValue(badge.requirement_type, userStats); + + // Check if badge should be awarded + if (currentValue >= badge.requirement_value) { + // Award the badge + const userBadge = await UserBadge.create({ + user_id: userId, + badge_id: badge.id, + progress_value: currentValue, + is_new: true, + }); + + newlyEarnedBadges.push({ + badge: { + id: badge.id, + name: badge.name, + description: badge.description, + icon: badge.icon, + category: badge.category, + rarity: badge.rarity, + points: badge.points, + }, + earned_at: userBadge.earned_at, + progress_value: currentValue, + }); + } + } + + return newlyEarnedBadges; +} + +module.exports = router; +module.exports.checkAndAwardBadges = checkAndAwardBadges; diff --git a/api/index.js b/api/index.js index 9fe18d8..0e06832 100644 --- a/api/index.js +++ b/api/index.js @@ -9,6 +9,7 @@ const signUp = require("../auth/index"); const chatRouter = require("./aichathistory"); const streakSessionRouter = require("./StreakSession"); const UserProgressRouter = require("./UserProgress"); +const badgesRouter = require("./badges"); router.use("/test-db", testDbRouter); router.use("/", taskOrganizerRouter); @@ -18,5 +19,6 @@ router.use("/signup", signUp.router); router.use("/chat", chatRouter); router.use("/sessions", streakSessionRouter); router.use("/progress", UserProgressRouter); +router.use("/badges", badgesRouter); module.exports = router; diff --git a/components/UserAreaChart/AreaChart.jsx b/components/UserAreaChart/AreaChart.jsx new file mode 100644 index 0000000..403cfa5 --- /dev/null +++ b/components/UserAreaChart/AreaChart.jsx @@ -0,0 +1,125 @@ +"use client"; + +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export const description = "Area chart showing daily progress trends"; + +export function ProgressAreaChart({ data }) { + if (!data || data.length === 0) { + return ( + + + šŸ“ˆ Daily Progress Trends + + Flashcard accuracy and study minutes over time + + + +
+

No data available

+

+ Complete some study sessions to see your progress +

+
+
+
+ ); + } + + return ( + + + šŸ“ˆ Daily Progress Trends + + Flashcard accuracy and study minutes over time + + + + + + + + + + { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((entry, index) => ( +

+ {entry.name}: {entry.value} +

+ ))} +
+ ); + } + return null; + }} + /> + + +
+
+
+
+ ); +} diff --git a/components/UserBarChart/BarChart.jsx b/components/UserBarChart/BarChart.jsx new file mode 100644 index 0000000..7b236de --- /dev/null +++ b/components/UserBarChart/BarChart.jsx @@ -0,0 +1,96 @@ +"use client"; + +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export const description = "Bar chart showing weekly study activity"; + +export function WeeklyActivityBarChart({ data }) { + if (!data || data.length === 0) { + return ( + + + ā° Study Time Analysis + Time spent studying each day + + +
+

No data available

+

+ Complete some study sessions to see your activity +

+
+
+
+ ); + } + + return ( + + + ā° Study Time Analysis + Time spent studying each day + + + + + + + + { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((entry, index) => ( +

+ {entry.name}: {entry.value} minutes +

+ ))} +
+ ); + } + return null; + }} + /> + +
+
+
+
+ ); +} diff --git a/components/UserRadialChart/RadialChart.jsx b/components/UserRadialChart/RadialChart.jsx new file mode 100644 index 0000000..d59713f --- /dev/null +++ b/components/UserRadialChart/RadialChart.jsx @@ -0,0 +1,124 @@ +"use client"; + +import { TrendingUp } from "lucide-react"; +import { + RadialBar, + RadialBarChart, + ResponsiveContainer, + Tooltip, +} from "recharts"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export const description = + "Streak progress radial chart showing current streak vs target"; + +// Transform streak data for radial chart +const transformStreakData = (streakData) => { + if (!streakData) return []; + + // Handle both normalized and raw streak data + const current = streakData.current || streakData.currentStreak || 0; + const goal = streakData.goal || streakData.nextMilestone || 30; + const percent = + streakData.percent || Math.min(100, Math.round((current / goal) * 100)); + + // Create data for the radial chart + return [ + { + name: "Current Progress", + value: percent, + fill: "#3b82f6", + streak: current, + goal: goal, + }, + ]; +}; + +export function StreakProgressRadialChart({ data: streakData }) { + const transformedData = transformStreakData(streakData); + + // Handle both normalized and raw streak data + const current = streakData?.current || streakData?.currentStreak || 0; + const goal = streakData?.goal || streakData?.nextMilestone || 30; + const percent = + streakData?.percent || Math.min(100, Math.round((current / goal) * 100)); + + // Calculate trend (comparing current vs goal) + const isTrendingUp = current > 0; + const progressToGoal = goal > 0 ? Math.round((current / goal) * 100) : 0; + + return ( + + + šŸ”„ Streak Progress + + Current streak progress towards next milestone + + + + + + + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.name}

+

+ {data.streak} / {data.goal} days ({data.value}%) +

+
+ ); + } + return null; + }} + /> +
+
+
+ +
+ {isTrendingUp ? ( + <> + Great Progress! + + ) : ( + <> + Keep Going! + + )} +
+
+ {progressToGoal}% to next milestone ({goal} days) +
+
+ {current} day{current !== 1 ? "s" : ""} streak +
+
+
+ ); +} diff --git a/database/Badge.js b/database/Badge.js new file mode 100644 index 0000000..30e4eb1 --- /dev/null +++ b/database/Badge.js @@ -0,0 +1,33 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Badge = db.define("Badge", { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + name: { type: DataTypes.STRING, allowNull: false, unique: true }, + description: { type: DataTypes.TEXT, allowNull: false }, + icon: { type: DataTypes.STRING, allowNull: false }, // emoji or icon name + category: { + type: DataTypes.ENUM("streak", "quiz", "accuracy", "speed", "milestone"), + allowNull: false, + }, + requirement_type: { + type: DataTypes.ENUM( + "streak_days", + "quiz_count", + "accuracy_percentage", + "completion_time", + "total_days" + ), + allowNull: false, + }, + requirement_value: { type: DataTypes.INTEGER, allowNull: false }, + rarity: { + type: DataTypes.ENUM("common", "rare", "epic", "legendary"), + allowNull: false, + defaultValue: "common", + }, + points: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, // points awarded when earned + created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, +}); + +module.exports = Badge; diff --git a/database/UserBadge.js b/database/UserBadge.js new file mode 100644 index 0000000..37f87f3 --- /dev/null +++ b/database/UserBadge.js @@ -0,0 +1,13 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const UserBadge = db.define("UserBadge", { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + user_id: { type: DataTypes.INTEGER, allowNull: false }, // foreign key to User.id + badge_id: { type: DataTypes.INTEGER, allowNull: false }, // foreign key to Badge.id + earned_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + progress_value: { type: DataTypes.INTEGER, allowNull: false }, // the value when badge was earned + is_new: { type: DataTypes.BOOLEAN, defaultValue: true }, // for showing "NEW" indicator +}); + +module.exports = UserBadge; diff --git a/database/index.js b/database/index.js index 44702f9..35d4842 100644 --- a/database/index.js +++ b/database/index.js @@ -9,6 +9,8 @@ const Session = require("./Session"); const AiChatHistory = require("./aichathistory"); const StreakSession = require("./StreakSession"); const UserProgress = require("./UserProgress"); +const Badge = require("./Badge"); +const UserBadge = require("./UserBadge"); // Define associations User.hasMany(Tasks, { foreignKey: "user_id" }); // One user can have many tasks @@ -35,6 +37,13 @@ UserProgress.belongsTo(AiChatHistory, { foreignKey: "ai_chat_history_id" }); User.hasMany(UserProgress, { foreignKey: "user_id" }); AiChatHistory.hasMany(UserProgress, { foreignKey: "ai_chat_history_id" }); +// Badge associations +User.hasMany(UserBadge, { foreignKey: "user_id" }); +UserBadge.belongsTo(User, { foreignKey: "user_id" }); + +Badge.hasMany(UserBadge, { foreignKey: "badge_id" }); +UserBadge.belongsTo(Badge, { foreignKey: "badge_id" }); + module.exports = { db, User, @@ -45,4 +54,6 @@ module.exports = { Session, StreakSession, UserProgress, + Badge, + UserBadge, }; diff --git a/database/seed.js b/database/seed.js index 1d54e2e..136f616 100644 --- a/database/seed.js +++ b/database/seed.js @@ -7,6 +7,8 @@ const { Session, AiChatHistory, UserProgress, + Badge, + UserBadge, } = require("./index"); const seed = async () => { @@ -77,6 +79,7 @@ const seed = async () => { // Create flashcard attempts for this day for (let j = 0; j < flashcardAttempts; j++) { const isCorrect = Math.random() > 0.3; // 70% accuracy on average + const duration = Math.floor(Math.random() * 30000) + 10000; // 10-40 seconds userProgressData.push({ user_id: 1, ai_chat_history_id: 1, // flashcard @@ -84,12 +87,15 @@ const seed = async () => { is_correct: isCorrect, score: null, card_index: j, + duration_ms: duration, // āœ… Add duration + session_id: `session_${date.toISOString().split("T")[0]}_${j}`, // āœ… Add session ID }); } // Create quiz attempts for this day for (let j = 0; j < quizAttempts; j++) { const score = Math.floor(Math.random() * 30) + 70; // 70-100 score + const duration = Math.floor(Math.random() * 120000) + 60000; // 1-3 minutes userProgressData.push({ user_id: 1, ai_chat_history_id: 2, // quiz @@ -98,6 +104,10 @@ const seed = async () => { ), is_correct: null, score: score, + duration_ms: duration, // āœ… Add duration + session_id: `session_${date.toISOString().split("T")[0]}_${ + j + flashcardAttempts + }`, // āœ… Add session ID }); } } @@ -147,6 +157,77 @@ const seed = async () => { remind: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), }); + // Create badges + const badges = await Badge.bulkCreate([ + { + name: "Week Warrior", + description: "Maintain a 7-day study streak", + icon: "šŸ”„", + category: "streak", + requirement_type: "streak_days", + requirement_value: 7, + rarity: "common", + points: 100, + }, + { + name: "Quiz Master", + description: "Complete 50 quizzes", + icon: "🧠", + category: "quiz", + requirement_type: "quiz_count", + requirement_value: 50, + rarity: "rare", + points: 250, + }, + { + name: "Accuracy Ace", + description: "Achieve 90% flashcard accuracy", + icon: "šŸŽÆ", + category: "accuracy", + requirement_type: "accuracy_percentage", + requirement_value: 90, + rarity: "epic", + points: 500, + }, + { + name: "Speed Demon", + description: + "Complete activities with fast average time (under 30 seconds)", + icon: "⚔", + category: "speed", + requirement_type: "completion_time", + requirement_value: 30000, // 30 seconds in milliseconds + rarity: "rare", + points: 300, + }, + { + name: "Century Club", + description: "Maintain a 100-day study streak", + icon: "šŸ‘‘", + category: "milestone", + requirement_type: "streak_days", + requirement_value: 100, + rarity: "legendary", + points: 1000, + }, + ]); + + // Create UserBadge entries to link users with badges + await UserBadge.bulkCreate([ + { + user_id: 1, + badge_id: 1, // Week Warrior - user has 7 day streak + earned_at: new Date(), + progress_value: 7, // 7 day streak achieved + }, + { + user_id: 1, + badge_id: 4, // Speed Demon - user has fast completion time + earned_at: new Date(), + progress_value: 52682, // completion time in ms + }, + ]); + console.log("🌱 Seeded the database"); } catch (error) { console.error("āŒ Error seeding database:", error); From 0e676f69d97e2c8bb8f299391132871bfee4a23d Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Mon, 18 Aug 2025 22:57:55 -0400 Subject: [PATCH 39/53] Deleted some files --- components/UserAreaChart/AreaChart.jsx | 125 --------------------- components/UserBarChart/BarChart.jsx | 96 ---------------- components/UserRadialChart/RadialChart.jsx | 124 -------------------- 3 files changed, 345 deletions(-) delete mode 100644 components/UserAreaChart/AreaChart.jsx delete mode 100644 components/UserBarChart/BarChart.jsx delete mode 100644 components/UserRadialChart/RadialChart.jsx diff --git a/components/UserAreaChart/AreaChart.jsx b/components/UserAreaChart/AreaChart.jsx deleted file mode 100644 index 403cfa5..0000000 --- a/components/UserAreaChart/AreaChart.jsx +++ /dev/null @@ -1,125 +0,0 @@ -"use client"; - -import { - Area, - AreaChart, - CartesianGrid, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; - -export const description = "Area chart showing daily progress trends"; - -export function ProgressAreaChart({ data }) { - if (!data || data.length === 0) { - return ( - - - šŸ“ˆ Daily Progress Trends - - Flashcard accuracy and study minutes over time - - - -
-

No data available

-

- Complete some study sessions to see your progress -

-
-
-
- ); - } - - return ( - - - šŸ“ˆ Daily Progress Trends - - Flashcard accuracy and study minutes over time - - - - - - - - - - { - if (active && payload && payload.length) { - return ( -
-

{label}

- {payload.map((entry, index) => ( -

- {entry.name}: {entry.value} -

- ))} -
- ); - } - return null; - }} - /> - - -
-
-
-
- ); -} diff --git a/components/UserBarChart/BarChart.jsx b/components/UserBarChart/BarChart.jsx deleted file mode 100644 index 7b236de..0000000 --- a/components/UserBarChart/BarChart.jsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { - Bar, - BarChart, - CartesianGrid, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; - -export const description = "Bar chart showing weekly study activity"; - -export function WeeklyActivityBarChart({ data }) { - if (!data || data.length === 0) { - return ( - - - ā° Study Time Analysis - Time spent studying each day - - -
-

No data available

-

- Complete some study sessions to see your activity -

-
-
-
- ); - } - - return ( - - - ā° Study Time Analysis - Time spent studying each day - - - - - - - - { - if (active && payload && payload.length) { - return ( -
-

{label}

- {payload.map((entry, index) => ( -

- {entry.name}: {entry.value} minutes -

- ))} -
- ); - } - return null; - }} - /> - -
-
-
-
- ); -} diff --git a/components/UserRadialChart/RadialChart.jsx b/components/UserRadialChart/RadialChart.jsx deleted file mode 100644 index d59713f..0000000 --- a/components/UserRadialChart/RadialChart.jsx +++ /dev/null @@ -1,124 +0,0 @@ -"use client"; - -import { TrendingUp } from "lucide-react"; -import { - RadialBar, - RadialBarChart, - ResponsiveContainer, - Tooltip, -} from "recharts"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; - -export const description = - "Streak progress radial chart showing current streak vs target"; - -// Transform streak data for radial chart -const transformStreakData = (streakData) => { - if (!streakData) return []; - - // Handle both normalized and raw streak data - const current = streakData.current || streakData.currentStreak || 0; - const goal = streakData.goal || streakData.nextMilestone || 30; - const percent = - streakData.percent || Math.min(100, Math.round((current / goal) * 100)); - - // Create data for the radial chart - return [ - { - name: "Current Progress", - value: percent, - fill: "#3b82f6", - streak: current, - goal: goal, - }, - ]; -}; - -export function StreakProgressRadialChart({ data: streakData }) { - const transformedData = transformStreakData(streakData); - - // Handle both normalized and raw streak data - const current = streakData?.current || streakData?.currentStreak || 0; - const goal = streakData?.goal || streakData?.nextMilestone || 30; - const percent = - streakData?.percent || Math.min(100, Math.round((current / goal) * 100)); - - // Calculate trend (comparing current vs goal) - const isTrendingUp = current > 0; - const progressToGoal = goal > 0 ? Math.round((current / goal) * 100) : 0; - - return ( - - - šŸ”„ Streak Progress - - Current streak progress towards next milestone - - - - - - - { - if (active && payload && payload.length) { - const data = payload[0].payload; - return ( -
-

{data.name}

-

- {data.streak} / {data.goal} days ({data.value}%) -

-
- ); - } - return null; - }} - /> -
-
-
- -
- {isTrendingUp ? ( - <> - Great Progress! - - ) : ( - <> - Keep Going! - - )} -
-
- {progressToGoal}% to next milestone ({goal} days) -
-
- {current} day{current !== 1 ? "s" : ""} streak -
-
-
- ); -} From c604b16ec50f5c456b4900afa3693999f5594fab Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Tue, 19 Aug 2025 11:36:03 -0400 Subject: [PATCH 40/53] implemented google oauth --- api/TaskOrganizer.js | 152 ++++++++++++++++-------------------------- api/index.js | 4 -- app.js | 4 +- auth/index.js | 152 +++++++++++++++++++++++++++++++++++++++--- config/googleOAuth.js | 139 ++++++++++++++++++++++++++++++++++++++ database/seed.js | 10 +-- database/user.js | 26 +++++++- 7 files changed, 371 insertions(+), 116 deletions(-) create mode 100644 config/googleOAuth.js diff --git a/api/TaskOrganizer.js b/api/TaskOrganizer.js index 64d55b9..2939be1 100644 --- a/api/TaskOrganizer.js +++ b/api/TaskOrganizer.js @@ -3,34 +3,30 @@ const router = express.Router(); const { Op } = require("sequelize"); const { Tasks } = require("../database"); - -// GET all tasks for a user -router.get("/tasks/:userId", async (req, res) => { +// ────────────────────────────────────────────────────────────── +// NOTE: These routes assume app.js has: +// app.use("/api", authenticateJWT, apiRouter) +// so req.user is always available here. +// ────────────────────────────────────────────────────────────── + +// GET all tasks for the logged-in user +router.get("/tasks", async (req, res) => { try { - const userId = req.params.userId; // storing the user ID from the URL - const tasks = await Tasks.findAll({ where: { user_id: userId } }); //This is storing all the data that .findall is getting form the model in Tasks in this case it is filtering where the user ID equals the one found in the URL - res.json(tasks); //Outputting it as a json + const tasks = await Tasks.findAll({ + where: { user_id: req.user.id }, + order: [["createdAt", "DESC"]], + }); + res.json(tasks); } catch (error) { console.error("āŒ Error fetching tasks:", error); res.status(500).json({ error: "Failed to fetch tasks" }); } }); -//POST a new task -//The goal of this is to store the data that the user is sending into the Tasks model -router.post("/tasks/:userId", async (req, res) => { +// POST a new task for the logged-in user +router.post("/tasks", async (req, res) => { try { - const { userId } = req.params; - - //Gets data from the body of the request and stores it in the variables - const { - className, - assignment, - description, - status, - deadline, - priority, - } = req.body; + const { className, assignment, description, status, deadline, priority } = req.body; const newTask = await Tasks.create({ className, @@ -39,7 +35,7 @@ router.post("/tasks/:userId", async (req, res) => { status, deadline, priority, - user_id: userId, // + user_id: req.user.id, // ← use the ID from JWT }); res.status(201).json(newTask); @@ -49,39 +45,22 @@ router.post("/tasks/:userId", async (req, res) => { } }); - -// UPDATE a task by ID -router.put("/tasks/:userId/:taskId", async (req, res) => { +// UPDATE a task by ID (must belong to the logged-in user) +router.put("/tasks/:taskId", async (req, res) => { try { - const { taskId, userId } = req.params; + const { taskId } = req.params; + const { className, assignment, description, status, deadline, priority } = req.body; - //This is storing all of the updated data from the request body(When the user updates an - // "assignment,description" etc we are stroing that new data in these variables - const { className, assignment, description, status, deadline, priority } = - req.body; - - // Finds the task of a specefic user + // Ownership check baked into the WHERE const task = await Tasks.findOne({ - where: { - id: taskId, - user_id: userId, - } + where: { id: taskId, user_id: req.user.id }, }); if (!task) { return res.status(404).json({ error: "Task not found" }); } - // Update the task - await task.update({ - className, - assignment, - description, - status, - deadline, - priority, - }); - + await task.update({ className, assignment, description, status, deadline, priority }); res.json(task); } catch (error) { console.error("āŒ Error updating task:", error); @@ -89,25 +68,20 @@ router.put("/tasks/:userId/:taskId", async (req, res) => { } }); -//DELETE -router.delete("/tasks/:userId/:taskId", async (req, res) => { +// DELETE a task by ID (must belong to the logged-in user) +router.delete("/tasks/:taskId", async (req, res) => { try { - const { userId, taskId } = req.params; + const { taskId } = req.params; - // Searches database and finds the task of a specefic user const task = await Tasks.findOne({ - where: { - id: taskId, - user_id: userId, - }, + where: { id: taskId, user_id: req.user.id }, }); if (!task) { return res.status(404).json({ error: "Task not found for this user" }); } - //If task exists it deletes it - await task.destroy(); + await task.destroy(); res.json({ message: "Task deleted successfully", deletedTask: task }); } catch (error) { console.error("āŒ Error deleting task:", error); @@ -115,17 +89,14 @@ router.delete("/tasks/:userId/:taskId", async (req, res) => { } }); -//PATCH just update the status -router.patch("/tasks/:userId/:taskId", async (req, res) => { +// PATCH status only (must belong to the logged-in user) +router.patch("/tasks/:taskId", async (req, res) => { try { - const { userId, taskId } = req.params; + const { taskId } = req.params; const { status } = req.body; const task = await Tasks.findOne({ - where: { - id: taskId, - user_id: userId, - }, + where: { id: taskId, user_id: req.user.id }, }); if (!task) { @@ -133,7 +104,6 @@ router.patch("/tasks/:userId/:taskId", async (req, res) => { } await task.update({ status }); - res.json(task); } catch (error) { console.error("āŒ Error updating task status:", error); @@ -141,64 +111,58 @@ router.patch("/tasks/:userId/:taskId", async (req, res) => { } }); -//GET -//Filter by status , 'Pending' -- 'Completed' --or 'In-progress' - -router.get("/tasks/:userId/status/:statusTask", async (req, res) => { +// FILTER: by status +router.get("/tasks/status/:statusTask", async (req, res) => { try { - const {userId,statusTask} = req.params; // storing the user ID from the URL - const filteredTasks = await Tasks.findAll({ where: - { user_id: userId, - status: statusTask,} }); //This is storing all the data that .findall is getting form the model in Tasks in this case it is filtering where status equals the one found in the uRL - res.json(filteredTasks); //Outputting it as a json + const { statusTask } = req.params; + + const filteredTasks = await Tasks.findAll({ + where: { user_id: req.user.id, status: statusTask }, + order: [["createdAt", "DESC"]], + }); + + res.json(filteredTasks); } catch (error) { - console.error("āŒ Error fetching tasks:", error); - res.status(500).json({ error: "Failed to fetch tasks" }); + console.error("āŒ Error filtering by status:", error); + res.status(500).json({ error: "Failed to filter tasks by status" }); } }); - -//Filter Tasks by priority -router.get("/tasks/:userId/priority/:priority", async (req, res) => { +// FILTER: by priority +router.get("/tasks/priority/:priority", async (req, res) => { try { - const { userId, priority } = req.params; + const { priority } = req.params; const prioritizedTasks = await Tasks.findAll({ - where: { - user_id: userId, - priority: priority, - }, + where: { user_id: req.user.id, priority }, + order: [["createdAt", "DESC"]], }); res.json(prioritizedTasks); } catch (error) { - console.error("āŒ Error filtering tasks by priority:", error); + console.error("āŒ Error filtering by priority:", error); res.status(500).json({ error: "Failed to filter tasks by priority" }); } }); -//Filter tasks by className - -router.get("/tasks/:userId/class/:className", async (req, res) => { +// FILTER: by className (case-insensitive substring) +router.get("/tasks/class/:className", async (req, res) => { try { - const { userId, className } = req.params; + const { className } = req.params; const classTasks = await Tasks.findAll({ where: { - user_id: userId, - className: { - [Op.iLike]: `%${className}%`, // makes it so that the user can type the name without case sensetive restrictions - }, + user_id: req.user.id, + className: { [Op.iLike]: `%${className}%` }, }, + order: [["createdAt", "DESC"]], }); res.json(classTasks); } catch (error) { - console.error("āŒ Error filtering tasks by class name:", error); + console.error("āŒ Error filtering by class name:", error); res.status(500).json({ error: "Failed to filter tasks by class name" }); } }); - - module.exports = router; diff --git a/api/index.js b/api/index.js index f264d37..b06cadb 100644 --- a/api/index.js +++ b/api/index.js @@ -1,19 +1,15 @@ const express = require("express"); const router = express.Router(); const testDbRouter = require("./test-db"); - - const taskOrganizerRouter = require("./TaskOrganizer"); const timerData = require("./timerData"); const CalculatorRouter = require("./Calculator"); -const signUp = require("../auth/index"); const chatRouter = require("./aichathistory"); router.use("/test-db", testDbRouter); router.use("/", taskOrganizerRouter); router.use("/grade-calculator", CalculatorRouter); router.use("/", timerData); -router.use("/signup", signUp.router); router.use("/chat", chatRouter); module.exports = router; diff --git a/app.js b/app.js index 8da27e1..12fbf52 100644 --- a/app.js +++ b/app.js @@ -6,12 +6,12 @@ const jwt = require("jsonwebtoken"); const cookieParser = require("cookie-parser"); const app = express(); const apiRouter = require("./api"); -const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); const { Model } = require("sequelize"); const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; +const { router: authRouter, authenticateJWT } = require("./auth"); // body parser middleware app.use(express.json()); @@ -27,7 +27,7 @@ app.use(cookieParser()); app.use(morgan("dev")); // logging middleware app.use(express.static(path.join(__dirname, "public"))); // serve static files from public folder -app.use("/api", apiRouter); // mount api router +app.use("/api", authenticateJWT, apiRouter); // mount api router app.use("/auth", authRouter); // mount auth router // error handling middleware diff --git a/auth/index.js b/auth/index.js index d4d9496..32b39c9 100644 --- a/auth/index.js +++ b/auth/index.js @@ -1,5 +1,6 @@ const express = require("express"); const jwt = require("jsonwebtoken"); +const googleOAuth = require("../config/googleOAuth"); const { User } = require("../database"); const router = express.Router(); @@ -106,6 +107,14 @@ router.post("/signup", async (req, res) => { try { const { username, firstName, lastName, password, email } = req.body; + console.log("šŸ“ Signup attempt:", { + username, + firstName, + lastName, + email, + passwordProvided: !!password, + }); + if (!username || !password) { return res .status(400) @@ -118,20 +127,40 @@ router.post("/signup", async (req, res) => { .send({ error: "Password must be at least 6 characters long" }); } - // Check if user already exists - const existingUser = await User.findOne({ where: { username } }); - if (existingUser) { + // Check if user already exists by username + const existingUserByUsername = await User.findOne({ where: { username } }); + if (existingUserByUsername) { return res.status(409).send({ error: "Username already exists" }); } + // Check if user already exists by email (if email provided) + if (email) { + const existingUserByEmail = await User.findOne({ where: { email } }); + if (existingUserByEmail) { + return res.status(409).send({ error: "Email already exists" }); + } + } + // Create new user const passwordHash = User.hashPassword(password); - const user = await User.create({ + const userData = { firstName, lastName, username, - email, + email: email || null, // Handle optional email passwordHash, + }; + + console.log("šŸ”Ø Creating user with data:", { + ...userData, + passwordHash: "[HIDDEN]", + }); + + const user = await User.create(userData); + + console.log("āœ… User created successfully:", { + id: user.id, + username: user.username, }); // Generate JWT token @@ -148,18 +177,38 @@ router.post("/signup", async (req, res) => { res.cookie("token", token, { httpOnly: true, - secure: true, + secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); res.send({ message: "User created successfully", - user: { id: user.id, username: user.username }, + user: { id: user.id, username: user.username, email: user.email }, }); } catch (error) { - console.error("Signup error:", error); - res.sendStatus(500); + console.error("āŒ Signup error:", error); + console.error("āŒ Error details:", { + message: error.message, + name: error.name, + sql: error.sql, + fields: error.fields, + }); + + // Send more specific error messages + if (error.name === "SequelizeUniqueConstraintError") { + return res + .status(409) + .send({ error: "Username or email already exists" }); + } + + if (error.name === "SequelizeValidationError") { + return res + .status(400) + .send({ error: error.errors.map((e) => e.message).join(", ") }); + } + + res.status(500).send({ error: "Internal server error during signup" }); } }); @@ -175,7 +224,7 @@ router.post("/login", async (req, res) => { // Find user const user = await User.findOne({ where: { username } }); - user.checkPassword(password); + if (!user) { return res.status(401).send({ error: "Invalid credentials" }); } @@ -236,4 +285,87 @@ router.get("/me", (req, res) => { }); }); +// ───────────────────────────────────────────────────────────── +// GOOGLE OAUTH ROUTES (Manual Implementation) +// ───────────────────────────────────────────────────────────── + +// Initiate Google OAuth +router.get("/google", (req, res) => { + const authUrl = googleOAuth.getAuthUrl(); + console.log("šŸš€ Redirecting to Google OAuth:", authUrl); + res.redirect(authUrl); +}); + +// Handle Google OAuth callback +router.get("/google/callback", async (req, res) => { + try { + const { code, error } = req.query; + + if (error) { + console.error("āŒ Google OAuth error:", error); + return res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/login?error=oauth_failed` + ); + } + + if (!code) { + console.error("āŒ No authorization code received"); + return res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/login?error=oauth_failed` + ); + } + + console.log("šŸ”„ Processing Google OAuth callback with code..."); + + // Exchange code for access token + const tokenData = await googleOAuth.getAccessToken(code); + console.log("āœ… Access token received"); + + // Get user profile from Google + const profile = await googleOAuth.getUserProfile(tokenData.access_token); + console.log("āœ… User profile received"); + + // Process user (create/find in database) + const user = await googleOAuth.processGoogleUser(profile); + console.log("šŸŽ‰ Google OAuth successful for user:", user.username); + + // Generate JWT token (SAME format as regular login) + const token = jwt.sign( + { + id: user.id, + username: user.username, + auth0Id: user.auth0Id, + googleId: user.googleId, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + // Set SAME httpOnly cookie as regular login + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + console.log("āœ… JWT token set for Google user:", user.username); + + // Redirect to frontend homepage + res.redirect(process.env.FRONTEND_URL || "http://localhost:3000"); + } catch (error) { + console.error("āŒ Google OAuth callback error:", error); + res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/login?error=oauth_error` + ); + } +}); + module.exports = { router, authenticateJWT }; diff --git a/config/googleOAuth.js b/config/googleOAuth.js new file mode 100644 index 0000000..4d19d13 --- /dev/null +++ b/config/googleOAuth.js @@ -0,0 +1,139 @@ +const { User } = require("../database"); + +// Manual Google OAuth helper functions +const googleOAuth = { + // Generate Google OAuth URL + getAuthUrl() { + const baseUrl = "https://accounts.google.com/o/oauth2/v2/auth"; + const params = new URLSearchParams({ + client_id: process.env.GOOGLE_CLIENT_ID, + redirect_uri: `${ + process.env.BACKEND_URL || "http://localhost:8080" + }/auth/google/callback`, + response_type: "code", + scope: "profile email", + access_type: "offline", + }); + return `${baseUrl}?${params.toString()}`; + }, + + // Exchange authorization code for access token + async getAccessToken(code) { + const response = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + code, + grant_type: "authorization_code", + redirect_uri: `${ + process.env.BACKEND_URL || "http://localhost:8080" + }/auth/google/callback`, + }), + }); + + if (!response.ok) { + throw new Error("Failed to get access token"); + } + + return await response.json(); + }, + + // Get user profile from Google + async getUserProfile(accessToken) { + const response = await fetch( + `https://www.googleapis.com/oauth2/v2/userinfo?access_token=${accessToken}` + ); + + if (!response.ok) { + throw new Error("Failed to get user profile"); + } + + return await response.json(); + }, + + // Process Google user and create/find in database + async processGoogleUser(profile) { + try { + console.log("šŸ” Google OAuth Profile:", { + id: profile.id, + email: profile.email, + name: profile.name, + firstName: profile.given_name, + lastName: profile.family_name, + picture: profile.picture, + }); + + const googleId = profile.id; + const email = profile.email; + const firstName = profile.given_name; + const lastName = profile.family_name; + const profilePicture = profile.picture; + + // 1. Check if user exists by googleId + let user = await User.findOne({ where: { googleId } }); + + if (user) { + console.log("āœ… Existing Google user found:", user.username); + return user; + } + + // 2. If not found by googleId, check by email (link existing account) + if (email) { + user = await User.findOne({ where: { email } }); + + if (user) { + console.log( + "šŸ”— Linking Google account to existing user:", + user.username + ); + // Link Google account to existing user + user.googleId = googleId; + user.profilePicture = profilePicture; + await user.save(); + return user; + } + } + + // 3. Create new user + console.log("šŸ†• Creating new Google user"); + + // Generate unique username from email or Google name + let username = + email?.split("@")[0] || + profile.name?.replace(/\s+/g, "").toLowerCase() || + `google_user_${Date.now()}`; + + // Ensure username is unique + let finalUsername = username; + let counter = 1; + while (await User.findOne({ where: { username: finalUsername } })) { + finalUsername = `${username}_${counter}`; + counter++; + } + + const userData = { + googleId, + email: email || null, + firstName: firstName || null, + lastName: lastName || null, + username: finalUsername, + profilePicture: profilePicture || null, + passwordHash: null, // Google users don't have passwords + }; + + user = await User.create(userData); + console.log("āœ… New Google user created:", user.username); + + return user; + } catch (error) { + console.error("āŒ Google OAuth error:", error); + throw error; + } + }, +}; + +module.exports = googleOAuth; diff --git a/database/seed.js b/database/seed.js index 8e5cef6..850205c 100644 --- a/database/seed.js +++ b/database/seed.js @@ -8,16 +8,18 @@ const seed = async () => { const user = await User.bulkCreate([ { + firstName: "Benjamin", + lastName: "Test", username: "benjamin", email: "benjamin@example.com", - password: "supersecurepassword", - role: "student", + passwordHash: User.hashPassword("supersecurepassword"), }, { + firstName: "David", + lastName: "Test", username: "David", email: "David@example.com", - password: "supersecurepassword2", - role: "student", + passwordHash: User.hashPassword("supersecurepassword2"), }, ]); diff --git a/database/user.js b/database/user.js index 0090353..ba4c18d 100644 --- a/database/user.js +++ b/database/user.js @@ -19,13 +19,35 @@ const User = db.define("user", { username: { type: DataTypes.STRING, allowNull: false, + unique: true, + }, + + auth0Id: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + googleId: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + profilePicture: { + type: DataTypes.STRING, + allowNull: true, }, email: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, // Allow null for OAuth users who might not provide email unique: true, validate: { - isEmail: true, + isEmail: { + msg: "Must be a valid email address", + }, + }, + set(value) { + // Convert empty strings to null to avoid unique constraint issues + this.setDataValue("email", value === "" ? null : value); }, }, passwordHash: { From 44c9b13b3bba21a43035c3a29092b3c9ca3dd6d2 Mon Sep 17 00:00:00 2001 From: KidCharles Date: Tue, 19 Aug 2025 14:24:11 -0400 Subject: [PATCH 41/53] fixing backend changes. Updating seed file --- .gitignore | 1 + api/Calculator.js | 22 ++++++++++++---------- database/Calculator.js | 12 ++++++++---- database/seed.js | 7 ++++--- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 400bedc..e0bcb55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .env .vercel +/database/.db.js \ No newline at end of file diff --git a/api/Calculator.js b/api/Calculator.js index 34b2f9f..8e6cf1b 100644 --- a/api/Calculator.js +++ b/api/Calculator.js @@ -4,25 +4,27 @@ const { Calculator } = require("../database"); // POST: Calculate a final grade. Creates a new grade entry in db router.post("/new-grade-entry", async (req, res) => { - const { user_id, assignment_type, assignment_grade, assignment_weight } = + const { user_id, assignment_type, assignment_name, assignment_grade, assignment_weight } = req.body; - + console.log(req.body.user_id) try { // check that required fields are not omitted if ( !assignment_type || + !assignment_name || assignment_weight == null || assignment_grade == null ) { return res.status(400).json({ error: "Missing required fields" }); } - console.log(user_id, assignment_type, assignment_grade, assignment_weight); + // Create new grade entry in db const newGradeEntry = await Calculator.create({ - user_id, - assignment_type, - assignment_grade, - assignment_weight, + user_id: req.body.user_id, + assignment_type: req.body.assignment_type, + assignment_name: req.body.assignment_name, + assignment_grade: req.body.assignment_grade, + assignment_weight: req.body.assignment_weight }); newGradeEntry.save() @@ -38,7 +40,7 @@ router.post("/new-grade-entry", async (req, res) => { router.get("/grade-entries/:userId", async (req, res) => { try { const userId = req.params.userId; - const entries = await Calculator.findAll({ where: { id: userId } }); + const entries = await Calculator.findAll({ where: { user_id: userId } }); res.status(200).json(entries); } catch (error) { console.error("Error fetching previous grade entries for user", error); @@ -48,7 +50,7 @@ router.get("/grade-entries/:userId", async (req, res) => { } }); -// GET: Fetch a user's specific grade-calculator entry +// GET: Fetch a specific grade-calculator entry router.get("/grade-entry/:entryId", async (req, res) => { try { const entryId = req.params.entryId; @@ -65,7 +67,7 @@ router.get("/grade-entry/:entryId", async (req, res) => { } }); -// PUT: Update a specific grade entry +// PUT: Update a specific grade entry by ID router.put("/grade-entry/:entryId", async (req, res) => { try { const entryId = req.params.entryId; diff --git a/database/Calculator.js b/database/Calculator.js index 01bc9ec..67c91ff 100644 --- a/database/Calculator.js +++ b/database/Calculator.js @@ -13,16 +13,20 @@ const Calculator = db.define('Calculator', { }, assignment_type: { type: DataTypes.ENUM('Homework', 'Quiz', 'Midterm', 'Final'), - allowNull: true, + allowNull: false, + }, + assignment_name: { + type: DataTypes.STRING, + allowNull: false, }, assignment_grade: { type: DataTypes.INTEGER, - allowNull: true, + allowNull: false, }, assignment_weight: { type: DataTypes.INTEGER, - allowNull: true, + allowNull: false, }, }); -module.exports = Calculator; +module.exports = Calculator; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index 8e5cef6..a1c7815 100644 --- a/database/seed.js +++ b/database/seed.js @@ -44,10 +44,11 @@ const seed = async () => { ]); // Add calculator entry const calculator = await Calculator.create({ - assessment: "Midterm Exam", - grade: 85, - weight: 25, user_id: user[0].id, + assignment_type: "Homework", + assignment_name: "First Homework", + assignment_grade: 85, + assignment_weight: 25, }); // Add study session From c72f18fae7f9b743f24f525f79dc90625887123a Mon Sep 17 00:00:00 2001 From: KidCharles Date: Tue, 19 Aug 2025 14:30:26 -0400 Subject: [PATCH 42/53] removed console.log --- api/Calculator.js | 1 - 1 file changed, 1 deletion(-) diff --git a/api/Calculator.js b/api/Calculator.js index 8e6cf1b..5ef4157 100644 --- a/api/Calculator.js +++ b/api/Calculator.js @@ -6,7 +6,6 @@ const { Calculator } = require("../database"); router.post("/new-grade-entry", async (req, res) => { const { user_id, assignment_type, assignment_name, assignment_grade, assignment_weight } = req.body; - console.log(req.body.user_id) try { // check that required fields are not omitted if ( From c0b19f271d75378528b0ba60073f3cd8980ec02b Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Tue, 19 Aug 2025 16:32:04 -0400 Subject: [PATCH 43/53] Fix authenctication with JWT functions --- api/StreakSession.js | 47 +++-- api/UserProgress.js | 435 +++++++++++++++++++++++++++++++++++++-- api/badges.js | 83 ++++++++ auth/index.js | 3 +- database/UserProgress.js | 1 - database/db.js | 2 +- database/seed.js | 9 +- 7 files changed, 540 insertions(+), 40 deletions(-) diff --git a/api/StreakSession.js b/api/StreakSession.js index eab9824..b550c61 100644 --- a/api/StreakSession.js +++ b/api/StreakSession.js @@ -2,6 +2,7 @@ const express = require("express"); const router = express.Router(); const { StreakSession, UserProgress } = require("../database"); const { Op } = require("sequelize"); +const { authenticateJWT } = require("../auth"); // Calculate streak based on study activity (not just session starts) const calculateStreak = async (userId) => { @@ -170,15 +171,11 @@ router.get("/streak/:userId", async (req, res) => { }); // Start a study session -router.post("/start", async (req, res) => { +router.post("/start", authenticateJWT, async (req, res) => { try { - const { userId } = req.body; + const userId = req.user.id; // Get user ID from JWT token - if (!userId) { - return res.status(400).json({ error: "userId is required" }); - } - - // Check for existing active session + // Check for existing active session and end it if found const existingSession = await StreakSession.findOne({ where: { user_id: userId, @@ -196,10 +193,12 @@ router.post("/start", async (req, res) => { }); if (existingSession) { - return res.status(409).json({ - error: "Active session already exists", - session: existingSession, - }); + // End the existing session before starting a new one + existingSession.endTime = new Date(); + await existingSession.save(); + console.log( + `Ended existing session ${existingSession.id} for user ${userId}` + ); } const session = await StreakSession.create({ @@ -219,13 +218,9 @@ router.post("/start", async (req, res) => { }); // End a study session -router.post("/end", async (req, res) => { +router.post("/end", authenticateJWT, async (req, res) => { try { - const { userId } = req.body; - - if (!userId) { - return res.status(400).json({ error: "userId is required" }); - } + const userId = req.user.id; // Get user ID from JWT token const session = await StreakSession.findOne({ where: { @@ -405,4 +400,22 @@ router.get("/stats/:userId", async (req, res) => { } }); +// Protected endpoint to get current user's streak data +router.get("/streak", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const streakInfo = await calculateStreak(userId); + + res.json({ + current: streakInfo.currentStreak, + longest: streakInfo.longestStreak, + lastStudyDate: streakInfo.lastStudyDate, + nextMilestone: Math.max(7, Math.ceil(streakInfo.currentStreak / 7) * 7), + }); + } catch (error) { + console.error("Current user streak error:", error); + res.status(500).json({ error: "Failed to fetch streak data" }); + } +}); + module.exports = router; diff --git a/api/UserProgress.js b/api/UserProgress.js index 40a0d15..ce8dacb 100644 --- a/api/UserProgress.js +++ b/api/UserProgress.js @@ -2,6 +2,7 @@ const express = require("express"); const router = express.Router(); const { Op } = require("sequelize"); const { UserProgress, AiChatHistory } = require("../database"); +const { authenticateJWT } = require("../auth"); // Import badge checking function const { checkAndAwardBadges } = require("./badges"); @@ -274,10 +275,10 @@ router.post("/record", async (req, res) => { }); // Record flashcard progress when user studies -router.post("/flashcard-progress", async (req, res) => { +router.post("/flashcard-progress", authenticateJWT, async (req, res) => { + const userId = req.user.id; // Get user ID from JWT token const { - user_id, - ai_chat_history_id, + ai_chat_history_id, // This is actually the quiz_id from the frontend card_index, is_correct, duration_ms, @@ -287,21 +288,32 @@ router.post("/flashcard-progress", async (req, res) => { try { // Validate required fields if ( - !user_id || !ai_chat_history_id || card_index === undefined || is_correct === undefined ) { return res.status(400).json({ error: - "Missing required fields: user_id, ai_chat_history_id, card_index, and is_correct are required", + "Missing required fields: ai_chat_history_id, card_index, and is_correct are required", }); } - // Create new progress entry + // First, find the AiChatHistory record by quiz_id to get the actual id + const aiChatHistory = await AiChatHistory.findOne({ + where: { quiz_id: ai_chat_history_id }, + }); + + if (!aiChatHistory) { + return res.status(404).json({ + error: "Flashcard set not found", + details: `No flashcard set found with quiz_id: ${ai_chat_history_id}`, + }); + } + + // Create new progress entry using the actual ai_chat_history_id const progressEntry = await UserProgress.create({ - user_id, - ai_chat_history_id, + user_id: userId, + ai_chat_history_id: aiChatHistory.id, // Use the actual integer ID card_index, is_correct, score: null, // null for flashcards @@ -311,7 +323,7 @@ router.post("/flashcard-progress", async (req, res) => { }); // āœ… Check for newly earned badges - const newlyEarnedBadges = await checkAndAwardBadges(user_id); + const newlyEarnedBadges = await checkAndAwardBadges(userId); res.status(201).json({ message: "Flashcard progress recorded successfully", @@ -325,23 +337,35 @@ router.post("/flashcard-progress", async (req, res) => { }); // Record quiz progress when user takes quiz -router.post("/quiz-progress", async (req, res) => { - const { user_id, ai_chat_history_id, score, duration_ms, session_id } = - req.body; +router.post("/quiz-progress", authenticateJWT, async (req, res) => { + const userId = req.user.id; // Get user ID from JWT token + const { ai_chat_history_id, score, duration_ms, session_id } = req.body; try { // Validate required fields - if (!user_id || !ai_chat_history_id || score === undefined) { + if (!ai_chat_history_id || score === undefined) { return res.status(400).json({ error: - "Missing required fields: user_id, ai_chat_history_id, and score are required", + "Missing required fields: ai_chat_history_id and score are required", }); } - // Create new progress entry + // First, find the AiChatHistory record by quiz_id to get the actual id + const aiChatHistory = await AiChatHistory.findOne({ + where: { quiz_id: ai_chat_history_id }, + }); + + if (!aiChatHistory) { + return res.status(404).json({ + error: "Quiz not found", + details: `No quiz found with quiz_id: ${ai_chat_history_id}`, + }); + } + + // Create new progress entry using the actual ai_chat_history_id const progressEntry = await UserProgress.create({ - user_id, - ai_chat_history_id, + user_id: userId, + ai_chat_history_id: aiChatHistory.id, // Use the actual integer ID card_index: null, // null for quizzes is_correct: null, // null for quizzes score, @@ -351,7 +375,7 @@ router.post("/quiz-progress", async (req, res) => { }); // āœ… Check for newly earned badges - const newlyEarnedBadges = await checkAndAwardBadges(user_id); + const newlyEarnedBadges = await checkAndAwardBadges(userId); res.status(201).json({ message: "Quiz progress recorded successfully", @@ -641,4 +665,379 @@ router.get("/daily-chart/:userId", async (req, res) => { } }); +// Protected endpoint to get current user's data +router.get("/current-user", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + + // Get user summary + const summary = await UserProgress.findAll({ + where: { user_id: userId }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + order: [["studied_at", "DESC"]], + }); + + // Calculate all-time stats + const allTimeStats = summary.reduce( + (stats, record) => { + if (record.AiChatHistory?.response_type === "flashcard") { + stats.totalFlashcards++; + if (record.is_correct) stats.correctFlashcards++; + stats.totalStudyTime += record.duration_ms || 0; + } else if (record.AiChatHistory?.response_type === "quiz") { + stats.totalQuizzes++; + if (record.score != null) { + stats.totalQuizScore += record.score; + stats.quizCount++; + } + stats.totalStudyTime += record.duration_ms || 0; + } + return stats; + }, + { + totalFlashcards: 0, + correctFlashcards: 0, + totalQuizzes: 0, + totalQuizScore: 0, + quizCount: 0, + totalStudyTime: 0, + } + ); + + const flashAccuracy = + allTimeStats.totalFlashcards > 0 + ? Math.round( + (allTimeStats.correctFlashcards / allTimeStats.totalFlashcards) * + 100 + ) + : 0; + + const avgQuizScore = + allTimeStats.quizCount > 0 + ? Math.round(allTimeStats.totalQuizScore / allTimeStats.quizCount) + : 0; + + // Get today's stats + const today = new Date(); + const startOfDay = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + 0, + 0, + 0, + 0 + ); + const endOfDay = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + 23, + 59, + 59, + 999 + ); + + const todayStats = summary.filter( + (record) => + record.studied_at >= startOfDay && record.studied_at <= endOfDay + ); + + const todayFlashcards = todayStats.filter( + (r) => r.AiChatHistory?.response_type === "flashcard" + ); + const todayQuizzes = todayStats.filter( + (r) => r.AiChatHistory?.response_type === "quiz" + ); + + const todayFlashAccuracy = + todayFlashcards.length > 0 + ? Math.round( + (todayFlashcards.filter((r) => r.is_correct).length / + todayFlashcards.length) * + 100 + ) + : 0; + + const todayQuizScore = + todayQuizzes.filter((r) => r.score != null).length > 0 + ? Math.round( + todayQuizzes + .filter((r) => r.score != null) + .reduce((sum, r) => sum + r.score, 0) / + todayQuizzes.filter((r) => r.score != null).length + ) + : 0; + + res.json({ + user: { + id: req.user.id, + username: req.user.username, + email: req.user.email, + }, + today: { + flashcardAccuracy: todayFlashAccuracy, + quizScore: todayQuizScore, + flashcardCount: todayFlashcards.length, + quizCount: todayQuizzes.length, + studyTime: Math.round( + todayStats.reduce((sum, r) => sum + (r.duration_ms || 0), 0) / 60000 + ), + }, + all_time: { + totalSessions: summary.length, + totalPoints: allTimeStats.totalFlashcards + allTimeStats.totalQuizzes, + totalStudyTime: Math.round(allTimeStats.totalStudyTime / 60000) + "m", + flashAccuracy: flashAccuracy, + avgQuizScore: avgQuizScore, + totalFlashcards: allTimeStats.totalFlashcards, + totalQuizzes: allTimeStats.totalQuizzes, + }, + }); + } catch (error) { + console.error("Current user data error:", error); + res.status(500).json({ error: "Failed to fetch user data" }); + } +}); + +// Protected endpoint to get current user's daily chart data +router.get("/daily-chart", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const chartData = []; + const today = new Date(); + + // Get data for the last 7 days + for (let i = 6; i >= 0; i--) { + const date = new Date(today); + date.setDate(today.getDate() - i); + + const start = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + 0, + 0, + 0, + 0 + ); + const end = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + 23, + 59, + 59, + 999 + ); + + const rows = await UserProgress.findAll({ + where: { + user_id: userId, + studied_at: { [Op.between]: [start, end] }, + }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + order: [["studied_at", "ASC"]], + }); + + const flashAttempts = rows.filter( + (r) => r.AiChatHistory?.response_type === "flashcard" + ); + const quizRows = rows.filter( + (r) => r.AiChatHistory?.response_type === "quiz" + ); + + const flashStudied = flashAttempts.length; + const flashCorrect = flashAttempts.filter( + (r) => r.is_correct === true + ).length; + const flashAccuracy = flashStudied + ? Math.round((flashCorrect / flashStudied) * 100) + : 0; + + const quizAttempts = quizRows.filter((r) => r.score != null).length; + const quizAvgScore = quizAttempts + ? Math.round( + quizRows + .filter((r) => r.score != null) + .reduce((s, r) => s + r.score, 0) / quizAttempts + ) + : 0; + + // Calculate daily study time + const dailyStudyTime = rows.reduce((total, record) => { + return total + (record.duration_ms || 0); + }, 0); + + chartData.push({ + date: date.toISOString().split("T")[0], + day: date.toLocaleDateString("en-US", { weekday: "short" }), + flashcardAccuracy: flashAccuracy, + quizScore: quizAvgScore, + flashcardCount: flashStudied, + quizCount: quizAttempts, + duration_ms: dailyStudyTime, + }); + } + + res.json({ + chartData, + dateRange: { + start: new Date(today.getTime() - 6 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + end: today.toISOString().split("T")[0], + }, + }); + } catch (e) { + console.error("Daily chart data error āŒ", e); + res.status(500).json({ error: "Server error" }); + } +}); + +// Protected endpoint to get current user's summary data +router.get("/summary", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + + // Get user summary + const summary = await UserProgress.findAll({ + where: { user_id: userId }, + include: [ + { + model: AiChatHistory, + attributes: ["id", "response_type"], + }, + ], + order: [["studied_at", "DESC"]], + }); + + // Calculate all-time stats + const allTimeStats = summary.reduce( + (stats, record) => { + if (record.AiChatHistory?.response_type === "flashcard") { + stats.totalFlashcards++; + if (record.is_correct) stats.correctFlashcards++; + stats.totalStudyTime += record.duration_ms || 0; + } else if (record.AiChatHistory?.response_type === "quiz") { + stats.totalQuizzes++; + if (record.score != null) { + stats.totalQuizScore += record.score; + stats.quizCount++; + } + stats.totalStudyTime += record.duration_ms || 0; + } + return stats; + }, + { + totalFlashcards: 0, + correctFlashcards: 0, + totalQuizzes: 0, + totalQuizScore: 0, + quizCount: 0, + totalStudyTime: 0, + } + ); + + const flashAccuracy = + allTimeStats.totalFlashcards > 0 + ? Math.round( + (allTimeStats.correctFlashcards / allTimeStats.totalFlashcards) * + 100 + ) + : 0; + + const avgQuizScore = + allTimeStats.quizCount > 0 + ? Math.round(allTimeStats.totalQuizScore / allTimeStats.quizCount) + : 0; + + // Get today's stats + const today = new Date(); + const startOfDay = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + 0, + 0, + 0, + 0 + ); + const endOfDay = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + 23, + 59, + 59, + 999 + ); + + const todayStats = summary.filter( + (record) => + record.studied_at >= startOfDay && record.studied_at <= endOfDay + ); + + const todayFlashcards = todayStats.filter( + (r) => r.AiChatHistory?.response_type === "flashcard" + ); + const todayQuizzes = todayStats.filter( + (r) => r.AiChatHistory?.response_type === "quiz" + ); + + const todayFlashAccuracy = + todayFlashcards.length > 0 + ? Math.round( + (todayFlashcards.filter((r) => r.is_correct).length / + todayFlashcards.length) * + 100 + ) + : 0; + + const todayQuizScore = + todayQuizzes.filter((r) => r.score != null).length > 0 + ? Math.round( + todayQuizzes + .filter((r) => r.score != null) + .reduce((sum, r) => sum + r.score, 0) / + todayQuizzes.filter((r) => r.score != null).length + ) + : 0; + + res.json({ + today: { + flashcardAccuracy: todayFlashAccuracy, + quizScore: todayQuizScore, + flashcardCount: todayFlashcards.length, + quizCount: todayQuizzes.length, + studyTime: Math.round( + todayStats.reduce((sum, r) => sum + (r.duration_ms || 0), 0) / 60000 + ), + }, + all_time: { + totalSessions: summary.length, + totalPoints: allTimeStats.totalFlashcards + allTimeStats.totalQuizzes, + totalStudyTime: Math.round(allTimeStats.totalStudyTime / 60000) + "m", + flashAccuracy: flashAccuracy, + avgQuizScore: avgQuizScore, + totalFlashcards: allTimeStats.totalFlashcards, + totalQuizzes: allTimeStats.totalQuizzes, + }, + }); + } catch (error) { + console.error("Current user summary error:", error); + res.status(500).json({ error: "Failed to fetch user summary" }); + } +}); + module.exports = router; diff --git a/api/badges.js b/api/badges.js index 57ea3ef..ae0d58f 100644 --- a/api/badges.js +++ b/api/badges.js @@ -7,6 +7,7 @@ const { UserProgress, AiChatHistory, } = require("../database"); +const { authenticateJWT } = require("../auth"); // Get all available badges router.get("/", async (req, res) => { @@ -309,5 +310,87 @@ async function checkAndAwardBadges(userId) { return newlyEarnedBadges; } +// Protected endpoint to get current user's badge progress +router.get("/progress", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + + // Get all badges + const allBadges = await Badge.findAll({ + order: [ + ["category", "ASC"], + ["requirement_value", "ASC"], + ], + }); + + // Get user's earned badges + const earnedBadges = await UserBadge.findAll({ + where: { user_id: userId }, + include: [ + { + model: Badge, + attributes: [ + "id", + "name", + "description", + "icon", + "category", + "rarity", + "points", + ], + }, + ], + }); + + const earnedBadgeIds = earnedBadges.map((ub) => ub.badge_id); + + // Calculate user's current stats + const userStats = await calculateUserStats(userId); + + // Build progress array + const badgeProgress = allBadges.map((badge) => { + const earned = earnedBadges.find((ub) => ub.badge_id === badge.id); + const currentValue = getCurrentValue(badge.requirement_type, userStats); + const isEarned = earned !== undefined; + const progress = Math.min( + 100, + Math.round((currentValue / Math.max(1, badge.requirement_value)) * 100) + ); + + return { + badge: { + id: badge.id, + name: badge.name, + description: badge.description, + icon: badge.icon, + category: badge.category, + rarity: badge.rarity, + points: badge.points, + requirement_type: badge.requirement_type, + requirement_value: badge.requirement_value, + }, + earned: isEarned, + earned_at: earned?.earned_at, + current_value: currentValue, + progress_percentage: progress, + is_new: earned?.is_new || false, + }; + }); + + const totalEarned = earnedBadges.length; + const totalBadges = allBadges.length; + + res.json({ + badgeProgress, + totalEarned, + totalBadges, + userStats, + }); + } catch (error) { + console.error("Error fetching badge progress:", error); + res.status(500).json({ error: "Failed to fetch badge progress" }); + } +}); + module.exports = router; module.exports.checkAndAwardBadges = checkAndAwardBadges; diff --git a/auth/index.js b/auth/index.js index d4d9496..e934e6e 100644 --- a/auth/index.js +++ b/auth/index.js @@ -175,7 +175,8 @@ router.post("/login", async (req, res) => { // Find user const user = await User.findOne({ where: { username } }); - user.checkPassword(password); + + // Check if user exists first if (!user) { return res.status(401).send({ error: "Invalid credentials" }); } diff --git a/database/UserProgress.js b/database/UserProgress.js index cc77d1f..76cf94a 100644 --- a/database/UserProgress.js +++ b/database/UserProgress.js @@ -10,7 +10,6 @@ const UserProgress = db.define("UserProgress", { score: { type: DataTypes.INTEGER, allowNull: true }, // for quizzes only duration_ms: { type: DataTypes.INTEGER, allowNull: true }, // for flashcards only session_id: { type: DataTypes.STRING, allowNull: true }, // for flashcards only - studied_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, }); diff --git a/database/db.js b/database/db.js index d20af9e..3b580f1 100644 --- a/database/db.js +++ b/database/db.js @@ -6,7 +6,7 @@ const pg = require("pg"); const dbName = "capstone-2"; const db = new Sequelize( - process.env.DATABASE_URL || `postgres://localhost:5432/${capstone-2}`, + process.env.DATABASE_URL || `postgres://localhost:5432/${dbName}`, { logging: false, // comment this line to enable SQL logging } diff --git a/database/seed.js b/database/seed.js index 136f616..91306ec 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,4 +1,5 @@ const db = require("./db"); +const { nanoid } = require("nanoid"); const { User, Tasks, @@ -20,13 +21,13 @@ const seed = async () => { { username: "benjamin", email: "benjamin@example.com", - password: "supersecurepassword", + passwordHash: User.hashPassword("supersecurepassword"), role: "student", }, { username: "David", email: "David@example.com", - password: "supersecurepassword2", + passwordHash: User.hashPassword("supersecurepassword2"), role: "student", }, ]); @@ -39,6 +40,7 @@ const seed = async () => { ai_response: "JSON flashcards...", response_type: "flashcard", status: "success", + quiz_id: nanoid(8), // Generate unique ID for flashcards }, { user_id: 1, @@ -46,6 +48,7 @@ const seed = async () => { ai_response: "JSON quiz...", response_type: "quiz", status: "success", + quiz_id: nanoid(8), // Generate unique ID for quiz }, { user_id: 1, @@ -53,6 +56,7 @@ const seed = async () => { ai_response: "JSON flashcards...", response_type: "flashcard", status: "success", + quiz_id: nanoid(8), // Generate unique ID for flashcards }, { user_id: 1, @@ -60,6 +64,7 @@ const seed = async () => { ai_response: "JSON quiz...", response_type: "quiz", status: "success", + quiz_id: nanoid(8), // Generate unique ID for quiz }, ]); From 3fad577732d97ff7253db5666bfb98234d8a26ab Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Tue, 19 Aug 2025 18:24:28 -0400 Subject: [PATCH 44/53] used token to get the users data --- api/timerData.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/timerData.js b/api/timerData.js index dfbdbc8..7a86988 100644 --- a/api/timerData.js +++ b/api/timerData.js @@ -2,9 +2,10 @@ const express = require("express"); const router = express.Router(); const { Session } = require("../database"); const { Sequelize } = require("sequelize"); +const { authenticateJWT } = require("../auth"); //getting a study durations by userId(foregin key that references the id in users table) -router.get("/data/:userId", async (req, res) => { - const { userId } = req.params; +router.get("/data", authenticateJWT, async (req, res) => { + const userId = req.user.id; try { const sessions = await Session.findAll({ where: { From 3d617cf71b36309f18ea030aac86e9991cdfc17c Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Tue, 19 Aug 2025 18:27:41 -0400 Subject: [PATCH 45/53] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index e0bcb55..400bedc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ node_modules .env .vercel -/database/.db.js \ No newline at end of file From 692e9bd8c604db65e33103b513a6ee6458bce5eb Mon Sep 17 00:00:00 2001 From: pedrosortega Date: Tue, 19 Aug 2025 18:47:46 -0400 Subject: [PATCH 46/53] fixed user model issue --- database/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/index.js b/database/index.js index 35d4842..b9ddc71 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,7 @@ const Sequelize = require("sequelize"); const db = require("./db"); -const User = require("./User"); +const User = require("./user"); const Tasks = require("./Tasks"); const Calculator = require("./Calculator"); const Reminder = require("./Reminder"); From fdd2690806ff0a6205d6b1753501d8640e4eafd3 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Tue, 19 Aug 2025 20:20:39 -0400 Subject: [PATCH 47/53] Fix nanoid ES module compatibility issue by downgrading to v3.3.7 --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index ef1b8f5..dce5db0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", - "nanoid": "^5.1.5", + "nanoid": "^3.3.7", "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7" @@ -1012,9 +1012,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", - "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -1023,10 +1023,10 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^18 || >=20" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/negotiator": { diff --git a/package.json b/package.json index e7a8209..c34d071 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", - "nanoid": "^5.1.5", + "nanoid": "^3.3.7", "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7" From f4dfde11e75b03ac0bcc01e83be95712964a8491 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Tue, 19 Aug 2025 20:31:35 -0400 Subject: [PATCH 48/53] Add enhanced debugging to session endpoints and fix nanoid compatibility --- FlashcardPage.jsx | 464 +++++++++++++++++++++++++++++++++++++++++++ api/StreakSession.js | 92 +++++++-- 2 files changed, 539 insertions(+), 17 deletions(-) create mode 100644 FlashcardPage.jsx diff --git a/FlashcardPage.jsx b/FlashcardPage.jsx new file mode 100644 index 0000000..e8968bd --- /dev/null +++ b/FlashcardPage.jsx @@ -0,0 +1,464 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { GradientBars } from "@/components/ui/gradient-bars"; +import ThemeToggleButton from "@/components/ui/theme-toggle-button"; +import { ShareButtonDemo } from "@/components/shareButtonComponent"; +import { Hourglass } from "ldrs/react"; + +export default function FlashcardPage() { + const { flashcardId } = useParams(); + const [loading, setLoading] = useState(true); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [responseType, setResponseType] = useState(null); + const [showAnswer, setShowAnswer] = useState(false); + const [currentIndex, setCurrentIndex] = useState(0); + const [sessionId] = useState(`session_${Date.now()}`); + const [startTime] = useState(Date.now()); + const [user, setUser] = useState(null); + const [completedCards, setCompletedCards] = useState(new Set()); + + const navigate = useRouter(); + + // Get current authenticated user + useEffect(() => { + const getCurrentUser = async () => { + try { + console.log("Fetching current user..."); + const response = await fetch( + "http://localhost:8080/api/progress/current-user", + { + credentials: "include", + } + ); + + console.log("User response status:", response.status); + + if (!response.ok) { + if (response.status === 401) { + // User not authenticated, redirect to login + console.log("User not authenticated, redirecting to login"); + navigate.push("/LogIn"); + return; + } + throw new Error("Failed to fetch user data"); + } + + const userData = await response.json(); + console.log("User data received:", userData); + + if (userData && userData.user) { + setUser(userData.user); + console.log("User set:", userData.user); + } else { + console.log("No user data, redirecting to login"); + navigate.push("/LogIn"); + } + } catch (error) { + console.error("Error fetching user:", error); + navigate.push("/LogIn"); + } + }; + + getCurrentUser(); + }, [navigate]); + + // Progress tracking function + const recordProgress = async (cardIndex, isCorrect) => { + if (!user) { + console.error("No authenticated user found"); + return; + } + + try { + const duration = Date.now() - startTime; + + const response = await fetch( + "http://localhost:8080/api/progress/flashcard-progress", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + ai_chat_history_id: flashcardId, + card_index: cardIndex, + is_correct: isCorrect, + duration_ms: duration, + session_id: sessionId, + }), + } + ); + + if (!response.ok) { + throw new Error(`Failed to record progress: ${response.status}`); + } + + console.log( + `Progress recorded: Card ${cardIndex}, Correct: ${isCorrect}` + ); + } catch (error) { + console.error("Failed to record progress:", error); + } + }; + + const startSession = async () => { + if (!user) { + throw new Error("No authenticated user found"); + } + + try { + const response = await fetch("http://localhost:8080/api/sessions/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + `Failed to start session: ${response.status} - ${ + errorData.error || errorData.details || "Unknown error" + }` + ); + } + + const result = await response.json(); + console.log("Session started:", result); + return result; + } catch (err) { + console.error("Failed to start session:", err); + throw err; // Re-throw to be handled by caller + } + }; + + const endSession = async () => { + if (!user) return; + + try { + const response = await fetch("http://localhost:8080/api/sessions/end", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }); + + if (!response.ok) { + throw new Error(`Failed to end session: ${response.status}`); + } + + console.log("Session ended"); + } catch (err) { + console.error("Failed to end session:", err); + } + }; + + useEffect(() => { + if (!flashcardId || !user) return; + + async function initializeSession() { + try { + // First try to start the session + await startSession(); + + // Then fetch the flashcard data + await fetchData(); + } catch (error) { + console.error("Failed to initialize session:", error); + setError("Failed to start study session. Please try logging in again."); + } + } + + async function fetchData() { + try { + setLoading(true); + setError(null); + + const res = await fetch( + `http://localhost:8080/api/chat/flashcards/${flashcardId}`, + { + credentials: "include", + } + ); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + const json = await res.json(); + + if (json.error) { + throw new Error(json.details || json.error); + } + + if (json.data && Array.isArray(json.data)) { + setData(json.data); + setResponseType(json.response_type || "unknown"); + } else { + throw new Error("No quiz data found in response"); + } + + if (!json.data || json.data.length === 0) { + throw new Error("Quiz data is empty"); + } + } catch (err) { + console.error("Failed to fetch Flashcards:", err); + setError(err.message || "Failed to load Flashcards"); + } finally { + setLoading(false); + } + } + + initializeSession(); + + return () => { + endSession(); + }; + }, [flashcardId, user]); + + const isFlashcard = + data && + (data[0].front || data[0].question) && + (data[0].back || data[0].answer || data[0].correct); + + const handleFlashcardNext = () => { + if (!data) return; + + const next = currentIndex + 1; + if (next < data.length) { + setCurrentIndex(next); + setShowAnswer(false); + } else { + // Loop back to first card + setCurrentIndex(0); + setShowAnswer(false); + } + }; + + const handleFlashcardPrevious = () => { + if (!data) return; + + const prev = currentIndex - 1; + if (prev >= 0) { + setCurrentIndex(prev); + setShowAnswer(false); + } else { + // Loop to last card + setCurrentIndex(data.length - 1); + setShowAnswer(false); + } + }; + + // Handle flashcard answer (correct/incorrect) with auto-advance + const handleFlashcardAnswer = async (isCorrect) => { + // Record progress for the current card + await recordProgress(currentIndex, isCorrect); + + // Mark this card as completed + setCompletedCards((prev) => new Set([...prev, currentIndex])); + + // Auto-advance to next card after a short delay + setTimeout(() => { + const next = currentIndex + 1; + if (next < data.length) { + setCurrentIndex(next); + setShowAnswer(false); + } else { + // All cards completed, redirect to dashboard + navigate.push(`/dashboard/${user.id}`); + } + }, 1000); // 1 second delay to show the answer feedback + + console.log( + `Card ${currentIndex + 1}: ${isCorrect ? "Correct" : "Incorrect"}` + ); + }; + + // Check if all cards are completed + const allCardsCompleted = data && completedCards.size === data.length; + + // Don't render anything until we have user data + if (!user) { + return ( +
+ +
+ Authenticating user... +
+
+ ); + } + + if (loading) { + return ( +
+ +
+ Please wait while we prepare your content +
+
+ ); + } + + if (error) { + return ( +
+
+ Error Loading Flashcards +
+
{error}
+ +
+ ); + } + + if (!data || data.length === 0) { + return ( +
+
+ No Flashcard data available +
+
+ ); + } + + if (isFlashcard) { + const current = data[currentIndex]; + const frontText = current.front || current.question; + const backText = current.back || current.answer || current.correct; + const isCardCompleted = completedCards.has(currentIndex); + + return ( +
+ +
+ +
+
+ +
+ + {/* User info */} +
+ Studying as: {user.username} +
+ + {/* Progress indicator */} +
+ Card {currentIndex + 1} of {data.length} +
+ Completed: {completedCards.size}/{data.length} +
+
+ + {/* Flashcard */} +
+ {!showAnswer ? ( +
+

Question

+

{frontText}

+
+ ) : ( +
+

Answer

+

{backText}

+ {isCardCompleted && ( +
+ āœ“ Card completed +
+ )} +
+ )} +
+ + {/* Metadata */} + {(current.difficulty || current.cognitive_skill) && ( +
+ {current.difficulty && ( + + Difficulty: {current.difficulty} + + )} + {current.cognitive_skill && ( + + Skill: {current.cognitive_skill} + + )} +
+ )} + + {/* Answer buttons - only show when answer is visible */} + {showAnswer && ( +
+ + +
+ )} + + {/* Navigation buttons */} +
+ + + + + +
+ + {/* Completion message */} + {allCardsCompleted && ( +
+
+ šŸŽ‰ All flashcards completed! +
+
+ Redirecting to dashboard... +
+
+ )} +
+
+
+ ); + } + + // Fallback for unknown format + return ( +
+
Unknown content format
+
+ The content couldn't be displayed as a quiz or flashcards +
+
+ ); +} diff --git a/api/StreakSession.js b/api/StreakSession.js index b550c61..89e47f3 100644 --- a/api/StreakSession.js +++ b/api/StreakSession.js @@ -170,50 +170,79 @@ router.get("/streak/:userId", async (req, res) => { } }); -// Start a study session +// Start a study session - enhanced debugging router.post("/start", authenticateJWT, async (req, res) => { try { const userId = req.user.id; // Get user ID from JWT token + console.log("šŸš€ Starting session for user:", userId); + console.log("šŸ” User object:", req.user); + + // First, let's check if the user actually exists in the database + const { User } = require("../database"); + const user = await User.findByPk(userId); + if (!user) { + console.error("āŒ User not found in database:", userId); + return res.status(404).json({ error: "User not found" }); + } + console.log("āœ… User found:", user.username); // Check for existing active session and end it if found + console.log("šŸ” Checking for existing sessions..."); const existingSession = await StreakSession.findOne({ where: { user_id: userId, endTime: null, }, - attributes: [ - "id", - "user_id", - "startTime", - "endTime", - "createdAt", - "updatedAt", - ], // Only select existing columns order: [["startTime", "DESC"]], }); if (existingSession) { - // End the existing session before starting a new one + console.log("ā¹ļø Ending existing session:", existingSession.id); existingSession.endTime = new Date(); await existingSession.save(); - console.log( - `Ended existing session ${existingSession.id} for user ${userId}` - ); + console.log("āœ… Existing session ended"); } - const session = await StreakSession.create({ + const sessionData = { user_id: userId, startTime: new Date(), + session_type: "study" + }; + console.log("šŸ“ Creating new session with data:", sessionData); + + const session = await StreakSession.create(sessionData); + console.log("āœ… Session created successfully:", { + id: session.id, + user_id: session.user_id, + startTime: session.startTime, + session_type: session.session_type }); res.status(201).json({ message: "Session started", - session, + session: { + id: session.id, + user_id: session.user_id, + startTime: session.startTime, + session_type: session.session_type + }, timestamp: new Date().toISOString(), }); } catch (error) { - console.error("Session start error:", error); - res.status(500).json({ error: "Failed to start session" }); + console.error("āŒ Session start error:", error); + console.error("āŒ Error details:", { + message: error.message, + stack: error.stack, + name: error.name, + code: error.code, + sql: error.sql + }); + res.status(500).json({ + error: "Failed to start session", + details: error.message, + errorName: error.name, + errorCode: error.code + }); } }); @@ -418,4 +447,33 @@ router.get("/streak", authenticateJWT, async (req, res) => { } }); +// Debug endpoint to check table structure +router.get("/debug/table", async (req, res) => { + try { + // Try to describe the table structure + const tableInfo = await StreakSession.describe(); + res.json({ + message: "Table structure retrieved", + tableInfo, + modelAttributes: Object.keys(StreakSession.rawAttributes), + }); + } catch (error) { + console.error("Table debug error:", error); + res.status(500).json({ + error: "Failed to get table info", + details: error.message, + stack: error.stack, + }); + } +}); + +// Debug endpoint to check authentication +router.get("/debug/auth", authenticateJWT, (req, res) => { + res.json({ + message: "Authentication successful", + user: req.user, + timestamp: new Date().toISOString(), + }); +}); + module.exports = router; From f6fb2728ef271d3d2a22dbf0ccc201a36c03c2ad Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Tue, 19 Aug 2025 20:36:48 -0400 Subject: [PATCH 49/53] Delete FlashcardPage.jsx --- FlashcardPage.jsx | 464 ---------------------------------------------- 1 file changed, 464 deletions(-) delete mode 100644 FlashcardPage.jsx diff --git a/FlashcardPage.jsx b/FlashcardPage.jsx deleted file mode 100644 index e8968bd..0000000 --- a/FlashcardPage.jsx +++ /dev/null @@ -1,464 +0,0 @@ -"use client"; - -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { GradientBars } from "@/components/ui/gradient-bars"; -import ThemeToggleButton from "@/components/ui/theme-toggle-button"; -import { ShareButtonDemo } from "@/components/shareButtonComponent"; -import { Hourglass } from "ldrs/react"; - -export default function FlashcardPage() { - const { flashcardId } = useParams(); - const [loading, setLoading] = useState(true); - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const [responseType, setResponseType] = useState(null); - const [showAnswer, setShowAnswer] = useState(false); - const [currentIndex, setCurrentIndex] = useState(0); - const [sessionId] = useState(`session_${Date.now()}`); - const [startTime] = useState(Date.now()); - const [user, setUser] = useState(null); - const [completedCards, setCompletedCards] = useState(new Set()); - - const navigate = useRouter(); - - // Get current authenticated user - useEffect(() => { - const getCurrentUser = async () => { - try { - console.log("Fetching current user..."); - const response = await fetch( - "http://localhost:8080/api/progress/current-user", - { - credentials: "include", - } - ); - - console.log("User response status:", response.status); - - if (!response.ok) { - if (response.status === 401) { - // User not authenticated, redirect to login - console.log("User not authenticated, redirecting to login"); - navigate.push("/LogIn"); - return; - } - throw new Error("Failed to fetch user data"); - } - - const userData = await response.json(); - console.log("User data received:", userData); - - if (userData && userData.user) { - setUser(userData.user); - console.log("User set:", userData.user); - } else { - console.log("No user data, redirecting to login"); - navigate.push("/LogIn"); - } - } catch (error) { - console.error("Error fetching user:", error); - navigate.push("/LogIn"); - } - }; - - getCurrentUser(); - }, [navigate]); - - // Progress tracking function - const recordProgress = async (cardIndex, isCorrect) => { - if (!user) { - console.error("No authenticated user found"); - return; - } - - try { - const duration = Date.now() - startTime; - - const response = await fetch( - "http://localhost:8080/api/progress/flashcard-progress", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify({ - ai_chat_history_id: flashcardId, - card_index: cardIndex, - is_correct: isCorrect, - duration_ms: duration, - session_id: sessionId, - }), - } - ); - - if (!response.ok) { - throw new Error(`Failed to record progress: ${response.status}`); - } - - console.log( - `Progress recorded: Card ${cardIndex}, Correct: ${isCorrect}` - ); - } catch (error) { - console.error("Failed to record progress:", error); - } - }; - - const startSession = async () => { - if (!user) { - throw new Error("No authenticated user found"); - } - - try { - const response = await fetch("http://localhost:8080/api/sessions/start", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - `Failed to start session: ${response.status} - ${ - errorData.error || errorData.details || "Unknown error" - }` - ); - } - - const result = await response.json(); - console.log("Session started:", result); - return result; - } catch (err) { - console.error("Failed to start session:", err); - throw err; // Re-throw to be handled by caller - } - }; - - const endSession = async () => { - if (!user) return; - - try { - const response = await fetch("http://localhost:8080/api/sessions/end", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - }); - - if (!response.ok) { - throw new Error(`Failed to end session: ${response.status}`); - } - - console.log("Session ended"); - } catch (err) { - console.error("Failed to end session:", err); - } - }; - - useEffect(() => { - if (!flashcardId || !user) return; - - async function initializeSession() { - try { - // First try to start the session - await startSession(); - - // Then fetch the flashcard data - await fetchData(); - } catch (error) { - console.error("Failed to initialize session:", error); - setError("Failed to start study session. Please try logging in again."); - } - } - - async function fetchData() { - try { - setLoading(true); - setError(null); - - const res = await fetch( - `http://localhost:8080/api/chat/flashcards/${flashcardId}`, - { - credentials: "include", - } - ); - - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - - const json = await res.json(); - - if (json.error) { - throw new Error(json.details || json.error); - } - - if (json.data && Array.isArray(json.data)) { - setData(json.data); - setResponseType(json.response_type || "unknown"); - } else { - throw new Error("No quiz data found in response"); - } - - if (!json.data || json.data.length === 0) { - throw new Error("Quiz data is empty"); - } - } catch (err) { - console.error("Failed to fetch Flashcards:", err); - setError(err.message || "Failed to load Flashcards"); - } finally { - setLoading(false); - } - } - - initializeSession(); - - return () => { - endSession(); - }; - }, [flashcardId, user]); - - const isFlashcard = - data && - (data[0].front || data[0].question) && - (data[0].back || data[0].answer || data[0].correct); - - const handleFlashcardNext = () => { - if (!data) return; - - const next = currentIndex + 1; - if (next < data.length) { - setCurrentIndex(next); - setShowAnswer(false); - } else { - // Loop back to first card - setCurrentIndex(0); - setShowAnswer(false); - } - }; - - const handleFlashcardPrevious = () => { - if (!data) return; - - const prev = currentIndex - 1; - if (prev >= 0) { - setCurrentIndex(prev); - setShowAnswer(false); - } else { - // Loop to last card - setCurrentIndex(data.length - 1); - setShowAnswer(false); - } - }; - - // Handle flashcard answer (correct/incorrect) with auto-advance - const handleFlashcardAnswer = async (isCorrect) => { - // Record progress for the current card - await recordProgress(currentIndex, isCorrect); - - // Mark this card as completed - setCompletedCards((prev) => new Set([...prev, currentIndex])); - - // Auto-advance to next card after a short delay - setTimeout(() => { - const next = currentIndex + 1; - if (next < data.length) { - setCurrentIndex(next); - setShowAnswer(false); - } else { - // All cards completed, redirect to dashboard - navigate.push(`/dashboard/${user.id}`); - } - }, 1000); // 1 second delay to show the answer feedback - - console.log( - `Card ${currentIndex + 1}: ${isCorrect ? "Correct" : "Incorrect"}` - ); - }; - - // Check if all cards are completed - const allCardsCompleted = data && completedCards.size === data.length; - - // Don't render anything until we have user data - if (!user) { - return ( -
- -
- Authenticating user... -
-
- ); - } - - if (loading) { - return ( -
- -
- Please wait while we prepare your content -
-
- ); - } - - if (error) { - return ( -
-
- Error Loading Flashcards -
-
{error}
- -
- ); - } - - if (!data || data.length === 0) { - return ( -
-
- No Flashcard data available -
-
- ); - } - - if (isFlashcard) { - const current = data[currentIndex]; - const frontText = current.front || current.question; - const backText = current.back || current.answer || current.correct; - const isCardCompleted = completedCards.has(currentIndex); - - return ( -
- -
- -
-
- -
- - {/* User info */} -
- Studying as: {user.username} -
- - {/* Progress indicator */} -
- Card {currentIndex + 1} of {data.length} -
- Completed: {completedCards.size}/{data.length} -
-
- - {/* Flashcard */} -
- {!showAnswer ? ( -
-

Question

-

{frontText}

-
- ) : ( -
-

Answer

-

{backText}

- {isCardCompleted && ( -
- āœ“ Card completed -
- )} -
- )} -
- - {/* Metadata */} - {(current.difficulty || current.cognitive_skill) && ( -
- {current.difficulty && ( - - Difficulty: {current.difficulty} - - )} - {current.cognitive_skill && ( - - Skill: {current.cognitive_skill} - - )} -
- )} - - {/* Answer buttons - only show when answer is visible */} - {showAnswer && ( -
- - -
- )} - - {/* Navigation buttons */} -
- - - - - -
- - {/* Completion message */} - {allCardsCompleted && ( -
-
- šŸŽ‰ All flashcards completed! -
-
- Redirecting to dashboard... -
-
- )} -
-
-
- ); - } - - // Fallback for unknown format - return ( -
-
Unknown content format
-
- The content couldn't be displayed as a quiz or flashcards -
-
- ); -} From 25a20e258396907bb61e4a1784beeac71d1d1e96 Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Tue, 19 Aug 2025 22:20:23 -0400 Subject: [PATCH 50/53] Fix some route issue --- FlashcardPage.jsx | 464 ---------------------------------------------- auth/index.js | 70 +++++++ database/user.js | 5 + 3 files changed, 75 insertions(+), 464 deletions(-) delete mode 100644 FlashcardPage.jsx diff --git a/FlashcardPage.jsx b/FlashcardPage.jsx deleted file mode 100644 index e8968bd..0000000 --- a/FlashcardPage.jsx +++ /dev/null @@ -1,464 +0,0 @@ -"use client"; - -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { GradientBars } from "@/components/ui/gradient-bars"; -import ThemeToggleButton from "@/components/ui/theme-toggle-button"; -import { ShareButtonDemo } from "@/components/shareButtonComponent"; -import { Hourglass } from "ldrs/react"; - -export default function FlashcardPage() { - const { flashcardId } = useParams(); - const [loading, setLoading] = useState(true); - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const [responseType, setResponseType] = useState(null); - const [showAnswer, setShowAnswer] = useState(false); - const [currentIndex, setCurrentIndex] = useState(0); - const [sessionId] = useState(`session_${Date.now()}`); - const [startTime] = useState(Date.now()); - const [user, setUser] = useState(null); - const [completedCards, setCompletedCards] = useState(new Set()); - - const navigate = useRouter(); - - // Get current authenticated user - useEffect(() => { - const getCurrentUser = async () => { - try { - console.log("Fetching current user..."); - const response = await fetch( - "http://localhost:8080/api/progress/current-user", - { - credentials: "include", - } - ); - - console.log("User response status:", response.status); - - if (!response.ok) { - if (response.status === 401) { - // User not authenticated, redirect to login - console.log("User not authenticated, redirecting to login"); - navigate.push("/LogIn"); - return; - } - throw new Error("Failed to fetch user data"); - } - - const userData = await response.json(); - console.log("User data received:", userData); - - if (userData && userData.user) { - setUser(userData.user); - console.log("User set:", userData.user); - } else { - console.log("No user data, redirecting to login"); - navigate.push("/LogIn"); - } - } catch (error) { - console.error("Error fetching user:", error); - navigate.push("/LogIn"); - } - }; - - getCurrentUser(); - }, [navigate]); - - // Progress tracking function - const recordProgress = async (cardIndex, isCorrect) => { - if (!user) { - console.error("No authenticated user found"); - return; - } - - try { - const duration = Date.now() - startTime; - - const response = await fetch( - "http://localhost:8080/api/progress/flashcard-progress", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify({ - ai_chat_history_id: flashcardId, - card_index: cardIndex, - is_correct: isCorrect, - duration_ms: duration, - session_id: sessionId, - }), - } - ); - - if (!response.ok) { - throw new Error(`Failed to record progress: ${response.status}`); - } - - console.log( - `Progress recorded: Card ${cardIndex}, Correct: ${isCorrect}` - ); - } catch (error) { - console.error("Failed to record progress:", error); - } - }; - - const startSession = async () => { - if (!user) { - throw new Error("No authenticated user found"); - } - - try { - const response = await fetch("http://localhost:8080/api/sessions/start", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - `Failed to start session: ${response.status} - ${ - errorData.error || errorData.details || "Unknown error" - }` - ); - } - - const result = await response.json(); - console.log("Session started:", result); - return result; - } catch (err) { - console.error("Failed to start session:", err); - throw err; // Re-throw to be handled by caller - } - }; - - const endSession = async () => { - if (!user) return; - - try { - const response = await fetch("http://localhost:8080/api/sessions/end", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - }); - - if (!response.ok) { - throw new Error(`Failed to end session: ${response.status}`); - } - - console.log("Session ended"); - } catch (err) { - console.error("Failed to end session:", err); - } - }; - - useEffect(() => { - if (!flashcardId || !user) return; - - async function initializeSession() { - try { - // First try to start the session - await startSession(); - - // Then fetch the flashcard data - await fetchData(); - } catch (error) { - console.error("Failed to initialize session:", error); - setError("Failed to start study session. Please try logging in again."); - } - } - - async function fetchData() { - try { - setLoading(true); - setError(null); - - const res = await fetch( - `http://localhost:8080/api/chat/flashcards/${flashcardId}`, - { - credentials: "include", - } - ); - - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - - const json = await res.json(); - - if (json.error) { - throw new Error(json.details || json.error); - } - - if (json.data && Array.isArray(json.data)) { - setData(json.data); - setResponseType(json.response_type || "unknown"); - } else { - throw new Error("No quiz data found in response"); - } - - if (!json.data || json.data.length === 0) { - throw new Error("Quiz data is empty"); - } - } catch (err) { - console.error("Failed to fetch Flashcards:", err); - setError(err.message || "Failed to load Flashcards"); - } finally { - setLoading(false); - } - } - - initializeSession(); - - return () => { - endSession(); - }; - }, [flashcardId, user]); - - const isFlashcard = - data && - (data[0].front || data[0].question) && - (data[0].back || data[0].answer || data[0].correct); - - const handleFlashcardNext = () => { - if (!data) return; - - const next = currentIndex + 1; - if (next < data.length) { - setCurrentIndex(next); - setShowAnswer(false); - } else { - // Loop back to first card - setCurrentIndex(0); - setShowAnswer(false); - } - }; - - const handleFlashcardPrevious = () => { - if (!data) return; - - const prev = currentIndex - 1; - if (prev >= 0) { - setCurrentIndex(prev); - setShowAnswer(false); - } else { - // Loop to last card - setCurrentIndex(data.length - 1); - setShowAnswer(false); - } - }; - - // Handle flashcard answer (correct/incorrect) with auto-advance - const handleFlashcardAnswer = async (isCorrect) => { - // Record progress for the current card - await recordProgress(currentIndex, isCorrect); - - // Mark this card as completed - setCompletedCards((prev) => new Set([...prev, currentIndex])); - - // Auto-advance to next card after a short delay - setTimeout(() => { - const next = currentIndex + 1; - if (next < data.length) { - setCurrentIndex(next); - setShowAnswer(false); - } else { - // All cards completed, redirect to dashboard - navigate.push(`/dashboard/${user.id}`); - } - }, 1000); // 1 second delay to show the answer feedback - - console.log( - `Card ${currentIndex + 1}: ${isCorrect ? "Correct" : "Incorrect"}` - ); - }; - - // Check if all cards are completed - const allCardsCompleted = data && completedCards.size === data.length; - - // Don't render anything until we have user data - if (!user) { - return ( -
- -
- Authenticating user... -
-
- ); - } - - if (loading) { - return ( -
- -
- Please wait while we prepare your content -
-
- ); - } - - if (error) { - return ( -
-
- Error Loading Flashcards -
-
{error}
- -
- ); - } - - if (!data || data.length === 0) { - return ( -
-
- No Flashcard data available -
-
- ); - } - - if (isFlashcard) { - const current = data[currentIndex]; - const frontText = current.front || current.question; - const backText = current.back || current.answer || current.correct; - const isCardCompleted = completedCards.has(currentIndex); - - return ( -
- -
- -
-
- -
- - {/* User info */} -
- Studying as: {user.username} -
- - {/* Progress indicator */} -
- Card {currentIndex + 1} of {data.length} -
- Completed: {completedCards.size}/{data.length} -
-
- - {/* Flashcard */} -
- {!showAnswer ? ( -
-

Question

-

{frontText}

-
- ) : ( -
-

Answer

-

{backText}

- {isCardCompleted && ( -
- āœ“ Card completed -
- )} -
- )} -
- - {/* Metadata */} - {(current.difficulty || current.cognitive_skill) && ( -
- {current.difficulty && ( - - Difficulty: {current.difficulty} - - )} - {current.cognitive_skill && ( - - Skill: {current.cognitive_skill} - - )} -
- )} - - {/* Answer buttons - only show when answer is visible */} - {showAnswer && ( -
- - -
- )} - - {/* Navigation buttons */} -
- - - - - -
- - {/* Completion message */} - {allCardsCompleted && ( -
-
- šŸŽ‰ All flashcards completed! -
-
- Redirecting to dashboard... -
-
- )} -
-
-
- ); - } - - // Fallback for unknown format - return ( -
-
Unknown content format
-
- The content couldn't be displayed as a quiz or flashcards -
-
- ); -} diff --git a/auth/index.js b/auth/index.js index e934e6e..a732411 100644 --- a/auth/index.js +++ b/auth/index.js @@ -237,4 +237,74 @@ router.get("/me", (req, res) => { }); }); +// Update user profile route (protected) +router.put("/update-profile", authenticateJWT, async (req, res) => { + try { + const { username, email, studyGoal } = req.body; + const userId = req.user.id; + + // Find the user + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).send({ error: "User not found" }); + } + + // Check if username is being changed and if it's already taken + if (username && username !== user.username) { + const existingUser = await User.findOne({ where: { username } }); + if (existingUser) { + return res.status(409).send({ error: "Username already exists" }); + } + } + + // Check if email is being changed and if it's already taken + if (email && email !== user.email) { + const existingUser = await User.findOne({ where: { email } }); + if (existingUser) { + return res.status(409).send({ error: "Email already exists" }); + } + } + + // Update user fields + const updateData = {}; + if (username) updateData.username = username; + if (email) updateData.email = email; + if (studyGoal !== undefined) updateData.studyGoal = studyGoal; + + await user.update(updateData); + + // Generate new JWT token with updated user info + const token = jwt.sign( + { + id: user.id, + username: user.username, + auth0Id: user.auth0Id, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.send({ + message: "Profile updated successfully", + user: { + id: user.id, + username: user.username, + email: user.email, + studyGoal: user.studyGoal, + }, + }); + } catch (error) { + console.error("Profile update error:", error); + res.status(500).send({ error: "Failed to update profile" }); + } +}); + module.exports = { router, authenticateJWT }; diff --git a/database/user.js b/database/user.js index 0090353..eee376e 100644 --- a/database/user.js +++ b/database/user.js @@ -32,6 +32,11 @@ const User = db.define("user", { type: DataTypes.STRING, allowNull: true, }, + studyGoal: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 30, // Default 30 minutes per day + }, }); // Instance method to check password From 192059910004511757643c3d34ac2ce451d7b13f Mon Sep 17 00:00:00 2001 From: Elian Echavarria Date: Tue, 19 Aug 2025 23:53:27 -0400 Subject: [PATCH 51/53] fix cors frontend url --- app.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 0c764a4..3404991 100644 --- a/app.js +++ b/app.js @@ -17,7 +17,11 @@ const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; app.use(express.json()); app.use( cors({ - origin: [FRONTEND_URL, "http://localhost:3000"], + origin: [ + FRONTEND_URL, + "http://localhost:3000", + "https://lock-in-front-end.vercel.app", + ], credentials: true, }) ); From 642b308cd371ebc8392ec7d5ad541018bf689aa8 Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Wed, 20 Aug 2025 11:24:54 -0400 Subject: [PATCH 52/53] Implemented GoogleOauth routes in the back end --- CALENDAR_INTEGRATION.md | 413 ++++++++++++++++++++++++++++ api/calendar.js | 519 +++++++++++++++++++++++++++++++++++ app.js | 13 +- auth/index.js | 84 ++++++ config/googleOAuth.js | 87 +++++- database/Tasks.js | 9 + database/migrate-calendar.js | 64 +++++ database/user.js | 13 + package-lock.json | 92 +++++++ package.json | 1 + services/CalendarService.js | 330 ++++++++++++++++++++++ 11 files changed, 1617 insertions(+), 8 deletions(-) create mode 100644 CALENDAR_INTEGRATION.md create mode 100644 api/calendar.js create mode 100644 database/migrate-calendar.js create mode 100644 services/CalendarService.js diff --git a/CALENDAR_INTEGRATION.md b/CALENDAR_INTEGRATION.md new file mode 100644 index 0000000..c1ffa6a --- /dev/null +++ b/CALENDAR_INTEGRATION.md @@ -0,0 +1,413 @@ +# Google Calendar Integration + +This document provides comprehensive information about the Google Calendar integration implementation for the task management application. + +## Overview + +The calendar integration allows users to: + +- Grant Google Calendar permissions +- Create calendar events from tasks +- Update and delete calendar events +- Sync tasks with calendar events +- Manage calendar reminders + +## Architecture + +### Manual OAuth Flow + +The implementation uses a **manual OAuth flow** instead of Passport.js for better control and flexibility: + +- **`config/googleOAuth.js`**: Core OAuth helper functions +- **`services/CalendarService.js`**: Calendar API operations +- **`api/calendar.js`**: Calendar REST endpoints +- **`auth/index.js`**: OAuth callback handlers + +## Database Schema Changes + +### User Model Updates + +```javascript +// New fields added to User model +googleAccessToken: DataTypes.TEXT, // Google API access token +googleRefreshToken: DataTypes.TEXT, // Google API refresh token +calendarPermissions: DataTypes.BOOLEAN // Whether user granted calendar access +``` + +### Task Model Updates + +```javascript +// New fields added to Task model +calendarEventId: DataTypes.STRING, // Google Calendar event ID +hasReminder: DataTypes.BOOLEAN // Whether task has calendar reminder +``` + +## Setup Instructions + +### 1. Environment Variables + +Ensure these variables are set in your `.env` file: + +```env +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret +BACKEND_URL=http://localhost:8080 +FRONTEND_URL=http://localhost:3000 +``` + +### 2. Database Migration + +Run the migration to add new fields: + +```bash +node database/migrate-calendar.js +``` + +### 3. Test Integration + +Verify everything is working: + +```bash +node test-calendar.js +``` + +## API Endpoints + +### Authentication Endpoints + +#### Get Calendar Permission URL + +```http +GET /api/calendar/permissions/url +Authorization: Bearer +``` + +Returns a Google OAuth URL for calendar permissions. + +#### Calendar Permission Callback + +```http +GET /auth/google/calendar/callback?code=&state= +``` + +Handles the OAuth callback and stores calendar tokens. Redirects to: + +- Success: `http://localhost:3000/Tasks?calendar_success=permissions_granted` +- Error: `http://localhost:3000/Tasks?calendar_error=` + +### Calendar Management Endpoints + +#### Check Calendar Permissions + +```http +GET /api/calendar/permissions +Authorization: Bearer +``` + +Returns user's calendar permission status. + +#### Get User's Calendars + +```http +GET /api/calendar/calendars +Authorization: Bearer +``` + +Returns list of user's Google Calendars. + +#### Get Calendar Events + +```http +GET /api/calendar/events?timeMin=2024-01-01T00:00:00Z&timeMax=2024-12-31T23:59:59Z +Authorization: Bearer +``` + +Returns calendar events within specified time range. + +### Event Management Endpoints + +#### Create Calendar Event + +```http +POST /api/calendar/events +Authorization: Bearer +Content-Type: application/json + +{ + "title": "Complete Project", + "description": "Finish the task management app", + "startTime": "2024-01-15T10:00:00Z", + "endTime": "2024-01-15T11:00:00Z", + "timeZone": "America/New_York", + "taskId": 123 +} +``` + +#### Update Calendar Event + +```http +PUT /api/calendar/events/{eventId} +Authorization: Bearer +Content-Type: application/json + +{ + "title": "Updated Event Title", + "startTime": "2024-01-15T11:00:00Z", + "endTime": "2024-01-15T12:00:00Z" +} +``` + +#### Delete Calendar Event + +```http +DELETE /api/calendar/events/{eventId} +Authorization: Bearer +``` + +### Task-Calendar Sync Endpoints + +#### Sync Task with Calendar + +```http +POST /api/calendar/sync-task/{taskId} +Authorization: Bearer +Content-Type: application/json + +{ + "startTime": "2024-01-15T14:00:00Z", + "endTime": "2024-01-15T15:00:00Z", + "timeZone": "America/New_York" +} +``` + +#### Remove Task Calendar Sync + +```http +DELETE /api/calendar/sync-task/{taskId} +Authorization: Bearer +``` + +## Usage Flow + +### 1. Initial Setup + +1. User logs in through existing auth system +2. User requests calendar permissions via `/api/calendar/permissions/url` +3. User is redirected to Google OAuth consent screen +4. After consent, user is redirected back to `/auth/google/calendar/callback` +5. Backend stores access and refresh tokens + +### 2. Creating Calendar Events + +1. User creates or edits a task +2. Frontend sends request to `/api/calendar/sync-task/{taskId}` with timing info +3. Backend creates Google Calendar event +4. Task is updated with `calendarEventId` and `hasReminder: true` + +### 3. Managing Events + +1. User can view calendar events via `/api/calendar/events` +2. User can update events via `/api/calendar/events/{eventId}` +3. User can delete events via `/api/calendar/events/{eventId}` +4. Task sync is automatically maintained + +## Error Handling + +The implementation includes comprehensive error handling: + +### Token Refresh + +- Automatically refreshes expired access tokens using refresh tokens +- Graceful fallback when refresh fails +- Clear error messages for permission issues + +### API Errors + +- Standardized error responses +- Specific error codes for different scenarios +- Logging for debugging + +### Common Error Responses + +```json +// Permission required +{ + "error": "Calendar permissions required", + "needsPermission": true +} + +// Authentication failed +{ + "error": "User not authenticated with Google Calendar" +} + +// Validation error +{ + "error": "Title, start time, and end time are required" +} +``` + +## Security Considerations + +### Token Storage + +- Access tokens stored in database (encrypted in production) +- Refresh tokens stored securely +- Tokens scoped to minimum required permissions + +### API Protection + +- All endpoints require JWT authentication +- User can only access their own calendar data +- Input validation on all calendar operations + +### OAuth Security + +- Uses PKCE flow where possible +- State parameter for CSRF protection +- Secure redirect URI validation + +## Frontend Integration + +### Calendar Permission Flow + +```javascript +// Check if user has calendar permissions +const checkCalendarPermissions = async () => { + const response = await fetch("/api/calendar/permissions", { + credentials: "include", + }); + const data = await response.json(); + return data.hasPermissions; +}; + +// Request calendar permissions +const requestCalendarPermissions = async () => { + const response = await fetch("/api/calendar/permissions/url", { + credentials: "include", + }); + const data = await response.json(); + window.location.href = data.permissionUrl; +}; +``` + +### Creating Calendar Events + +```javascript +// Sync task with calendar +const syncTaskWithCalendar = async (taskId, eventData) => { + const response = await fetch(`/api/calendar/sync-task/${taskId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(eventData), + }); + + if (!response.ok) { + const error = await response.json(); + if (error.needsPermission) { + // Redirect to permissions flow + await requestCalendarPermissions(); + return; + } + throw new Error(error.error); + } + + return await response.json(); +}; +``` + +## Testing + +### Unit Tests + +Run the calendar integration tests: + +```bash +node test-calendar.js +``` + +### Integration Tests + +1. Start the server: `npm run start-dev` +2. Use Postman or curl to test API endpoints +3. Verify calendar events appear in Google Calendar + +### Test Scenarios + +- [ ] User can request calendar permissions +- [ ] OAuth callback processes correctly +- [ ] Calendar events are created successfully +- [ ] Events sync with tasks properly +- [ ] Token refresh works automatically +- [ ] Error handling works correctly + +## Troubleshooting + +### Common Issues + +#### "Calendar permissions required" + +- User needs to grant calendar permissions +- Check if user completed OAuth flow +- Verify tokens are stored in database + +#### "Failed to refresh access token" + +- Refresh token may be expired or invalid +- User needs to re-authorize +- Check Google Cloud Console settings + +#### "Calendar API error: 401" + +- Access token expired and refresh failed +- User needs to re-authorize +- Check token storage + +### Debug Steps + +1. Check environment variables are set +2. Verify database migration completed +3. Test OAuth flow manually +4. Check server logs for specific errors +5. Verify Google Cloud Console configuration + +## Deployment Notes + +### Production Environment + +- Set `NODE_ENV=production` +- Use HTTPS for OAuth callbacks +- Set secure cookie flags +- Use proper database encryption + +### Google Cloud Console + +- Configure OAuth consent screen +- Add production domain to authorized origins +- Set up proper redirect URIs +- Enable Calendar API + +## Future Enhancements + +### Potential Features + +- Multiple calendar support +- Recurring event support +- Calendar event templates +- Bulk calendar operations +- Calendar sync status dashboard +- Email notifications for events + +### Performance Optimizations + +- Batch calendar operations +- Caching for calendar data +- Incremental sync +- Background job processing + +--- + +For questions or issues, please refer to the main project documentation or create an issue in the repository. diff --git a/api/calendar.js b/api/calendar.js new file mode 100644 index 0000000..fa9a40c --- /dev/null +++ b/api/calendar.js @@ -0,0 +1,519 @@ +const express = require("express"); +const CalendarService = require("../services/CalendarService"); +const { authenticateJWT } = require("../auth"); +const { User, Tasks } = require("../database"); + +const router = express.Router(); +const calendarService = new CalendarService(); + +// Check if user has calendar permissions +router.get("/permissions", authenticateJWT, async (req, res) => { + try { + console.log("šŸ” Checking calendar permissions for user:", req.user.id); + + const user = await User.findByPk(req.user.id); + + if (!user) { + console.error("āŒ User not found:", req.user.id); + return res.status(404).json({ error: "User not found" }); + } + + console.log("šŸ“Š User calendar data:", { + id: user.id, + googleAccessToken: !!user.googleAccessToken, + googleRefreshToken: !!user.googleRefreshToken, + calendarPermissions: user.calendarPermissions, + }); + + const hasPermissions = calendarService.hasCalendarPermissions(user); + + console.log("āœ… Calendar permissions check result:", hasPermissions); + + res.json({ + hasPermissions, + calendarPermissions: user.calendarPermissions || false, + hasTokens: !!(user.googleAccessToken && user.googleRefreshToken), + }); + } catch (error) { + console.error("āŒ Error checking calendar permissions:", error); + console.error("Error details:", { + message: error.message, + stack: error.stack, + }); + res.status(500).json({ + error: "Failed to check calendar permissions", + details: error.message, + }); + } +}); + +// Get calendar permission URL +router.get("/permissions/url", authenticateJWT, (req, res) => { + try { + console.log("šŸ”— Generating calendar permission URL for user:", req.user.id); + const permissionUrl = calendarService.getCalendarPermissionUrl(req.user.id); + console.log( + "āœ… Permission URL generated:", + permissionUrl.substring(0, 100) + "..." + ); + res.json({ permissionUrl }); + } catch (error) { + console.error("āŒ Error generating calendar permission URL:", error); + console.error("Error details:", { + message: error.message, + stack: error.stack, + }); + res.status(500).json({ + error: "Failed to generate permission URL", + details: error.message, + }); + } +}); + +// Get user's calendars +router.get("/calendars", authenticateJWT, async (req, res) => { + try { + const user = await User.findByPk(req.user.id); + + if (!calendarService.hasCalendarPermissions(user)) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + const calendars = await calendarService.getCalendars(user); + res.json(calendars); + } catch (error) { + console.error("Error fetching calendars:", error); + + if (error.message.includes("not authenticated")) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + res.status(500).json({ error: "Failed to fetch calendars" }); + } +}); + +// Get calendar events +router.get("/events", authenticateJWT, async (req, res) => { + try { + const user = await User.findByPk(req.user.id); + + if (!calendarService.hasCalendarPermissions(user)) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + const options = { + calendarId: req.query.calendarId, + timeMin: req.query.timeMin, + timeMax: req.query.timeMax, + maxResults: req.query.maxResults + ? parseInt(req.query.maxResults) + : undefined, + singleEvents: req.query.singleEvents !== "false", + orderBy: req.query.orderBy || "startTime", + }; + + const events = await calendarService.getEvents(user, options); + res.json(events); + } catch (error) { + console.error("Error fetching calendar events:", error); + + if (error.message.includes("not authenticated")) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + res.status(500).json({ error: "Failed to fetch calendar events" }); + } +}); + +// Create calendar event from task +router.post("/events", authenticateJWT, async (req, res) => { + try { + const user = await User.findByPk(req.user.id); + + if (!calendarService.hasCalendarPermissions(user)) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + const { + title, + description, + startTime, + endTime, + timeZone, + calendarId, + reminders, + taskId, + } = req.body; + + // Validate required fields + if (!title || !startTime || !endTime) { + return res.status(400).json({ + error: "Title, start time, and end time are required", + }); + } + + const eventData = { + title, + description, + startTime, + endTime, + timeZone, + calendarId, + reminders, + }; + + const event = await calendarService.createEvent(user, eventData); + + // If this event is for a task, update the task with the calendar event ID + if (taskId) { + const task = await Tasks.findOne({ + where: { id: taskId, user_id: user.id }, + }); + + if (task) { + task.calendarEventId = event.id; + task.hasReminder = true; + await task.save(); + } + } + + res.status(201).json({ + success: true, + event, + message: "Calendar event created successfully", + }); + } catch (error) { + console.error("Error creating calendar event:", error); + + if (error.message.includes("not authenticated")) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + res.status(500).json({ error: "Failed to create calendar event" }); + } +}); + +// Update calendar event +router.put("/events/:eventId", authenticateJWT, async (req, res) => { + try { + const user = await User.findByPk(req.user.id); + + if (!calendarService.hasCalendarPermissions(user)) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + const { eventId } = req.params; + const { + title, + description, + startTime, + endTime, + timeZone, + calendarId, + reminders, + } = req.body; + + // Validate required fields + if (!title || !startTime || !endTime) { + return res.status(400).json({ + error: "Title, start time, and end time are required", + }); + } + + const eventData = { + title, + description, + startTime, + endTime, + timeZone, + reminders, + }; + + const event = await calendarService.updateEvent( + user, + eventId, + eventData, + calendarId || "primary" + ); + + res.json({ + success: true, + event, + message: "Calendar event updated successfully", + }); + } catch (error) { + console.error("Error updating calendar event:", error); + + if (error.message.includes("not authenticated")) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + res.status(500).json({ error: "Failed to update calendar event" }); + } +}); + +// Delete calendar event +router.delete("/events/:eventId", authenticateJWT, async (req, res) => { + try { + const user = await User.findByPk(req.user.id); + + if (!calendarService.hasCalendarPermissions(user)) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + const { eventId } = req.params; + const { calendarId } = req.query; + + await calendarService.deleteEvent(user, eventId, calendarId || "primary"); + + // Remove calendar event ID from any associated task + const task = await Tasks.findOne({ + where: { calendarEventId: eventId, user_id: user.id }, + }); + + if (task) { + task.calendarEventId = null; + task.hasReminder = false; + await task.save(); + } + + res.json({ + success: true, + message: "Calendar event deleted successfully", + }); + } catch (error) { + console.error("Error deleting calendar event:", error); + + if (error.message.includes("not authenticated")) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + res.status(500).json({ error: "Failed to delete calendar event" }); + } +}); + +// Sync task with calendar (create/update calendar event for a task) +router.post("/sync-task/:taskId", authenticateJWT, async (req, res) => { + try { + console.log( + "šŸ”„ Syncing task with calendar. TaskID:", + req.params.taskId, + "UserID:", + req.user.id + ); + + const user = await User.findByPk(req.user.id); + + if (!user) { + console.error("āŒ User not found for sync-task:", req.user.id); + return res.status(404).json({ error: "User not found" }); + } + + console.log("šŸ“Š User calendar permissions:", { + hasGoogleAccessToken: !!user.googleAccessToken, + hasGoogleRefreshToken: !!user.googleRefreshToken, + calendarPermissions: user.calendarPermissions, + }); + + if (!calendarService.hasCalendarPermissions(user)) { + console.log( + "āš ļø User does not have calendar permissions, returning graceful failure" + ); + return res.status(200).json({ + success: false, + error: "Calendar permissions required", + needsPermission: true, + message: + "Task created successfully, but calendar sync skipped (no permissions)", + }); + } + + const { taskId } = req.params; + const { startTime, endTime, timeZone, calendarId, reminders } = req.body; + + // Find the task + const task = await Tasks.findOne({ + where: { id: taskId, user_id: user.id }, + }); + + if (!task) { + return res.status(404).json({ error: "Task not found" }); + } + + // Validate required fields + if (!startTime || !endTime) { + console.log("āš ļø No timing data provided for calendar sync"); + return res.status(400).json({ + success: false, + error: "Start time and end time are required for calendar sync", + message: "Please provide start time and end time to sync with calendar", + needsTiming: true, + }); + } + + const eventData = { + title: task.assignment, + description: task.description || `Task: ${task.assignment}`, + startTime, + endTime, + timeZone, + calendarId, + reminders, + }; + + let event; + + // If task already has a calendar event, update it + if (task.calendarEventId) { + try { + event = await calendarService.updateEvent( + user, + task.calendarEventId, + eventData, + calendarId || "primary" + ); + } catch (error) { + // If update fails (e.g., event was deleted), create a new one + console.log( + "Failed to update existing event, creating new one:", + error.message + ); + event = await calendarService.createEvent(user, eventData); + task.calendarEventId = event.id; + } + } else { + // Create new calendar event + event = await calendarService.createEvent(user, eventData); + task.calendarEventId = event.id; + } + + task.hasReminder = true; + await task.save(); + + res.json({ + success: true, + event, + task, + message: "Task synced with calendar successfully", + }); + } catch (error) { + console.error("āŒ Error syncing task with calendar:", error); + console.error("Error details:", { + message: error.message, + stack: error.stack, + taskId: req.params.taskId, + userId: req.user.id, + }); + + if (error.message.includes("not authenticated")) { + return res.status(403).json({ + error: "Calendar permissions required", + needsPermission: true, + }); + } + + // Check if this is a token refresh failure + if ( + error.message.includes("Failed to refresh access token") || + error.message.includes("temporarily unavailable") + ) { + // Reset calendar permissions for this user since tokens are invalid + user.calendarPermissions = false; + user.googleAccessToken = null; + user.googleRefreshToken = null; + await user.save(); + + console.log( + "šŸ”„ Reset calendar permissions for user due to invalid tokens" + ); + + return res.status(200).json({ + success: false, + error: "Calendar authorization expired", + needsPermission: true, + message: + "Task created successfully, but calendar sync failed (please re-authorize Google Calendar)", + }); + } + + res.status(500).json({ + error: "Failed to sync task with calendar", + details: error.message, + }); + } +}); + +// Remove calendar sync from task +router.delete("/sync-task/:taskId", authenticateJWT, async (req, res) => { + try { + const user = await User.findByPk(req.user.id); + const { taskId } = req.params; + + // Find the task + const task = await Tasks.findOne({ + where: { id: taskId, user_id: user.id }, + }); + + if (!task) { + return res.status(404).json({ error: "Task not found" }); + } + + // Delete calendar event if it exists + if (task.calendarEventId && calendarService.hasCalendarPermissions(user)) { + try { + await calendarService.deleteEvent(user, task.calendarEventId); + } catch (error) { + console.log( + "Failed to delete calendar event (may already be deleted):", + error.message + ); + } + } + + // Remove calendar sync from task + task.calendarEventId = null; + task.hasReminder = false; + await task.save(); + + res.json({ + success: true, + task, + message: "Calendar sync removed from task", + }); + } catch (error) { + console.error("Error removing calendar sync from task:", error); + res.status(500).json({ error: "Failed to remove calendar sync from task" }); + } +}); + +module.exports = router; diff --git a/app.js b/app.js index 12fbf52..218fbf0 100644 --- a/app.js +++ b/app.js @@ -12,12 +12,18 @@ const { Model } = require("sequelize"); const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; const { router: authRouter, authenticateJWT } = require("./auth"); +const calendarRouter = require("./api/calendar"); // body parser middleware app.use(express.json()); app.use( cors({ - origin: [FRONTEND_URL, "http://localhost:3000"], + origin: [ + FRONTEND_URL, + "http://localhost:3000", + "http://localhost:3001", + "http://localhost:3002" + ], credentials: true, }) ); @@ -27,8 +33,9 @@ app.use(cookieParser()); app.use(morgan("dev")); // logging middleware app.use(express.static(path.join(__dirname, "public"))); // serve static files from public folder -app.use("/api", authenticateJWT, apiRouter); // mount api router -app.use("/auth", authRouter); // mount auth router +app.use("/auth", authRouter); // mount auth router (no auth required) +app.use("/api/calendar", authenticateJWT, calendarRouter); // mount calendar router FIRST +app.use("/api", authenticateJWT, apiRouter); // mount main api router SECOND // error handling middleware app.use((err, req, res, next) => { diff --git a/auth/index.js b/auth/index.js index 32b39c9..b48a3df 100644 --- a/auth/index.js +++ b/auth/index.js @@ -296,6 +296,22 @@ router.get("/google", (req, res) => { res.redirect(authUrl); }); +// Initiate Google Calendar Permission Flow +router.get("/google/calendar", authenticateJWT, (req, res) => { + try { + const calendarAuthUrl = googleOAuth.getCalendarAuthUrl(req.user.id); + console.log("šŸš€ Redirecting to Google Calendar OAuth:", calendarAuthUrl); + res.redirect(calendarAuthUrl); + } catch (error) { + console.error("āŒ Calendar permission error:", error); + res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/dashboard?calendar_error=permission_failed` + ); + } +}); + // Handle Google OAuth callback router.get("/google/callback", async (req, res) => { try { @@ -368,4 +384,72 @@ router.get("/google/callback", async (req, res) => { } }); +// Handle Google Calendar permission callback +router.get("/google/calendar/callback", async (req, res) => { + try { + const { code, error, state } = req.query; + + if (error) { + console.error("āŒ Google Calendar OAuth error:", error); + return res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/Tasks?calendar_error=permission_denied` + ); + } + + if (!code || !state) { + console.error("āŒ No authorization code or state received"); + return res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/Tasks?calendar_error=invalid_request` + ); + } + + console.log("šŸ”„ Processing Google Calendar permission callback..."); + + // Find user by ID from state parameter + const userId = state; + const user = await User.findByPk(userId); + + if (!user) { + console.error("āŒ User not found for calendar permission"); + return res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/login?error=user_not_found` + ); + } + + // Exchange code for access token with calendar scopes + const tokenData = await googleOAuth.getAccessTokenWithCalendarScopes(code); + console.log("āœ… Calendar access token received"); + + // Update user with calendar tokens + user.googleAccessToken = tokenData.access_token; + if (tokenData.refresh_token) { + user.googleRefreshToken = tokenData.refresh_token; + } + user.calendarPermissions = true; + await user.save(); + + console.log("šŸŽ‰ Calendar permissions granted for user:", user.username); + + // Redirect to frontend with success message + res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/Tasks?calendar_success=permissions_granted` + ); + } catch (error) { + console.error("āŒ Google Calendar callback error:", error); + res.redirect( + `${ + process.env.FRONTEND_URL || "http://localhost:3000" + }/Tasks?calendar_error=authorization_failed` + ); + } +}); + module.exports = { router, authenticateJWT }; diff --git a/config/googleOAuth.js b/config/googleOAuth.js index 4d19d13..a599665 100644 --- a/config/googleOAuth.js +++ b/config/googleOAuth.js @@ -2,7 +2,7 @@ const { User } = require("../database"); // Manual Google OAuth helper functions const googleOAuth = { - // Generate Google OAuth URL + // Generate Google OAuth URL (basic profile + email) getAuthUrl() { const baseUrl = "https://accounts.google.com/o/oauth2/v2/auth"; const params = new URLSearchParams({ @@ -17,8 +17,33 @@ const googleOAuth = { return `${baseUrl}?${params.toString()}`; }, + // Generate Google OAuth URL with Calendar permissions + getCalendarAuthUrl(userId) { + const baseUrl = "https://accounts.google.com/o/oauth2/v2/auth"; + const params = new URLSearchParams({ + client_id: process.env.GOOGLE_CLIENT_ID, + redirect_uri: `${ + process.env.BACKEND_URL || "http://localhost:8080" + }/auth/google/calendar/callback`, + response_type: "code", + scope: "profile email https://www.googleapis.com/auth/calendar.events", + access_type: "offline", + prompt: "consent", // Force consent to get refresh token + state: userId, // Pass user ID to identify user in callback + }); + return `${baseUrl}?${params.toString()}`; + }, + // Exchange authorization code for access token - async getAccessToken(code) { + async getAccessToken(code, isCalendarFlow = false) { + const redirectUri = isCalendarFlow + ? `${ + process.env.BACKEND_URL || "http://localhost:8080" + }/auth/google/calendar/callback` + : `${ + process.env.BACKEND_URL || "http://localhost:8080" + }/auth/google/callback`; + const response = await fetch("https://oauth2.googleapis.com/token", { method: "POST", headers: { @@ -29,9 +54,7 @@ const googleOAuth = { client_secret: process.env.GOOGLE_CLIENT_SECRET, code, grant_type: "authorization_code", - redirect_uri: `${ - process.env.BACKEND_URL || "http://localhost:8080" - }/auth/google/callback`, + redirect_uri: redirectUri, }), }); @@ -42,6 +65,60 @@ const googleOAuth = { return await response.json(); }, + // Exchange authorization code for access token specifically for calendar permissions + async getAccessTokenWithCalendarScopes(code) { + const redirectUri = `${ + process.env.BACKEND_URL || "http://localhost:8080" + }/auth/google/calendar/callback`; + + const response = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + code, + grant_type: "authorization_code", + redirect_uri: redirectUri, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + `Failed to get calendar access token: ${ + errorData.error || response.statusText + }` + ); + } + + return await response.json(); + }, + + // Refresh access token using refresh token + async refreshAccessToken(refreshToken) { + const response = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + refresh_token: refreshToken, + grant_type: "refresh_token", + }), + }); + + if (!response.ok) { + throw new Error("Failed to refresh access token"); + } + + return await response.json(); + }, + // Get user profile from Google async getUserProfile(accessToken) { const response = await fetch( diff --git a/database/Tasks.js b/database/Tasks.js index c49883b..b7415d9 100644 --- a/database/Tasks.js +++ b/database/Tasks.js @@ -27,6 +27,15 @@ const Tasks = db.define("Tasks", { type: DataTypes.DATE, defaultValue: DataTypes.NOW, }, + calendarEventId: { + type: DataTypes.STRING, + allowNull: true, + }, + hasReminder: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, }); module.exports = Tasks; diff --git a/database/migrate-calendar.js b/database/migrate-calendar.js new file mode 100644 index 0000000..bca9f7e --- /dev/null +++ b/database/migrate-calendar.js @@ -0,0 +1,64 @@ +const { db, User, Tasks } = require("./index"); + +async function migrateCalendarFields() { + try { + console.log("šŸ”„ Starting calendar fields migration..."); + + // Check if we need to sync the database (add new columns) + await db.sync({ alter: true }); + + console.log("āœ… Database schema updated with calendar fields"); + + // Update existing users to have default calendar permission values + const usersUpdated = await User.update( + { + calendarPermissions: false, + }, + { + where: { + calendarPermissions: null, + }, + } + ); + + console.log( + `šŸ“ Updated ${usersUpdated[0]} users with default calendar permissions` + ); + + // Update existing tasks to have default reminder values + const tasksUpdated = await Tasks.update( + { + hasReminder: false, + }, + { + where: { + hasReminder: null, + }, + } + ); + + console.log( + `šŸ“ Updated ${tasksUpdated[0]} tasks with default reminder values` + ); + + console.log("šŸŽ‰ Calendar fields migration completed successfully!"); + } catch (error) { + console.error("āŒ Calendar fields migration failed:", error); + throw error; + } +} + +// Run migration if this file is executed directly +if (require.main === module) { + migrateCalendarFields() + .then(() => { + console.log("āœ… Migration complete"); + process.exit(0); + }) + .catch((error) => { + console.error("āŒ Migration failed:", error); + process.exit(1); + }); +} + +module.exports = { migrateCalendarFields }; diff --git a/database/user.js b/database/user.js index ba4c18d..53aac7d 100644 --- a/database/user.js +++ b/database/user.js @@ -54,6 +54,19 @@ const User = db.define("user", { type: DataTypes.STRING, allowNull: true, }, + googleAccessToken: { + type: DataTypes.TEXT, + allowNull: true, + }, + googleRefreshToken: { + type: DataTypes.TEXT, + allowNull: true, + }, + calendarPermissions: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, }); // Instance method to check password diff --git a/package-lock.json b/package-lock.json index ef1b8f5..3e6ff86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "nanoid": "^5.1.5", + "node-fetch": "^3.3.2", "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7" @@ -338,6 +339,15 @@ "node": ">= 0.10" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -507,6 +517,29 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -537,6 +570,18 @@ "node": ">= 0.8" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1047,6 +1092,44 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -1791,6 +1874,15 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/win-node-env": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/win-node-env/-/win-node-env-0.6.1.tgz", diff --git a/package.json b/package.json index e7a8209..809acdf 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "nanoid": "^5.1.5", + "node-fetch": "^3.3.2", "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7" diff --git a/services/CalendarService.js b/services/CalendarService.js new file mode 100644 index 0000000..20e89cb --- /dev/null +++ b/services/CalendarService.js @@ -0,0 +1,330 @@ +const { User } = require("../database"); + +// Try to use native fetch, fallback to node-fetch for older Node versions +let fetch; +let URLSearchParams; + +try { + // Try native fetch first + if (globalThis.fetch && globalThis.URLSearchParams) { + fetch = globalThis.fetch; + URLSearchParams = globalThis.URLSearchParams; + console.log("🌐 Using native fetch and URLSearchParams"); + } else { + throw new Error("Native fetch not available"); + } +} catch (error) { + try { + // Fallback to node-fetch + fetch = require("node-fetch"); + URLSearchParams = require("url").URLSearchParams; + console.log("šŸ“¦ Using node-fetch and Node.js URLSearchParams"); + } catch (fetchError) { + console.error( + "āŒ Neither native fetch nor node-fetch available:", + fetchError + ); + throw new Error( + "Fetch not available. Please install node-fetch: npm install node-fetch" + ); + } +} + +class CalendarService { + constructor() { + this.calendarApiUrl = "https://www.googleapis.com/calendar/v3"; + } + + // Get calendar scopes for OAuth + static getCalendarScopes() { + return [ + "https://www.googleapis.com/auth/calendar", + "https://www.googleapis.com/auth/calendar.events", + ]; + } + + // Refresh access token using refresh token + async refreshAccessToken(user) { + if (!user.googleRefreshToken) { + throw new Error("No refresh token available"); + } + + try { + const response = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + refresh_token: user.googleRefreshToken, + grant_type: "refresh_token", + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + console.error("āŒ Token refresh failed:", { + status: response.status, + statusText: response.statusText, + error: errorData, + }); + throw new Error( + `Failed to refresh access token: ${response.status} ${ + errorData.error?.message || response.statusText + }` + ); + } + + const data = await response.json(); + + // Update user's access token + user.googleAccessToken = data.access_token; + await user.save(); + + return data.access_token; + } catch (error) { + console.error("Error refreshing access token:", error); + throw error; + } + } + + // Make authenticated request to Google Calendar API + async makeCalendarRequest(user, endpoint, options = {}) { + console.log( + `šŸ” Making calendar request: ${options.method || "GET"} ${endpoint}` + ); + + try { + let accessToken = user.googleAccessToken; + + if (!accessToken) { + throw new Error( + "User not authenticated with Google Calendar - no access token" + ); + } + + console.log(`šŸ”‘ Using access token: ${accessToken.substring(0, 20)}...`); + + // Try request with current token + let response = await this.attemptRequest(accessToken, endpoint, options); + console.log(`šŸ“” Initial response status: ${response.status}`); + + // If unauthorized, try refreshing token + if (response.status === 401) { + console.log("šŸ”„ Access token expired, refreshing..."); + accessToken = await this.refreshAccessToken(user); + console.log(`šŸ”‘ New access token: ${accessToken.substring(0, 20)}...`); + response = await this.attemptRequest(accessToken, endpoint, options); + console.log(`šŸ“” Retry response status: ${response.status}`); + } + + if (!response.ok) { + let errorData; + try { + errorData = await response.json(); + } catch (parseError) { + errorData = { error: { message: "Could not parse error response" } }; + } + + console.error(`āŒ Calendar API error response:`, { + status: response.status, + statusText: response.statusText, + errorData, + }); + + throw new Error( + `Calendar API error: ${response.status} - ${ + errorData.error?.message || response.statusText + }` + ); + } + + console.log("āœ… Calendar API request successful"); + return await response.json(); + } catch (error) { + console.error("āŒ Calendar API request failed:", { + message: error.message, + stack: error.stack, + endpoint, + method: options.method || "GET", + }); + throw new Error( + `Calendar service temporarily unavailable: ${error.message}` + ); + } + } + + // Helper method to attempt API request + async attemptRequest(accessToken, endpoint, options) { + const url = `${this.calendarApiUrl}${endpoint}`; + + return await fetch(url, { + method: options.method || "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + ...options.headers, + }, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + } + + // Get user's calendars + async getCalendars(user) { + if (!user.googleAccessToken) { + throw new Error("User not authenticated with Google Calendar"); + } + + return await this.makeCalendarRequest(user, "/calendars"); + } + + // Create calendar event + async createEvent(user, eventData) { + if (!user.googleAccessToken) { + throw new Error("User not authenticated with Google Calendar"); + } + + const calendarId = eventData.calendarId || "primary"; + + // Format event for Google Calendar API + const googleEvent = { + summary: eventData.title, + description: eventData.description || "", + start: { + dateTime: eventData.startTime, + timeZone: eventData.timeZone || "America/New_York", + }, + end: { + dateTime: eventData.endTime, + timeZone: eventData.timeZone || "America/New_York", + }, + reminders: { + useDefault: false, + overrides: eventData.reminders || [ + { method: "email", minutes: 24 * 60 }, // 1 day before + { method: "popup", minutes: 10 }, // 10 minutes before + ], + }, + }; + + const response = await this.makeCalendarRequest( + user, + `/calendars/${calendarId}/events`, + { + method: "POST", + body: googleEvent, + } + ); + + return response; + } + + // Update calendar event + async updateEvent(user, eventId, eventData, calendarId = "primary") { + if (!user.googleAccessToken) { + throw new Error("User not authenticated with Google Calendar"); + } + + // Format event for Google Calendar API + const googleEvent = { + summary: eventData.title, + description: eventData.description || "", + start: { + dateTime: eventData.startTime, + timeZone: eventData.timeZone || "America/New_York", + }, + end: { + dateTime: eventData.endTime, + timeZone: eventData.timeZone || "America/New_York", + }, + reminders: { + useDefault: false, + overrides: eventData.reminders || [ + { method: "email", minutes: 24 * 60 }, + { method: "popup", minutes: 10 }, + ], + }, + }; + + const response = await this.makeCalendarRequest( + user, + `/calendars/${calendarId}/events/${eventId}`, + { + method: "PUT", + body: googleEvent, + } + ); + + return response; + } + + // Delete calendar event + async deleteEvent(user, eventId, calendarId = "primary") { + if (!user.googleAccessToken) { + throw new Error("User not authenticated with Google Calendar"); + } + + await this.makeCalendarRequest( + user, + `/calendars/${calendarId}/events/${eventId}`, + { + method: "DELETE", + } + ); + + return { success: true, message: "Event deleted successfully" }; + } + + // Get calendar events + async getEvents(user, options = {}) { + if (!user.googleAccessToken) { + throw new Error("User not authenticated with Google Calendar"); + } + + const calendarId = options.calendarId || "primary"; + const params = new URLSearchParams(); + + if (options.timeMin) params.append("timeMin", options.timeMin); + if (options.timeMax) params.append("timeMax", options.timeMax); + if (options.maxResults) params.append("maxResults", options.maxResults); + if (options.singleEvents !== undefined) + params.append("singleEvents", options.singleEvents); + if (options.orderBy) params.append("orderBy", options.orderBy); + + const endpoint = `/calendars/${calendarId}/events${ + params.toString() ? `?${params.toString()}` : "" + }`; + + const response = await this.makeCalendarRequest(user, endpoint); + return response; + } + + // Check if user has calendar permissions + hasCalendarPermissions(user) { + return !!(user.googleAccessToken && user.calendarPermissions); + } + + // Generate calendar permission URL + getCalendarPermissionUrl(userId) { + const baseUrl = "https://accounts.google.com/o/oauth2/v2/auth"; + const scopes = CalendarService.getCalendarScopes().join(" "); + + const params = new URLSearchParams({ + client_id: process.env.GOOGLE_CLIENT_ID, + redirect_uri: `${ + process.env.BACKEND_URL || "http://localhost:8080" + }/auth/google/calendar/callback`, + response_type: "code", + scope: scopes, + access_type: "offline", + prompt: "consent", // Force consent to get refresh token + state: userId, // Pass user ID to identify user in callback + }); + + return `${baseUrl}?${params.toString()}`; + } +} + +module.exports = CalendarService; From 0f674886293a8c9751ab48e789d17a03a9eebd82 Mon Sep 17 00:00:00 2001 From: Benjamin Ayala Date: Wed, 20 Aug 2025 11:39:27 -0400 Subject: [PATCH 53/53] Merge --- app.js | 7 ------- package.json | 4 ---- 2 files changed, 11 deletions(-) diff --git a/app.js b/app.js index a5794c3..d715572 100644 --- a/app.js +++ b/app.js @@ -19,16 +19,9 @@ app.use(express.json()); app.use( cors({ origin: [ -<<<<<<< HEAD - FRONTEND_URL, - "http://localhost:3000", - "http://localhost:3001", - "http://localhost:3002" -======= FRONTEND_URL, "http://localhost:3000", "https://lock-in-front-end.vercel.app", ->>>>>>> 85051c302b64a345b871706398ab251cbd2433e0 ], credentials: true, }) diff --git a/package.json b/package.json index ff5c601..809acdf 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,8 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", -<<<<<<< HEAD "nanoid": "^5.1.5", "node-fetch": "^3.3.2", -======= - "nanoid": "^3.3.7", ->>>>>>> 85051c302b64a345b871706398ab251cbd2433e0 "openai": "^5.11.0", "pg": "^8.16.2", "sequelize": "^6.37.7"