Mantente al día con tus medicamentos.
diff --git a/backend/server.js b/backend/server.js index 1f52395..a321018 100644 --- a/backend/server.js +++ b/backend/server.js @@ -65,7 +65,8 @@ const sessionConfig = { resave: false, saveUninitialized: false, cookie: { - secure: process.env.NODE_ENV === 'production', + secure: process.env.COOKIE_SECURE === 'true', + sameSite: 'lax', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 // 24 hours } @@ -225,6 +226,22 @@ async function initDatabase() { `); } + if (pgPool) { + await pgPool.query(` + CREATE TABLE IF NOT EXISTS user_alerts ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + type TEXT NOT NULL CHECK (type IN ('reminder', 'availability', 'schedule')), + medicine_nregistro TEXT, + title TEXT, + detail TEXT, + schedule TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() + ) + `); + } + await dbRun(` CREATE TABLE IF NOT EXISTS push_subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -254,7 +271,7 @@ async function initDatabase() { `); await dbRun(`CREATE INDEX IF NOT EXISTS idx_push_subs_pharm ON push_subscriptions_pharmacy(medicine_nregistro, pharmacy_id)`); - if (!pgPool) { +if (!pgPool) { // SQLite-only migrations for existing deployments try { await dbRun('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0'); } catch {} try { await dbRun('ALTER TABLE users ADD COLUMN address TEXT'); } catch {} @@ -262,13 +279,32 @@ async function initDatabase() { try { await dbRun('ALTER TABLE users ADD COLUMN longitude REAL'); } catch {} } - // Add user_id to push tables (SQLite — no cross-DB FK) +// Add user_id to push tables (SQLite — no cross-DB FK) try { await dbRun('ALTER TABLE push_subscriptions ADD COLUMN user_id INTEGER'); } catch {} try { await dbRun('ALTER TABLE push_subscriptions_pharmacy ADD COLUMN user_id INTEGER'); } catch {} - console.log('Database initialized successfully'); - } catch (error) { - console.error('Error initializing database:', error); + // Create alerts table for persistent Avisos data + try { + await dbRun(` + CREATE TABLE IF NOT EXISTS user_alerts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + type TEXT NOT NULL CHECK (type IN ('reminder', 'availability', 'schedule')), + medicine_nregistro TEXT, + title TEXT, + detail TEXT, + schedule TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + `); + } catch (e) { + if (!/table already exists/i.test(e.message)) throw e; + } + } catch (err) { + console.error('initDatabase failed:', err); + throw err; } } @@ -630,6 +666,92 @@ app.put('/api/users/me', requireAuth, async (req, res) => { } }); +// ========== USER ALERTS API ========== + +// Get all alerts for the current user +app.get('/api/alerts', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const rows = await userDbAll( + 'SELECT id, type, medicine_nregistro, title, detail, schedule, created_at, updated_at FROM user_alerts WHERE user_id = ? ORDER BY created_at DESC', + [userId] + ); + res.json(rows); + } catch (error) { + console.error('Error fetching alerts:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Create a new alert +app.post('/api/alerts', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const { type, medicine_nregistro, title, detail, schedule } = req.body || {}; + + if (!type || !['reminder', 'availability', 'schedule'].includes(type)) { + return res.status(400).json({ error: 'Valid type is required (reminder, availability, schedule)' }); + } + + const result = await userDbRun( + 'INSERT INTO user_alerts (user_id, type, medicine_nregistro, title, detail, schedule) VALUES (?, ?, ?, ?, ?, ?)', + [userId, type, medicine_nregistro || null, title || null, detail || null, schedule || null] + ); + + const newAlert = await userDbGet('SELECT * FROM user_alerts WHERE id = ?', [result.lastID]); + res.status(201).json(newAlert); + } catch (error) { + console.error('Error creating alert:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Update an existing alert +app.put('/api/alerts/:id', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const alertId = parseInt(req.params.id); + const { medicine_nregistro, title, detail, schedule } = req.body || {}; + + const result = await userDbRun( + 'UPDATE user_alerts SET medicine_nregistro = ?, title = ?, detail = ?, schedule = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?', + [medicine_nregistro || null, title || null, detail || null, schedule || null, alertId, userId] + ); + + if (result.changes === 0) { + return res.status(404).json({ error: 'Alert not found' }); + } + + const updatedAlert = await userDbGet('SELECT * FROM user_alerts WHERE id = ?', [alertId]); + res.json(updatedAlert); + } catch (error) { + console.error('Error updating alert:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Delete an alert +app.delete('/api/alerts/:id', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const alertId = parseInt(req.params.id); + + const result = await userDbRun( + 'DELETE FROM user_alerts WHERE id = ? AND user_id = ?', + [alertId, userId] + ); + + if (result.changes === 0) { + return res.status(404).json({ error: 'Alert not found' }); + } + + res.status(204).end(); + } catch (error) { + console.error('Error deleting alert:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + // Geocode for any authenticated user (rate-limited). Same impl as /api/admin/geocode. app.get('/api/geocode', requireAuth, geocodeLimiter, async (req, res) => { const q = (req.query.q || '').trim(); diff --git a/frontend/index.html b/frontend/index.html index f3a88e2..4d57bd9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@
- +
- )}
- Mantente al día con tus medicamentos.
Cargando avisos...
} + {!loading && error &&{error}
} + + {!loading && !error && ( + <> + {/* Availability Section */} +No tienes suscripciones de disponibilidad.
+ ) : ( + availability.map((alert) => { + const cardColor = alert.scope === 'pharmacy' ? 'primary' : 'tertiary'; + const cardIcon = alert.scope === 'pharmacy' ? 'store' : 'bell'; + const key = `${alert.scope}:${alert.id}`; + return ( ++ 📍 {alert.pharmacy_address} +
+ )} +