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:
2026-01-22 19:32:12 +01:00
parent 95349af50b
commit abf761db07
596 changed files with 56405 additions and 51231 deletions

View File

@@ -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