Email Sorter Beta
Ich habe soweit automatisiert the Emails sortieren aber ich muss noch schauen was es fur bugs es gibt wenn die app online ist deswegen wurde ich mit diesen Commit die website veroffentlichen obwohjl es sein konnte das es noch nicht fertig ist und verkaufs bereit
This commit is contained in:
359
server/index.mjs
359
server/index.mjs
@@ -1,209 +1,168 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import { Client, Databases, Query } from 'node-appwrite';
|
||||
import Stripe from 'stripe';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
/**
|
||||
* EmailSorter Backend Server
|
||||
* Main entry point
|
||||
*/
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
import 'dotenv/config'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, join } from 'path'
|
||||
|
||||
const requiredEnvVars = [
|
||||
'APPWRITE_ENDPOINT',
|
||||
'APPWRITE_PROJECT_ID',
|
||||
'APPWRITE_API_KEY',
|
||||
'APPWRITE_DATABASE_ID',
|
||||
'STRIPE_SECRET_KEY',
|
||||
'STRIPE_WEBHOOK_SECRET'
|
||||
];
|
||||
// Config & Middleware
|
||||
import { config, validateConfig } from './config/index.mjs'
|
||||
import { errorHandler, asyncHandler, NotFoundError, ValidationError } from './middleware/errorHandler.mjs'
|
||||
import { respond } from './utils/response.mjs'
|
||||
import { logger, log } from './middleware/logger.mjs'
|
||||
import { limiters } from './middleware/rateLimit.mjs'
|
||||
|
||||
for (const envVar of requiredEnvVars) {
|
||||
if (!process.env[envVar]) {
|
||||
console.error(`Error: Missing required environment variable: ${envVar}`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Routes
|
||||
import oauthRoutes from './routes/oauth.mjs'
|
||||
import emailRoutes from './routes/email.mjs'
|
||||
import stripeRoutes from './routes/stripe.mjs'
|
||||
import apiRoutes from './routes/api.mjs'
|
||||
import analyticsRoutes from './routes/analytics.mjs'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
// Validate configuration
|
||||
validateConfig()
|
||||
|
||||
// Create Express app
|
||||
const app = express()
|
||||
|
||||
// Trust proxy (for rate limiting behind reverse proxy)
|
||||
app.set('trust proxy', 1)
|
||||
|
||||
// Request ID middleware
|
||||
app.use((req, res, next) => {
|
||||
req.id = Math.random().toString(36).substring(2, 15)
|
||||
res.setHeader('X-Request-ID', req.id)
|
||||
next()
|
||||
})
|
||||
|
||||
// CORS
|
||||
app.use(cors(config.cors))
|
||||
|
||||
// Request logging
|
||||
app.use(logger({
|
||||
skip: (req) => req.path === '/api/health' || req.path.startsWith('/assets'),
|
||||
}))
|
||||
|
||||
// Rate limiting
|
||||
app.use('/api', limiters.api)
|
||||
|
||||
// Static files
|
||||
app.use(express.static(join(__dirname, '..', 'public')))
|
||||
|
||||
// Body parsing (BEFORE routes, AFTER static)
|
||||
// Note: Stripe webhook needs raw body, handled in stripe routes
|
||||
app.use('/api', express.json({ limit: '1mb' }))
|
||||
app.use('/api', express.urlencoded({ extended: true }))
|
||||
|
||||
// Health check (no rate limit)
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
environment: config.nodeEnv,
|
||||
uptime: Math.floor(process.uptime()),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// API Routes
|
||||
app.use('/api/oauth', oauthRoutes)
|
||||
app.use('/api/email', emailRoutes)
|
||||
app.use('/api/subscription', stripeRoutes)
|
||||
app.use('/api/analytics', analyticsRoutes)
|
||||
app.use('/api', apiRoutes)
|
||||
|
||||
// Preferences endpoints (inline for simplicity)
|
||||
import { userPreferences } from './services/database.mjs'
|
||||
|
||||
app.get('/api/preferences', asyncHandler(async (req, res) => {
|
||||
const { userId } = req.query
|
||||
if (!userId) throw new ValidationError('userId ist erforderlich')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
respond.success(res, prefs?.preferences || {
|
||||
vipSenders: [],
|
||||
blockedSenders: [],
|
||||
customRules: [],
|
||||
priorityTopics: [],
|
||||
})
|
||||
}))
|
||||
|
||||
app.post('/api/preferences', asyncHandler(async (req, res) => {
|
||||
const { userId, ...preferences } = req.body
|
||||
if (!userId) throw new ValidationError('userId ist erforderlich')
|
||||
|
||||
await userPreferences.upsert(userId, preferences)
|
||||
respond.success(res, null, 'Einstellungen gespeichert')
|
||||
}))
|
||||
|
||||
// Legacy Stripe webhook endpoint
|
||||
app.use('/stripe', stripeRoutes)
|
||||
|
||||
// 404 handler for API routes
|
||||
app.use('/api/*', (req, res, next) => {
|
||||
next(new NotFoundError('Endpoint'))
|
||||
})
|
||||
|
||||
// SPA fallback for non-API routes
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(join(__dirname, '..', 'public', 'index.html'))
|
||||
})
|
||||
|
||||
// Global error handler (must be last)
|
||||
app.use(errorHandler)
|
||||
|
||||
// Graceful shutdown
|
||||
let server
|
||||
|
||||
function gracefulShutdown(signal) {
|
||||
log.info(`${signal} empfangen, Server wird heruntergefahren...`)
|
||||
|
||||
server.close(() => {
|
||||
log.info('HTTP Server geschlossen')
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
// Force close after 10 seconds
|
||||
setTimeout(() => {
|
||||
log.error('Erzwungenes Herunterfahren')
|
||||
process.exit(1)
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
|
||||
|
||||
const client = new Client()
|
||||
.setEndpoint(process.env.APPWRITE_ENDPOINT)
|
||||
.setProject(process.env.APPWRITE_PROJECT_ID)
|
||||
.setKey(process.env.APPWRITE_API_KEY);
|
||||
// Handle uncaught errors
|
||||
process.on('uncaughtException', (err) => {
|
||||
log.error('Uncaught Exception:', { error: err.message, stack: err.stack })
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
const databases = new Databases(client);
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
log.error('Unhandled Rejection:', { reason, promise })
|
||||
})
|
||||
|
||||
app.use(express.static(join(__dirname, '..', 'public')));
|
||||
app.use('/api', express.json());
|
||||
// Start server
|
||||
server = app.listen(config.port, () => {
|
||||
console.log('')
|
||||
log.success(`Server gestartet auf Port ${config.port}`)
|
||||
log.info(`Frontend URL: ${config.frontendUrl}`)
|
||||
log.info(`Environment: ${config.nodeEnv}`)
|
||||
console.log('')
|
||||
console.log(` 🌐 API: http://localhost:${config.port}/api`)
|
||||
console.log(` 💚 Health: http://localhost:${config.port}/api/health`)
|
||||
console.log('')
|
||||
})
|
||||
|
||||
app.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
||||
const sig = req.headers['stripe-signature'];
|
||||
|
||||
try {
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
process.env.STRIPE_WEBHOOK_SECRET
|
||||
);
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const session = event.data.object;
|
||||
const submissionId = session.metadata.submissionId;
|
||||
|
||||
if (submissionId) {
|
||||
await databases.updateDocument(
|
||||
process.env.APPWRITE_DATABASE_ID,
|
||||
'submissions',
|
||||
submissionId,
|
||||
{ status: 'paid' }
|
||||
);
|
||||
|
||||
await databases.createDocument(
|
||||
process.env.APPWRITE_DATABASE_ID,
|
||||
'orders',
|
||||
'unique()',
|
||||
{
|
||||
submissionId: submissionId,
|
||||
orderDataJson: JSON.stringify(session)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ received: true });
|
||||
} catch (err) {
|
||||
console.error('Webhook error:', err.message);
|
||||
res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/questions', async (req, res) => {
|
||||
try {
|
||||
const { productSlug } = req.query;
|
||||
|
||||
const productsResponse = await databases.listDocuments(
|
||||
process.env.APPWRITE_DATABASE_ID,
|
||||
'products',
|
||||
[Query.equal('slug', productSlug), Query.equal('isActive', true)]
|
||||
);
|
||||
|
||||
if (productsResponse.documents.length === 0) {
|
||||
return res.status(404).json({ error: 'Product not found' });
|
||||
}
|
||||
|
||||
const product = productsResponse.documents[0];
|
||||
|
||||
const questionsResponse = await databases.listDocuments(
|
||||
process.env.APPWRITE_DATABASE_ID,
|
||||
'questions',
|
||||
[
|
||||
Query.equal('productId', product.$id),
|
||||
Query.equal('isActive', true),
|
||||
Query.orderAsc('step'),
|
||||
Query.orderAsc('order')
|
||||
]
|
||||
);
|
||||
|
||||
res.json(questionsResponse.documents);
|
||||
} catch (error) {
|
||||
console.error('Error fetching questions:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch questions' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/submissions', async (req, res) => {
|
||||
try {
|
||||
const { productSlug, answers } = req.body;
|
||||
|
||||
const productsResponse = await databases.listDocuments(
|
||||
process.env.APPWRITE_DATABASE_ID,
|
||||
'products',
|
||||
[Query.equal('slug', productSlug)]
|
||||
);
|
||||
|
||||
if (productsResponse.documents.length === 0) {
|
||||
return res.status(404).json({ error: 'Product not found' });
|
||||
}
|
||||
|
||||
const product = productsResponse.documents[0];
|
||||
|
||||
const submission = await databases.createDocument(
|
||||
process.env.APPWRITE_DATABASE_ID,
|
||||
'submissions',
|
||||
'unique()',
|
||||
{
|
||||
productId: product.$id,
|
||||
status: 'draft',
|
||||
customerEmail: answers.email || null,
|
||||
customerName: answers.name || null,
|
||||
finalSummaryJson: JSON.stringify(answers),
|
||||
priceCents: product.priceCents,
|
||||
currency: product.currency
|
||||
}
|
||||
);
|
||||
|
||||
await databases.createDocument(
|
||||
process.env.APPWRITE_DATABASE_ID,
|
||||
'answers',
|
||||
'unique()',
|
||||
{
|
||||
submissionId: submission.$id,
|
||||
answersJson: JSON.stringify(answers)
|
||||
}
|
||||
);
|
||||
|
||||
res.json({ submissionId: submission.$id });
|
||||
} catch (error) {
|
||||
console.error('Error creating submission:', error);
|
||||
res.status(500).json({ error: 'Failed to create submission' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/checkout', async (req, res) => {
|
||||
try {
|
||||
const { submissionId } = req.body;
|
||||
|
||||
if (!submissionId) {
|
||||
return res.status(400).json({ error: 'Missing submissionId' });
|
||||
}
|
||||
|
||||
const submission = await databases.getDocument(
|
||||
process.env.APPWRITE_DATABASE_ID,
|
||||
'submissions',
|
||||
submissionId
|
||||
);
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: submission.currency,
|
||||
product_data: {
|
||||
name: 'Email Sortierer Service',
|
||||
},
|
||||
unit_amount: submission.priceCents,
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'payment',
|
||||
success_url: `${process.env.BASE_URL || 'http://localhost:3000'}/success.html`,
|
||||
cancel_url: `${process.env.BASE_URL || 'http://localhost:3000'}/cancel.html`,
|
||||
metadata: {
|
||||
submissionId: submissionId
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ url: session.url });
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
res.status(500).json({ error: 'Failed to create checkout session' });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
export default app
|
||||
|
||||
Reference in New Issue
Block a user