2025-07-16 12:00:50 -07:00

296 lines
8.9 KiB
JavaScript

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const session = require('express-session');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const InstagramStrategy = require('passport-instagram').Strategy;
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');
const bodyParser = require('body-parser');
const cors = require('cors');
const path = require('path');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Database setup
const db = new sqlite3.Database('./water_stations.db');
// Initialize database tables
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE,
email TEXT UNIQUE,
password_hash TEXT,
google_id TEXT,
instagram_id TEXT,
display_name TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS water_stations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
description TEXT,
created_by INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users (id)
)`);
db.run(`CREATE TABLE IF NOT EXISTS station_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
station_id INTEGER,
description TEXT,
last_refill_time DATETIME DEFAULT CURRENT_TIMESTAMP,
estimated_empty_time DATETIME,
updated_by INTEGER,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (station_id) REFERENCES water_stations (id),
FOREIGN KEY (updated_by) REFERENCES users (id)
)`);
});
// Middleware
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set to true in production with HTTPS
}));
app.use(passport.initialize());
app.use(passport.session());
// Passport configuration
passport.use(new LocalStrategy(
{ usernameField: 'username' },
async (username, password, done) => {
db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => {
if (err) return done(err);
if (!user) return done(null, false);
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) return done(null, false);
return done(null, user);
});
}
));
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/auth/google/callback"
}, (accessToken, refreshToken, profile, done) => {
db.get('SELECT * FROM users WHERE google_id = ?', [profile.id], (err, user) => {
if (err) return done(err);
if (user) {
return done(null, user);
} else {
db.run('INSERT INTO users (google_id, display_name, email) VALUES (?, ?, ?)',
[profile.id, profile.displayName, profile.emails[0].value],
function(err) {
if (err) return done(err);
db.get('SELECT * FROM users WHERE id = ?', [this.lastID], (err, user) => {
return done(err, user);
});
}
);
}
});
}));
passport.use(new InstagramStrategy({
clientID: process.env.INSTAGRAM_CLIENT_ID,
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET,
callbackURL: "/auth/instagram/callback"
}, (accessToken, refreshToken, profile, done) => {
db.get('SELECT * FROM users WHERE instagram_id = ?', [profile.id], (err, user) => {
if (err) return done(err);
if (user) {
return done(null, user);
} else {
db.run('INSERT INTO users (instagram_id, display_name) VALUES (?, ?)',
[profile.id, profile.displayName],
function(err) {
if (err) return done(err);
db.get('SELECT * FROM users WHERE id = ?', [this.lastID], (err, user) => {
return done(err, user);
});
}
);
}
});
}));
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
db.get('SELECT * FROM users WHERE id = ?', [id], (err, user) => {
done(err, user);
});
});
// Routes
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Authentication routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
app.get('/auth/instagram',
passport.authenticate('instagram')
);
app.get('/auth/instagram/callback',
passport.authenticate('instagram', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
app.post('/auth/login', passport.authenticate('local'), (req, res) => {
res.json({ success: true, user: req.user });
});
app.post('/auth/register', async (req, res) => {
const { username, email, password } = req.body;
try {
const hashedPassword = await bcrypt.hash(password, 10);
db.run('INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)',
[username, email, hashedPassword],
function(err) {
if (err) {
return res.status(400).json({ error: 'Username or email already exists' });
}
db.get('SELECT * FROM users WHERE id = ?', [this.lastID], (err, user) => {
if (err) return res.status(500).json({ error: 'Database error' });
req.login(user, (err) => {
if (err) return res.status(500).json({ error: 'Login error' });
res.json({ success: true, user: user });
});
});
}
);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
app.post('/auth/logout', (req, res) => {
req.logout(() => {
res.json({ success: true });
});
});
// API routes
app.get('/api/user', (req, res) => {
res.json({ user: req.user || null });
});
app.get('/api/stations', (req, res) => {
const query = `
SELECT
ws.*,
u.username as created_by_name,
su.description as latest_description,
su.last_refill_time,
su.estimated_empty_time,
su.updated_at as last_updated,
u2.username as updated_by_name
FROM water_stations ws
LEFT JOIN users u ON ws.created_by = u.id
LEFT JOIN (
SELECT station_id, description, last_refill_time, estimated_empty_time, updated_by, updated_at,
ROW_NUMBER() OVER (PARTITION BY station_id ORDER BY updated_at DESC) as rn
FROM station_updates
) su ON ws.id = su.station_id AND su.rn = 1
LEFT JOIN users u2 ON su.updated_by = u2.id
`;
db.all(query, [], (err, rows) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json(rows);
});
});
app.post('/api/stations', (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const { name, latitude, longitude, description } = req.body;
db.run('INSERT INTO water_stations (name, latitude, longitude, description, created_by) VALUES (?, ?, ?, ?, ?)',
[name, latitude, longitude, description, req.user.id],
function(err) {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ id: this.lastID, success: true });
}
);
});
app.post('/api/stations/:id/update', (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const { description, estimatedHours } = req.body;
const stationId = req.params.id;
const estimatedEmptyTime = new Date();
estimatedEmptyTime.setHours(estimatedEmptyTime.getHours() + parseInt(estimatedHours));
console.log("Updated by",req.user);
db.run('INSERT INTO station_updates (station_id, description, estimated_empty_time, updated_by) VALUES (?, ?, ?, ?)',
[stationId, description, estimatedEmptyTime.toISOString(), req.user.id],
function(err) {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ success: true });
}
);
});
app.get('/dashboard', (req, res) => {
if (!req.user) {
return res.redirect('/login');
}
res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
});
app.get('/login', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'login.html'));
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});