Importación de feature/registration

This commit is contained in:
Mateo Saldain 2025-09-05 00:45:16 +00:00
parent 25876e733b
commit cbcea72848
5 changed files with 178 additions and 14 deletions

View File

@ -1,6 +1,6 @@
# SuiteCoffee — Sistema de gestión para cafeterías (Dockerizado y multiservicio)
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 multitenencia, 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 multitenencia, 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

View File

@ -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:

View File

@ -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

View File

@ -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": ""

View File

@ -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}`);