Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions backend/controllers/csvDownload.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,30 @@ const { db } = require("../../database.js");
*/


// Add an escaping function to combat CSV injection and comma parsing issues
function escapeCSV(str) {
if (str === null || str === undefined) return '""';
let stringified = String(str);

// Prevent CSV Formula Injection by escaping =, +, -, @
if (/^[=+\-@]/.test(stringified)) {
stringified = "'" + stringified;
}

// Escape existing double quotes and wrap the whole string in quotes
return `"${stringified.replace(/"/g, '""')}"`;
}

async function downloadData(req, res) {
try {
const query = `
SELECT tasks.*, subjects.name AS subject_name
FROM tasks
LEFT JOIN subjects ON tasks.subject_id = subjects.id
WHERE tasks.user_id = ?
`;
const data = await new Promise((resolve, reject) => {
db.all(query, [], (err, rows) => {
db.all(query, [req.user.email], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
Expand All @@ -27,14 +42,14 @@ async function downloadData(req, res) {
const rows = [
["Task ID", "Subject", "Title", "Due At", "Status", "Priority", "Confidence Score", "Notes"],
...data.map(task => [
task.id,
task.subject_name,
task.title,
task.due_at,
task.status,
task.priority,
task.confidence_score,
`"${(task.notes || '').replace(/"/g, '""')}"`
escapeCSV(task.id),
escapeCSV(task.subject_name),
escapeCSV(task.title),
escapeCSV(task.due_at),
escapeCSV(task.status),
escapeCSV(task.priority),
escapeCSV(task.confidence_score),
escapeCSV(task.notes)
])
];

Expand Down
2 changes: 2 additions & 0 deletions database.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function initDb() {
// Subjects Table
db.run(`CREATE TABLE IF NOT EXISTS subjects (
id TEXT PRIMARY KEY,
user_id TEXT,
name TEXT NOT NULL,
short_code TEXT,
color TEXT,
Expand All @@ -17,6 +18,7 @@ function initDb() {
// Tasks Table
db.run(`CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
user_id TEXT,
subject_id TEXT,
title TEXT NOT NULL,
description TEXT,
Expand Down
65 changes: 65 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"sqlite3": "^6.0.1"
},
"engines": {
Expand Down
57 changes: 37 additions & 20 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const cors = require('cors');
const { db, initDb } = require('./database');
const { GoogleGenAI } = require('@google/genai');
const path = require('path');
const jwt = require('jsonwebtoken');
const csvDownloadRouter = require('./backend/routers/csvDownload.router.js');

const app = express();
Expand All @@ -13,6 +14,19 @@ app.use(express.json());
const page404Path = path.join(__dirname, '404.html');
const page500Path = path.join(__dirname, 'error.html');

// Auth Middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) return res.sendStatus(401);

jwt.verify(token, process.env.JWT_SECRET || 'secret_key', (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}

// Static
app.use('/css', express.static(path.join(__dirname, 'css')));
app.use('/js', express.static(path.join(__dirname, 'js')));
Expand Down Expand Up @@ -233,8 +247,8 @@ function nlpExtractTasksFromText(text) {
// ============================================================

// ================= SUBJECTS =================
app.get('/api/subjects', (req, res) => {
db.all('SELECT * FROM subjects', (err, rows) => {
app.get('/api/subjects', authenticateToken, (req, res) => {
db.all('SELECT * FROM subjects WHERE user_id = ?', [req.user.email], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
Expand All @@ -249,7 +263,7 @@ const ALLOWED_SUBJECT_COLORS = new Set([
'var(--color-text-secondary)',
]);

app.post('/api/subjects', (req, res) => {
app.post('/api/subjects', authenticateToken, (req, res) => {
const name = String(req.body?.name || '').trim();
let color = String(req.body?.color || '').trim() || 'var(--color-text-info)';
if (!name) {
Expand All @@ -261,8 +275,8 @@ app.post('/api/subjects', (req, res) => {
const shortCode = name.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 4) || 'SUB';
const id = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
db.run(
'INSERT INTO subjects (id, name, short_code, color) VALUES (?, ?, ?, ?)',
[id, name, shortCode, color],
'INSERT INTO subjects (id, user_id, name, short_code, color) VALUES (?, ?, ?, ?, ?)',
[id, req.user.email, name, shortCode, color],
function (err) {
if (err) return res.status(500).json({ error: err.message });
res.status(201).json({ id, name, short_code: shortCode, color });
Expand All @@ -271,15 +285,15 @@ app.post('/api/subjects', (req, res) => {
});

// ================= TASKS =================
app.get('/api/tasks', (req, res) => {
db.all('SELECT * FROM tasks ORDER BY due_at ASC', (err, rows) => {
app.get('/api/tasks', authenticateToken, (req, res) => {
db.all('SELECT * FROM tasks WHERE user_id = ? ORDER BY due_at ASC', [req.user.email], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});

// ================= ADD TASKS =================
app.post('/api/tasks', (req, res) => {
app.post('/api/tasks', authenticateToken, (req, res) => {
try {
const tasks = Array.isArray(req.body) ? req.body : [req.body];

Expand All @@ -292,8 +306,8 @@ app.post('/api/tasks', (req, res) => {
let errors = [];

const stmt = db.prepare(`INSERT INTO tasks
(id, subject_id, title, due_at, status, priority, confidence_score, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
(id, user_id, subject_id, title, due_at, status, priority, confidence_score, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);

let pending = tasks.length;

Expand All @@ -308,8 +322,8 @@ app.post('/api/tasks', (req, res) => {
}

db.get(
`SELECT * FROM tasks WHERE LOWER(title) = LOWER(?) AND subject_id = ? AND DATE(due_at) = DATE(?)`,
[t.title, t.subject_id, t.due_at],
`SELECT * FROM tasks WHERE LOWER(title) = LOWER(?) AND subject_id = ? AND DATE(due_at) = DATE(?) AND user_id = ?`,
[t.title, t.subject_id, t.due_at, req.user.email],
(err, existing) => {
if (err) {
errors.push({ task: t, error: err.message });
Expand All @@ -323,6 +337,7 @@ app.post('/api/tasks', (req, res) => {
const id = 'task_' + Date.now() + Math.random().toString(36).substr(2, 5);
stmt.run(
id,
req.user.email,
t.subject_id,
t.title,
t.due_at,
Expand Down Expand Up @@ -370,7 +385,7 @@ app.post('/api/tasks', (req, res) => {
});

// ================= UPDATE =================
app.put('/api/tasks/:id', (req, res) => {
app.put('/api/tasks/:id', authenticateToken, (req, res) => {
const { status, archived, title, subject_id, due_at, notes, priority } = req.body;

let query = 'UPDATE tasks SET ';
Expand All @@ -389,8 +404,8 @@ app.put('/api/tasks/:id', (req, res) => {
return res.status(400).json({ error: 'No fields to update' });
}

query += updates.join(', ') + ' WHERE id = ?';
params.push(req.params.id);
query += updates.join(', ') + ' WHERE id = ? AND user_id = ?';
params.push(req.params.id, req.user.email);

db.run(query, params, function (err) {
if (err) return res.status(500).json({ error: err.message });
Expand All @@ -399,10 +414,10 @@ app.put('/api/tasks/:id', (req, res) => {
});

// ================= DELETE =================
app.delete('/api/tasks/:id', (req, res) => {
app.delete('/api/tasks/:id', authenticateToken, (req, res) => {
db.run(
'DELETE FROM tasks WHERE id = ?',
[req.params.id],
'DELETE FROM tasks WHERE id = ? AND user_id = ?',
[req.params.id, req.user.email],
function (err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ success: true, changes: this.changes });
Expand Down Expand Up @@ -468,15 +483,17 @@ app.post('/api/auth/login', (req, res) => {
if (!user || user.password !== password) {
return res.status(401).json({ error: 'Invalid email or password' });
}
res.json({ success: true, email: user.email });

const token = jwt.sign({ email: user.email }, process.env.JWT_SECRET || 'secret_key', { expiresIn: '1h' });
res.json({ success: true, token, email: user.email });
});

// Intentional test route for verifying server error page behavior.
app.get('/debug/force-error', (req, res, next) => {
next(new Error('Intentional test error'));
});

app.use('/api', csvDownloadRouter);
app.use('/api', authenticateToken, csvDownloadRouter);

app.use('/api', (req, res) => {
return res.status(404).json({ error: 'API route not found' });
Expand Down