Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
237 lines (213 loc) · 8.68 KB
/
Copy pathserver.js
File metadata and controls
237 lines (213 loc) · 8.68 KB
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
const express = require('express');
const path = require('path');
const db = require('./database.js');
const bcrypt = require('bcrypt');
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
const rateLimit = require('express-rate-limit');
const { sanitizePII, parseInvoicePrompt } = require('./piiSanitizer.js');
require('dotenv').config();
const saltRounds = 10;
const app = express();
const port = 3000;
if (!process.env.SESSION_SECRET) {
console.warn("Warning: SESSION_SECRET is not set in .env file. Using a default, insecure secret. Please set a secret in a .env file for production.");
}
const a_secret = process.env.SESSION_SECRET || 'your-insecure-default-secret';
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, '/')));
// --- Session Setup ---
app.use(session({
store: new SQLiteStore({
db: 'invoicer.db',
dir: './'
}),
secret: a_secret,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
}
}));
// --- Middleware to check if user is authenticated ---
const isAuthenticated = (req, res, next) => {
if (req.session.userId) {
next();
} else {
res.redirect('/login.html');
}
};
// --- CSRF mitigation: reject cross-origin state-changing requests ---
const rejectCrossOrigin = (req, res, next) => {
const origin = req.get('origin') || req.get('referer');
if (!origin) return next();
const host = req.get('host');
if (new URL(origin).host !== host) {
return res.status(403).json({ error: 'Cross-origin request blocked.' });
}
next();
};
app.use(rejectCrossOrigin);
// --- Rate limiting for auth endpoints (brute-force protection) ---
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many attempts. Please try again later.' }
});
// --- ROUTES ---
app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'index.html')));
app.get('/dashboard', isAuthenticated, (req, res) => res.sendFile(path.join(__dirname, 'dashboard.html')));
// --- API ENDPOINTS ---
// AI / PII ENDPOINTS
app.post('/api/ai/sanitize', isAuthenticated, (req, res) => {
const { text } = req.body;
const result = sanitizePII(text || '');
res.json(result);
});
app.post('/api/ai/parse-prompt', isAuthenticated, (req, res) => {
const { prompt } = req.body;
if (!prompt || typeof prompt !== 'string') {
return res.status(400).json({ error: 'Prompt string is required.' });
}
const result = parseInvoicePrompt(prompt);
res.json(result);
});
// GET current user's email
app.get('/api/user/me', isAuthenticated, (req, res) => {
const userId = req.session.userId;
const sql = `SELECT email FROM users WHERE id = ?`;
db.get(sql, [userId], (err, row) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.json(row);
});
});
// GET suggestions for clients
app.get('/api/suggestions/clients', isAuthenticated, (req, res) => {
const userId = req.session.userId;
const sql = `
SELECT DISTINCT client FROM invoices
WHERE user_id = ?
AND client IS NOT NULL AND client != ''
`;
db.all(sql, [userId], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows.map(r => r.client));
});
});
// GET suggestions for descriptions
app.get('/api/suggestions/descriptions', isAuthenticated, (req, res) => {
const userId = req.session.userId;
const sql = `
SELECT DISTINCT description FROM invoice_items
WHERE invoice_id IN (SELECT id FROM invoices WHERE user_id = ?)
AND description IS NOT NULL AND description != ''
`;
db.all(sql, [userId], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows.map(r => r.description));
});
});
// GET invoice data
app.get('/api/invoices', isAuthenticated, (req, res) => {
const userId = req.session.userId;
const sql = `
SELECT
i.id as invoice_id, i.invoice_number, i.contractor_name, i.notes,
ii.client, ii.date, ii.description, ii.hours, ii.hourly_rate, ii.total
FROM invoices i
LEFT JOIN invoice_items ii ON i.id = ii.invoice_id
WHERE i.user_id = ?
`;
db.all(sql, [userId], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
// POST invoice data with auto-PII sanitization
app.post('/api/invoices', isAuthenticated, (req, res) => {
const userId = req.session.userId;
const { invoiceNumber, contractorName, notes, items } = req.body;
const sanitizedNotes = sanitizePII(notes || '').sanitizedText;
db.serialize(() => {
const deleteItemsSql = `DELETE FROM invoice_items WHERE invoice_id IN (SELECT id FROM invoices WHERE user_id = ?)`;
db.run(deleteItemsSql, [userId]);
const deleteInvoiceSql = `DELETE FROM invoices WHERE user_id = ?`;
db.run(deleteInvoiceSql, [userId], function(err) {
if (err) return res.status(500).json({ error: err.message });
const insertInvoiceSql = `INSERT INTO invoices (user_id, invoice_number, contractor_name, notes) VALUES (?, ?, ?, ?)`;
db.run(insertInvoiceSql, [userId, invoiceNumber, contractorName, sanitizedNotes], function(err) {
if (err) return res.status(500).json({ error: err.message });
const invoiceId = this.lastID;
if (items && items.length > 0) {
const insertItemSql = `INSERT INTO invoice_items (invoice_id, client, date, description, hours, hourly_rate, total) VALUES (?, ?, ?, ?, ?, ?, ?)`;
items.forEach(item => {
const sanitizedDesc = sanitizePII(item.description || '').sanitizedText;
db.run(insertItemSql, [invoiceId, item.client, item.date, sanitizedDesc, item.hours, item.hourlyRate, item.total]);
});
}
res.status(201).json({ message: 'Invoice saved successfully (PII sanitized)!', invoiceId: invoiceId });
});
});
});
});
// --- AUTH ENDPOINTS ---
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
app.post('/signup', authLimiter, (req, res) => {
const { email, password } = req.body;
if (typeof email !== 'string' || !EMAIL_RE.test(email)) {
return res.status(400).json({ error: 'A valid email address is required.' });
}
if (typeof password !== 'string' || password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters.' });
}
bcrypt.hash(password, saltRounds, (err, hash) => {
if (err) return res.status(500).json({ error: 'Error hashing password' });
const sql = `INSERT INTO users (email, password_hash) VALUES (?, ?)`;
db.run(sql, [email, hash], function(err) {
if (err) {
if (err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ error: 'Email already exists.' });
}
return res.status(500).json({ error: err.message });
}
res.status(201).json({ message: 'User created successfully!' });
});
});
});
app.post('/login', authLimiter, (req, res) => {
const { email, password } = req.body;
const sql = `SELECT * FROM users WHERE email = ?`;
db.get(sql, [email], (err, user) => {
if (err) return res.status(500).json({ error: err.message });
if (!user) return res.status(401).json({ error: 'Invalid email or password.' });
bcrypt.compare(password, user.password_hash, (err, isMatch) => {
if (err) return res.status(500).json({ error: 'Error comparing passwords' });
if (isMatch) {
req.session.userId = user.id;
res.status(200).json({ message: 'Login successful!' });
} else {
res.status(401).json({ error: 'Invalid email or password.' });
}
});
});
});
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Could not log out.' });
}
res.clearCookie('connect.sid');
res.status(200).json({ message: 'Logout successful.' });
});
});
app.listen(port, () => {
console.log(`Server is listening at http://localhost:${port}`);
});