Initial commit: TenderPilot MVP
This commit is contained in:
188
server.js
Executable file
188
server.js
Executable file
@@ -0,0 +1,188 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import pg from 'pg';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const pool = new pg.Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://tenderpilot:tenderpilot123@localhost:5432/tenderpilot'
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Auth token verification middleware
|
||||
const verifyToken = (req, res, next) => {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'No token provided' });
|
||||
|
||||
try {
|
||||
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
||||
next();
|
||||
} catch (e) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
// POST /api/auth/register
|
||||
app.post('/api/auth/register', async (req, res) => {
|
||||
try {
|
||||
const { email, password, company_name, tier } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email and password required' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const result = await pool.query(
|
||||
'INSERT INTO users (email, password_hash, company_name, tier) VALUES ($1, $2, $3, $4) RETURNING id, email, company_name, tier',
|
||||
[email, hashedPassword, company_name || '', tier || 'free']
|
||||
);
|
||||
|
||||
const user = result.rows[0];
|
||||
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET);
|
||||
|
||||
res.status(201).json({ user, token });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({ error: 'Email already exists' });
|
||||
}
|
||||
res.status(500).json({ error: 'Registration failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/login
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email and password required' });
|
||||
}
|
||||
|
||||
const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
const passwordMatch = await bcrypt.compare(password, user.password_hash);
|
||||
|
||||
if (!passwordMatch) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET);
|
||||
res.json({ user: { id: user.id, email: user.email, company_name: user.company_name, tier: user.tier }, token });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/tenders
|
||||
app.get('/api/tenders', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { search, sort, limit, offset } = req.query;
|
||||
let query = 'SELECT * FROM tenders WHERE status = $1';
|
||||
const params = ['open'];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (search) {
|
||||
query += ` AND (title ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`;
|
||||
params.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
query += ` ORDER BY ${sort === 'value' ? 'value_high DESC' : 'deadline ASC'} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
params.push(Math.min(parseInt(limit) || 20, 100), parseInt(offset) || 0);
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
res.json({ tenders: result.rows, total: result.rows.length });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Failed to fetch tenders' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/tenders/:id
|
||||
app.get('/api/tenders/:id', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM tenders WHERE id = $1', [req.params.id]);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Tender not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Failed to fetch tender' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/profile
|
||||
app.post('/api/profile', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { sectors, keywords, min_value, max_value, locations, authority_types } = req.body;
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO profiles (user_id, sectors, keywords, min_value, max_value, locations, authority_types)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
sectors = $2, keywords = $3, min_value = $4, max_value = $5, locations = $6, authority_types = $7, updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *`,
|
||||
[req.user.id, sectors || [], keywords || [], min_value || null, max_value || null, locations || [], authority_types || []]
|
||||
);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Failed to save profile' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/matches
|
||||
app.get('/api/matches', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT t.* FROM tenders t
|
||||
INNER JOIN matches m ON t.id = m.tender_id
|
||||
WHERE m.user_id = $1
|
||||
ORDER BY t.deadline ASC`,
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ matches: result.rows });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Failed to fetch matches' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3456;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user