Pre-reordenación

This commit is contained in:
Mateo Saldain 2025-09-05 04:02:39 +00:00
parent 8522d02170
commit 80778c0ed9
10 changed files with 1115 additions and 120 deletions

View File

@ -69,38 +69,26 @@ services:
#################
# --- Authentik db (solo interno)
authentik-db:
# image: postgres:16-alpine
environment:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASS}
# healthcheck:
# test: ["CMD-SHELL", "pg_isready -U authentik -d authentik"]
# interval: 10s
# timeout: 3s
# retries: 10
volumes:
- authentik-db:/var/lib/postgresql/data
networks:
net:
aliases: [ak-db]
# restart: unless-stopped
# --- Authentik Redis (solo interno)
authentik-redis:
# image: redis:7-alpine
command: ["redis-server", "--save", "", "--appendonly", "no"]
networks:
net:
aliases: [ak-redis]
# restart: unless-stopped
# --- Authentik Server (sin puertos públicos)
authentik:
# image: ghcr.io/goauthentik/server:latest
# depends_on:
# authentik-db: { condition: service_healthy }
# authentik-redis: { condition: service_started }
command: server
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_DEBUG: "false"
@ -112,22 +100,12 @@ services:
# Opcional: bootstrap automático del admin
AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD}
AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL}
# expose:
# - "9000" # HTTP interno
# - "9443" # HTTPS interno
networks:
net:
aliases: [authentik]
# restart: unless-stopped
# Habilitá ESTO SOLO si querés abrir la UI local:
profiles: ["ak-ui"]
ports:
- 9000:9000
- 9443:9443
aliases: [authentik]
# --- Authentik Worker
authentik-worker:
# image: ghcr.io/goauthentik/server:latest
command: worker
depends_on:
authentik-db: { condition: service_healthy }

View File

@ -6,13 +6,13 @@ services:
manso:
image: node:20-bookworm
depends_on:
db:
condition: service_healthy
tenants:
condition: service_healthy
expose:
- ${APP_LOCAL_PORT}
# depends_on:
# db:
# condition: service_healthy
# tenants:
# condition: service_healthy
# expose:
# - ${APP_LOCAL_PORT}
working_dir: /app
user: "${UID:-1000}:${GID:-1000}"
volumes:
@ -35,29 +35,29 @@ services:
profiles: [manso]
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
volumes:
- suitecoffee-db:/var/lib/postgresql/data
networks:
net:
aliases: [dev-db]
# db:
# image: postgres:16
# environment:
# POSTGRES_DB: ${DB_NAME}
# POSTGRES_USER: ${DB_USER}
# POSTGRES_PASSWORD: ${DB_PASS}
# volumes:
# - suitecoffee-db:/var/lib/postgresql/data
# networks:
# net:
# aliases: [dev-db]
tenants:
image: postgres:16
environment:
POSTGRES_DB: ${TENANTS_DB_NAME}
POSTGRES_USER: ${TENANTS_DB_USER}
POSTGRES_PASSWORD: ${TENANTS_DB_PASS}
volumes:
- tenants-db:/var/lib/postgresql/data
networks:
net:
aliases: [dev-tenants]
# tenants:
# image: postgres:16
# environment:
# POSTGRES_DB: ${TENANTS_DB_NAME}
# POSTGRES_USER: ${TENANTS_DB_USER}
# POSTGRES_PASSWORD: ${TENANTS_DB_PASS}
# volumes:
# - tenants-db:/var/lib/postgresql/data
# networks:
# net:
# aliases: [dev-tenants]
volumes:
tenants-db:

View File

@ -10,13 +10,17 @@
"license": "ISC",
"dependencies": {
"chalk": "^5.6.0",
"connect-redis": "^9.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"ejs": "^3.1.10",
"express": "^5.1.0",
"express-ejs-layouts": "^2.5.1",
"express-session": "^1.18.2",
"ioredis": "^5.7.0",
"pg": "^8.16.3",
"pg-format": "^1.0.4",
"redis": "^5.8.2",
"serve-favicon": "^2.5.1"
},
"devDependencies": {
@ -29,6 +33,72 @@
"dev": true,
"license": "MIT"
},
"node_modules/@ioredis/commands": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz",
"integrity": "sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==",
"license": "MIT"
},
"node_modules/@redis/bloom": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.2.tgz",
"integrity": "sha512-855DR0ChetZLarblio5eM0yLwxA9Dqq50t8StXKp5bAtLT0G+rZ+eRzzqxl37sPqQKjUudSYypz55o6nNhbz0A==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/client": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.2.tgz",
"integrity": "sha512-WtMScno3+eBpTac1Uav2zugXEoXqaU23YznwvFgkPwBQVwEHTDgOG7uEAObtZ/Nyn8SmAMbqkEubJaMOvnqdsQ==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@redis/json": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.2.tgz",
"integrity": "sha512-uxpVfas3I0LccBX9rIfDgJ0dBrUa3+0Gc8sEwmQQH0vHi7C1Rx1Qn8Nv1QWz5bohoeIXMICFZRcyDONvum2l/w==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/search": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.2.tgz",
"integrity": "sha512-cNv7HlgayavCBXqPXgaS97DRPVWFznuzsAmmuemi2TMCx5scwLiP50TeZvUS06h/MG96YNPe6A0Zt57yayfxwA==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/time-series": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.2.tgz",
"integrity": "sha512-g2NlHM07fK8H4k+613NBsk3y70R2JIM2dPMSkhIjl2Z17SYvaYKdusz85d7VYOrZBWtDrHV/WD2E3vGu+zni8A==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"license": "MIT",
@ -174,11 +244,33 @@
"fsevents": "~2.3.2"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"dev": true,
"license": "MIT"
},
"node_modules/connect-redis": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-9.0.0.tgz",
"integrity": "sha512-QwzyvUePTMvEzG1hy45gZYw3X3YHrjmEdSkayURlcZft7hqadQ3X39wYkmCqblK2rGlw+XItELYt6GnyG6DEIQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"express-session": ">=1",
"redis": ">=5"
}
},
"node_modules/content-disposition": {
"version": "1.0.0",
"license": "MIT",
@ -265,6 +357,15 @@
}
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"license": "MIT",
@ -396,6 +497,46 @@
"node_modules/express-ejs-layouts": {
"version": "2.5.1"
},
"node_modules/express-session": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.1.0",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/express-session/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/express-session/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/filelist": {
"version": "1.0.4",
"license": "Apache-2.0",
@ -589,6 +730,30 @@
"version": "2.0.4",
"license": "ISC"
},
"node_modules/ioredis": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz",
"integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.3.0",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"license": "MIT",
@ -658,6 +823,18 @@
"node": ">=10"
}
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"license": "MIT",
@ -783,6 +960,15 @@
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"license": "ISC",
@ -967,6 +1153,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"license": "MIT",
@ -998,6 +1193,43 @@
"node": ">=8.10.0"
}
},
"node_modules/redis": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-5.8.2.tgz",
"integrity": "sha512-31vunZj07++Y1vcFGcnNWEf5jPoTkGARgfWI4+Tk55vdwHxhAvug8VEtW7Cx+/h47NuJTEg/JL77zAwC6E0OeA==",
"license": "MIT",
"dependencies": {
"@redis/bloom": "5.8.2",
"@redis/client": "5.8.2",
"@redis/json": "5.8.2",
"@redis/search": "5.8.2",
"@redis/time-series": "5.8.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/router": {
"version": "2.2.0",
"license": "MIT",
@ -1204,6 +1436,12 @@
"node": ">= 10.x"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.2",
"license": "MIT",
@ -1260,6 +1498,18 @@
"node": ">= 0.6"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"license": "MIT",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/undefsafe": {
"version": "2.0.5",
"dev": true,

View File

@ -16,13 +16,17 @@
},
"dependencies": {
"chalk": "^5.6.0",
"connect-redis": "^9.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"ejs": "^3.1.10",
"express": "^5.1.0",
"express-ejs-layouts": "^2.5.1",
"express-session": "^1.18.2",
"ioredis": "^5.7.0",
"pg": "^8.16.3",
"pg-format": "^1.0.4",
"redis": "^5.8.2",
"serve-favicon": "^2.5.1"
},
"keywords": [],

View File

@ -12,6 +12,14 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
//Redis
import session from 'express-session';
import { createClient } from 'redis';
import * as connectRedis from 'connect-redis';
const RedisStore = connectRedis.default || connectRedis.RedisStore;
const redis = createClient({ url: process.env.REDIS_URL || 'redis://authentik-redis:6379' });
await redis.connect();
// Variables de Entorno
import dotenv from 'dotenv';
@ -36,6 +44,18 @@ app.use(express.json());
app.use(express.json({ limit: '1mb' }));
app.use(express.static(path.join(__dirname, 'pages')));
app.use(session({
name: 'sc.sid',
store: new RedisStore({ client: redis, prefix: 'sess:' }),
secret: process.env.SESSION_SECRET || 'change-me',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
},
}));
// ----------------------------------------------------------
// Motor de vistas EJS
@ -72,6 +92,14 @@ const dbConfig = {
const pool = new Pool(dbConfig);
const tenantsPool = new Pool({
host: process.env.TENANTS_HOST || 'dev-tenants',
port: Number(process.env.TENANTS_PORT || 5432),
user: process.env.TENANTS_USER || 'postgres',
password: process.env.TENANTS_PASS || 'postgres',
database: process.env.TENANTS_DB || 'dev-postgres',
});
// ----------------------------------------------------------
// Seguridad: Tablas permitidas
// ----------------------------------------------------------
@ -103,6 +131,63 @@ async function getClient() {
return client;
}
export async function withTenant(req, res, next) {
const client = await tenantsPool.connect();
try {
await client.query('BEGIN');
const uuid = getTenantUuid(req);
const schema = `schema_tenant_${uuid}`;
// Usa la función helper si la creaste en la DB (recomendado)
// await client.query('SELECT public.f_set_search_path($1)', [schema]);
// Alternativa directa si aún no tienes la función:
await client.query(`SET LOCAL search_path TO ${schema.replace(/"/g, '')}`);
req.pg = client;
req.pgSchema = schema;
next();
} catch (e) {
try { if (client) await client.query('ROLLBACK'); } catch {}
if (client) client.release();
return res.status(400).json({ error: e.message });
}
}
// Cierra la transacción y libera la conexión
export async function done(req, res, next) {
try {
if (req.pg) await req.pg.query('COMMIT');
} catch (e) {
try { if (req.pg) await req.pg.query('ROLLBACK'); } catch {}
} finally {
if (req.pg) req.pg.release();
}
next?.();
}
function requireAuth(req, res, next) {
if (!req.session?.user) return res.status(401).json({ error: 'no-auth' });
next();
}
function getTenantUuid(req) {
// 1) header enviado por el front (fetchWithTenant)
const h = req.get('x-tenant-uuid');
if (h) return String(h).replace(/-/g, '');
// 2) sesión del login OIDC
const s = req.session?.user?.tenant_uuid;
if (s) return String(s).replace(/-/g, '');
throw new Error('Tenant no especificado');
}
app.get('/api/productos', requireAuth, withTenant, async (req, res, next) => {
const { rows } = await req.pg.query('SELECT * FROM productos ORDER BY id');
res.json(rows);
}, done);
app.use((req,res,next)=>{ res.locals.user = req.session?.user || null; next(); });
// ----------------------------------------------------------
// Introspección de esquema
// ----------------------------------------------------------

View File

@ -9,16 +9,21 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"axios": "^1.11.0",
"bcrypt": "^5.1.1",
"chalk": "^5.6.0",
"connect-redis": "^9.0.0",
"cookie-session": "^2.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-ejs-layouts": "^2.5.1",
"openid-client": "^5.6.5",
"express-session": "^1.18.2",
"ioredis": "^5.7.0",
"openid-client": "^5.7.1",
"pg": "^8.16.3",
"pg-format": "^1.0.4"
"pg-format": "^1.0.4",
"redis": "^5.8.2"
},
"devDependencies": {
"cross-env": "^10.0.0",
@ -32,6 +37,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@ioredis/commands": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz",
"integrity": "sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==",
"license": "MIT"
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@ -52,6 +63,66 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@redis/bloom": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.2.tgz",
"integrity": "sha512-855DR0ChetZLarblio5eM0yLwxA9Dqq50t8StXKp5bAtLT0G+rZ+eRzzqxl37sPqQKjUudSYypz55o6nNhbz0A==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/client": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.2.tgz",
"integrity": "sha512-WtMScno3+eBpTac1Uav2zugXEoXqaU23YznwvFgkPwBQVwEHTDgOG7uEAObtZ/Nyn8SmAMbqkEubJaMOvnqdsQ==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@redis/json": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.2.tgz",
"integrity": "sha512-uxpVfas3I0LccBX9rIfDgJ0dBrUa3+0Gc8sEwmQQH0vHi7C1Rx1Qn8Nv1QWz5bohoeIXMICFZRcyDONvum2l/w==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/search": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.2.tgz",
"integrity": "sha512-cNv7HlgayavCBXqPXgaS97DRPVWFznuzsAmmuemi2TMCx5scwLiP50TeZvUS06h/MG96YNPe6A0Zt57yayfxwA==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/@redis/time-series": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.2.tgz",
"integrity": "sha512-g2NlHM07fK8H4k+613NBsk3y70R2JIM2dPMSkhIjl2Z17SYvaYKdusz85d7VYOrZBWtDrHV/WD2E3vGu+zni8A==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.8.2"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -126,6 +197,23 @@
"node": ">=10"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -286,6 +374,15 @@
"node": ">=10"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@ -295,12 +392,37 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/connect-redis": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-9.0.0.tgz",
"integrity": "sha512-QwzyvUePTMvEzG1hy45gZYw3X3YHrjmEdSkayURlcZft7hqadQ3X39wYkmCqblK2rGlw+XItELYt6GnyG6DEIQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"express-session": ">=1",
"redis": ">=5"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -446,12 +568,30 @@
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -547,6 +687,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -609,6 +764,46 @@
"resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
"integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
},
"node_modules/express-session": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.1.0",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/express-session/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/express-session/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -639,6 +834,63 @@
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -837,6 +1089,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@ -929,6 +1196,30 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ioredis": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz",
"integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.3.0",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -1027,6 +1318,18 @@
"node": ">= 0.6"
}
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -1564,6 +1867,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@ -1586,6 +1895,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -1637,6 +1955,43 @@
"node": ">=8.10.0"
}
},
"node_modules/redis": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-5.8.2.tgz",
"integrity": "sha512-31vunZj07++Y1vcFGcnNWEf5jPoTkGARgfWI4+Tk55vdwHxhAvug8VEtW7Cx+/h47NuJTEg/JL77zAwC6E0OeA==",
"license": "MIT",
"dependencies": {
"@redis/bloom": "5.8.2",
"@redis/client": "5.8.2",
"@redis/json": "5.8.2",
"@redis/search": "5.8.2",
"@redis/time-series": "5.8.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -1879,6 +2234,12 @@
"node": ">= 10.x"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@ -2014,6 +2375,18 @@
"node": ">= 0.6"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"license": "MIT",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",

View File

@ -15,16 +15,21 @@
"nodemon": "^3.1.10"
},
"dependencies": {
"axios": "^1.11.0",
"bcrypt": "^5.1.1",
"chalk": "^5.6.0",
"connect-redis": "^9.0.0",
"cookie-session": "^2.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-ejs-layouts": "^2.5.1",
"express-session": "^1.18.2",
"ioredis": "^5.7.0",
"openid-client": "^5.7.1",
"pg": "^8.16.3",
"pg-format": "^1.0.4",
"openid-client": "^5.6.5",
"cookie-session": "^2.0.0"
"redis": "^5.8.2"
},
"keywords": [],
"description": ""

46
services/auth/src/ak.js Normal file
View File

@ -0,0 +1,46 @@
// services/auth/src/ak.js
import axios from 'axios';
const AK = axios.create({
baseURL: `${process.env.AUTHENTIK_BASE_URL}/api/v3`,
headers: { Authorization: `Bearer ${process.env.AUTHENTIK_TOKEN}` },
timeout: 10000,
});
// Busca usuario por email (case-insensitive)
export async function akFindUserByEmail(email) {
const { data } = await AK.get('/core/users/', { params: { search: email }});
// filtra exacto por email si querés evitar colisiones de 'search'
return data.results?.find(u => (u.email || '').toLowerCase() === email.toLowerCase()) || null;
}
// Crea usuario en Authentik con atributo tenant_uuid y lo agrega a un grupo (opcional)
export async function akCreateUser({ email, displayName, tenantUuid, addToGroupId }) {
// 1) crear usuario
const { data: user } = await AK.post('/core/users/', {
username: email, // en Authentik el username puede ser el email
name: displayName || email,
email,
is_active: true,
attributes: { tenant_uuid: tenantUuid }, // <-- para tu claim custom
});
// 2) agregar a grupo por defecto (opcional)
if (addToGroupId) {
await AK.post(`/core/users/${user.pk}/groups/`, { group: addToGroupId });
}
return user; // contiene pk y uuid
}
// Opcional: setear/forzar password inicial (si querés flujo con password local en Authentik)
export async function akSetPassword(userPk, password, requireChange = true) {
try {
await AK.post(`/core/users/${userPk}/set_password/`, {
password, require_change: requireChange,
});
} catch (e) {
// Si tu instancia no permite setear password por API, capturá y usá un flow de "reset password"
throw new Error('No se pudo establecer la contraseña en Authentik por API');
}
}

View File

@ -5,10 +5,22 @@ import expressLayouts from 'express-ejs-layouts';
import cors from 'cors';
import { Pool } from 'pg';
import bcrypt from'bcrypt';
import crypto from 'node:crypto';
import session from 'express-session';
import { createClient } from 'redis';
import * as connectRedis from 'connect-redis';
const RedisStore = connectRedis.default || connectRedis.RedisStore;
const redis = createClient({ url: process.env.REDIS_URL || 'redis://authentik-redis:6379' });
await redis.connect();
import { Issuer, generators } from 'openid-client';
import cookieSession from 'cookie-session';
import { akFindUserByEmail, akCreateUser, akSetPassword } from './ak.js';
// Rutas
import path from 'path';
import { fileURLToPath } from 'url';
@ -41,6 +53,39 @@ app.use(express.json());
app.set('trust proxy', true);
app.use(express.static(path.join(__dirname, 'pages')));
/* 1) Motor de vistas apuntando a /auth/src/views */
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
/* 2) Estáticos si usás /css/main.css dentro de /auth/src/public */
app.use(express.static(path.join(__dirname, 'public')));
/* 3) Exponer user a las vistas (opcional, cómodo) */
app.use((req, res, next) => {
res.locals.user = req.session?.user || null;
next();
});
/* 4) Página de login (renderiza el EJS de arriba)
- Mantén /auth/login para iniciar OIDC (redirección a Authentik)
- Usa /login para mostrar la página con el botón */
app.get('/login', (req, res) => {
res.render('login'); // -> /auth/src/views/login.ejs
});
app.use(session({
name: 'sc.sid',
store: new RedisStore({ client: redis, prefix: 'sess:' }),
secret: process.env.SESSION_SECRET || 'change-me',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
},
}));
app.use(cookieSession({
name: 'sid',
@ -52,7 +97,7 @@ app.use(cookieSession({
// Configuración de conexión PostgreSQL
const dbConfig = {
const poolMeta = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
@ -60,8 +105,23 @@ const dbConfig = {
port: process.env.DB_LOCAL_PORT
};
const pool = new Pool(dbConfig);
const pool = new Pool(poolMeta);
const poolTenants = new Pool({ // apunta al servidor/base multi-tenant
host: process.env.TENANTS_HOST, // dev-tenants
user: process.env.TENANTS_USER,
password: process.env.TENANTS_PASS,
database: process.env.TENANTS_DB, // dev-postgres
port: process.env.TENANTS_PORT,
});
const tenantsPool = new Pool({
host: process.env.TENANTS_HOST, // dev-tenants
user: process.env.TENANTS_USER,
password: process.env.TENANTS_PASS,
database: process.env.TENANTS_DB, // dev-postgres
port: process.env.TENANTS_PORT
});
async function verificarConexion() {
try {
@ -78,17 +138,27 @@ async function verificarConexion() {
// Descubrimiento OIDC (una sola vez)
let oidcClient;
async function getClient() {
if (oidcClient) return oidcClient;
const ISSUER = process.env.OIDC_ISSUER_INTERNAL; // ej: http://authentik:9000/application/o/suitecoffee/
const issuer = await Issuer.discover(`${ISSUER}.well-known/openid-configuration`);
(async () => {
const issuer = await Issuer.discover(process.env.OIDC_ISSUER); // debe coincidir EXACTO
oidcClient = new issuer.Client({
client_id: process.env.OIDC_CLIENT_ID,
client_secret: process.env.OIDC_CLIENT_SECRET,
redirect_uris: [`${process.env.BASE_URL}${process.env.OIDC_REDIRECT_PATH}`],
response_types: ['code']
redirect_uris: [process.env.OIDC_REDIRECT_URI],
response_types: ['code'],
});
return oidcClient;
})().catch(err => {
console.error('Error inicializando OIDC:', err);
process.exit(1);
});
// util para resolver tenant si aún no usás claim tenant_uuid
async function lookupTenantByEmail(email) {
const { rows } = await poolMeta.query(
`SELECT tenant_uuid FROM app_user WHERE email = $1 ORDER BY id LIMIT 1`,
[email.toLowerCase()]
);
return rows[0]?.tenant_uuid || null;
}
// === Servir páginas estáticas ===
@ -108,62 +178,125 @@ app.get('/planes', async (req, res) => {
}
});
app.post('/api/users/register', async (req, res, next) => {
const { email, display_name, tenant_uuid, role, password } = req.body;
app.post('/api/registro', async (req, res) => {
const {
nombre_empresa,
rut,
correo,
telefono,
direccion,
logo,
clave_acceso,
plan_id
} = req.body;
if (!email || !tenant_uuid) {
return res.status(400).json({ error: 'email y tenant_uuid son obligatorios' });
}
const client = await poolMeta.connect();
try {
const client = await pool.connect();
await client.query('BEGIN');
// 1. Hashear la contraseña
const hash = await bcrypt.hash(clave_acceso, 10);
// 0) idempotencia: si ya existe en tu DB, devolvés 409 o retornás el existente
const { rows: existing } = await client.query(
`SELECT id, email, ak_sub FROM app_user WHERE email = $1`, [email]
);
if (existing.length) {
await client.query('ROLLBACK');
return res.status(409).json({ error: 'El usuario ya existe en SuiteCoffee' });
}
// 2. Insertar el tenant
const result = await client.query(`
INSERT INTO tenant (
nombre_empresa, rut, correo, telefono, direccion, logo,
clave_acceso, plan_id, nombre_base_datos
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, 'TEMPORAL'
)
RETURNING uuid;
`, [
nombre_empresa, rut, correo, telefono, direccion, logo,
hash, plan_id
]);
// 1) crear/obtener usuario en Authentik
let akUser = await akFindUserByEmail(email);
if (!akUser) {
akUser = await akCreateUser({
email,
displayName: display_name,
tenantUuid: tenant_uuid.replace(/-/g, ''),
addToGroupId: process.env.AUTHENTIK_DEFAULT_GROUP_ID || null,
});
// Si querés asignar una clave inicial (no recomendado en prod), descomentá:
// if (password) await akSetPassword(akUser.pk, password, true);
}
const uuid = result.rows[0].uuid;
const nombre_base_datos = `tenantdb_${uuid}`.replace(/-/g, '').substring(0, 24); // ajustamos para longitud segura
// el 'sub' lo tendrás recién tras login OIDC; guardamos el uuid interno si te sirve
const _role = role || 'owner';
// 3. Actualizar el campo nombre_base_datos
await client.query(`
UPDATE tenant SET nombre_base_datos = $1 WHERE uuid = $2
`, [nombre_base_datos, uuid]);
// 2) crear usuario local (sin password, dependemos del SSO)
await client.query(
`INSERT INTO app_user (email, display_name, tenant_uuid, ak_user_uuid, role)
VALUES ($1, $2, $3, $4, $5)`,
[email, display_name || null, tenant_uuid.replace(/-/g, ''), akUser.uuid, _role]
);
client.release();
await client.query('COMMIT');
return res.status(201).json({
message: 'Tenant registrado correctamente',
uuid,
nombre_base_datos
message: 'Usuario registrado',
email, tenant_uuid, role: _role,
authentik_user_uuid: akUser.uuid,
next: '/auth/login' // redirigí a OIDC
});
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Error al registrar tenant' });
await client.query('ROLLBACK');
next(err);
} finally {
client.release();
}
});
// app.post('/api/registro', async (req, res) => {
// const {
// nombre_empresa, rut, correo, telefono, direccion, logo,
// clave_acceso, plan_id
// } = req.body;
// const clientMeta = await poolMeta.connect();
// const clientTen = await poolTenants.connect();
// try {
// await clientMeta.query('BEGIN');
// // 1) Generar UUID sin guiones
// const uuid = crypto.randomUUID().replace(/-/g, '');
// const hash = await bcrypt.hash(clave_acceso, 10);
// // 2) Provisionar en PG (dev-tenants/dev-postgres)
// const { rows: [prov] } = await clientTen.query(
// `SELECT public.f_tenant_provision($1::text, $2::text) AS data`,
// [uuid, `tenant_${uuid}`]
// );
// const info = prov.data; // { tenant_uuid, schema, role, user, password, ... }
// // 3) Guardar metadatos en suitecoffee_db (tu tabla 'tenant')
// await clientMeta.query(`
// INSERT INTO tenant (
// uuid, nombre_empresa, rut, correo, telefono, direccion, logo,
// clave_acceso, plan_id,
// schema_name, role_name, user_name
// ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
// `, [
// uuid, nombre_empresa, rut, correo, telefono, direccion, logo,
// hash, plan_id,
// info.schema, info.role, info.user
// ]);
// await clientMeta.query('COMMIT');
// // 4) Devolver credenciales del usuario de DB si las necesitás (mejor *no* persistir la password)
// return res.status(201).json({
// message: 'Tenant registrado correctamente',
// uuid,
// schema: info.schema,
// db_user: info.user,
// db_password: info.password // muéstrala *una vez* y recomendación: NO guardarla
// });
// } catch (err) {
// await clientMeta.query('ROLLBACK');
// console.error(err);
// return res.status(500).json({ error: 'Error al registrar tenant' });
// } finally {
// clientMeta.release();
// clientTen.release();
// }
// });
app.post('/api/login', async (req, res) => {
const { correo, clave_acceso } = req.body;
@ -202,6 +335,56 @@ app.post('/api/login', async (req, res) => {
}
});
app.get('/auth/login', (req, res) => {
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
req.session.code_verifier = code_verifier;
const url = oidcClient.authorizationUrl({
scope: 'openid profile email offline_access tenant', // incluye tu scope custom “tenant”
code_challenge: code_challenge,
code_challenge_method: 'S256',
});
res.redirect(url);
});
// ------------- Middleware ----------------
function getTenantUuid(req) {
// Ejemplo 1: header
if (req.headers['x-tenant-uuid']) return String(req.headers['x-tenant-uuid']);
// Ejemplo 2: si más adelante usás JWT:
// return req.user?.tenantUuid;
throw new Error('Tenant no especificado');
}
async function withTenant(req, res, next) {
const client = await tenantsPool.connect();
try {
await client.query('BEGIN');
const uuid = getTenantUuid(req).replace(/-/g, '');
const schema = `schema_tenant_${uuid}`;
await client.query(`SELECT public.f_set_search_path($1)`, [schema]);
// guardamos el cliente en req para reutilizar en los handlers
req.pg = client;
req.pgSchema = schema;
next();
} catch (e) {
if (req.pg) await req.pg.query('ROLLBACK');
if (req.pg) req.pg.release();
return res.status(400).json({ error: e.message });
}
}
// Al final de cada handler, hacé COMMIT y release
async function done(req, res, next) {
try { if (req.pg) await req.pg.query('COMMIT'); }
finally { if (req.pg) req.pg.release(); }
}
// --- login: redirige a Authentik con PKCE
app.get('/auth/login', async (req, res) => {
@ -223,24 +406,70 @@ app.get('/auth/login', async (req, res) => {
res.redirect(authUrl);
});
app.use((req,res,next)=>{ res.locals.user = req.session?.user || null; next(); });
// --- callback: intercambia code por tokens y guarda sesión mínima
app.get(process.env.OIDC_REDIRECT_PATH || '/auth/callback', async (req, res) => {
const client = await getClient();
const { state, code } = req.query;
// --- RUTA: callback (enlaza la sesión con tu usuario local)
app.get('/auth/callback', async (req, res, next) => {
try {
const params = oidcClient.callbackParams(req);
const tokenSet = await oidcClient.callback(
process.env.OIDC_REDIRECT_URI,
params,
{ code_verifier: req.session.code_verifier }
);
const claims = tokenSet.claims(); // { sub, email, tenant_uuid?, ... }
if (!state || state !== req.session.state) {
return res.status(400).send('state inválido');
}
const params = { state, code, code_verifier: req.session.code_verifier };
const tokenSet = await client.callback(`${process.env.BASE_URL}${process.env.OIDC_REDIRECT_PATH}`, params, { state });
const email = (claims.email || '').toLowerCase();
const sub = claims.sub;
const tenantUuid = (claims.tenant_uuid || await lookupTenantByEmail(email))?.replace(/-/g, '');
// Guarda lo que necesites para pruebas (id_token y claims)
req.session.user = tokenSet.claims();
req.session.id_token = tokenSet.id_token;
req.session.access_token = tokenSet.access_token;
if (!tenantUuid) {
return res.status(403).send('No se pudo determinar el tenant del usuario.');
}
// Redirigí a donde quieras (página de bienvenida)
res.redirect('/auth/me');
// Asegurar presencia del usuario en tu DB y enlazar el sub de OIDC
const { rows } = await poolMeta.query(
`SELECT id, ak_sub FROM app_user WHERE email=$1 AND tenant_uuid=$2`,
[email, tenantUuid]
);
let userId;
if (rows.length) {
userId = rows[0].id;
if (!rows[0].ak_sub) {
await poolMeta.query(`UPDATE app_user SET ak_sub=$1 WHERE id=$2`, [sub, userId]);
}
} else {
// “just in time” create (opcional): lo das de alta si no existe aún en tu app
const ins = await poolMeta.query(
`INSERT INTO app_user (email, tenant_uuid, ak_sub, role)
VALUES ($1,$2,$3,'staff') RETURNING id`,
[email, tenantUuid, sub]
);
userId = ins.rows[0].id;
}
// Sesión de aplicación (lo que el resto del backend necesita)
req.session.user = { id: userId, email, tenant_uuid: tenantUuid, sub };
req.session.regenerate(err => {
if (err) return next(err);
req.session.user = { id: userId, email, tenant_uuid: tenantUuid, sub };
req.session.save(err2 => {
if (err2) return next(err2);
return res.redirect('/');
});
});
// redirige a la app (home o dashboard)
res.redirect('/');
} catch (e) { next(e); }
});
// (Opcional) logout “local”
app.post('/auth/logout', (req, res) => {
req.session.destroy(() => res.clearCookie('sc.sid').status(204).end());
});
// --- ver quién soy (para probar)

View File

@ -0,0 +1,25 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"/>
<title>Iniciar sesión | SuiteCoffee</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/css/main.css"/>
</head>
<body class="container">
<header class="my-4">
<h1>SuiteCoffee — Acceso</h1>
</header>
<% if (user) { %>
<p>Ya iniciaste sesión como <strong><%= user.email %></strong>.</p>
<p>Continuar a <a href="/">la aplicación</a></p>
<% } else { %>
<div class="card p-4">
<p>Usamos inicio de sesión único (SSO) con nuestro Identity Provider.</p>
<!-- Esta URL dispara el flujo OIDC hacia Authentik -->
<a class="btn btn-primary btn-lg" href="/auth/login">Iniciar sesión con SuiteCoffee SSO</a>
</div>
<% } %>
</body>
</html>