Add backend unit/integration tests, Playwright e2e tests, README, cookie Secure fix
SonarQube Analysis / sonarqube (push) Successful in 4m20s
SonarQube Analysis / sonarqube (push) Successful in 4m20s
This commit is contained in:
Executable
+50
@@ -0,0 +1,50 @@
|
||||
import express from 'express';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import path from 'path';
|
||||
import itemsRouter from './routes/items';
|
||||
import { router as paypalRouter, webhookRouter as paypalWebhookRouter } from './routes/paypal';
|
||||
import adminRouter from './routes/admin';
|
||||
import adminCustomersRouter from './routes/adminCustomers';
|
||||
import demoRouter from './routes/demo';
|
||||
import customersRouter from './routes/customers';
|
||||
import publicRouter from './routes/public';
|
||||
import { attachCustomer } from './middleware/customerAuth';
|
||||
|
||||
const app = express();
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
app.use('/webhooks/paypal', express.json(), paypalWebhookRouter);
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(attachCustomer);
|
||||
app.use('/uploads', express.static(process.env.UPLOADS_DIR || '/app/uploads'));
|
||||
|
||||
app.get('/api/config', (_req, res) => {
|
||||
const clientId = process.env.PAYPAL_CLIENT_ID;
|
||||
const isPlaceholder = !clientId || clientId.length < 10 || clientId === 'REPLACE_WITH_PAYPAL_CLIENT_ID';
|
||||
res.json({
|
||||
paypalClientId: isPlaceholder ? null : clientId,
|
||||
demoMode: process.env.DEMO_MODE !== 'false',
|
||||
currency: process.env.SITE_CURRENCY || 'USD'
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/items', itemsRouter);
|
||||
app.use('/api/checkout/paypal', paypalRouter);
|
||||
app.use('/api/checkout/demo', demoRouter);
|
||||
app.use('/api/admin/customers', adminCustomersRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/api/customers', customersRouter);
|
||||
app.use('/', publicRouter);
|
||||
|
||||
// Skip serving the built frontend during tests — there's no /public dir yet
|
||||
// at that point, and tests only care about the API surface.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
const staticDir = path.join(__dirname, '..', 'public');
|
||||
app.use(express.static(staticDir));
|
||||
app.get('*', (_req, res) => {
|
||||
res.sendFile(path.join(staticDir, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
export default app;
|
||||
@@ -5,8 +5,9 @@ import { pool } from '../db';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/app/uploads';
|
||||
const storage = multer.diskStorage({
|
||||
destination: '/app/uploads',
|
||||
destination: UPLOADS_DIR,
|
||||
filename: (_req, file, cb) => {
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${Date.now()}-${Math.round(Math.random() * 1e6)}${ext}`);
|
||||
|
||||
@@ -4,17 +4,16 @@ import crypto from 'node:crypto';
|
||||
import { pool } from '../db';
|
||||
import { requireCustomer } from '../middleware/customerAuth';
|
||||
import { sendMail } from '../mailer';
|
||||
import { MARKETING_CONSENT_TEXT, isValidEmail } from '../utils';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const SESSION_DAYS = 30;
|
||||
const MARKETING_CONSENT_TEXT =
|
||||
'I want to receive occasional emails about new one-of-a-kind items from Redefined Designs. I can unsubscribe at any time.';
|
||||
|
||||
function setSessionCookie(res: Response, token: string) {
|
||||
res.cookie('rd_session', token, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: SESSION_DAYS * 24 * 60 * 60 * 1000
|
||||
});
|
||||
@@ -43,7 +42,7 @@ function publicCustomer(c: any) {
|
||||
|
||||
router.post('/register', async (req: Request, res: Response) => {
|
||||
const { email, password, name, marketingConsent } = req.body;
|
||||
if (!email || !password || String(password).length < 8) {
|
||||
if (!email || !isValidEmail(String(email)) || !password || String(password).length < 8) {
|
||||
return res.status(400).json({ error: 'valid email and password (min 8 chars) required' });
|
||||
}
|
||||
const normalizedEmail = String(email).toLowerCase().trim();
|
||||
@@ -143,7 +142,6 @@ router.post('/change-password', requireCustomer, async (req: Request, res: Respo
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
// Explicit, revocable marketing consent (GDPR Art. 7 / ePrivacy)
|
||||
router.post('/me/consent', requireCustomer, async (req: Request, res: Response) => {
|
||||
const consent = !!req.body.marketingConsent;
|
||||
await pool.query(
|
||||
@@ -163,7 +161,6 @@ router.get('/me/orders', requireCustomer, async (req: Request, res: Response) =>
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// GDPR right to access / data portability
|
||||
router.get('/me/export', requireCustomer, async (req: Request, res: Response) => {
|
||||
const { rows: customerRows } = await pool.query(`SELECT * FROM customers WHERE id = $1`, [req.customerId]);
|
||||
const { rows: orderRows } = await pool.query(`SELECT * FROM orders WHERE customer_id = $1`, [req.customerId]);
|
||||
@@ -175,7 +172,6 @@ router.get('/me/export', requireCustomer, async (req: Request, res: Response) =>
|
||||
});
|
||||
});
|
||||
|
||||
// GDPR right to erasure — order records kept for accounting but stripped of the customer link
|
||||
router.delete('/me', requireCustomer, async (req: Request, res: Response) => {
|
||||
await pool.query(`UPDATE orders SET customer_id = NULL WHERE customer_id = $1`, [req.customerId]);
|
||||
await pool.query(`DELETE FROM customers WHERE id = $1`, [req.customerId]);
|
||||
|
||||
+1
-44
@@ -1,48 +1,5 @@
|
||||
import express from 'express';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import path from 'path';
|
||||
import app from './app';
|
||||
import { pool } from './db';
|
||||
import itemsRouter from './routes/items';
|
||||
import { router as paypalRouter, webhookRouter as paypalWebhookRouter } from './routes/paypal';
|
||||
import adminRouter from './routes/admin';
|
||||
import adminCustomersRouter from './routes/adminCustomers';
|
||||
import demoRouter from './routes/demo';
|
||||
import customersRouter from './routes/customers';
|
||||
import publicRouter from './routes/public';
|
||||
import { attachCustomer } from './middleware/customerAuth';
|
||||
|
||||
const app = express();
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
app.use('/webhooks/paypal', express.json(), paypalWebhookRouter);
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(attachCustomer);
|
||||
app.use('/uploads', express.static('/app/uploads'));
|
||||
|
||||
app.get('/api/config', (_req, res) => {
|
||||
const clientId = process.env.PAYPAL_CLIENT_ID;
|
||||
const isPlaceholder = !clientId || clientId.length < 10 || clientId === 'REPLACE_WITH_PAYPAL_CLIENT_ID';
|
||||
res.json({
|
||||
paypalClientId: isPlaceholder ? null : clientId,
|
||||
demoMode: process.env.DEMO_MODE !== 'false',
|
||||
currency: process.env.SITE_CURRENCY || 'USD'
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/items', itemsRouter);
|
||||
app.use('/api/checkout/paypal', paypalRouter);
|
||||
app.use('/api/checkout/demo', demoRouter);
|
||||
app.use('/api/admin/customers', adminCustomersRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/api/customers', customersRouter);
|
||||
app.use('/', publicRouter);
|
||||
|
||||
const staticDir = path.join(__dirname, '..', 'public');
|
||||
app.use(express.static(staticDir));
|
||||
app.get('*', (_req, res) => {
|
||||
res.sendFile(path.join(staticDir, 'index.html'));
|
||||
});
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
export function toCents(price: string | number): number {
|
||||
const n = typeof price === 'string' ? parseFloat(price) : price;
|
||||
if (Number.isNaN(n) || n < 0) {
|
||||
throw new Error('invalid price');
|
||||
}
|
||||
return Math.round(n * 100);
|
||||
}
|
||||
|
||||
export function formatPrice(cents: number): string {
|
||||
return `$${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
export function isValidEmail(email: string): boolean {
|
||||
return EMAIL_RE.test(email.trim());
|
||||
}
|
||||
|
||||
export const MARKETING_CONSENT_TEXT =
|
||||
'I want to receive occasional emails about new one-of-a-kind items from Redefined Designs. I can unsubscribe at any time.';
|
||||
Reference in New Issue
Block a user