diff --git a/.env.example b/.env.example index 93d0213..4b2969b 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,12 @@ POSTGRES_PASSWORD=changeme # Sessions SESSION_SECRET=change-me-to-a-long-random-string -# Authentik OIDC +# Set to true to test locally without a real Authentik instance: +# /auth/login shows a simple form to pick a test name instead of redirecting to OIDC. +# NEVER set this to true outside local development. +DEV_BYPASS_AUTH=false + +# Authentik OIDC (not needed if DEV_BYPASS_AUTH=true) AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/octane-website/ OIDC_CLIENT_ID= OIDC_CLIENT_SECRET= diff --git a/README.md b/README.md index 5787a34..2d80bfc 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,32 @@ docker compose up --build Les migrations SQL (`src/db/migrations/*.sql`) sont exécutées automatiquement au démarrage du conteneur `app`, de façon idempotente (une table `schema_migrations` garde la trace des fichiers déjà appliqués). -## Développement local (sans Docker) +## Tester en local sans Authentik (ex: dans WSL) + +Pas besoin d'avoir Authentik pour essayer l'application en premier lieu. Un mode `DEV_BYPASS_AUTH` remplace la redirection OIDC par un simple formulaire "choisissez un nom" — **à n'utiliser qu'en local, jamais en production**. + +```bash +cp .env.example .env +``` + +Dans `.env`, mettre : + +``` +DEV_BYPASS_AUTH=true +DATABASE_URL=postgres://octane:changeme@postgres:5432/octane +POSTGRES_PASSWORD=changeme +SESSION_SECRET=une-longue-chaine-aleatoire +``` + +(Les variables `AUTHENTIK_*` / `OIDC_*` peuvent rester vides tant que `DEV_BYPASS_AUTH=true`.) + +Puis, avec Docker Compose (fichier séparé `docker-compose.dev.yml`, sans dépendance au réseau Authentik) : + +```bash +docker compose -f docker-compose.dev.yml up --build +``` + +Ou sans Docker du tout, avec un Postgres local : ```bash npm install @@ -163,6 +188,10 @@ npm run migrate npm start ``` +Ouvrir `http://localhost:3000` : vous serez redirigé vers `/auth/login`, qui affiche un formulaire pour choisir un nom (et cocher "Compte admin" si besoin) au lieu de passer par Authentik. Chaque nom saisi crée un utilisateur distinct et persistant en base — pratique pour tester le vote sur les suggestions avec plusieurs "personnes" (ouvrez un autre navigateur ou une fenêtre de navigation privée pour vous connecter sous un second nom). + +Une fois satisfait, repassez `DEV_BYPASS_AUTH=false` et configurez les variables `AUTHENTIK_*`/`OIDC_*` avant de déployer avec `docker-compose.yml` (celui avec le réseau Authentik). + ## Structure du projet ``` diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..e364c60 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,25 @@ +# Standalone compose file for local testing (e.g. in WSL) without a real Authentik instance. +# Use with DEV_BYPASS_AUTH=true in .env. Run with: +# docker compose -f docker-compose.dev.yml up --build +services: + app: + build: . + restart: unless-stopped + env_file: .env + ports: + - "${APP_PORT:-3000}:3000" + depends_on: + - postgres + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: octane + POSTGRES_USER: octane + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - pgdata_dev:/var/lib/postgresql/data + +volumes: + pgdata_dev: diff --git a/src/auth/routes.js b/src/auth/routes.js index bbf2dad..78f5473 100644 --- a/src/auth/routes.js +++ b/src/auth/routes.js @@ -6,9 +6,63 @@ const asyncHandler = require('../lib/asyncHandler'); const router = express.Router(); +function escapeHtml(str) { + return String(str ?? '').replace(/[&<>"']/g, (c) => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[c])); +} + +function establishSession(req, res, user, returnTo) { + req.session.regenerate((err) => { + if (err) throw err; + req.session.userId = user.id; + req.session.save((saveErr) => { + if (saveErr) throw saveErr; + res.redirect(returnTo || '/'); + }); + }); +} + +// Dev-only login: no Authentik involved, lets you pick a name/admin flag +// to test the app locally. Only active when DEV_BYPASS_AUTH=true. +async function devLogin(req, res) { + const { name, admin, returnTo } = req.query; + + if (!name) { + res.set('Content-Type', 'text/html'); + return res.send(` + +
Authentik n'est pas connecté (DEV_BYPASS_AUTH=true). Choisissez une identité de test.
+ + +`); + } + + const user = await usersRepo.upsertFromClaims({ + sub: `dev:${name}`, + name, + email: `${String(name).toLowerCase().replace(/\s+/g, '.')}@dev.local`, + isAdmin: admin === '1' || admin === 'true', + }); + + establishSession(req, res, user, returnTo); +} + router.get( '/login', asyncHandler(async (req, res) => { + if (config.devBypassAuth) { + return devLogin(req, res); + } + const oidcConfig = getOidcConfig(); const codeVerifier = client.randomPKCECodeVerifier(); const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); @@ -36,6 +90,10 @@ router.get( router.get( '/callback', asyncHandler(async (req, res) => { + if (config.devBypassAuth) { + return res.redirect('/auth/login'); + } + const oidcConfig = getOidcConfig(); const pending = req.session.oidc; if (!pending) { @@ -68,14 +126,7 @@ router.get( delete req.session.oidc; delete req.session.returnTo; - req.session.regenerate((err) => { - if (err) throw err; - req.session.userId = user.id; - req.session.save((saveErr) => { - if (saveErr) throw saveErr; - res.redirect(returnTo); - }); - }); + establishSession(req, res, user, returnTo); }) ); diff --git a/src/config.js b/src/config.js index 65517c9..a2ee3cc 100644 --- a/src/config.js +++ b/src/config.js @@ -1,5 +1,7 @@ require('dotenv').config(); +const devBypassAuth = process.env.DEV_BYPASS_AUTH === 'true'; + function required(name) { const value = process.env[name]; if (!value && process.env.NODE_ENV !== 'test') { @@ -13,9 +15,13 @@ module.exports = { port: parseInt(process.env.PORT, 10) || 3000, databaseUrl: required('DATABASE_URL'), sessionSecret: required('SESSION_SECRET'), - authentikIssuerUrl: required('AUTHENTIK_ISSUER_URL'), - oidcClientId: required('OIDC_CLIENT_ID'), - oidcClientSecret: required('OIDC_CLIENT_SECRET'), - oidcRedirectUri: required('OIDC_REDIRECT_URI'), + // DEV_BYPASS_AUTH lets you run the app locally without a real Authentik + // instance: /auth/login prompts for a name instead of redirecting to OIDC. + // Never enable this outside local development. + devBypassAuth, + authentikIssuerUrl: devBypassAuth ? process.env.AUTHENTIK_ISSUER_URL : required('AUTHENTIK_ISSUER_URL'), + oidcClientId: devBypassAuth ? process.env.OIDC_CLIENT_ID : required('OIDC_CLIENT_ID'), + oidcClientSecret: devBypassAuth ? process.env.OIDC_CLIENT_SECRET : required('OIDC_CLIENT_SECRET'), + oidcRedirectUri: devBypassAuth ? process.env.OIDC_REDIRECT_URI : required('OIDC_REDIRECT_URI'), adminGroupName: process.env.ADMIN_GROUP_NAME || 'octane-admins', }; diff --git a/src/server.js b/src/server.js index 3bd7005..ea0b289 100644 --- a/src/server.js +++ b/src/server.js @@ -5,7 +5,11 @@ const { runMigrations } = require('./db/migrate'); async function main() { await runMigrations(); - await initOidc(); + if (config.devBypassAuth) { + console.warn('DEV_BYPASS_AUTH is enabled — Authentik/OIDC is skipped. Do not use this in production.'); + } else { + await initOidc(); + } const app = createApp(); app.listen(config.port, () => {