515 lines
16 KiB
JavaScript
515 lines
16 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)
|
|
)`);
|
|
});
|
|
|
|
// 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' });
|
|
}
|
|
});
|
|
|
|
// 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`,
|
|
[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}`);
|
|
}); |