File size: 3,373 Bytes
f6c14df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acc0caa
 
 
 
 
 
 
 
 
f6c14df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
require('dotenv').config();
const express = require('express');
const cors    = require('cors');
const helmet  = require('helmet');
const rateLimit = require('express-rate-limit');

const authRoutes    = require('./routes/auth');
const taskRoutes    = require('./routes/tasks');
const adminRoutes   = require('./routes/admin');
const commentRoutes = require('./routes/comments');

const app = express();

// On HuggingFace, nginx proxies /api/* to this process on 127.0.0.1:5000
// so requests arrive without an Origin header — CORS is only needed for local dev
// HuggingFace Spaces embeds the app in an iframe — must disable frameguard
// so X-Frame-Options doesn't block the embed
app.use(helmet({
  contentSecurityPolicy: false,
  frameguard: false,
  crossOriginEmbedderPolicy: false,
  crossOriginOpenerPolicy: false,
  crossOriginResourcePolicy: false,
}));

const allowedOrigins = [
  process.env.FRONTEND_URL || 'http://localhost:3000',
  'http://localhost:3000',
  'http://127.0.0.1:3000',
];

app.use(cors({
  origin: (origin, cb) => {
    // Allow requests with no origin (like mobile apps or curl)
    if (!origin) return cb(null, true);
    
    // Allow Hugging Face domains or locally allowed origins
    if (origin.endsWith('.hf.space') || origin.includes('huggingface.co') || allowedOrigins.includes(origin)) {
      return cb(null, true);
    }
    
    // In production, be a bit more flexible if we're on HF
    if (process.env.NODE_ENV === 'production' && origin.includes('hf.space')) {
      return cb(null, true);
    }

    // Allow all in development for easier debugging
    if (process.env.NODE_ENV !== 'production') {
      return cb(null, true);
    }

    cb(new Error(`CORS blocked for origin: ${origin}`));
  },
  credentials: true,
  methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
}));

const generalLimiter = rateLimit({ windowMs: 15*60*1000, max: 500, standardHeaders: true, legacyHeaders: false });
const authLimiter    = rateLimit({ windowMs: 15*60*1000, max: 20,  message: { error: 'Too many attempts. Try again later.' } });

app.use(generalLimiter);
app.use(express.json({ limit: '2mb' }));
app.use(express.urlencoded({ extended: true }));

app.get('/health', (req, res) =>
  res.json({ status: 'ok', service: 'taskflow-api', ts: new Date().toISOString() })
);

app.use('/api/auth',               authLimiter, authRoutes);
app.use('/api/tasks',              taskRoutes);
app.use('/api/tasks/:id/comments', commentRoutes);
app.use('/api/admin',              adminRoutes);

app.use((req, res) => res.status(404).json({ error: 'Endpoint not found.' }));

// Global error handler
app.use((err, req, res, next) => {
  console.error('[Global Error Manager]:', err.message || err);
  
  // Specific handling for CORS errors to make them more helpful
  if (err.message && err.message.includes('CORS')) {
    return res.status(403).json({ 
      error: 'Security block: origin not allowed.', 
      details: process.env.NODE_ENV === 'production' ? undefined : err.message 
    });
  }

  res.status(500).json({ error: 'An unexpected error occurred.' });
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, '127.0.0.1', () =>
  console.log(`Taskflow API on port ${PORT} [${process.env.NODE_ENV || 'production'}]`)
);

module.exports = app;