SOA/private/app.py
Alexis Bruneteau 907c476567 fix
2025-06-28 12:30:33 +02:00

862 lines
32 KiB
Python

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 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'
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)
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)
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)
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()
event = {
"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 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__":
app.run(host='0.0.0.0',port=5002, debug=True)