| import express from 'express'; |
| import mongoose from 'mongoose'; |
| import path from 'path'; |
| import jwt from 'jsonwebtoken'; |
| import bcrypt from 'bcryptjs'; |
|
|
| async function startServer() { |
| const app = express(); |
| const PORT = Number(process.env.PORT || 7860); |
|
|
| app.use(express.json({ limit: '50mb' })); |
|
|
| |
| const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/buildtrack_db'; |
| let isMongoConnected = false; |
| try { |
| await mongoose.connect(mongoURI, { serverSelectionTimeoutMS: 2000 }); |
| isMongoConnected = true; |
| console.log('Connected to MongoDB'); |
| } catch (error) { |
| console.error('Failed to connect to MongoDB, falling back to in-memory store', error.message); |
| } |
|
|
| |
| |
| const inMemoryDB: Record<string, any[]> = {}; |
|
|
| app.get('/api/health', (req, res) => { |
| res.json({ status: 'ok', db: isMongoConnected ? mongoose.connection.readyState : 'in-memory' }); |
| }); |
|
|
| |
| const JWT_SECRET = process.env.JWT_SECRET || '5aeb6c98c7622543d05e89904d09aed7b33f45a616204b31d4a18de8397c3e7d'; |
| const createLocalAvatar = (name: string) => { |
| const initials = name |
| .split(/\s+/) |
| .filter(Boolean) |
| .slice(0, 2) |
| .map(part => part[0]?.toUpperCase() || '') |
| .join('') || 'U'; |
| return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96"><rect width="96" height="96" rx="24" fill="#2563eb"/><text x="50%" y="54%" dominant-baseline="middle" text-anchor="middle" font-family="Arial, sans-serif" font-size="34" font-weight="700" fill="#fff">${initials}</text></svg>`)}`; |
| }; |
|
|
| app.post('/api/auth/signup', async (req, res) => { |
| const { name, email, password, role } = req.body; |
| if (!email || !password || !name) return res.status(400).json({ error: "Missing fields" }); |
|
|
| try { |
| const passwordHash = await bcrypt.hash(password, 10); |
| let user; |
|
|
| if (!isMongoConnected) { |
| if (!inMemoryDB['users']) inMemoryDB['users'] = []; |
| if (inMemoryDB['users'].find(u => u.email === email)) return res.status(400).json({ error: "User exists" }); |
| user = { id: `user-${Date.now()}`, uid: `user-${Date.now()}`, name, email, passwordHash, role, avatar: createLocalAvatar(name), createdAt: new Date().toISOString() }; |
| inMemoryDB['users'].push(user); |
| } else { |
| if (await mongoose.connection.db.collection('users').findOne({ email })) return res.status(400).json({ error: "User exists" }); |
| user = { id: `user-${Date.now()}`, uid: `user-${Date.now()}`, name, email, passwordHash, role, avatar: createLocalAvatar(name), createdAt: new Date().toISOString() }; |
| await mongoose.connection.db.collection('users').insertOne(user); |
| } |
|
|
| res.status(201).json({ message: "User created" }); |
| } catch (e) { |
| res.status(500).json({ error: "Signup failed" }); |
| } |
| }); |
|
|
| app.post('/api/auth/login', async (req, res) => { |
| const { email, password } = req.body; |
| if (!email || !password) return res.status(400).json({ error: "Email and password required" }); |
|
|
| try { |
| let user; |
| if (!isMongoConnected) { |
| user = (inMemoryDB['users'] || []).find(u => u.email === email); |
| } else { |
| user = await mongoose.connection.db.collection('users').findOne({ email }); |
| } |
|
|
| if (!user || !(await bcrypt.compare(password, user.passwordHash))) { |
| return res.status(401).json({ error: "Invalid credentials" }); |
| } |
|
|
| const token = jwt.sign({ uid: user.uid, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: '7d' }); |
| res.json({ token, user }); |
| } catch (e) { |
| res.status(500).json({ error: "Login failed" }); |
| } |
| }); |
|
|
| app.get('/api/auth/me', async (req, res) => { |
| const authHeader = req.headers['authorization']; |
| const token = authHeader && authHeader.split(' ')[1]; |
| if (!token) return res.status(401).json({ error: "Missing token" }); |
|
|
| jwt.verify(token, JWT_SECRET, async (err: any, decoded: any) => { |
| if (err) return res.status(403).json({ error: "Invalid token" }); |
| try { |
| let user; |
| if (!isMongoConnected) { |
| user = (inMemoryDB['users'] || []).find(u => u.uid === decoded.uid); |
| } else { |
| user = await mongoose.connection.db.collection('users').findOne({ uid: decoded.uid }); |
| } |
| if (!user) return res.status(404).json({ error: "User not found" }); |
| res.json({ user }); |
| } catch (e) { |
| res.status(500).json({ error: "Server error" }); |
| } |
| }); |
| }); |
|
|
| |
| const authenticateToken = (req: express.Request, res: express.Response, next: express.NextFunction) => { |
| const authHeader = req.headers['authorization']; |
| const token = authHeader && authHeader.split(' ')[1]; |
| if (token == null) return res.status(401).json({ error: "Unauthorized" }); |
|
|
| jwt.verify(token, JWT_SECRET, (err: any, user: any) => { |
| if (err) return res.status(403).json({ error: "Forbidden" }); |
| (req as any).user = user; |
| next(); |
| }); |
| }; |
|
|
| |
| app.use('/api/collections', authenticateToken); |
|
|
| app.get('/api/collections/:name', async (req, res) => { |
| try { |
| if (!isMongoConnected) { |
| return res.json(inMemoryDB[req.params.name] || []); |
| } |
| const docs = await mongoose.connection.db.collection(req.params.name).find().toArray(); |
| res.json(docs); |
| } catch (e) { |
| res.status(500).json({ error: 'Failed to fetch collection' }); |
| } |
| }); |
|
|
| app.post('/api/collections/:name', async (req, res) => { |
| try { |
| if (!isMongoConnected) { |
| if (!inMemoryDB[req.params.name]) inMemoryDB[req.params.name] = []; |
| const doc = { ...req.body }; |
| if (!doc.id) doc.id = Date.now().toString() + Math.random().toString(); |
| inMemoryDB[req.params.name].push(doc); |
| return res.json(doc); |
| } |
| const doc = { ...req.body }; |
| if (!doc.id) doc.id = new mongoose.Types.ObjectId().toString(); |
| await mongoose.connection.db.collection(req.params.name).insertOne(doc); |
| res.json(doc); |
| } catch (e) { |
| res.status(500).json({ error: 'Failed to insert document' }); |
| } |
| }); |
|
|
| app.put('/api/collections/:name/:id', async (req, res) => { |
| try { |
| if (!isMongoConnected) { |
| if (!inMemoryDB[req.params.name]) inMemoryDB[req.params.name] = []; |
| const idx = inMemoryDB[req.params.name].findIndex(d => d.id === req.params.id); |
| const updateData = { ...req.body }; |
| if (idx >= 0) { |
| inMemoryDB[req.params.name][idx] = { ...inMemoryDB[req.params.name][idx], ...updateData }; |
| } else { |
| inMemoryDB[req.params.name].push({ id: req.params.id, ...updateData }); |
| } |
| return res.json({ id: req.params.id, ...updateData }); |
| } |
| const updateData = { ...req.body }; |
| delete updateData._id; |
| |
| await mongoose.connection.db.collection(req.params.name).updateOne( |
| { id: req.params.id }, |
| { $set: updateData }, |
| { upsert: true } |
| ); |
| res.json({ id: req.params.id, ...updateData }); |
| } catch (e) { |
| res.status(500).json({ error: 'Failed to update document' }); |
| } |
| }); |
|
|
| app.delete('/api/collections/:name/:id', async (req, res) => { |
| try { |
| if (!isMongoConnected) { |
| if (inMemoryDB[req.params.name]) { |
| inMemoryDB[req.params.name] = inMemoryDB[req.params.name].filter(d => d.id !== req.params.id); |
| } |
| return res.json({ success: true }); |
| } |
| await mongoose.connection.db.collection(req.params.name).deleteOne({ id: req.params.id }); |
| res.json({ success: true }); |
| } catch (e) { |
| res.status(500).json({ error: 'Failed to delete document' }); |
| } |
| }); |
|
|
| |
| if (process.env.NODE_ENV !== 'production') { |
| const { createServer: createViteServer } = await import('vite'); |
| const vite = await createViteServer({ |
| server: { middlewareMode: true }, |
| appType: 'custom', |
| }); |
|
|
| app.get('/', async (req, res, next) => { |
| try { |
| const indexHtmlPath = path.join(process.cwd(), 'index.html'); |
| const { readFile } = await import('node:fs/promises'); |
| const template = await readFile(indexHtmlPath, 'utf-8'); |
| const html = await vite.transformIndexHtml(req.originalUrl, template); |
| res.status(200).set({ 'Content-Type': 'text/html' }).end(html); |
| } catch (error) { |
| vite.ssrFixStacktrace(error as Error); |
| next(error); |
| } |
| }); |
|
|
| app.use(vite.middlewares); |
| } else { |
| const distPath = path.join(process.cwd(), 'dist'); |
| app.use(express.static(distPath)); |
| app.get(/.*/, (req, res) => { |
| res.sendFile(path.join(distPath, 'index.html')); |
| }); |
| } |
|
|
| app.listen(PORT, '0.0.0.0', () => { |
| console.log(`Server running on http://localhost:${PORT}`); |
| }); |
| } |
|
|
| startServer(); |
|
|