85 lines
2.7 KiB
JavaScript
85 lines
2.7 KiB
JavaScript
import 'dotenv/config';
|
|
import express from 'express';
|
|
import cors from 'cors';
|
|
import pg from 'pg';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const { Pool } = pg;
|
|
import { dirname, resolve as resolvePath } from 'node:path';
|
|
import { config as loadEnv } from 'dotenv';
|
|
|
|
import { corsOptions } from '../shared/cors/policy.js';
|
|
import { requestLog } from '../shared/logging/request-log.js';
|
|
import { logger } from '../shared/logging/logger.js';
|
|
import { errorHandler } from '../shared/errors/handler.js';
|
|
import { loginLimiter, verifyLimiter } from '../shared/rate-limit/login.js';
|
|
import { extractBearerToken, verifyToken, TokenError } from '../shared/auth/verify.js';
|
|
|
|
import { createAuthRoutes } from './routes.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// auth/ lives at inventory-server/auth/, so .env one level up
|
|
loadEnv({ path: resolvePath(__dirname, '../.env') });
|
|
|
|
if (!process.env.JWT_SECRET) {
|
|
logger.error('JWT_SECRET is not set; refusing to start');
|
|
process.exit(1);
|
|
}
|
|
|
|
logger.info({
|
|
host: process.env.DB_HOST,
|
|
user: process.env.DB_USER,
|
|
database: process.env.DB_NAME,
|
|
port: process.env.DB_PORT,
|
|
auth_port: process.env.AUTH_PORT,
|
|
}, 'starting auth server');
|
|
|
|
const app = express();
|
|
const port = Number(process.env.AUTH_PORT) || 3011;
|
|
|
|
const pool = new Pool({
|
|
host: process.env.DB_HOST,
|
|
user: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME,
|
|
port: Number(process.env.DB_PORT) || 5432,
|
|
});
|
|
|
|
app.use(requestLog());
|
|
app.use(express.json({ limit: '1mb' }));
|
|
app.use(cors(corsOptions));
|
|
|
|
// Caddy forward_auth target: JWT signature check only, no DB hit.
|
|
// Returns 200 with X-User-Id / X-User-Username on success; 401 otherwise.
|
|
// Per-service middleware re-verifies independently; these headers are informational.
|
|
app.all('/verify', verifyLimiter, (req, res) => {
|
|
try {
|
|
const token = extractBearerToken(req.headers.authorization);
|
|
const decoded = verifyToken(token, process.env.JWT_SECRET);
|
|
res.set('X-User-Id', String(decoded.userId));
|
|
if (decoded.username) res.set('X-User-Username', decoded.username);
|
|
res.status(200).end();
|
|
} catch (err) {
|
|
if (err instanceof TokenError) {
|
|
return res.status(401).json({ error: err.message });
|
|
}
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
});
|
|
|
|
// Login route gets its own rate limiter to slow credential stuffing.
|
|
app.use('/login', loginLimiter);
|
|
|
|
// Mount user-management + /login + /me from routes.js
|
|
app.use('/', createAuthRoutes({ pool }));
|
|
|
|
app.get('/health', (req, res) => res.json({ status: 'healthy' }));
|
|
|
|
app.use(errorHandler);
|
|
|
|
app.listen(port, () => {
|
|
logger.info({ port }, 'auth server listening');
|
|
});
|