Files
NanoJason/backend/server.js

548 lines
17 KiB
JavaScript

const express = require('express');
const cors = require('cors');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const sqlite3 = require('sqlite3').verbose();
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const app = express();
const PORT = process.env.PORT || 12004;
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, '..')));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
// Initialize SQLite database
const db = new sqlite3.Database('./mindshift.db');
// Create tables
db.serialize(() => {
// Users table
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME
)`);
// Mood tracking table
db.run(`CREATE TABLE IF NOT EXISTS mood_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
mood_type TEXT NOT NULL,
intensity INTEGER NOT NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Thoughts table
db.run(`CREATE TABLE IF NOT EXISTS thoughts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
situation TEXT NOT NULL,
automatic_thought TEXT NOT NULL,
emotion TEXT NOT NULL,
emotion_intensity INTEGER NOT NULL,
cognitive_distortion TEXT,
alternative_thought TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Gratitude entries table
db.run(`CREATE TABLE IF NOT EXISTS gratitude_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
entry TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Progress tracking table
db.run(`CREATE TABLE IF NOT EXISTS progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date DATE NOT NULL,
mood_score INTEGER,
thoughts_count INTEGER DEFAULT 0,
gratitude_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, date),
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Notifications table
db.run(`CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
type TEXT DEFAULT 'info',
read BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Exercise Logs table
db.run(`CREATE TABLE IF NOT EXISTS exercise_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
exercise_type TEXT NOT NULL,
duration INTEGER NOT NULL,
completed BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
});
// JWT authentication middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
}
// Auth routes
app.post('/api/auth/register', async (req, res) => {
try {
const { email, password, name } = req.body;
if (!email || !password || !name) {
return res.status(400).json({ error: 'Email, password, and name are required' });
}
// Check if user already exists
db.get('SELECT id FROM users WHERE email = ?', [email], async (err, user) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
if (user) {
return res.status(400).json({ error: 'User already exists' });
}
// Hash password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Insert new user
db.run('INSERT INTO users (email, password, name) VALUES (?, ?, ?)',
[email, hashedPassword, name], function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
// Generate JWT
const token = jwt.sign(
{ id: this.lastID, email, name },
JWT_SECRET,
{ expiresIn: '24h' }
);
res.status(201).json({
message: 'User created successfully',
token,
user: {
id: this.lastID,
email,
name
}
});
});
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
app.post('/api/auth/login', (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
// Find user
db.get('SELECT * FROM users WHERE email = ?', [email], async (err, user) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check password
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Update last login
db.run('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', [user.id]);
// Generate JWT
const token = jwt.sign(
{ id: user.id, email: user.email, name: user.name },
JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
message: 'Login successful',
token,
user: {
id: user.id,
email: user.email,
name: user.name
}
});
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Get user profile
app.get('/api/user/profile', authenticateToken, (req, res) => {
db.get('SELECT id, email, name, created_at, last_login FROM users WHERE id = ?', [req.user.id], (err, user) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
});
// Mood tracking
app.post('/api/mood/track', authenticateToken, (req, res) => {
try {
const { moodType, intensity, notes } = req.body;
if (!moodType || !intensity || intensity < 1 || intensity > 10) {
return res.status(400).json({ error: 'Invalid mood data' });
}
// Insert mood entry
db.run('INSERT INTO mood_entries (user_id, mood_type, intensity, notes) VALUES (?, ?, ?, ?)',
[req.user.id, moodType, intensity, notes || null], function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
// Update or create progress entry for today
const today = new Date().toISOString().split('T')[0];
db.run(`INSERT OR REPLACE INTO progress (user_id, date, mood_score, thoughts_count, gratitude_count)
VALUES (?, ?, COALESCE((SELECT mood_score FROM progress WHERE user_id = ? AND date = ?), ?),
COALESCE((SELECT thoughts_count FROM progress WHERE user_id = ? AND date = ?), 0),
COALESCE((SELECT gratitude_count FROM progress WHERE user_id = ? AND date = ?), 0))`,
[req.user.id, today, req.user.id, today, intensity, req.user.id, today, req.user.id, today],
function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json({
message: 'Mood tracked successfully',
entryId: this.lastID
});
});
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Get mood history
app.get('/api/mood/history', authenticateToken, (req, res) => {
const limit = parseInt(req.query.limit) || 30;
db.all('SELECT * FROM mood_entries WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
[req.user.id, limit], (err, entries) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.json(entries);
});
});
// Thought recording
app.post('/api/thoughts', authenticateToken, (req, res) => {
try {
const { situation, automaticThought, emotion, emotionIntensity, cognitiveDistortion, alternativeThought } = req.body;
if (!situation || !automaticThought || !emotion || !emotionIntensity || emotionIntensity < 1 || emotionIntensity > 10) {
return res.status(400).json({ error: 'Invalid thought data' });
}
// Insert thought entry
db.run('INSERT INTO thoughts (user_id, situation, automatic_thought, emotion, emotion_intensity, cognitive_distortion, alternative_thought) VALUES (?, ?, ?, ?, ?, ?, ?)',
[req.user.id, situation, automaticThought, emotion, emotionIntensity, cognitiveDistortion || null, alternativeThought || null], function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
// Update progress entry for today
const today = new Date().toISOString().split('T')[0];
db.run(`INSERT OR REPLACE INTO progress (user_id, date, mood_score, thoughts_count, gratitude_count)
VALUES (?, ?, COALESCE((SELECT mood_score FROM progress WHERE user_id = ? AND date = ?), 0),
COALESCE((SELECT thoughts_count FROM progress WHERE user_id = ? AND date = ?), 0) + 1,
COALESCE((SELECT gratitude_count FROM progress WHERE user_id = ? AND date = ?), 0))`,
[req.user.id, today, req.user.id, today, req.user.id, today, req.user.id, today],
function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json({
message: 'Thought recorded successfully',
entryId: this.lastID
});
});
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Get thoughts history
app.get('/api/thoughts', authenticateToken, (req, res) => {
const limit = parseInt(req.query.limit) || 30;
db.all('SELECT * FROM thoughts WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
[req.user.id, limit], (err, entries) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.json(entries);
});
});
// Gratitude entries
app.post('/api/gratitude', authenticateToken, (req, res) => {
try {
const { entry } = req.body;
if (!entry || entry.trim() === '') {
return res.status(400).json({ error: 'Gratitude entry is required' });
}
// Insert gratitude entry
db.run('INSERT INTO gratitude_entries (user_id, entry) VALUES (?, ?)',
[req.user.id, entry], function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
// Update progress entry for today
const today = new Date().toISOString().split('T')[0];
db.run(`INSERT OR REPLACE INTO progress (user_id, date, mood_score, thoughts_count, gratitude_count)
VALUES (?, ?, COALESCE((SELECT mood_score FROM progress WHERE user_id = ? AND date = ?), 0),
COALESCE((SELECT thoughts_count FROM progress WHERE user_id = ? AND date = ?), 0),
COALESCE((SELECT gratitude_count FROM progress WHERE user_id = ? AND date = ?), 0) + 1)`,
[req.user.id, today, req.user.id, today, req.user.id, today, req.user.id, today],
function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json({
message: 'Gratitude entry added successfully',
entryId: this.lastID
});
});
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Log Exercise Session
app.post('/api/exercises', authenticateToken, (req, res) => {
try {
const { exerciseType, duration } = req.body;
if (!exerciseType || !duration) {
return res.status(400).json({ error: 'Type and duration required' });
}
db.run('INSERT INTO exercise_logs (user_id, exercise_type, duration) VALUES (?, ?, ?)',
[req.user.id, exerciseType, duration], function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json({ success: true, id: this.lastID });
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Get gratitude entries
app.get('/api/gratitude', authenticateToken, (req, res) => {
const limit = parseInt(req.query.limit) || 30;
db.all('SELECT * FROM gratitude_entries WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
[req.user.id, limit], (err, entries) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.json(entries);
});
});
// Get progress data
app.get('/api/progress', authenticateToken, (req, res) => {
const days = parseInt(req.query.days) || 30;
db.all('SELECT * FROM progress WHERE user_id = ? ORDER BY date DESC LIMIT ?',
[req.user.id, days], (err, entries) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.json(entries);
});
});
// Get dashboard stats
app.get('/api/dashboard/stats', authenticateToken, (req, res) => {
const today = new Date().toISOString().split('T')[0];
// Get today's progress
db.get('SELECT * FROM progress WHERE user_id = ? AND date = ?', [req.user.id, today], (err, todayProgress) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
// Get this week's mood entries
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const weekAgoStr = weekAgo.toISOString().split('T')[0];
db.all('SELECT AVG(intensity) as avgMood, COUNT(*) as count FROM mood_entries WHERE user_id = ? AND created_at >= ?',
[req.user.id, weekAgoStr], (err, weekMood) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
// Get total counts
db.get(`SELECT
(SELECT COUNT(*) FROM mood_entries WHERE user_id = ?) as totalMoods,
(SELECT COUNT(*) FROM thoughts WHERE user_id = ?) as totalThoughts,
(SELECT COUNT(*) FROM gratitude_entries WHERE user_id = ?) as totalGratitude,
(SELECT COUNT(*) FROM exercise_logs WHERE user_id = ?) as totalSessions`,
[req.user.id, req.user.id, req.user.id, req.user.id], (err, totals) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.json({
today: todayProgress || { mood_score: 0, thoughts_count: 0, gratitude_count: 0 },
week: {
avgMood: weekMood[0].avgMood || 0,
moodCount: weekMood[0].count || 0
},
totals: totals
});
});
});
});
});
// Notifications
app.get('/api/notifications', authenticateToken, (req, res) => {
db.all('SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC', [req.user.id], (err, notifications) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.json(notifications);
});
});
app.post('/api/notifications', authenticateToken, (req, res) => {
const { title, message, type } = req.body;
if (!title || !message) {
return res.status(400).json({ error: 'Title and message are required' });
}
db.run('INSERT INTO notifications (user_id, title, message, type) VALUES (?, ?, ?, ?)',
[req.user.id, title, message, type || 'info'], function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
// Return the created notification
db.get('SELECT * FROM notifications WHERE id = ?', [this.lastID], (err, notification) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json(notification);
});
});
});
app.put('/api/notifications/:id/read', authenticateToken, (req, res) => {
const notificationId = req.params.id;
db.run('UPDATE notifications SET read = 1 WHERE id = ? AND user_id = ?',
[notificationId, req.user.id], function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
if (this.changes === 0) {
return res.status(404).json({ error: 'Notification not found' });
}
res.json({ success: true });
});
});
app.delete('/api/notifications/:id', authenticateToken, (req, res) => {
const notificationId = req.params.id;
db.run('DELETE FROM notifications WHERE id = ? AND user_id = ?',
[notificationId, req.user.id], function(err) {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
if (this.changes === 0) {
return res.status(404).json({ error: 'Notification not found' });
}
res.json({ success: true });
});
});
// Start server
app.listen(PORT, () => {
console.log(`MindShift server running on port ${PORT}`);
});