This commit is contained in:
Alexis Bruneteau 2025-06-28 12:30:33 +02:00
parent fc9431db44
commit 907c476567
7 changed files with 823 additions and 236 deletions

View File

@ -56,24 +56,29 @@ Listen 443
# OIDC config - point to Keycloak via auth.local # OIDC config - point to Keycloak via auth.local
OIDCProviderMetadataURL https://auth.local/realms/master/.well-known/openid-configuration OIDCProviderMetadataURL https://auth.local/realms/master/.well-known/openid-configuration
OIDCClientID SOA OIDCClientID soa
OIDCRedirectURI https://api.local/api/redirect OIDCRedirectURI https://api.local/api/private/redirect
OIDCClientSecret NuLgdHzPldRauqIln0I0TN5216PgX3Ty OIDCClientSecret mysecret
OIDCCryptoPassphrase fdfd8280-13b5-11f0-a320-080027e6dc53 OIDCCryptoPassphrase fdfd8280-13b5-11f0-a320-080027e6dc53
OIDCPassClaimsAs both OIDCPassClaimsAs headers
OIDCClaimPrefix OIDC-
OIDCPassUserInfoAs claims
OIDCRemoteUserClaim email OIDCRemoteUserClaim email
OIDCScope "openid email profile"
OIDCSessionInactivityTimeout 86400 OIDCSessionInactivityTimeout 86400
OIDCSSLValidateServer Off OIDCSSLValidateServer Off
# Proxy public API (no auth) # Proxy public API (no auth)
ProxyPass /public/ http://public_api:5001/ ProxyPass /api/public http://public_api:5001/
ProxyPassReverse /public/ http://public_api:5001/ ProxyPassReverse /api/public http://public_api:5001/
# Proxy private API (OIDC protected) # Proxy private API (OIDC protected)
ProxyPass /api/ http://user_api:5002/ ProxyPass /api/private http://private_api:5002/api/private
ProxyPassReverse /api/ http://user_api:5002/ ProxyPassReverse /api/private http://private_api:5002/api/private
<Location /api> <Location /api/private>
AuthType openid-connect AuthType openid-connect
Require valid-user Require valid-user
RequestHeader set X-User-Email "%{HTTP_OIDC_EMAIL}i"
RequestHeader set X-User-Name "%{HTTP_OIDC_PREFERRED_USERNAME}i"
</Location> </Location>
</VirtualHost> </VirtualHost>

View File

@ -44,7 +44,7 @@ services:
networks: networks:
- soa - soa
user_api: private_api:
build: build:
context: ./private context: ./private
depends_on: depends_on:
@ -65,7 +65,7 @@ services:
depends_on: depends_on:
- keycloak - keycloak
- public_api - public_api
- user_api - private_api
volumes: volumes:
- ./apache/logs:/usr/local/apache2/conf/logs - ./apache/logs:/usr/local/apache2/conf/logs
environment: environment:

View File

@ -2,8 +2,8 @@
# Variables # Variables
KC_HOST="http://localhost:8080" KC_HOST="http://localhost:8080"
REALM="myrealm" REALM="master"
CLIENT_ID="myclient" CLIENT_ID="soa"
CLIENT_SECRET="mysecret" CLIENT_SECRET="mysecret"
USERNAME="alexis" USERNAME="alexis"
PASSWORD="password" PASSWORD="password"
@ -73,79 +73,3 @@ setup_keycloak() {
# Lancer le setup # Lancer le setup
wait_for_keycloak wait_for_keycloak
setup_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

View File

@ -3,7 +3,7 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN pip install flask flask_sqlalchemy pyjwt requests pymysql cryptography redis RUN pip install flask flask_sqlalchemy pyjwt requests pymysql cryptography redis python-jose
CMD ["python", "app.py"] CMD ["python", "app.py"]

View File

@ -1,11 +1,13 @@
from flask import Flask, jsonify, request, abort from flask import Flask, request, jsonify, g, abort
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.exc import IntegrityError
from jose import jwt, JWTError
import requests import requests
import jwt
import time import time
import pymysql import pymysql
import redis import redis
import json import json
from functools import wraps
app = Flask(__name__) app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://myuser:mypassword@mysql:3306/mydb' app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://myuser:mypassword@mysql:3306/mydb'
@ -13,6 +15,7 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app) db = SQLAlchemy(app)
MYSQL_HOST = "mysql" MYSQL_HOST = "mysql"
MYSQL_PORT = 3306 MYSQL_PORT = 3306
MYSQL_USER = "myuser" MYSQL_USER = "myuser"
@ -38,81 +41,821 @@ while True:
print("Waiting for MySQL...", e) print("Waiting for MySQL...", e)
time.sleep(2) time.sleep(2)
class Visite(db.Model): print('Creating DB')
id = db.Column(db.Integer, primary_key=True)
galerie_id = db.Column(db.Integer, nullable=False)
class Critique(db.Model): # Keycloak config
id = db.Column(db.Integer, primary_key=True) KEYCLOAK_REALM = "master"
oeuvre_id = db.Column(db.Integer, nullable=False) KEYCLOAK_URL = "http://keycloak:8080"
texte = db.Column(db.Text, nullable=False) CLIENT_ID = "soa"
username = db.Column(db.String(100), nullable=False)
ISSUER = f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}"
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"
for _ in range(30):
try:
r = requests.get("http://keycloak:8080/realms/master/.well-known/openid-configuration")
if r.status_code == 200:
break
except Exception:
pass
time.sleep(2)
else:
raise Exception("Keycloak is not available after waiting")
jwks = requests.get(JWKS_URL).json()["keys"]
@app.route("/", methods=["GET"])
def index():
return f"User API - Authenticated as {request.user}", 200
@app.route("/galerie/<int:galerie_id>/entrer", methods=["POST"]) def get_signing_key(token):
def entrer_galerie(galerie_id): unverified_header = jwt.get_unverified_header(token)
visite = Visite(galerie_id=galerie_id) kid = unverified_header.get("kid")
db.session.add(visite) for key in jwks:
if key["kid"] == kid:
return key
raise Exception("Public key not found.")
# Decorator for OIDC protection
def oidc_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
# Get user info from Apache headers
user_email = request.headers.get("OIDC_email")
username = request.headers.get("OIDC_user") or user_email
if not user_email or not username:
return jsonify({"error": "Not authenticated"}), 401
# Find or create user in DB
user = User.query.filter_by(email=user_email).first()
if not user:
user = User(
username=username,
email=user_email,
alias=username,
)
db.session.add(user)
db.session.commit() db.session.commit()
return jsonify({"message": "Entré dans la galerie"}), 201
@app.route("/galerie/<int:galerie_id>/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("/oeuvres", methods=["POST"])
def create_oeuvre():
data = flask.request.get_json()
titre = data.get("titre")
if not titre:
return {"error": "Titre requis"}, 400
oeuvre = Oeuvre(titre=titre, exposee=True)
db.session.add(oeuvre)
db.session.commit()
# Publier l'événement
event = { event = {
"type": "oeuvre_created", "type": "user_created",
"data": {"id": oeuvre.id, "titre": oeuvre.titre} "data": {"id": user.id, "alias": user.alias}
} }
redis_client.publish('events', json.dumps(event)) redis_client.publish('events', json.dumps(event))
return {"id": oeuvre.id, "titre": oeuvre.titre}, 201 g.db_user = user
@app.route("/oeuvre/<int:oeuvre_id>/critiquer", methods=["POST"]) return f(*args, **kwargs)
def critiquer_oeuvre(oeuvre_id): return wrapper
data = request.get_json() @app.route("/api/private/debug-headers")
if not data or not data.get("texte"): def debug_headers():
return jsonify(dict(request.headers))
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(255), unique=True, nullable=False)
alias = db.Column(db.String(255), nullable=False)
first_name = db.Column(db.String(100))
last_name = db.Column(db.String(100))
bio = db.Column(db.Text)
profile_picture_url = db.Column(db.String(255))
created_at = db.Column(db.DateTime, server_default=db.func.now())
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
class Gallery(db.Model):
__tablename__ = "galleries"
id = db.Column(db.Integer, primary_key=True)
owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
title = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
is_public = db.Column(db.Boolean, default=False)
publication_date = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, server_default=db.func.now())
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
class Artwork(db.Model):
__tablename__ = "artworks"
id = db.Column(db.Integer, primary_key=True)
gallery_id = db.Column(db.Integer, db.ForeignKey('galleries.id'), nullable=False)
creator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
title = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
image_url = db.Column(db.String(255), nullable=False)
medium = db.Column(db.String(100))
dimensions = db.Column(db.String(50))
creation_year = db.Column(db.Integer)
price = db.Column(db.Numeric(10, 2))
is_visible = db.Column(db.Boolean, default=True)
is_for_sale = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, server_default=db.func.now())
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
class GalleryMember(db.Model):
__tablename__ = "gallery_members"
gallery_id = db.Column(db.Integer, db.ForeignKey('galleries.id'), primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
role = db.Column(db.String(50), nullable=False, default='viewer')
status = db.Column(db.String(50), nullable=False, default='pending')
invited_at = db.Column(db.DateTime, server_default=db.func.now())
entered_at = db.Column(db.DateTime)
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
class ArtworkReview(db.Model):
__tablename__ = "artwork_reviews"
id = db.Column(db.Integer, primary_key=True)
artwork_id = db.Column(db.Integer, db.ForeignKey('artworks.id'), nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
grade = db.Column(db.Integer)
description = db.Column(db.Text)
parent_ar_id = db.Column(db.Integer, db.ForeignKey('artwork_reviews.id'))
created_at = db.Column(db.DateTime, server_default=db.func.now())
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
__table_args__ = (db.CheckConstraint('grade >= 0 AND grade <= 5', name='check_grade_range_artwork'),)
class GalleryReview(db.Model):
__tablename__ = "gallery_reviews"
id = db.Column(db.Integer, primary_key=True)
gallery_id = db.Column(db.Integer, db.ForeignKey('galleries.id'), nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
grade = db.Column(db.Integer)
description = db.Column(db.Text)
parent_gr_id = db.Column(db.Integer, db.ForeignKey('gallery_reviews.id'))
created_at = db.Column(db.DateTime, server_default=db.func.now())
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
__table_args__ = (db.CheckConstraint('grade >= 0 AND grade <= 5', name='check_grade_range_gallery'),)
with app.app_context():
db.create_all() # Ensure all tables are created if they do not exist
@app.route("/api/private/redirect")
def oidc_redirect():
code = request.args.get("code")
if not code:
return "Missing code", 400
# Exchange code for tokens
token_url = "https://auth.local/realms/master/protocol/openid-connect/token"
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": "https://api.local/api/private/redirect",
"client_id": "soa",
"client_secret": "mysecret"
}
resp = requests.post(token_url, data=data)
if resp.status_code != 200:
return "Token exchange failed", 400
tokens = resp.json()
# Store tokens in session, or set as cookie, or return to frontend
# Example: set as cookie (not for production, just for demo)
response = redirect("/") # or wherever you want
response.set_cookie("access_token", tokens["access_token"], httponly=True, secure=True)
return response
# User profile
# Retrieve the authenticated user's profile information.
@app.route("/api/private/me", methods=["GET"])
@oidc_required
def get_me():
user = g.db_user
return jsonify({
"id": user.id,
"username": user.username,
"email": user.email,
"alias": user.alias,
"first_name": user.first_name,
"last_name": user.last_name,
"bio": user.bio,
"profile_picture_url": user.profile_picture_url,
"created_at": user.created_at,
"updated_at": user.updated_at
})
# Update the authenticated user's editable profile fields (alias, first name, last name, bio, profile picture).
@app.route("/api/private/me", methods=["PUT"])
@oidc_required
def update_me():
data = request.json
user = g.db_user
user.alias = data.get("alias", user.alias)
user.first_name = data.get("first_name", user.first_name)
user.last_name = data.get("last_name", user.last_name)
user.bio = data.get("bio", user.bio)
user.profile_picture_url = data.get("profile_picture_url", user.profile_picture_url)
db.session.commit()
event = {
"type": "user_updated",
"data": {"id": user.id, "alias": user.alias}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"message": "Profile updated"})
# Invitations
# Send an invitation to another user to join the specified gallery.
@app.route("/api/private/gallery/<int:gallery_id>/invite", methods=["POST"])
@oidc_required
def invite_user(gallery_id):
data = request.json
invited_user_id = data.get("user_id")
role = data.get("role", "viewer")
gallery = Gallery.query.get_or_404(gallery_id)
if gallery.owner_id != g.db_user.id:
abort(403)
user = User.query.get(invited_user_id)
if not user:
abort(404)
invitation = GalleryMember(
gallery_id=gallery_id,
user_id=invited_user_id,
role=role,
status="pending"
)
try:
db.session.add(invitation)
db.session.commit()
event = {
"type": "invitation_sent",
"data": {"user_id": invitation.user_id, "gallery_id": invitation.gallery_id}
}
redis_client.publish('events', json.dumps(event))
except IntegrityError:
db.session.rollback()
return jsonify({"error": "Invitation already exists"}), 409
return jsonify({"message": "Invitation sent"}), 201
# Allow an invited user to accept or reject a gallery invitation.
@app.route("/api/private/invitations/<int:gallery_id>/respond", methods=["PUT"])
@oidc_required
def respond_invitation(gallery_id):
data = request.json
status = data.get("status")
if status not in ["accepted", "rejected"]:
abort(400) abort(400)
critique = Critique(oeuvre_id=oeuvre_id, texte=data["texte"], username=request.user) invitation = GalleryMember.query.filter_by(gallery_id=gallery_id, user_id=g.db_user.id).first_or_404()
db.session.add(critique) if invitation.status != "pending":
abort(403)
invitation.status = status
if status == "accepted":
invitation.entered_at = db.func.now()
db.session.commit() db.session.commit()
return jsonify({"message": "Critique ajoutée"}), 201
# ROUTE CREATION GALERIE
@app.route("/galeries", methods=["POST"])
def create_galerie():
data = request.get_json()
nom = data.get("nom")
if not nom:
return {"error": "Nom requis"}, 400
galerie = Galerie(nom=nom, auteur=request.user)
db.session.add(galerie)
db.session.commit()
# Publier l'événement
event = { event = {
"type": "galerie_created", "type": "invitation_answered",
"data": {"id": galerie.id, "nom": galerie.nom, "auteur": galerie.auteur} "data": {"user_id": invitation.user_id, "gallery_id": invitation.gallery_id, "answer": invitation.status}
} }
redis_client.publish('events', json.dumps(event)) redis_client.publish('events', json.dumps(event))
return {"id": galerie.id, "nom": galerie.nom, "auteur": galerie.auteur}, 201 return jsonify({"message": f"Invitation {status}"})
# List all invitations received by the authenticated user (with status).
@app.route("/api/private/invitations/received", methods=["GET"])
@oidc_required
def get_received_invitations():
invitations = GalleryMember.query.filter_by(user_id=g.db_user.id).all()
result = []
for inv in invitations:
gal = Gallery.query.get(inv.gallery_id)
own = User.query.get(gal.owner_id)
result.append({
"gallery_id": inv.gallery_id,
"gallery_title": gal.title,
"gallery_description": gal.description,
"owner": own.alias,
"role": inv.role,
"status": inv.status,
"invited_at": inv.invited_at,
"entered_at": inv.entered_at,
"updated_at": inv.updated_at
})
return jsonify(result)
# Galleries
# List all galleries accessible to the user (public, owned, or where they are a member).
@app.route("/api/private/galleries", methods=["GET"])
@oidc_required
def get_galleries():
user_id = g.db_user.id
public = Gallery.query.filter_by(is_public=True)
owned = Gallery.query.filter_by(owner_id=user_id)
member = Gallery.query.join(GalleryMember, Gallery.id==GalleryMember.gallery_id).filter(GalleryMember.user_id==user_id, GalleryMember.status=="accepted")
galleries = public.union(owned).union(member).all()
result = []
for gal in galleries:
own = User.query.get(gal.owner_id)
result.append({
"id": gal.id,
"title": gal.title,
"description": gal.description,
"owner": own.alias,
"is_public": gal.is_public,
"publication_date": gal.publication_date,
})
return jsonify(result)
# Show details of a single gallery (enforcing public or member access).
@app.route("/api/private/gallery/<int:gallery_id>", methods=["GET"])
@oidc_required
def get_gallery(gallery_id):
gal = Gallery.query.get_or_404(gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
own = User.query.get(gal.owner_id)
return jsonify({
"id": gal.id,
"title": gal.title,
"description": gal.description,
"owner": own.alias,
"is_public": gal.is_public,
"publication_date": gal.publication_date,
"created_at": gal.created_at,
"updated_at": gal.updated_at
})
# Create a new gallery for the authenticated user.
@app.route("/api/private/gallery", methods=["POST"])
@oidc_required
def create_gallery():
data = request.json
gallery = Gallery(
owner_id=g.db_user.id,
title=data.get("title"),
description=data.get("description"),
is_public=data.get("is_public", False),
publication_date=data.get("publication_date")
)
db.session.add(gallery)
db.session.commit()
event = {
"type": "gallery_created",
"data": {"user_id": gallery.owner_id, "gallery_id": gallery.id}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"id": gallery.id, "message": "Gallery created"}), 201
# Update a gallery's title, description, public flag, and publication date (owner only).
@app.route("/api/private/gallery/<int:gallery_id>", methods=["PUT"])
@oidc_required
def update_gallery(gallery_id):
gal = Gallery.query.get_or_404(gallery_id)
if gal.owner_id != g.db_user.id:
abort(403)
data = request.json
gal.title = data.get("title", gal.title)
gal.description = data.get("description", gal.description)
gal.is_public = data.get("is_public", gal.is_public)
gal.publication_date = data.get("publication_date", gal.publication_date)
db.session.commit()
event = {
"type": "gallery_updated",
"data": {"user_id": gal.owner_id, "gallery_id": gal.id}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"message": "Gallery updated"})
# Retrieve the list of galleries owned by the authenticated user.
@app.route("/api/private/galleries/mine", methods=["GET"])
@oidc_required
def get_my_galleries():
user_id = g.db_user.id
galleries = Gallery.query.filter_by(owner_id=user_id).all()
result = []
for gal in galleries:
result.append({
"id": gal.id,
"title": gal.title,
"description": gal.description,
"is_public": gal.is_public,
"publication_date": gal.publication_date,
"created_at": gal.created_at,
"updated_at": gal.updated_at
})
return jsonify(result)
# List all members of a gallery (including the owner), with roles and join dates.
@app.route("/api/private/gallery/<int:gallery_id>/members", methods=["GET"])
@oidc_required
def get_gallery_members(gallery_id):
gal = Gallery.query.get_or_404(gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
members = GalleryMember.query.filter_by(gallery_id=gallery_id, status="accepted").all()
result = []
owner = User.query.get(gal.owner_id)
result.append({
"user_id": owner.id,
"alias": owner.alias,
"bio": owner.bio,
"profile_picture_url": owner.profile_picture_url,
"role": "owner",
"entered_at": gal.created_at
})
for mem in members:
user = User.query.get(mem.user_id)
result.append({
"user_id": user.id,
"alias": user.alias,
"bio": user.bio,
"profile_picture_url": user.profile_picture_url,
"role": mem.role,
"entered_at": mem.entered_at
})
return jsonify(result)
# Artworks
# List artworks in a gallery, filtering by visibility and access.
@app.route("/api/private/gallery/<int:gallery_id>/artworks", methods=["GET"])
@oidc_required
def get_gallery_artworks(gallery_id):
gal = Gallery.query.get_or_404(gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
if gal.owner_id != g.db_user.id:
artworks = Artwork.query.filter_by(gallery_id=gallery_id, is_visible=True).all()
else:
artworks = Artwork.query.filter_by(gallery_id=gallery_id).all()
result = []
for art in artworks:
cre = User.query.get(art.creator_id)
result.append({
"id": art.id,
"title": art.title,
"description": art.description,
"creator": cre.alias,
"image_url": art.image_url,
"medium": art.medium,
"dimensions": art.dimensions,
"creation_year": art.creation_year,
"is_visible": art.is_visible,
"price": art.price,
"is_for_sale": art.is_for_sale
})
return jsonify(result)
# Retrieve detailed information about a single artwork (visibility & access checks).
@app.route("/api/private/artwork/<int:artwork_id>", methods=["GET"])
@oidc_required
def get_artwork(artwork_id):
art = Artwork.query.get_or_404(artwork_id)
gal = Gallery.query.get(art.gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=art.gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
if art.creator_id != g.db_user.id and not art.is_visible:
abort(404)
cre = User.query.get(art.creator_id)
return jsonify({
"id": art.id,
"gallery_id": art.gallery_id,
"creator": cre.alias,
"title": art.title,
"description": art.description,
"image_url": art.image_url,
"medium": art.medium,
"dimensions": art.dimensions,
"creation_year": art.creation_year,
"is_visible": art.is_visible,
"price": art.price,
"is_for_sale": art.is_for_sale,
"created_at": art.created_at,
"updated_at": art.updated_at
})
# Add a new artwork to the specified gallery (owner only).
@app.route("/api/private/gallery/<int:gallery_id>/artwork", methods=["POST"])
@oidc_required
def create_artwork(gallery_id):
gallery = Gallery.query.get_or_404(gallery_id)
if gallery.owner_id != g.db_user.id:
abort(403)
data = request.json
artwork = Artwork(
gallery_id=gallery_id,
creator_id=g.db_user.id,
title=data.get("title"),
description=data.get("description"),
image_url=data.get("image_url"),
medium=data.get("medium"),
dimensions=data.get("dimensions"),
creation_year=data.get("creation_year"),
price=data.get("price"),
is_visible=data.get("is_visible", True),
is_for_sale=data.get("is_for_sale", False)
)
db.session.add(artwork)
db.session.commit()
event = {
"type": "artwork_created",
"data": {"user_id": artwork.creator_id, "artwork_id": artwork.id}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"id": artwork.id, "message": "Artwork created"}), 201
# Update an existing artwork's details (creator only).
@app.route("/api/private/artwork/<int:artwork_id>", methods=["PUT"])
@oidc_required
def update_artwork(artwork_id):
art = Artwork.query.get_or_404(artwork_id)
if art.creator_id != g.db_user.id:
abort(403)
data = request.json
art.title = data.get("title", art.title)
art.description = data.get("description", art.description)
art.image_url = data.get("image_url", art.image_url)
art.medium = data.get("medium", art.medium)
art.dimensions = data.get("dimensions", art.dimensions)
art.creation_year = data.get("creation_year", art.creation_year)
art.price = data.get("price", art.price)
art.is_visible = data.get("is_visible", art.is_visible)
art.is_for_sale = data.get("is_for_sale", art.is_for_sale)
db.session.commit()
event = {
"type": "artwork_updated",
"data": {"user_id": art.creator_id, "artwork_id": art.id}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"message": "Artwork updated"})
# List all artworks created by the authenticated user.
@app.route("/api/private/artworks/mine", methods=["GET"])
@oidc_required
def get_my_artworks():
artworks = Artwork.query.filter_by(creator_id=g.db_user.id).all()
result = []
for art in artworks:
result.append({
"id": art.id,
"gallery_id": art.gallery_id,
"title": art.title,
"description": art.description,
"image_url": art.image_url,
"medium": art.medium,
"dimensions": art.dimensions,
"creation_year": art.creation_year,
"is_visible": art.is_visible,
"price": art.price,
"is_for_sale": art.is_for_sale,
"created_at": art.created_at,
"updated_at": art.updated_at
})
return jsonify(result)
# Gallery reviews
# List all reviews for a given gallery (with access checks).
@app.route("/api/private/gallery/<int:gallery_id>/reviews", methods=["GET"])
@oidc_required
def get_gallery_reviews(gallery_id):
gal = Gallery.query.get_or_404(gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
reviews = GalleryReview.query.filter_by(gallery_id=gal.id).all()
result = []
for rev in reviews:
aut = User.query.get(rev.author_id)
result.append({
"id": rev.id,
"author": aut.alias,
"grade": rev.grade,
"description": rev.description,
"parent_gr_id": rev.parent_gr_id,
"created_at": rev.created_at,
"updated_at": rev.updated_at
})
return jsonify(result)
# Submit a new review for the specified gallery (access enforced).
@app.route("/api/private/gallery/<int:gallery_id>/review", methods=["POST"])
@oidc_required
def create_gallery_review(gallery_id):
gal = Gallery.query.get_or_404(gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
data = request.json
review = GalleryReview(
gallery_id=gal.id,
author_id=g.db_user.id,
grade=data.get("grade"),
description=data.get("description"),
parent_gr_id=data.get("parent_gr_id")
)
db.session.add(review)
db.session.commit()
event = {
"type": "gallery_review_created",
"data": {"user_id": review.author_id, "gallery_id": review.gallery_id, "gallery_review_id": review.id}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"id": review.id, "message": "Review created"}), 201
# Edit an existing gallery review (author only).
@app.route("/api/private/galleries/review/<int:review_id>", methods=["PUT"])
@oidc_required
def update_gallery_review(review_id):
rev = GalleryReview.query.get_or_404(review_id)
if rev.author_id != g.db_user.id:
abort(403)
gal = Gallery.query.get_or_404(rev.gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=rev.gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
data = request.json
rev.grade = data.get("grade", rev.grade)
rev.description = data.get("description", rev.description)
db.session.commit()
event = {
"type": "gallery_review_updated",
"data": {"user_id": rev.author_id, "gallery_id": rev.gallery_id, "gallery_review_id": rev.id}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"message": "Review updated"})
# Retrieve all gallery reviews written by the authenticated user.
@app.route("/api/private/galleries/reviews/given", methods=["GET"])
@oidc_required
def get_given_gallery_reviews():
reviews = GalleryReview.query.filter_by(author_id=g.db_user.id).all()
result = []
for rev in reviews:
gal = Gallery.query.get(rev.gallery_id)
result.append({
"review_id": rev.id,
"gallery_id": gal.id,
"gallery_title": gal.title,
"grade": rev.grade,
"description": rev.description,
"parent_gr_id": rev.parent_gr_id,
"created_at": rev.created_at,
"updated_at": rev.updated_at
})
return jsonify(result)
# List all reviews received on galleries owned by the authenticated user.
@app.route("/api/private/galleries/reviews/received", methods=["GET"])
@oidc_required
def get_received_gallery_reviews():
galleries = Gallery.query.filter_by(owner_id=g.db_user.id).all()
result = []
for gal in galleries:
reviews = GalleryReview.query.filter_by(gallery_id=gal.id).all()
for rev in reviews:
author = User.query.get(rev.author_id)
result.append({
"review_id": rev.id,
"gallery_id": gal.id,
"gallery_title": gal.title,
"author": author.alias,
"grade": rev.grade,
"description": rev.description,
"parent_gr_id": rev.parent_gr_id,
"created_at": rev.created_at,
"updated_at": rev.updated_at
})
return jsonify(result)
# Artwork reviews
# List all reviews for a given artwork (with access checks).
@app.route("/api/private/artwork/<int:artwork_id>/reviews", methods=["GET"])
@oidc_required
def get_artwork_reviews(artwork_id):
art = Artwork.query.get_or_404(artwork_id)
gal = Gallery.query.get(art.gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=art.gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
if art.creator_id != g.db_user.id and not art.is_visible:
abort(404)
reviews = ArtworkReview.query.filter_by(artwork_id=art.id).all()
result = []
for rev in reviews:
aut = User.query.get(rev.author_id)
result.append({
"id": rev.id,
"author": aut.alias,
"grade": rev.grade,
"description": rev.description,
"parent_ar_id": rev.parent_ar_id,
"created_at": rev.created_at,
"updated_at": rev.updated_at
})
return jsonify(result)
# Submit a new review for the specified artwork (access to gallery enforced).
@app.route("/api/private/artwork/<int:artwork_id>/review", methods=["POST"])
@oidc_required
def create_artwork_review(artwork_id):
art = Artwork.query.get_or_404(artwork_id)
gal = Gallery.query.get(art.gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=art.gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
if art.creator_id != g.db_user.id and not art.is_visible:
abort(404)
data = request.json
review = ArtworkReview(
artwork_id=art.id,
author_id=g.db_user.id,
grade=data.get("grade"),
description=data.get("description"),
parent_ar_id=data.get("parent_ar_id")
)
db.session.add(review)
db.session.commit()
event = {
"type": "artwork_review_created",
"data": {"user_id": review.author_id, "artwork_id": review.artwork_id_id, "artwork_review_id": review.id}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"id": review.id, "message": "Review created"}), 201
# Edit an existing artwork review (author only).
@app.route("/api/private/artworks/review/<int:review_id>", methods=["PUT"])
@oidc_required
def update_artwork_review(review_id):
rev = ArtworkReview.query.get_or_404(review_id)
if rev.author_id != g.db_user.id:
abort(403)
art = Artwork.query.get_or_404(rev.artwork_id)
gal = Gallery.query.get(art.gallery_id)
if not gal.is_public:
member = GalleryMember.query.filter_by(gallery_id=art.gallery_id, user_id=g.db_user.id, status="accepted").first()
if gal.owner_id != g.db_user.id and not member:
abort(403)
if art.creator_id != g.db_user.id and not art.is_visible:
abort(404)
data = request.json
rev.grade = data.get("grade", rev.grade)
rev.description = data.get("description", rev.description)
db.session.commit()
event = {
"type": "artwork_review_updated",
"data": {"user_id": rev.author_id, "artwork_id": rev.artwork_id_id, "artwork_review_id": rev.id}
}
redis_client.publish('events', json.dumps(event))
return jsonify({"message": "Review updated"})
# Retrieve all artwork reviews written by the authenticated user.
@app.route("/api/private/artworks/reviews/given", methods=["GET"])
@oidc_required
def get_given_artwork_reviews():
reviews = ArtworkReview.query.filter_by(author_id=g.db_user.id).all()
result = []
for rev in reviews:
art = Artwork.query.get(rev.artwork_id)
result.append({
"review_id": rev.id,
"artwork_id": art.id,
"artwork_title": art.title,
"grade": rev.grade,
"description": rev.description,
"parent_ar_id": rev.parent_ar_id,
"created_at": rev.created_at,
"updated_at": rev.updated_at
})
return jsonify(result)
# List all reviews received on artworks owned by the authenticated user.
@app.route("/api/private/artworks/reviews/received", methods=["GET"])
@oidc_required
def get_received_artwork_reviews():
artworks = Artwork.query.filter_by(creator_id=g.db_user.id).all()
result = []
for art in artworks:
reviews = ArtworkReview.query.filter_by(artwork_id=art.id).all()
for rev in reviews:
author = User.query.get(rev.author_id)
result.append({
"review_id": rev.id,
"artwork_id": art.id,
"artwork_title": art.title,
"author": author.alias,
"grade": rev.grade,
"description": rev.description,
"parent_ar_id": rev.parent_ar_id,
"created_at": rev.created_at,
"updated_at": rev.updated_at
})
return jsonify(result)
if __name__ == "__main__": if __name__ == "__main__":
with app.app_context():
db.create_all()
app.run(host='0.0.0.0',port=5002, debug=True) app.run(host='0.0.0.0',port=5002, debug=True)

View File

@ -1,9 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install flask flask_sqlalchemy pyjwt requests pymysql cryptography redis
CMD ["python", "app.py"]

View File

@ -1,76 +0,0 @@
import time
import pymysql
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
import redis
import json
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://myuser:mypassword@mysql:3306/mydb'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
MYSQL_HOST = "mysql"
MYSQL_PORT = 3306
MYSQL_USER = "myuser"
MYSQL_PASSWORD = "mypassword"
MYSQL_DB = "mydb"
# Connexion Redis
redis_client = redis.Redis(host='redis', port=6379, decode_responses=True)
while True:
try:
conn = pymysql.connect(
host=MYSQL_HOST,
port=MYSQL_PORT,
user=MYSQL_USER,
password=MYSQL_PASSWORD,
database=MYSQL_DB
)
conn.close()
print("MySQL is up - continuing.")
break
except pymysql.err.OperationalError as e:
print("Waiting for MySQL...", e)
time.sleep(2)
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(host='0.0.0.0',port=5001, debug=True)