From d940ccced85f09172338f5a4c95a3fc5309bf81f Mon Sep 17 00:00:00 2001 From: Alexis Bruneteau Date: Mon, 2 Jun 2025 16:40:29 +0200 Subject: [PATCH] init --- apache/site.conf | 18 ++++++ docker-compose.yml | 51 +++++++++++++++ keyclock-setup.sh | 151 +++++++++++++++++++++++++++++++++++++++++++++ private/Dockerfile | 9 +++ private/app.py | 83 +++++++++++++++++++++++++ public/Dockerfile | 9 +++ public/app.py | 47 ++++++++++++++ 7 files changed, 368 insertions(+) create mode 100644 apache/site.conf create mode 100644 docker-compose.yml create mode 100755 keyclock-setup.sh create mode 100644 private/Dockerfile create mode 100644 private/app.py create mode 100644 public/Dockerfile create mode 100644 public/app.py diff --git a/apache/site.conf b/apache/site.conf new file mode 100644 index 0000000..cdee7c7 --- /dev/null +++ b/apache/site.conf @@ -0,0 +1,18 @@ + + ServerName app.local + + SSLEngine on + SSLCertificateFile "/etc/apache2/ssl/cert.pem" + SSLCertificateKeyFile "/etc/apache2/ssl/key.pem" + + ProxyPreserveHost On + + # API Publique + ProxyPass /api/pub/ http://localhost:5001/ + ProxyPassReverse /api/pub/ http://localhost:5001/ + + # API Utilisateur + ProxyPass /api/user/ http://localhost:5002/ + ProxyPassReverse /api/user/ http://localhost:5002/ + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dd76bfc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +version: '3.8' + +services: + keycloak-db: + image: postgres:15 + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + volumes: + - keycloak-db:/var/lib/postgresql/data + + keycloak: + image: quay.io/keycloak/keycloak:24.0.3 + command: start-dev + environment: + KC_DB: postgres + KC_DB_URL_HOST: keycloak-db + KC_DB_URL_DATABASE: keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: password + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + ports: + - "8080:8080" + depends_on: + - keycloak-db + + public_api: + build: + context: ./public + volumes: + - ./db.sqlite:/app/db.sqlite + ports: + - "5001:5001" + depends_on: + - keycloak + + user_api: + build: + context: ./private + volumes: + - ./db.sqlite:/app/db.sqlite + ports: + - "5002:5002" + depends_on: + - keycloak + +volumes: + keycloak-db: + diff --git a/keyclock-setup.sh b/keyclock-setup.sh new file mode 100755 index 0000000..54767d7 --- /dev/null +++ b/keyclock-setup.sh @@ -0,0 +1,151 @@ +#!/bin/bash + +# Variables +KC_HOST="http://localhost:8080" +REALM="myrealm" +CLIENT_ID="myclient" +CLIENT_SECRET="mysecret" +USERNAME="alexis" +PASSWORD="password" + +# Fonction d'attente +wait_for_keycloak() { + echo "⏳ Attente de Keycloak..." + until curl -s "$KC_HOST" > /dev/null; do + sleep 2 + done + echo "✅ Keycloak est prêt." +} + +# Obtenir un token admin +get_admin_token() { + curl -s -X POST "$KC_HOST/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin" \ + -d "password=admin" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" | + jq -r .access_token +} + +# Créer un realm, client et utilisateur +setup_keycloak() { + TOKEN=$(get_admin_token) + + echo "🛠️ Création du realm $REALM..." + curl -s -X POST "$KC_HOST/admin/realms" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"realm\":\"$REALM\",\"enabled\":true}" > /dev/null + + echo "🛠️ Création du client $CLIENT_ID..." + curl -s -X POST "$KC_HOST/admin/realms/$REALM/clients" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"clientId\": \"$CLIENT_ID\", + \"enabled\": true, + \"publicClient\": false, + \"secret\": \"$CLIENT_SECRET\", + \"redirectUris\": [\"*\"], + \"standardFlowEnabled\": true + }" > /dev/null + + echo "👤 Création de l'utilisateur $USERNAME..." + curl -s -X POST "$KC_HOST/admin/realms/$REALM/users" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"$USERNAME\", + \"enabled\": true, + \"credentials\": [{ + \"type\": \"password\", + \"value\": \"$PASSWORD\", + \"temporary\": false + }] + }" > /dev/null + + echo "✅ Configuration terminée !" + echo "🔐 Utilisateur: $USERNAME / $PASSWORD" + echo "🪪 Client secret: $CLIENT_SECRET" +} + +# Lancer le setup +wait_for_keycloak +setup_keycloak +#!/bin/bash + +# Variables +KC_HOST="http://localhost:8080" +REALM="myrealm" +CLIENT_ID="myclient" +CLIENT_SECRET="mysecret" +USERNAME="alexis" +PASSWORD="password" + +# Fonction d'attente +wait_for_keycloak() { + echo "⏳ Attente de Keycloak..." + until curl -s "$KC_HOST" > /dev/null; do + sleep 2 + done + echo "✅ Keycloak est prêt." +} + +# Obtenir un token admin +get_admin_token() { + curl -s -X POST "$KC_HOST/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin" \ + -d "password=admin" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" | + jq -r .access_token +} + +# Créer un realm, client et utilisateur +setup_keycloak() { + TOKEN=$(get_admin_token) + + echo "🛠️ Création du realm $REALM..." + curl -s -X POST "$KC_HOST/admin/realms" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"realm\":\"$REALM\",\"enabled\":true}" > /dev/null + + echo "🛠️ Création du client $CLIENT_ID..." + curl -s -X POST "$KC_HOST/admin/realms/$REALM/clients" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"clientId\": \"$CLIENT_ID\", + \"enabled\": true, + \"publicClient\": false, + \"secret\": \"$CLIENT_SECRET\", + \"redirectUris\": [\"*\"], + \"standardFlowEnabled\": true + }" > /dev/null + + echo "👤 Création de l'utilisateur $USERNAME..." + curl -s -X POST "$KC_HOST/admin/realms/$REALM/users" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"$USERNAME\", + \"enabled\": true, + \"credentials\": [{ + \"type\": \"password\", + \"value\": \"$PASSWORD\", + \"temporary\": false + }] + }" > /dev/null + + echo "✅ Configuration terminée !" + echo "🔐 Utilisateur: $USERNAME / $PASSWORD" + echo "🪪 Client secret: $CLIENT_SECRET" +} + +# Lancer le setup +wait_for_keycloak +setup_keycloak + diff --git a/private/Dockerfile b/private/Dockerfile new file mode 100644 index 0000000..27e4f76 --- /dev/null +++ b/private/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY . . + +RUN pip install flask flask_sqlalchemy pyjwt requests + +CMD ["python", "app.py"] + diff --git a/private/app.py b/private/app.py new file mode 100644 index 0000000..b7d1d5c --- /dev/null +++ b/private/app.py @@ -0,0 +1,83 @@ +--- user_api/app.py --- +from flask import Flask, jsonify, request, abort +from flask_sqlalchemy import SQLAlchemy +import requests +import jwt + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../db.sqlite' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) + +# Configuration Keycloak +KEYCLOAK_URL = "http://localhost:8080" +REALM = "myrealm" +CLIENT_ID = "myclient" + +class Visite(db.Model): + id = db.Column(db.Integer, primary_key=True) + galerie_id = db.Column(db.Integer, nullable=False) + +class Critique(db.Model): + id = db.Column(db.Integer, primary_key=True) + oeuvre_id = db.Column(db.Integer, nullable=False) + texte = db.Column(db.Text, nullable=False) + username = db.Column(db.String(100), nullable=False) + +def verify_token(auth_header): + if not auth_header.startswith("Bearer "): + return None + token = auth_header.split(" ")[1] + try: + jwks_url = f"{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/certs" + jwks = requests.get(jwks_url).json() + public_keys = {k['kid']: jwt.algorithms.RSAAlgorithm.from_jwk(k) for k in jwks['keys']} + unverified = jwt.decode(token, options={"verify_signature": False}) + kid = unverified['kid'] + decoded = jwt.decode(token, key=public_keys[kid], algorithms=['RS256'], audience=CLIENT_ID) + return decoded['preferred_username'] + except Exception as e: + print(f"JWT verification failed: {e}") + return None + +@app.before_request +def authenticate(): + auth = request.headers.get("Authorization", "") + user = verify_token(auth) + if not user: + return jsonify({"error": "Unauthorized"}), 403 + request.user = user + +@app.route("/", methods=["GET"]) +def index(): + return f"User API - Authenticated as {request.user}", 200 + +@app.route("/galerie//entrer", methods=["POST"]) +def entrer_galerie(galerie_id): + visite = Visite(galerie_id=galerie_id) + db.session.add(visite) + db.session.commit() + return jsonify({"message": "Entré dans la galerie"}), 201 + +@app.route("/galerie//sortir", methods=["POST"]) +def sortir_galerie(galerie_id): + Visite.query.filter_by(galerie_id=galerie_id).delete() + db.session.commit() + return jsonify({"message": "Sorti de la galerie"}), 200 + +@app.route("/oeuvre//critiquer", methods=["POST"]) +def critiquer_oeuvre(oeuvre_id): + data = request.get_json() + if not data or not data.get("texte"): + abort(400) + critique = Critique(oeuvre_id=oeuvre_id, texte=data["texte"], username=request.user) + db.session.add(critique) + db.session.commit() + return jsonify({"message": "Critique ajoutée"}), 201 + +if __name__ == "__main__": + with app.app_context(): + db.create_all() + app.run(port=5002) + diff --git a/public/Dockerfile b/public/Dockerfile new file mode 100644 index 0000000..27e4f76 --- /dev/null +++ b/public/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY . . + +RUN pip install flask flask_sqlalchemy pyjwt requests + +CMD ["python", "app.py"] + diff --git a/public/app.py b/public/app.py new file mode 100644 index 0000000..bb1b59d --- /dev/null +++ b/public/app.py @@ -0,0 +1,47 @@ +--- public_api/app.py --- +from flask import Flask, jsonify +from flask_sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../db.sqlite' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) + +class Artiste(db.Model): + id = db.Column(db.Integer, primary_key=True) + nom = db.Column(db.String(100), nullable=False) + +class Galerie(db.Model): + id = db.Column(db.Integer, primary_key=True) + nom = db.Column(db.String(100), nullable=False) + +class Oeuvre(db.Model): + id = db.Column(db.Integer, primary_key=True) + titre = db.Column(db.String(200), nullable=False) + exposee = db.Column(db.Boolean, default=False) + +@app.route("/", methods=["GET"]) +def index(): + return "Public API", 200 + +@app.route("/artistes", methods=["GET"]) +def get_artistes(): + artistes = Artiste.query.all() + return jsonify([{"id": a.id, "nom": a.nom} for a in artistes]), 200 + +@app.route("/galeries", methods=["GET"]) +def get_galeries(): + galeries = Galerie.query.all() + return jsonify([{"id": g.id, "nom": g.nom} for g in galeries]), 200 + +@app.route("/oeuvres", methods=["GET"]) +def get_oeuvres(): + oeuvres = Oeuvre.query.filter_by(exposee=True).all() + return jsonify([{"id": o.id, "titre": o.titre} for o in oeuvres]), 200 + +if __name__ == "__main__": + with app.app_context(): + db.create_all() + app.run(port=5001) +