From cbcea728484124004c37a8abfd4eabf9a08c48a7 Mon Sep 17 00:00:00 2001 From: msaldain Date: Fri, 5 Sep 2025 00:45:16 +0000 Subject: [PATCH] =?UTF-8?q?Importaci=C3=B3n=20de=20feature/registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- compose.dev.yaml | 87 +++++++++++++++++++++++++++++++++++++- compose.yaml | 22 +++++----- services/auth/package.json | 4 +- services/auth/src/index.js | 77 +++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7ea441f..ec2c050 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SuiteCoffee — Sistema de gestión para cafeterías (Dockerizado y multi‑servicio) -SuiteCoffee es un sistema modular pensado para la **gestión de cafeterías** (y negocios afines), con servicios Node.js para **aplicación** y **autenticación**, bases de datos **PostgreSQL** separadas para negocio y multi‑tenencia, y un **stack Docker Compose** que facilita levantar entornos de **desarrollo** y **producción**. Incluye herramientas auxiliares como **Nginx Proxy Manager (NPM)** y **CloudBeaver** para administrar bases de datos desde el navegador. +SuiteCoffee es un sistema modular pensado para la **gestión de cafeterías** (y negocios afines), con servicios Node.js para **aplicación** y Authentik **autenticación**, bases de datos **PostgreSQL** separadas para negocio y multi‑tenencia, y un **stack Docker Compose** que facilita levantar entornos de **desarrollo** y **producción**. Incluye herramientas auxiliares como **Nginx Proxy Manager (NPM)** y **CloudBeaver** para administrar bases de datos desde el navegador. > Repositorio: https://gitea.mateosaldain.uy/msaldain/SuiteCoffee.git diff --git a/compose.dev.yaml b/compose.dev.yaml index 9b6aee1..5129083 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -63,10 +63,95 @@ services: networks: net: aliases: [dev-tenants] - + + ################# + ### Authentik ### + ################# + # --- 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 } + environment: + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_DEBUG: "false" + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__USER: authentik + AUTHENTIK_POSTGRESQL__NAME: authentik + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASS} + AUTHENTIK_REDIS__HOST: authentik-redis + # 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: + - "127.0.0.1:9000:9000" # SOLO localhost + + # --- Authentik Worker + authentik-worker: + image: ghcr.io/goauthentik/server:latest + command: worker + depends_on: + authentik-db: { condition: service_healthy } + authentik-redis: { condition: service_started } + environment: + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__USER: authentik + AUTHENTIK_POSTGRESQL__NAME: authentik + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASS} + AUTHENTIK_REDIS__HOST: authentik-redis + profiles: ["ak-ui"] + networks: { + net: { + + } + } + restart: unless-stopped + volumes: tenants-db: suitecoffee-db: + authentik-db: networks: net: diff --git a/compose.yaml b/compose.yaml index 4d658b1..bfd0e00 100644 --- a/compose.yaml +++ b/compose.yaml @@ -31,17 +31,17 @@ services: # start_period: 20s # restart: unless-stopped - # auth: - # depends_on: - # db: - # condition: service_healthy - # healthcheck: - # test: ["CMD-SHELL", "curl -fsS http://localhost:${AUTH_DOCKER_PORT}/health || exit 1"] - # interval: 10s - # timeout: 3s - # retries: 10 - # start_period: 15s - # restart: unless-stopped + auth: + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:${AUTH_DOCKER_PORT}/health || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + start_period: 15s + restart: unless-stopped db: image: postgres:16 diff --git a/services/auth/package.json b/services/auth/package.json index 3ffd613..1604129 100644 --- a/services/auth/package.json +++ b/services/auth/package.json @@ -22,7 +22,9 @@ "express": "^5.1.0", "express-ejs-layouts": "^2.5.1", "pg": "^8.16.3", - "pg-format": "^1.0.4" + "pg-format": "^1.0.4", + "openid-client": "^5.6.5", + "cookie-session": "^2.0.0" }, "keywords": [], "description": "" diff --git a/services/auth/src/index.js b/services/auth/src/index.js index a5479f6..9a75308 100644 --- a/services/auth/src/index.js +++ b/services/auth/src/index.js @@ -6,6 +6,9 @@ import cors from 'cors'; import { Pool } from 'pg'; import bcrypt from'bcrypt'; +import { Issuer, generators } from 'openid-client'; +import cookieSession from 'cookie-session'; + // Rutas import path from 'path'; import { fileURLToPath } from 'url'; @@ -39,6 +42,14 @@ app.set('trust proxy', true); app.use(express.static(path.join(__dirname, 'pages'))); +app.use(cookieSession({ + name: 'sid', + secret: process.env.SESSION_SECRET, + httpOnly: true, + sameSite: 'lax', + secure: false // en prod detrás de https: true +})); + // Configuración de conexión PostgreSQL const dbConfig = { @@ -65,6 +76,20 @@ 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`); + 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'] + }); + return oidcClient; +} // === Servir páginas estáticas === @@ -178,6 +203,58 @@ app.post('/api/login', async (req, res) => { }); +// --- login: redirige a Authentik con PKCE +app.get('/auth/login', async (req, res) => { + const client = await getClient(); + const state = generators.state(); + const code_verifier = generators.codeVerifier(); + const code_challenge = generators.codeChallenge(code_verifier); + + req.session.state = state; + req.session.code_verifier = code_verifier; + + const authUrl = client.authorizationUrl({ + scope: 'openid profile email', + state, + code_challenge, + code_challenge_method: 'S256' + }); + + res.redirect(authUrl); +}); + +// --- 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; + + 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 }); + + // 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; + + // Redirigí a donde quieras (página de bienvenida) + res.redirect('/auth/me'); +}); + +// --- ver quién soy (para probar) +app.get('/auth/me', (req, res) => { + if (!req.session?.user) return res.status(401).json({ error: 'no autenticado' }); + res.json({ user: req.session.user }); +}); + +// --- logout simple (borra cookie) +app.post('/auth/logout', (req, res) => { + req.session = null; + res.status(204).end(); +}); + // Colores personalizados let primaryColor = chalk.hex('#'+`${process.env.COL_PRI}`); let secondaryColor = chalk.hex('#'+`${process.env.COL_SEC}`);