This commit is contained in:
seb
2026-07-31 05:54:13 +02:00
parent 6db7d4d75c
commit db8b192094
32 changed files with 100 additions and 2048 deletions

View File

@@ -7,7 +7,7 @@ import cookieParser from 'cookie-parser';
import { loggerMiddleware } from './middleware/logger.js';
import env from '../config/env.js';
import { authMiddleware } from './middleware/auth.js';
import { ensureConnection } from '../db/client.js';
import { ensureConnection } from '../db/store.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

View File

@@ -6,17 +6,17 @@ import { meJson } from '../auth/meJson.js';
const userCache = new Map();
const USER_CACHE_TTL = 10 * 60 * 1000;
export function getCachedUser(id) {
const cached = userCache.get(id);
export function getCachedUser(email) {
const cached = userCache.get(email);
if (cached && cached.expiresAt > Date.now()) {
return cached.user;
}
userCache.delete(id);
userCache.delete(email);
return null;
}
export function setCachedUser(id, user) {
userCache.set(id, { user, expiresAt: Date.now() + USER_CACHE_TTL });
export function setCachedUser(email, user) {
userCache.set(email, { user, expiresAt: Date.now() + USER_CACHE_TTL });
if (userCache.size > 1000) {
const now = Date.now();
for (const [k, v] of userCache) {
@@ -25,8 +25,8 @@ export function setCachedUser(id, user) {
}
}
export function clearCachedUser(id) {
userCache.delete(id);
export function clearCachedUser(email) {
userCache.delete(email);
}
function authMeJson(res, req, status, body) {
@@ -51,10 +51,10 @@ export const authMiddleware = async (req, res, next) => {
try {
const decoded = jwt.verify(token, env.jwtSecret);
let user = getCachedUser(decoded.id);
let user = getCachedUser(decoded.email);
if (!user) {
user = db.getAllowedEmailById(decoded.id);
if (user) setCachedUser(decoded.id, user);
user = await db.getAllowedEmail(decoded.email);
if (user) setCachedUser(decoded.email, user);
}
if (!user) {
return authMeJson(res, req, 403, { error: 'User is no longer authorized' });
@@ -63,6 +63,7 @@ export const authMiddleware = async (req, res, next) => {
req.user = {
id: user.id,
email: user.email,
selectedCompanyId: user.selectedCompanyId,
...(decoded.picture && { picture: decoded.picture }),
};

View File

@@ -1,34 +0,0 @@
import { getArticle } from '../../../services/article/getArticle.js';
/**
* Registers the full article lookup route backed by MSSQL.
* @param {import('express').Application} app - Express app
* @param {string} [basePath=''] - Optional URL prefix for all routes in this module
*/
export function register(app, basePath = '') {
/**
* @route GET /article/get
* @description Look up a single article by exact article number, EAN, or barcode.
* @query {string} target - Article number, packaging EAN, or barcode
* @returns {Object} 200 - `{ article }` with enriched article data
* @returns {Object} 400 - `{ error }` when target is missing
* @returns {Object} 404 - `{ error }` when no article matches
*/
app.get(`${basePath}/article/get`, async (req, res) => {
const target = req.query.target;
if (!target || !String(target).trim()) {
return res.status(400).json({ error: 'Query parameter "target" is required' });
}
try {
const article = await getArticle(String(target));
if (!article) {
return res.status(404).json({ error: 'Article not found' });
}
return res.status(200).json({ article });
} catch (error) {
return res.status(500).json({ error: error.message });
}
});
}

View File

@@ -1,36 +0,0 @@
import { parseFilter, searchArticles } from '../../../services/article/searchArticles.js';
/**
* Registers the article search route backed by MSSQL.
* @param {import('express').Application} app - Express app
* @param {string} [basePath=''] - Optional URL prefix for all routes in this module
*/
export function register(app, basePath = '') {
/**
* @route GET /article/search
* @description Search articles by partial match on name, number, EAN, barcode, or supplier number.
* @query {string} target - Space-separated search terms
* @query {string} [filter] - Optional comma-separated category filter IDs (empty = all)
* @query {number} [offset=0] - Number of results to skip
* @query {number} [maxCount=10] - Maximum number of results to return
* @returns {Object} 200 - `{ categories, length, recordset }`
* @returns {Object} 400 - `{ error }` when target is missing
*/
app.get(`${basePath}/article/search`, async (req, res) => {
const target = req.query.target;
if (!target || !String(target).trim()) {
return res.status(400).json({ error: 'Query parameter "target" is required' });
}
try {
const filter = parseFilter(req.query.filter);
const offset = req.query.offset !== undefined ? Number(req.query.offset) : 0;
const maxCount = req.query.maxCount !== undefined ? Number(req.query.maxCount) : 10;
const result = await searchArticles(String(target), filter, offset, maxCount);
return res.status(200).json(result);
} catch (error) {
return res.status(500).json({ error: error.message });
}
});
}

View File

@@ -15,7 +15,7 @@ export function register(app, basePath = '') {
* @route POST /auth/login
* @description Verifies a Google ID token, checks allowlist, and sets an HTTP-only `token` cookie.
* @body {string} idToken - Google ID token from Google Identity Services
* @returns {Object} 200 - `{ message, user }` with `{ id, email, picture? }` in `user`
* @returns {Object} 200 - `{ message, user }` with `{ id, email, selectedCompanyId, picture? }` in `user`
* @returns {Object} 400 - Missing token or Google client not configured
* @returns {Object} 401 - Invalid Google token
* @returns {Object} 403 - Email not on allowlist
@@ -43,7 +43,7 @@ export function register(app, basePath = '') {
return res.status(401).json({ error: 'Invalid Google account' });
}
const allowed = db.getAllowedEmail(email);
const allowed = await db.getAllowedEmail(email);
if (!allowed) {
console.warn(`Login rejected: email not authorized (${email})`);
return res.status(403).json({ error: 'Email is not authorized' });
@@ -52,6 +52,7 @@ export function register(app, basePath = '') {
const user = {
id: allowed.id,
email: allowed.email,
selectedCompanyId: allowed.selectedCompanyId,
...(payload.picture && { picture: payload.picture }),
};
const token = jwt.sign(user, env.jwtSecret, { expiresIn: '1d' });

View File

@@ -18,8 +18,8 @@ export function register(app, basePath = '') {
if (token) {
try {
const decoded = jwt.verify(token, env.jwtSecret, { ignoreExpiration: true });
if (decoded?.id) {
clearCachedUser(decoded.id);
if (decoded?.email) {
clearCachedUser(decoded.email);
}
} catch {
// Ignore errors during cache clearing on logout

View File

@@ -9,11 +9,11 @@ export function register(app, basePath = '') {
/**
* @route GET /auth/me
* @description Returns the authenticated user from `req.user` and the Google client ID.
* @returns {Object} 200 - `{ user: { id, email, picture? }, googleClientId }`
* @returns {Object} 200 - `{ user: { id, email, selectedCompanyId, picture? }, googleClientId }`
*/
app.get(`${basePath}/auth/me`, (req, res) => {
const { id, email, picture } = req.user;
const user = { id, email, ...(picture && { picture }) };
const { id, email, selectedCompanyId, picture } = req.user;
const user = { id, email, selectedCompanyId, ...(picture && { picture }) };
meJson(res, 200, { user });
});
}

View File

@@ -1,4 +1,4 @@
import { ensureConnection } from '../../db/client.js';
import { ensureConnection } from '../../db/store.js';
/**
* Registers a database connectivity health check route.

View File

@@ -1,20 +0,0 @@
import { getCategories } from '../../../services/misc/getCategories.js';
/**
* @param {import('express').Application} app
* @param {string} [basePath='']
*/
export function register(app, basePath = '') {
/**
* @route GET /misc/getCategories
* @returns {Object} 200 - `{ categories, tree }`
*/
app.get(`${basePath}/misc/getCategories`, async (_req, res) => {
try {
const result = await getCategories();
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
}

View File

@@ -1,19 +0,0 @@
import { getSteuer } from '../../../services/misc/getSteuer.js';
/**
* @param {import('express').Application} app
* @param {string} [basePath='']
*/
export function register(app, basePath = '') {
/**
* @route GET /misc/getSteuer
* @returns {Object} 200 - `{ steuerklassen }`
*/
app.get(`${basePath}/misc/getSteuer`, async (_req, res) => {
try {
res.status(200).json(await getSteuer());
} catch (error) {
res.status(500).json({ error: error.message });
}
});
}