Implement DATEV export functionality in DataViewer and enhance TransactionsTable with selection features and improved row styling. Update environment variables and add devServer configuration in webpack for better development experience.
This commit is contained in:
@@ -1,16 +1,5 @@
|
||||
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();
|
||||
const checkAuthorizedEmail = async (req, res, next) => {
|
||||
const userEmail = req.user?.email;
|
||||
|
||||
if (!userEmail) {
|
||||
return res.status(401).json({
|
||||
@@ -19,27 +8,75 @@ const checkAuthorizedEmail = (req, res, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!emailList.includes(userEmail)) {
|
||||
return res.status(403).json({
|
||||
error: 'Access denied',
|
||||
message: 'Your email address is not authorized to access this application'
|
||||
try {
|
||||
const authorized = await isEmailAuthorized(userEmail);
|
||||
|
||||
if (!authorized) {
|
||||
return res.status(403).json({
|
||||
error: 'Access denied',
|
||||
message: 'Your email address is not authorized to access this application'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Authorization check failed:', error);
|
||||
return res.status(500).json({
|
||||
error: 'Authorization check failed',
|
||||
message: 'Unable to verify authorization. Please try again.'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
const isEmailAuthorized = (email) => {
|
||||
const isEmailAuthorized = async (email) => {
|
||||
const authorizedEmails = process.env.AUTHORIZED_EMAILS;
|
||||
const userEmail = email.toLowerCase();
|
||||
|
||||
// If no authorized emails are configured, deny all users
|
||||
if (!authorizedEmails || authorizedEmails.trim() === '') {
|
||||
return false;
|
||||
console.log(`🔍 Checking authorization for email: ${userEmail}`);
|
||||
|
||||
// First check environment variable
|
||||
if (authorizedEmails && authorizedEmails.trim() !== '') {
|
||||
const emailList = authorizedEmails.split(',').map(e => e.trim().toLowerCase());
|
||||
if (emailList.includes(userEmail)) {
|
||||
console.log(`✅ Email authorized via AUTHORIZED_EMAILS environment variable`);
|
||||
return true;
|
||||
}
|
||||
console.log(`❌ Email not found in AUTHORIZED_EMAILS environment variable`);
|
||||
} else {
|
||||
console.log(`⚠️ No AUTHORIZED_EMAILS configured, checking database...`);
|
||||
}
|
||||
|
||||
const emailList = authorizedEmails.split(',').map(e => e.trim().toLowerCase());
|
||||
const userEmail = email.toLowerCase();
|
||||
return emailList.includes(userEmail);
|
||||
// Then check database
|
||||
console.log(`🗄️ Checking database authorization for: ${userEmail}`);
|
||||
try {
|
||||
const { executeQuery } = require('../config/database');
|
||||
|
||||
const query = `
|
||||
SELECT TOP 1 1 as authorized
|
||||
FROM dbo.tAdresse
|
||||
WHERE cMail = @email
|
||||
AND kKunde IN (
|
||||
SELECT [kKunde]
|
||||
FROM [Kunde].[tKundeEigenesFeld]
|
||||
WHERE kAttribut = 219 OR kAttribut = 220 AND nWertInt = 1
|
||||
)
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query, { email: userEmail });
|
||||
const isAuthorized = result.recordset && result.recordset.length > 0;
|
||||
|
||||
if (isAuthorized) {
|
||||
console.log(`✅ Email authorized via database (tKundeEigenesFeld with kAttribut 219/220)`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`❌ Email not authorized via database`);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`💥 Database authorization check failed for ${userEmail}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -35,7 +35,7 @@ router.post('/google', async (req, res) => {
|
||||
console.log(`👤 Google token verified for: ${email}`);
|
||||
|
||||
// Check if email is authorized
|
||||
const authorized = isEmailAuthorized(email);
|
||||
const authorized = await isEmailAuthorized(email);
|
||||
console.log(`🔒 Email authorization check for ${email}: ${authorized ? 'ALLOWED' : 'DENIED'}`);
|
||||
|
||||
if (!authorized) {
|
||||
@@ -46,50 +46,15 @@ router.post('/google', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 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)');
|
||||
}
|
||||
// Create user object from Google data (no database storage needed)
|
||||
const user = {
|
||||
id: googleId,
|
||||
email,
|
||||
name,
|
||||
picture,
|
||||
google_id: googleId,
|
||||
};
|
||||
console.log('✅ User object created from Google authentication');
|
||||
|
||||
// Generate JWT token
|
||||
const jwtToken = generateToken(user);
|
||||
|
||||
@@ -103,10 +103,51 @@ const getJTLTransactions = async () => {
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query);
|
||||
return result.recordset || [];
|
||||
const transactions = result.recordset || [];
|
||||
|
||||
// Get PDF documents for each transaction
|
||||
const pdfQuery = `SELECT kUmsatzBeleg, kZahlungsabgleichUmsatz, textContent, markDown, extraction, datevlink FROM tUmsatzBeleg`;
|
||||
const pdfResult = await executeQuery(pdfQuery);
|
||||
|
||||
for(const item of pdfResult.recordset){
|
||||
for(const transaction of transactions){
|
||||
if(item.kZahlungsabgleichUmsatz == transaction.kZahlungsabgleichUmsatz){
|
||||
if(!transaction.pdfs) transaction.pdfs = [];
|
||||
transaction.pdfs.push({
|
||||
kUmsatzBeleg: item.kUmsatzBeleg,
|
||||
content: item.textContent,
|
||||
markDown: item.markDown,
|
||||
extraction: item.extraction,
|
||||
datevlink: item.datevlink
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get links for each transaction
|
||||
const linksQuery = `
|
||||
SELECT kZahlungsabgleichUmsatzLink, kZahlungsabgleichUmsatz, linktarget, linktype, note,
|
||||
tPdfObjekt.kPdfObjekt, tPdfObjekt.textContent, tPdfObjekt.markDown,
|
||||
tPdfObjekt.extraction
|
||||
FROM tZahlungsabgleichUmsatzLink
|
||||
LEFT JOIN tPdfObjekt ON (tZahlungsabgleichUmsatzLink.linktarget = tPdfObjekt.kLieferantenbestellung)
|
||||
WHERE linktype = 'kLieferantenBestellung'
|
||||
`;
|
||||
const linksResult = await executeQuery(linksQuery);
|
||||
|
||||
for(const item of linksResult.recordset){
|
||||
for(const transaction of transactions){
|
||||
if(item.kZahlungsabgleichUmsatz == transaction.kZahlungsabgleichUmsatz){
|
||||
if(!transaction.links) transaction.links = [];
|
||||
transaction.links.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transactions;
|
||||
} catch (error) {
|
||||
console.error('Error fetching JTL transactions:', error);
|
||||
return [];
|
||||
throw error; // Re-throw the error so the caller knows the database is unavailable
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,10 +197,13 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
|
||||
// Get JTL transactions for comparison
|
||||
let jtlTransactions = [];
|
||||
let jtlDatabaseAvailable = false;
|
||||
try {
|
||||
jtlTransactions = await getJTLTransactions();
|
||||
jtlDatabaseAvailable = true;
|
||||
} catch (error) {
|
||||
console.log('JTL database not available, continuing without JTL data');
|
||||
jtlDatabaseAvailable = false;
|
||||
}
|
||||
|
||||
// Filter JTL transactions for the selected time period
|
||||
@@ -213,9 +257,13 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
hasJTL: !!jtlMatch,
|
||||
hasJTL: jtlDatabaseAvailable ? !!jtlMatch : undefined,
|
||||
jtlId: jtlMatch ? jtlMatch.kZahlungsabgleichUmsatz : null,
|
||||
isFromCSV: true
|
||||
isFromCSV: true,
|
||||
jtlDatabaseAvailable,
|
||||
// Include document data from JTL match
|
||||
pdfs: jtlMatch ? jtlMatch.pdfs || [] : [],
|
||||
links: jtlMatch ? jtlMatch.links || [] : []
|
||||
};
|
||||
});
|
||||
|
||||
@@ -258,7 +306,10 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
hasJTL: true,
|
||||
jtlId: jtl.kZahlungsabgleichUmsatz,
|
||||
isFromCSV: false,
|
||||
isJTLOnly: true
|
||||
isJTLOnly: true,
|
||||
// Include document data from JTL transaction
|
||||
pdfs: jtl.pdfs || [],
|
||||
links: jtl.links || []
|
||||
}));
|
||||
|
||||
// Combine CSV and JTL-only transactions
|
||||
@@ -275,10 +326,11 @@ router.get('/transactions/:timeRange', authenticateToken, async (req, res) => {
|
||||
.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
|
||||
jtlMatches: jtlDatabaseAvailable ? allTransactions.filter(t => t.hasJTL === true && t.isFromCSV).length : undefined,
|
||||
jtlMissing: jtlDatabaseAvailable ? allTransactions.filter(t => t.hasJTL === false && t.isFromCSV).length : undefined,
|
||||
jtlOnly: jtlDatabaseAvailable ? allTransactions.filter(t => t.isJTLOnly).length : undefined,
|
||||
csvOnly: jtlDatabaseAvailable ? allTransactions.filter(t => t.hasJTL === false && t.isFromCSV).length : undefined,
|
||||
jtlDatabaseAvailable
|
||||
};
|
||||
|
||||
res.json({
|
||||
@@ -440,4 +492,70 @@ router.get('/datev/:timeRange', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get PDF from tUmsatzBeleg
|
||||
router.get('/pdf/umsatzbeleg/:kUmsatzBeleg', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { kUmsatzBeleg } = req.params;
|
||||
const { executeQuery } = require('../config/database');
|
||||
|
||||
const query = `
|
||||
SELECT content, datevlink
|
||||
FROM dbo.tUmsatzBeleg
|
||||
WHERE kUmsatzBeleg = @kUmsatzBeleg AND content IS NOT NULL
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query, {
|
||||
kUmsatzBeleg: parseInt(kUmsatzBeleg)
|
||||
});
|
||||
|
||||
if (!result.recordset || result.recordset.length === 0) {
|
||||
return res.status(404).json({ error: 'PDF not found' });
|
||||
}
|
||||
|
||||
const pdfData = result.recordset[0];
|
||||
const filename = `Umsatzbeleg_${kUmsatzBeleg}_${pdfData.datevlink || 'document'}.pdf`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
|
||||
res.send(pdfData.content);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching PDF from tUmsatzBeleg:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch PDF' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get PDF from tPdfObjekt
|
||||
router.get('/pdf/pdfobject/:kPdfObjekt', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { kPdfObjekt } = req.params;
|
||||
const { executeQuery } = require('../config/database');
|
||||
|
||||
const query = `
|
||||
SELECT content, datevlink, kLieferantenbestellung
|
||||
FROM dbo.tPdfObjekt
|
||||
WHERE kPdfObjekt = @kPdfObjekt AND content IS NOT NULL
|
||||
`;
|
||||
|
||||
const result = await executeQuery(query, {
|
||||
kPdfObjekt: parseInt(kPdfObjekt)
|
||||
});
|
||||
|
||||
if (!result.recordset || result.recordset.length === 0) {
|
||||
return res.status(404).json({ error: 'PDF not found' });
|
||||
}
|
||||
|
||||
const pdfData = result.recordset[0];
|
||||
const filename = `PdfObjekt_${kPdfObjekt}_LB${pdfData.kLieferantenbestellung}_${pdfData.datevlink || 'document'}.pdf`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
|
||||
res.send(pdfData.content);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching PDF from tPdfObjekt:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch PDF' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user