fix
This commit is contained in:
parent
fc9431db44
commit
907c476567
@ -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>
|
@ -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:
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
@ -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"]
|
||||||
|
|
||||||
|
865
private/app.py
865
private/app.py
@ -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')
|
||||||
|
|
||||||
|
# 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)
|
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)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
oeuvre_id = db.Column(db.Integer, nullable=False)
|
owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
texte = db.Column(db.Text, nullable=False)
|
title = db.Column(db.String(255), nullable=False)
|
||||||
username = db.Column(db.String(100), 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"])
|
class GalleryMember(db.Model):
|
||||||
def index():
|
__tablename__ = "gallery_members"
|
||||||
return f"User API - Authenticated as {request.user}", 200
|
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"])
|
class ArtworkReview(db.Model):
|
||||||
def entrer_galerie(galerie_id):
|
__tablename__ = "artwork_reviews"
|
||||||
visite = Visite(galerie_id=galerie_id)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
db.session.add(visite)
|
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()
|
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_updated",
|
||||||
"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
|
return jsonify({"message": "Profile updated"})
|
||||||
|
|
||||||
@app.route("/oeuvre/<int:oeuvre_id>/critiquer", methods=["POST"])
|
|
||||||
def critiquer_oeuvre(oeuvre_id):
|
# Invitations
|
||||||
data = request.get_json()
|
|
||||||
if not data or not data.get("texte"):
|
# 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)
|
||||||
|
|
||||||
|
@ -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"]
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user