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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ FROM python:3.11-slim
WORKDIR /app
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"]

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 sqlalchemy.exc import IntegrityError
from jose import jwt, JWTError
import requests
import jwt
import time
import pymysql
import redis
import json
from functools import wraps
app = Flask(__name__)
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)
MYSQL_HOST = "mysql"
MYSQL_PORT = 3306
MYSQL_USER = "myuser"
@ -38,81 +41,821 @@ while True:
print("Waiting for MySQL...", e)
time.sleep(2)
class Visite(db.Model):
print('Creating DB')
# Keycloak config
KEYCLOAK_REALM = "master"
KEYCLOAK_URL = "http://keycloak:8080"
CLIENT_ID = "soa"
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"]
def get_signing_key(token):
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header.get("kid")
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()
event = {
"type": "user_created",
"data": {"id": user.id, "alias": user.alias}
}
redis_client.publish('events', json.dumps(event))
g.db_user = user
return f(*args, **kwargs)
return wrapper
@app.route("/api/private/debug-headers")
def debug_headers():
return jsonify(dict(request.headers))
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
galerie_id = db.Column(db.Integer, nullable=False)
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 Critique(db.Model):
class Gallery(db.Model):
__tablename__ = "galleries"
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)
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())
@app.route("/", methods=["GET"])
def index():
return f"User API - Authenticated as {request.user}", 200
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())
@app.route("/galerie/<int:galerie_id>/entrer", methods=["POST"])
def entrer_galerie(galerie_id):
visite = Visite(galerie_id=galerie_id)
db.session.add(visite)
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()
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 = {
"type": "oeuvre_created",
"data": {"id": oeuvre.id, "titre": oeuvre.titre}
"type": "user_updated",
"data": {"id": user.id, "alias": user.alias}
}
redis_client.publish('events', json.dumps(event))
return {"id": oeuvre.id, "titre": oeuvre.titre}, 201
return jsonify({"message": "Profile updated"})
@app.route("/oeuvre/<int:oeuvre_id>/critiquer", methods=["POST"])
def critiquer_oeuvre(oeuvre_id):
data = request.get_json()
if not data or not data.get("texte"):
# 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)
critique = Critique(oeuvre_id=oeuvre_id, texte=data["texte"], username=request.user)
db.session.add(critique)
invitation = GalleryMember.query.filter_by(gallery_id=gallery_id, user_id=g.db_user.id).first_or_404()
if invitation.status != "pending":
abort(403)
invitation.status = status
if status == "accepted":
invitation.entered_at = db.func.now()
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 = {
"type": "galerie_created",
"data": {"id": galerie.id, "nom": galerie.nom, "auteur": galerie.auteur}
"type": "invitation_answered",
"data": {"user_id": invitation.user_id, "gallery_id": invitation.gallery_id, "answer": invitation.status}
}
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__":
with app.app_context():
db.create_all()
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)