genesis
This commit is contained in:
82
src/config/database.js
Normal file
82
src/config/database.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const sql = require('mssql');
|
||||
|
||||
const config = {
|
||||
server: process.env.DB_SERVER,
|
||||
database: process.env.DB_DATABASE,
|
||||
user: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: parseInt(process.env.DB_PORT) || 1433,
|
||||
options: {
|
||||
encrypt: false, // Disable encryption to avoid TLS warnings with IP addresses
|
||||
trustServerCertificate: true,
|
||||
enableArithAbort: true,
|
||||
},
|
||||
pool: {
|
||||
max: 10,
|
||||
min: 0,
|
||||
idleTimeoutMillis: 30000,
|
||||
},
|
||||
};
|
||||
|
||||
let poolPromise;
|
||||
|
||||
const getPool = () => {
|
||||
if (!poolPromise) {
|
||||
poolPromise = new sql.ConnectionPool(config).connect().then(pool => {
|
||||
console.log('✅ Connected to MSSQL database');
|
||||
return pool;
|
||||
}).catch(err => {
|
||||
console.error('❌ Database connection failed:', err);
|
||||
poolPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return poolPromise;
|
||||
};
|
||||
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
if (!process.env.DB_SERVER) {
|
||||
console.log('⚠️ Database configuration not found. Application will run without database.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const pool = await getPool();
|
||||
const result = await pool.request().query('SELECT 1 as test');
|
||||
console.log('✅ Database connection test successful');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Database connection test failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const executeQuery = async (query, params = {}) => {
|
||||
if (!process.env.DB_SERVER) {
|
||||
throw new Error('Database not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const pool = await getPool();
|
||||
const request = pool.request();
|
||||
|
||||
// Add parameters to the request
|
||||
Object.keys(params).forEach(key => {
|
||||
request.input(key, params[key]);
|
||||
});
|
||||
|
||||
const result = await request.query(query);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Database query error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
config,
|
||||
getPool,
|
||||
testConnection,
|
||||
executeQuery,
|
||||
sql,
|
||||
};
|
||||
36
src/database/schema.sql
Normal file
36
src/database/schema.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- FibDash Database Schema
|
||||
-- Run these commands in your MSSQL database
|
||||
|
||||
-- Create Users table
|
||||
CREATE TABLE Users (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
google_id NVARCHAR(255) UNIQUE NOT NULL,
|
||||
email NVARCHAR(255) UNIQUE NOT NULL,
|
||||
name NVARCHAR(255) NOT NULL,
|
||||
picture NVARCHAR(500),
|
||||
created_at DATETIME2 DEFAULT GETDATE(),
|
||||
last_login DATETIME2,
|
||||
is_active BIT DEFAULT 1
|
||||
);
|
||||
|
||||
-- Create UserPreferences table
|
||||
CREATE TABLE UserPreferences (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
theme NVARCHAR(50) DEFAULT 'light',
|
||||
language NVARCHAR(10) DEFAULT 'en',
|
||||
notifications_enabled BIT DEFAULT 1,
|
||||
created_at DATETIME2 DEFAULT GETDATE(),
|
||||
updated_at DATETIME2 DEFAULT GETDATE(),
|
||||
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IX_Users_Email ON Users(email);
|
||||
CREATE INDEX IX_Users_GoogleId ON Users(google_id);
|
||||
CREATE INDEX IX_UserPreferences_UserId ON UserPreferences(user_id);
|
||||
|
||||
-- Insert sample data (optional)
|
||||
-- Note: This will only work after you have real Google user data
|
||||
-- INSERT INTO Users (google_id, email, name, picture)
|
||||
-- VALUES ('sample_google_id', 'user@example.com', 'Lorem Ipsum User', 'https://example.com/picture.jpg');
|
||||
78
src/index.js
Normal file
78
src/index.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
const authRoutes = require('./routes/auth');
|
||||
const dashboardRoutes = require('./routes/dashboard');
|
||||
const adminRoutes = require('./routes/admin');
|
||||
const dataRoutes = require('./routes/data');
|
||||
const dbConfig = require('./config/database');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/dashboard', dashboardRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/data', dataRoutes);
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Debug login page (development only)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
app.get('/debug-login', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../debug-login.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// Serve static files in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// Serve static files from dist directory
|
||||
app.use(express.static(path.join(__dirname, '../dist'), {
|
||||
maxAge: '1y', // Cache static assets for 1 year
|
||||
etag: true,
|
||||
}));
|
||||
|
||||
// Handle client-side routing - serve index.html for all non-API routes
|
||||
app.get('*', (req, res, next) => {
|
||||
// Skip API routes
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return next();
|
||||
}
|
||||
res.sendFile(path.join(__dirname, '../dist/index.html'));
|
||||
});
|
||||
|
||||
console.log('📦 Serving static files from dist/ directory');
|
||||
} else {
|
||||
console.log('🔧 Development mode - static files served by webpack-dev-server');
|
||||
}
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Not found' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on port ${PORT}`);
|
||||
console.log(`📊 Dashboard: http://localhost:3000`);
|
||||
console.log(`🔗 API: http://localhost:${PORT}/api`);
|
||||
|
||||
// Test database connection
|
||||
const { testConnection } = require('./config/database');
|
||||
testConnection();
|
||||
});
|
||||
46
src/middleware/auth.js
Normal file
46
src/middleware/auth.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { isEmailAuthorized } = require('./emailAuth');
|
||||
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret', (err, user) => {
|
||||
if (err) {
|
||||
return res.status(403).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
// Additional check: ensure the user's email is still authorized
|
||||
if (!isEmailAuthorized(user.email)) {
|
||||
return res.status(403).json({
|
||||
error: 'Access denied',
|
||||
message: 'Your email address is no longer authorized to access this application'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
const generateToken = (user) => {
|
||||
return jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
picture: user.picture,
|
||||
},
|
||||
process.env.JWT_SECRET || 'fallback_secret',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authenticateToken,
|
||||
generateToken,
|
||||
};
|
||||
48
src/middleware/emailAuth.js
Normal file
48
src/middleware/emailAuth.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const checkAuthorizedEmail = (req, res, next) => {
|
||||
const authorizedEmails = process.env.AUTHORIZED_EMAILS;
|
||||
|
||||
// If no authorized emails are configured, deny all users
|
||||
if (!authorizedEmails || authorizedEmails.trim() === '') {
|
||||
return res.status(403).json({
|
||||
error: 'Access denied',
|
||||
message: 'No authorized users configured. Contact administrator.'
|
||||
});
|
||||
}
|
||||
|
||||
const emailList = authorizedEmails.split(',').map(email => email.trim().toLowerCase());
|
||||
const userEmail = req.user?.email?.toLowerCase();
|
||||
|
||||
if (!userEmail) {
|
||||
return res.status(401).json({
|
||||
error: 'User email not found',
|
||||
message: 'Authentication required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!emailList.includes(userEmail)) {
|
||||
return res.status(403).json({
|
||||
error: 'Access denied',
|
||||
message: 'Your email address is not authorized to access this application'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
const isEmailAuthorized = (email) => {
|
||||
const authorizedEmails = process.env.AUTHORIZED_EMAILS;
|
||||
|
||||
// If no authorized emails are configured, deny all users
|
||||
if (!authorizedEmails || authorizedEmails.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const emailList = authorizedEmails.split(',').map(e => e.trim().toLowerCase());
|
||||
const userEmail = email.toLowerCase();
|
||||
return emailList.includes(userEmail);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
checkAuthorizedEmail,
|
||||
isEmailAuthorized,
|
||||
};
|
||||
110
src/routes/admin.js
Normal file
110
src/routes/admin.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const express = require('express');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { checkAuthorizedEmail } = require('../middleware/emailAuth');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Check if user is admin (first email in the list or specific admin email)
|
||||
const checkAdminAccess = (req, res, next) => {
|
||||
const authorizedEmails = process.env.AUTHORIZED_EMAILS;
|
||||
if (!authorizedEmails || authorizedEmails.trim() === '') {
|
||||
return res.status(403).json({ error: 'No authorized emails configured' });
|
||||
}
|
||||
|
||||
const emailList = authorizedEmails.split(',').map(email => email.trim().toLowerCase());
|
||||
const userEmail = req.user?.email?.toLowerCase();
|
||||
|
||||
// First email in the list is considered admin, or check for specific admin emails
|
||||
const adminEmails = [emailList[0]]; // First email is admin
|
||||
|
||||
if (!adminEmails.includes(userEmail)) {
|
||||
return res.status(403).json({
|
||||
error: 'Admin access required',
|
||||
message: 'Only administrators can access this resource'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Get current authorized emails (admin only)
|
||||
router.get('/authorized-emails', authenticateToken, checkAdminAccess, (req, res) => {
|
||||
const authorizedEmails = process.env.AUTHORIZED_EMAILS;
|
||||
if (!authorizedEmails) {
|
||||
return res.json({ emails: [] });
|
||||
}
|
||||
|
||||
const emailList = authorizedEmails.split(',').map(email => email.trim());
|
||||
res.json({ emails: emailList });
|
||||
});
|
||||
|
||||
// Add authorized email (admin only)
|
||||
router.post('/authorized-emails', authenticateToken, checkAdminAccess, (req, res) => {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
return res.status(400).json({ error: 'Valid email address required' });
|
||||
}
|
||||
|
||||
const authorizedEmails = process.env.AUTHORIZED_EMAILS || '';
|
||||
const emailList = authorizedEmails.split(',').map(e => e.trim()).filter(e => e);
|
||||
|
||||
const newEmail = email.trim().toLowerCase();
|
||||
if (emailList.map(e => e.toLowerCase()).includes(newEmail)) {
|
||||
return res.status(400).json({ error: 'Email already authorized' });
|
||||
}
|
||||
|
||||
emailList.push(email.trim());
|
||||
|
||||
// Note: This only updates the runtime environment variable
|
||||
// For persistent changes, you'd need to update the .env file
|
||||
process.env.AUTHORIZED_EMAILS = emailList.join(',');
|
||||
|
||||
res.json({
|
||||
message: 'Email added successfully',
|
||||
emails: emailList,
|
||||
note: 'Changes are temporary. Update .env file for permanent changes.'
|
||||
});
|
||||
});
|
||||
|
||||
// Remove authorized email (admin only)
|
||||
router.delete('/authorized-emails/:email', authenticateToken, checkAdminAccess, (req, res) => {
|
||||
const emailToRemove = req.params.email.toLowerCase();
|
||||
const authorizedEmails = process.env.AUTHORIZED_EMAILS || '';
|
||||
const emailList = authorizedEmails.split(',').map(e => e.trim()).filter(e => e);
|
||||
|
||||
const filteredEmails = emailList.filter(email => email.toLowerCase() !== emailToRemove);
|
||||
|
||||
if (filteredEmails.length === emailList.length) {
|
||||
return res.status(404).json({ error: 'Email not found in authorized list' });
|
||||
}
|
||||
|
||||
// Don't allow removing the last admin email
|
||||
if (filteredEmails.length === 0) {
|
||||
return res.status(400).json({ error: 'Cannot remove all authorized emails' });
|
||||
}
|
||||
|
||||
// Note: This only updates the runtime environment variable
|
||||
process.env.AUTHORIZED_EMAILS = filteredEmails.join(',');
|
||||
|
||||
res.json({
|
||||
message: 'Email removed successfully',
|
||||
emails: filteredEmails,
|
||||
note: 'Changes are temporary. Update .env file for permanent changes.'
|
||||
});
|
||||
});
|
||||
|
||||
// Get system info (admin only)
|
||||
router.get('/system-info', authenticateToken, checkAdminAccess, (req, res) => {
|
||||
res.json({
|
||||
authorizedEmailsConfigured: !!process.env.AUTHORIZED_EMAILS,
|
||||
totalAuthorizedEmails: process.env.AUTHORIZED_EMAILS ? process.env.AUTHORIZED_EMAILS.split(',').length : 0,
|
||||
currentUser: req.user.email,
|
||||
isAdmin: true,
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
133
src/routes/auth.js
Normal file
133
src/routes/auth.js
Normal file
@@ -0,0 +1,133 @@
|
||||
const express = require('express');
|
||||
const { OAuth2Client } = require('google-auth-library');
|
||||
const { generateToken, authenticateToken } = require('../middleware/auth');
|
||||
const { executeQuery } = require('../config/database');
|
||||
const { isEmailAuthorized } = require('../middleware/emailAuth');
|
||||
|
||||
const router = express.Router();
|
||||
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
|
||||
|
||||
// Google OAuth login
|
||||
router.post('/google', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.body;
|
||||
|
||||
console.log('🔍 Login attempt with token:', token ? 'Present' : 'Missing');
|
||||
|
||||
if (!token) {
|
||||
console.log('❌ No token provided');
|
||||
return res.status(400).json({ error: 'Token is required' });
|
||||
}
|
||||
|
||||
// Verify Google token
|
||||
console.log('🔐 Verifying Google token...');
|
||||
const ticket = await client.verifyIdToken({
|
||||
idToken: token,
|
||||
audience: process.env.GOOGLE_CLIENT_ID,
|
||||
});
|
||||
|
||||
const payload = ticket.getPayload();
|
||||
const googleId = payload['sub'];
|
||||
const email = payload['email'];
|
||||
const name = payload['name'];
|
||||
const picture = payload['picture'];
|
||||
|
||||
console.log(`👤 Google token verified for: ${email}`);
|
||||
|
||||
// Check if email is authorized
|
||||
const authorized = isEmailAuthorized(email);
|
||||
console.log(`🔒 Email authorization check for ${email}: ${authorized ? 'ALLOWED' : 'DENIED'}`);
|
||||
|
||||
if (!authorized) {
|
||||
console.log(`❌ Access denied for ${email}`);
|
||||
return res.status(403).json({
|
||||
error: 'Access denied',
|
||||
message: 'Your email address is not authorized to access this application'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user exists in database (optional - auth works without DB)
|
||||
let user;
|
||||
try {
|
||||
// Only try database operations if DB is configured
|
||||
if (process.env.DB_SERVER) {
|
||||
const userResult = await executeQuery(
|
||||
'SELECT * FROM Users WHERE email = @email',
|
||||
{ email }
|
||||
);
|
||||
|
||||
if (userResult.recordset.length > 0) {
|
||||
// User exists, update last login
|
||||
user = userResult.recordset[0];
|
||||
await executeQuery(
|
||||
'UPDATE Users SET last_login = GETDATE(), picture = @picture WHERE id = @id',
|
||||
{ picture, id: user.id }
|
||||
);
|
||||
} else {
|
||||
// Create new user
|
||||
const insertResult = await executeQuery(
|
||||
`INSERT INTO Users (google_id, email, name, picture, created_at, last_login)
|
||||
OUTPUT INSERTED.*
|
||||
VALUES (@googleId, @email, @name, @picture, GETDATE(), GETDATE())`,
|
||||
{ googleId, email, name, picture }
|
||||
);
|
||||
user = insertResult.recordset[0];
|
||||
}
|
||||
console.log('✅ Database operations completed successfully');
|
||||
} else {
|
||||
console.log('⚠️ No database configured, using fallback user object');
|
||||
throw new Error('No database configured');
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.error('Database error during authentication:', dbError.message);
|
||||
// Fallback: create user object without database
|
||||
user = {
|
||||
id: googleId,
|
||||
email,
|
||||
name,
|
||||
picture,
|
||||
google_id: googleId,
|
||||
};
|
||||
console.log('✅ Using fallback user object (no database)');
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const jwtToken = generateToken(user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token: jwtToken,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
picture: user.picture,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Google authentication error:', error);
|
||||
res.status(401).json({ error: 'Invalid Google token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify JWT token
|
||||
router.get('/verify', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
// Token is already verified by middleware
|
||||
res.json({
|
||||
success: true,
|
||||
user: req.user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout (client-side token removal)
|
||||
router.post('/logout', authenticateToken, (req, res) => {
|
||||
res.json({ success: true, message: 'Logged out successfully' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
103
src/routes/dashboard.js
Normal file
103
src/routes/dashboard.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const express = require('express');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { executeQuery } = require('../config/database');
|
||||
const { checkAuthorizedEmail } = require('../middleware/emailAuth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get dashboard data
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
let dashboardData = {
|
||||
stats: [
|
||||
{ title: 'Total Users', value: 'N/A', icon: 'PeopleIcon', color: '#1976d2' },
|
||||
{ title: 'Revenue', value: 'N/A', icon: 'TrendingUpIcon', color: '#388e3c' },
|
||||
{ title: 'Reports', value: 'N/A', icon: 'AssessmentIcon', color: '#f57c00' },
|
||||
{ title: 'Growth', value: 'N/A', icon: 'TimelineIcon', color: '#7b1fa2' },
|
||||
],
|
||||
recentActivity: []
|
||||
};
|
||||
|
||||
try {
|
||||
// Only try database operations if configured
|
||||
if (process.env.DB_SERVER) {
|
||||
// Try to fetch real data from database
|
||||
const userCountResult = await executeQuery('SELECT COUNT(*) as count FROM Users');
|
||||
const userCount = userCountResult.recordset[0]?.count || 0;
|
||||
|
||||
// Update stats with real data
|
||||
dashboardData.stats[0].value = userCount.toString();
|
||||
|
||||
// Fetch recent activity
|
||||
const activityResult = await executeQuery(`
|
||||
SELECT TOP 10
|
||||
CONCAT('User ', name, ' logged in') as description,
|
||||
FORMAT(last_login, 'yyyy-MM-dd HH:mm') as timestamp
|
||||
FROM Users
|
||||
WHERE last_login IS NOT NULL
|
||||
ORDER BY last_login DESC
|
||||
`);
|
||||
|
||||
dashboardData.recentActivity = activityResult.recordset || [];
|
||||
console.log('✅ Dashboard data loaded from database');
|
||||
} else {
|
||||
console.log('⚠️ No database configured, using mock dashboard data');
|
||||
// Update with mock data
|
||||
dashboardData.stats[0].value = '1';
|
||||
dashboardData.stats[1].value = '$0';
|
||||
dashboardData.stats[2].value = '0';
|
||||
dashboardData.stats[3].value = '0%';
|
||||
dashboardData.recentActivity = [
|
||||
{ description: 'System started without database', timestamp: new Date().toISOString().slice(0, 16) }
|
||||
];
|
||||
}
|
||||
|
||||
} catch (dbError) {
|
||||
console.error('Database query error in dashboard:', dbError.message);
|
||||
// Keep fallback data
|
||||
console.log('✅ Using fallback dashboard data');
|
||||
}
|
||||
|
||||
res.json(dashboardData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Dashboard error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch dashboard data' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get user-specific data
|
||||
router.get('/user', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
let userData = {
|
||||
profile: req.user,
|
||||
preferences: {},
|
||||
activity: []
|
||||
};
|
||||
|
||||
try {
|
||||
// Fetch user preferences from database
|
||||
const prefsResult = await executeQuery(
|
||||
'SELECT * FROM UserPreferences WHERE user_id = @userId',
|
||||
{ userId }
|
||||
);
|
||||
|
||||
if (prefsResult.recordset.length > 0) {
|
||||
userData.preferences = prefsResult.recordset[0];
|
||||
}
|
||||
|
||||
} catch (dbError) {
|
||||
console.error('Database query error for user data:', dbError);
|
||||
}
|
||||
|
||||
res.json(userData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('User data error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch user data' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
352
src/routes/data.js
Normal file
352
src/routes/data.js
Normal file
@@ -0,0 +1,352 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Parse CSV data
|
||||
const parseCSV = () => {
|
||||
try {
|
||||
const csvPath = path.join(__dirname, '../../data.csv');
|
||||
const csvData = fs.readFileSync(csvPath, 'utf8');
|
||||
const lines = csvData.split('\n');
|
||||
const headers = lines[0].split(';').map(h => h.replace(/"/g, ''));
|
||||
|
||||
const transactions = [];
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line.trim()) continue;
|
||||
|
||||
// Parse CSV line (handle semicolon-separated values with quotes)
|
||||
const values = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const char = line[j];
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (char === ';' && !inQuotes) {
|
||||
values.push(current);
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
values.push(current); // Add last value
|
||||
|
||||
if (values.length >= headers.length) {
|
||||
const transaction = {};
|
||||
headers.forEach((header, index) => {
|
||||
transaction[header] = values[index] || '';
|
||||
});
|
||||
|
||||
// Parse date and amount
|
||||
if (transaction['Buchungstag']) {
|
||||
const dateParts = transaction['Buchungstag'].split('.');
|
||||
if (dateParts.length === 3) {
|
||||
// Convert DD.MM.YY to proper date
|
||||
const day = dateParts[0];
|
||||
const month = dateParts[1];
|
||||
const year = '20' + dateParts[2]; // Assuming 20xx
|
||||
transaction.parsedDate = new Date(year, month - 1, day);
|
||||
transaction.monthYear = `${year}-${month.padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse amount
|
||||
if (transaction['Betrag']) {
|
||||
const amount = transaction['Betrag'].replace(',', '.').replace(/[^-0-9.]/g, '');
|
||||
transaction.numericAmount = parseFloat(amount) || 0;
|
||||
}
|
||||
|
||||
transactions.push(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
return transactions;
|
||||
} catch (error) {
|
||||
console.error('Error parsing CSV:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Get available months
|
||||
router.get('/months', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const transactions = parseCSV();
|
||||
const months = [...new Set(transactions
|
||||
.filter(t => t.monthYear)
|
||||
.map(t => t.monthYear)
|
||||
)].sort().reverse(); // Newest first
|
||||
|
||||
res.json({ months });
|
||||
} catch (error) {
|
||||
console.error('Error getting months:', error);
|
||||
res.status(500).json({ error: 'Failed to load months' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get database transactions for JTL comparison
|
||||
const getJTLTransactions = async () => {
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
const query = `
|
||||
SELECT
|
||||
cKonto, cKontozusatz, cName, dBuchungsdatum,
|
||||
tZahlungsabgleichUmsatz.kZahlungsabgleichUmsatz,
|
||||
cVerwendungszweck, fBetrag, tUmsatzKontierung.data
|
||||
FROM [eazybusiness].[dbo].[tZahlungsabgleichUmsatz]
|
||||
LEFT JOIN tUmsatzKontierung ON (tUmsatzKontierung.kZahlungsabgleichUmsatz = tZahlungsabgleichUmsatz.kZahlungsabgleichUmsatz)
|
||||
ORDER BY dBuchungsdatum desc, tZahlungsabgleichUmsatz.kZahlungsabgleichUmsatz desc
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query);
|
||||
return result.recordset || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching JTL transactions:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Get transactions for a specific month
|
||||
router.get('/transactions/:monthYear', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { monthYear } = req.params;
|
||||
const transactions = parseCSV();
|
||||
|
||||
const monthTransactions = transactions
|
||||
.filter(t => t.monthYear === monthYear)
|
||||
.sort((a, b) => b.parsedDate - a.parsedDate); // Newest first
|
||||
|
||||
// Get JTL transactions for comparison
|
||||
let jtlTransactions = [];
|
||||
try {
|
||||
jtlTransactions = await getJTLTransactions();
|
||||
} catch (error) {
|
||||
console.log('JTL database not available, continuing without JTL data');
|
||||
}
|
||||
|
||||
// Filter JTL transactions for the selected month
|
||||
const [year, month] = monthYear.split('-');
|
||||
const jtlMonthTransactions = jtlTransactions.filter(jtl => {
|
||||
const jtlDate = new Date(jtl.dBuchungsdatum);
|
||||
return jtlDate.getFullYear() === parseInt(year) &&
|
||||
jtlDate.getMonth() === parseInt(month) - 1;
|
||||
});
|
||||
|
||||
// Add JTL status to each CSV transaction
|
||||
const transactionsWithJTL = monthTransactions.map(transaction => {
|
||||
// Try to match by amount and date (approximate matching)
|
||||
const amount = transaction.numericAmount;
|
||||
const transactionDate = transaction.parsedDate;
|
||||
|
||||
const jtlMatch = jtlMonthTransactions.find(jtl => {
|
||||
const jtlAmount = parseFloat(jtl.fBetrag) || 0;
|
||||
const jtlDate = new Date(jtl.dBuchungsdatum);
|
||||
|
||||
// Match by amount (exact) and date (same day)
|
||||
const amountMatch = Math.abs(amount - jtlAmount) < 0.01;
|
||||
const dateMatch = transactionDate && jtlDate &&
|
||||
transactionDate.getFullYear() === jtlDate.getFullYear() &&
|
||||
transactionDate.getMonth() === jtlDate.getMonth() &&
|
||||
transactionDate.getDate() === jtlDate.getDate();
|
||||
|
||||
return amountMatch && dateMatch;
|
||||
});
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
hasJTL: !!jtlMatch,
|
||||
jtlId: jtlMatch ? jtlMatch.kZahlungsabgleichUmsatz : null,
|
||||
isFromCSV: true
|
||||
};
|
||||
});
|
||||
|
||||
// Find JTL transactions that don't have CSV matches (red rows)
|
||||
const unmatchedJTLTransactions = jtlMonthTransactions
|
||||
.filter(jtl => {
|
||||
const jtlAmount = parseFloat(jtl.fBetrag) || 0;
|
||||
const jtlDate = new Date(jtl.dBuchungsdatum);
|
||||
|
||||
// Check if this JTL transaction has a CSV match
|
||||
const hasCSVMatch = monthTransactions.some(transaction => {
|
||||
const amount = transaction.numericAmount;
|
||||
const transactionDate = transaction.parsedDate;
|
||||
|
||||
const amountMatch = Math.abs(amount - jtlAmount) < 0.01;
|
||||
const dateMatch = transactionDate && jtlDate &&
|
||||
transactionDate.getFullYear() === jtlDate.getFullYear() &&
|
||||
transactionDate.getMonth() === jtlDate.getMonth() &&
|
||||
transactionDate.getDate() === jtlDate.getDate();
|
||||
|
||||
return amountMatch && dateMatch;
|
||||
});
|
||||
|
||||
return !hasCSVMatch;
|
||||
})
|
||||
.map(jtl => ({
|
||||
// Convert JTL format to CSV-like format for display
|
||||
'Buchungstag': new Date(jtl.dBuchungsdatum).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: '2-digit'
|
||||
}),
|
||||
'Verwendungszweck': jtl.cVerwendungszweck || '',
|
||||
'Buchungstext': 'JTL Transaction',
|
||||
'Beguenstigter/Zahlungspflichtiger': jtl.cName || '',
|
||||
'Betrag': jtl.fBetrag ? jtl.fBetrag.toString().replace('.', ',') : '0,00',
|
||||
numericAmount: parseFloat(jtl.fBetrag) || 0,
|
||||
parsedDate: new Date(jtl.dBuchungsdatum),
|
||||
monthYear: monthYear,
|
||||
hasJTL: true,
|
||||
jtlId: jtl.kZahlungsabgleichUmsatz,
|
||||
isFromCSV: false,
|
||||
isJTLOnly: true
|
||||
}));
|
||||
|
||||
// Combine CSV and JTL-only transactions
|
||||
const allTransactions = [...transactionsWithJTL, ...unmatchedJTLTransactions]
|
||||
.sort((a, b) => b.parsedDate - a.parsedDate);
|
||||
|
||||
// Calculate summary
|
||||
const summary = {
|
||||
totalTransactions: allTransactions.length,
|
||||
totalIncome: allTransactions
|
||||
.filter(t => t.numericAmount > 0)
|
||||
.reduce((sum, t) => sum + t.numericAmount, 0),
|
||||
totalExpenses: allTransactions
|
||||
.filter(t => t.numericAmount < 0)
|
||||
.reduce((sum, t) => sum + Math.abs(t.numericAmount), 0),
|
||||
netAmount: allTransactions.reduce((sum, t) => sum + t.numericAmount, 0),
|
||||
jtlMatches: allTransactions.filter(t => t.hasJTL && t.isFromCSV).length,
|
||||
jtlMissing: allTransactions.filter(t => !t.hasJTL && t.isFromCSV).length,
|
||||
jtlOnly: allTransactions.filter(t => t.isJTLOnly).length,
|
||||
csvOnly: allTransactions.filter(t => !t.hasJTL && t.isFromCSV).length
|
||||
};
|
||||
|
||||
res.json({
|
||||
transactions: allTransactions,
|
||||
summary,
|
||||
monthYear
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting transactions:', error);
|
||||
res.status(500).json({ error: 'Failed to load transactions' });
|
||||
}
|
||||
});
|
||||
|
||||
// DATEV export functionality
|
||||
const buildDatevHeader = (periodStart, periodEnd) => {
|
||||
const ts = new Date().toISOString().replace(/[-T:\.Z]/g, '').slice(0, 17); // yyyymmddHHMMSSfff
|
||||
const meta = {
|
||||
consultant: 1001,
|
||||
client: 10001,
|
||||
fyStart: periodStart.slice(0, 4) + '0101', // fiscal year start
|
||||
accLength: 4,
|
||||
description: 'Bank Statement Export',
|
||||
currency: 'EUR'
|
||||
};
|
||||
|
||||
return [
|
||||
'"EXTF"', 700, 21, '"Buchungsstapel"', 12, ts,
|
||||
'', '', '', '', // 7‑10 spare
|
||||
meta.consultant, meta.client, // 11, 12
|
||||
meta.fyStart, meta.accLength, // 13, 14
|
||||
periodStart, periodEnd, // 15, 16
|
||||
'"' + meta.description + '"',
|
||||
'AM', 1, 0, 1, meta.currency
|
||||
].join(';');
|
||||
};
|
||||
|
||||
const DATEV_COLS = [
|
||||
'Umsatz (ohne Soll/Haben-Kz)', 'Soll/Haben-Kennzeichen', 'WKZ Umsatz',
|
||||
'Kurs', 'Basis-Umsatz', 'WKZ Basis-Umsatz', 'Konto',
|
||||
'Gegenkonto (ohne BU-Schlüssel)', 'BU-Schlüssel', 'Belegdatum',
|
||||
'Belegfeld 1', 'Belegfeld 2', 'Skonto', 'Buchungstext',
|
||||
'Postensperre', 'Diverse Adressnummer', 'Geschäftspartnerbank',
|
||||
'Sachverhalt', 'Zinssperre', 'Beleglink'
|
||||
].join(';');
|
||||
|
||||
const formatDatevAmount = (amount) => {
|
||||
return Math.abs(amount).toFixed(2).replace('.', ',');
|
||||
};
|
||||
|
||||
const formatDatevDate = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const parts = dateString.split('.');
|
||||
if (parts.length === 3) {
|
||||
const day = parts[0].padStart(2, '0');
|
||||
const month = parts[1].padStart(2, '0');
|
||||
return day + month;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const quote = (str, maxLen = 60) => {
|
||||
if (!str) return '""';
|
||||
return '"' + str.slice(0, maxLen).replace(/"/g, '""') + '"';
|
||||
};
|
||||
|
||||
// DATEV export endpoint
|
||||
router.get('/datev/:monthYear', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { monthYear } = req.params;
|
||||
const [year, month] = monthYear.split('-');
|
||||
|
||||
// Get transactions for the month
|
||||
const transactions = parseCSV();
|
||||
const monthTransactions = transactions
|
||||
.filter(t => t.monthYear === monthYear)
|
||||
.sort((a, b) => a.parsedDate - b.parsedDate); // Oldest first for DATEV
|
||||
|
||||
if (!monthTransactions.length) {
|
||||
return res.status(404).json({ error: 'No transactions found for this month' });
|
||||
}
|
||||
|
||||
// Build DATEV format
|
||||
const periodStart = `${year}${month.padStart(2, '0')}01`;
|
||||
const periodEnd = new Date(year, month, 0).toISOString().slice(0, 10).replace(/-/g, '');
|
||||
|
||||
const header = buildDatevHeader(periodStart, periodEnd);
|
||||
|
||||
const rows = monthTransactions.map((transaction, index) => {
|
||||
const amount = Math.abs(transaction.numericAmount);
|
||||
const isDebit = transaction.numericAmount < 0 ? 'S' : 'H'; // S = Soll (debit), H = Haben (credit)
|
||||
|
||||
return [
|
||||
formatDatevAmount(amount), // #1 Umsatz
|
||||
isDebit, // #2 Soll/Haben
|
||||
quote('EUR', 3), // #3 WKZ Umsatz
|
||||
'', '', '', // #4-6 (no FX)
|
||||
'1200', // #7 Konto (Bank account)
|
||||
transaction.numericAmount < 0 ? '4000' : '8400', // #8 Gegenkonto (expense/income)
|
||||
'', // #9 BU-Schlüssel
|
||||
formatDatevDate(transaction['Buchungstag']), // #10 Belegdatum
|
||||
quote((index + 1).toString(), 36), // #11 Belegfeld 1 (sequential number)
|
||||
'', '', // #12, #13
|
||||
quote(transaction['Verwendungszweck'] || transaction['Buchungstext'], 60), // #14 Buchungstext
|
||||
'', '', '', '', '', // #15-19 unused
|
||||
'' // #20 Beleglink
|
||||
].join(';');
|
||||
});
|
||||
|
||||
const csv = [header, DATEV_COLS, ...rows].join('\r\n');
|
||||
|
||||
// Set headers for file download
|
||||
const filename = `DATEV_${year}_${month.padStart(2, '0')}.csv`;
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=latin1');
|
||||
|
||||
// Convert to latin1 encoding for DATEV compatibility
|
||||
const buffer = Buffer.from(csv, 'utf8');
|
||||
res.send(buffer);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating DATEV export:', error);
|
||||
res.status(500).json({ error: 'Failed to generate DATEV export' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user