diff --git a/.gitignore b/.gitignore index d230d7d50..dbbffb437 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ opac/webapp/media/images/*.* # nodejs/gulp/etc node_modules/ +data \ No newline at end of file diff --git a/data/db/.gitkeep b/data/db/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/jwt_public.pem b/jwt_public.pem new file mode 100644 index 000000000..22bcf41c3 --- /dev/null +++ b/jwt_public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg2CAMS9xU5wlcIgD+lHv +HjmeehX1FjB0T3oJl2e5XEnI7OvqrtQTnp8Y9PQd8tjUICgeFr17mzvHDHgwBhRd +A0h0oZp0EJTr7DdsBGleV5yjLpFl8rTW8DHwWrrj0Lz91Ym527qpiosyuoBKhFRV +bN9P5ZepUyYKVzEuxPAIb2R2NBde6Ggf5p7y3RtYDRZsK0tzy2KlQree2u8Dau6a +8uS7F66h9lxg8gz6iNe2zz1OFQL1efgR4pxMYryVH58Qo3VPcgCTbf1YUoXmvnaR +iXK/otcp3RemJtsz8u5gAi81Rk71co6YRGcUF9C+IyGjx+N4qzX4iC+CY/TsuBPm +xwIDAQAB +-----END PUBLIC KEY----- diff --git a/opac/webapp/config/default.py b/opac/webapp/config/default.py index ed183774f..d8ac365be 100644 --- a/opac/webapp/config/default.py +++ b/opac/webapp/config/default.py @@ -690,3 +690,12 @@ ANALYTICS_AGENT_DARKVISITORS_ENABLED = os.environ.get('OPAC_ANALYTICS_AGENT_DARKVISITORS_ENABLED', 'False').lower() in ('true', '1', 't', 'yes', 'y') ANALYTICS_AGENT_DARKVISITORS_PROJECT_KEY = os.environ.get("OPAC_ANALYTICS_AGENT_DARKVISITORS_PROJECT_KEY") +# JWT PARA ENDPOINT CORE TO COLLECTION +JWT_PUBLIC_KEY_PATH = os.environ.get("JWT_PUBLIC_KEY_PATH", default="/app/jwt_public.pem") +with open(JWT_PUBLIC_KEY_PATH, "rb") as f: + JWT_PUBLIC_KEY_PEM = f.read() + +JWT_ALG = "RS256" +JWT_ISS = os.environ.get("JWT_ISS", default="Scielo Core") +JWT_AUD = os.environ.get("JWT_AUD", default="opac_5") + diff --git a/opac/webapp/controllers.py b/opac/webapp/controllers.py index c812400c8..2b20f7950 100644 --- a/opac/webapp/controllers.py +++ b/opac/webapp/controllers.py @@ -6,11 +6,9 @@ ou outras camadas superiores, evitando assim que as camadas superiores acessem diretamente a camada inferior de modelos. """ -import logging import io +import logging import re -import requests -from bs4 import BeautifulSoup from collections import OrderedDict from datetime import datetime from uuid import uuid4 @@ -24,26 +22,18 @@ from flask_mongoengine import Pagination from legendarium.formatter import descriptive_very_short_format from mongoengine import Q -from mongoengine.errors import InvalidQueryError -from opac_schema.v1.models import ( - Article, - Collection, - Issue, - Journal, - News, - Pages, - PressRelease, - Sponsor, - LastIssue, -) +from opac_schema.v1.models import (Article, Collection, Issue, Journal, + LastIssue, News, Pages, PressRelease, + Sponsor) from scieloh5m5 import h5m5 from slugify import slugify from webapp import dbsql from .choices import INDEX_NAME, JOURNAL_STATUS, STUDY_AREAS +from .factory import ArticleFactory, IssueFactory, JournalFactory from .models import User -from .factory import JournalFactory, IssueFactory, ArticleFactory from .utils import utils +from .utils.handler_with_logo import handler_with_logo HIGHLIGHTED_TYPES = ( "article-commentary", @@ -174,6 +164,143 @@ def get_collection_tweets(): return [] +def extract_collection_names(json_data): + data ={} + for name in json_data.get("collection_names"): + if name.get("language"): + lang = name.get("language").get("code2") + data[lang] = name.get("text") + return data + + +def set_attributtes_logos(collection, logos, name_logos=["home_logo", "logo_menu", "header_logo"], langs=["pt", "en", "es"]): + """ + Atribuí os logos do modelo collection. (home_logo, logo_menu, header_logo) + Ex: + "logos": { + "homepage": { + "pt": "http://localhost/media/original_images/wjcm_glogo_F5dW55p.gif", + "en": "http://localhost/media/original_images/zcr_glogo_eCDkZlK.gif" + }, + "header": { + "pt": "http://localhost/media/original_images/yt_glogo_8AxTwaK.gif" + } + } + """ + if not logos: + return None + list_logo = [logos.get(key, "") for key in ["homepage", "header", "menu"]] + for logo_data in list_logo: + if not logo_data: + continue + for name_logo, lang in zip(name_logos, langs): + if hasattr(collection, f"{name_logo}_{lang}"): + logo_info = handler_with_logo(logo_url=logo_data.get(lang), folder=f"img/{name_logo}") + if rel_path := logo_info.get("rel_path"): + collection_logo = f"http://{current_app.config['SERVER_NAME']}{rel_path}" + setattr(collection, f"{name_logo}_{lang}", collection_logo) + +def complete_collection(json_data): + collection = get_current_collection() + if not collection or not json_data: + return None + + code = json_data.get("code") + main_name = json_data.get('main_name') + + if code and collection.acronym != code: + collection.acronym = code + if main_name and collection.name != main_name: + collection.name = main_name + + sponsors = handler_collection_sponsors(json_data.get("supporting_organizations")) + collection_names = extract_collection_names(json_data=json_data) + set_attributtes_logos(collection=collection, logos=json_data.get("logos")) + collection.name_pt = collection_names.get('pt') + collection.name_es = collection_names.get('es') + collection.name_en = collection_names.get('en') + collection.sponsors = sponsors + + collection.save() + + +def upsert_sponsor_by_acronym(data, order): + """ + Cria ou atualiza objetos sponsors e retorna o objeto Sponsor + Order é de acordo com a ordem recebida no payload do endpoint update_collection. + Ex data: + Um dicionario com os dados do sponsor + { + "acronym": "SP", + "name": "Sponsor", + "url": "https://www.sponsor.com" + "logo_url": "https://core.scielo.org/media/original_images/logo.png" + } + """ + import time + + name = data.get('name', '').strip() + url = data.get('url') or None + logo_url = data.get("logo_url") or None + + logo = handler_with_logo(logo_url=logo_url, folder="img/sponsors") + + # Por causa do order unique, evitar error ao mudar a ordem + temp_order = -int(time.time()*1000) # incrementa um valor aleatório em order. + rel_path = logo.get("rel_path") + obj = Sponsor.objects(name=name).modify( + upsert=True, + new=True, + set__url=url, + set__order=temp_order, + set__logo_url=f"http://{current_app.config['SERVER_NAME']}{rel_path}" if rel_path else None, + ) + existing = Sponsor.objects(order=order, _id__ne=obj.id).first() + if existing: + existing.modify(set__order=temp_order - 1) + obj.modify(set__order=order) + return obj + + +def handler_collection_sponsors(data): + """ + Recebe uma lista de dados sobre os sponsors e retorna uma lista de objetos sponsors + Ex data: + Uma lista de dicionarios com os dados dos sponsors + [ + { + "organization": { + "acronym": "SP", + "name": "Sponsor", + "url": "https://www.sponsor.com" + "logo_url": "https://core.scielo.org/media/original_images/logo.png" + "location": { + "country_name": "Brasil", + "country_acronym": "BR", + "country_acron3": "BRA", + "state_name": "SP", + "state_acronym": "SP", + "city_name": "São Paulo" + + } + }, + } + ] + """ + + sponsors = [] + if not data: + return sponsors + if not isinstance(data, list): + raise ValueError("data must be a list of sponsor dicts") + + for i, item in enumerate(data): + sponsor = upsert_sponsor_by_acronym(item.get("organization"), order=i) + if sponsor is not None: + sponsors.append(sponsor) + return sponsors + + # -------- PRESSRELEASES -------- diff --git a/opac/webapp/main/decorators.py b/opac/webapp/main/decorators.py new file mode 100644 index 000000000..4d5b7c76d --- /dev/null +++ b/opac/webapp/main/decorators.py @@ -0,0 +1,22 @@ +import logging +from functools import wraps + +from flask import g, jsonify +from jwt import PyJWTError + +from .helper import get_bearer_token, verify_jwt + + +def require_jwt(f): + @wraps(f) + def wrapper(*args, **kwargs): + token = get_bearer_token() + if not token: + return jsonify({"detail": "Missing Bearer token"}), 401 + try: + g.jwt = verify_jwt(token) + except PyJWTError as e: + logging.error("Erro na validação JWT:", str(e)) + return jsonify({"detail": f"Invalid token: {str(e)}"}), 401 + return f(*args, **kwargs) + return wrapper \ No newline at end of file diff --git a/opac/webapp/main/helper.py b/opac/webapp/main/helper.py index 4d97886cb..ca011eba1 100644 --- a/opac/webapp/main/helper.py +++ b/opac/webapp/main/helper.py @@ -1,3 +1,4 @@ +import logging import datetime from functools import wraps @@ -68,3 +69,35 @@ def auth(): ), 401, ) + + +def get_bearer_token(): + auth = request.headers.get("Authorization", "") + if auth.startswith("Bearer "): + return auth[len("Bearer "):] + return None + +def get_jwt_public_key(): + public_key = current_app.config["JWT_PUBLIC_KEY_PATH"] + try: + with open(public_key, "rb") as f: + return f.read() + except (FileNotFoundError, PermissionError, OSError) as e: + logging.error(f"Error reading public key: {e}") + + +def verify_jwt(token): + public_key = get_jwt_public_key() + + if not public_key: + return None + + return jwt.decode( + token, + public_key, + algorithms=[current_app.config["JWT_ALG"]], + audience=current_app.config["JWT_AUD"], + issuer=current_app.config["JWT_ISS"], + # options={"require": ["exp", "iat", "nbf", "iss", "aud"]}, + leeway=30, + ) \ No newline at end of file diff --git a/opac/webapp/main/views.py b/opac/webapp/main/views.py index 022827a60..63c849154 100644 --- a/opac/webapp/main/views.py +++ b/opac/webapp/main/views.py @@ -1,5 +1,4 @@ # coding: utf-8 -import re import json import logging import mimetypes @@ -11,36 +10,23 @@ import requests from bs4 import BeautifulSoup from feedwerk.atom import AtomFeed -from flask import ( - Response, - abort, - current_app, - g, - jsonify, - make_response, - redirect, - render_template, - request, - send_from_directory, - session, - url_for, -) +from flask import (Response, abort, current_app, g, jsonify, make_response, + redirect, render_template, request, send_from_directory, + session, url_for) from flask_babelex import gettext as _ from legendarium.formatter import descriptive_short_format from lxml import etree -from opac_schema.v1.models import Article, Collection, Issue, Journal +from opac_schema.v1.models import Article, Collection, Journal from packtools import HTMLGenerator from webapp import babel, cache, controllers, forms from webapp.choices import STUDY_AREAS -from webapp.controllers import create_press_release_record from webapp.config.lang_names import display_original_lang_name +from webapp.controllers import create_press_release_record from webapp.utils import utils -from webapp.utils.caching import cache_key_with_lang, cache_key_with_lang_with_qs -from webapp.main.errors import page_not_found, internal_server_error - -from . import helper +from webapp.utils.caching import (cache_key_with_lang, + cache_key_with_lang_with_qs) -from . import main, restapi +from . import decorators, helper, main, restapi logger = logging.getLogger(__name__) @@ -2187,7 +2173,6 @@ def pressrelease(*args): """ payload = request.get_json() - params = request.args.to_dict() if not payload.get("journal_id"): return jsonify({"failed": True, "id": 1}), 200 @@ -2222,3 +2207,11 @@ def journal_last_issues(*args): def remover_tags_html(texto): soup = BeautifulSoup(texto, 'html.parser') return soup.get_text() + + +@restapi.route("/update_collection", methods=["POST", "PUT"]) +@decorators.require_jwt +def update_collection(*args): + payload = request.get_json() + controllers.complete_collection(payload.get("results")) + return jsonify({"success": True}) diff --git a/opac/webapp/templates/includes/footer.html b/opac/webapp/templates/includes/footer.html index 2291abac3..be3e241fa 100644 --- a/opac/webapp/templates/includes/footer.html +++ b/opac/webapp/templates/includes/footer.html @@ -81,35 +81,36 @@
-
- CAPESCAPES - CNPqCNPq - FAPESPFAPESP - BVSBVS - BIREMEBIREME - FAP-UNIFESPFAP-UNIFESP -
-
-
-
- {% if g.collection and g.collection.sponsors %} - {% for sponsor in g.collection.sponsors|sort(attribute='order') %} - {% if sponsor.url and sponsor.logo_url %} - - {% if sponsor.logo_url %} + {% if g.collection.sponsors %} +
+ {% if g.collection and g.collection.sponsors %} + {% for sponsor in g.collection.sponsors|sort(attribute='order') %} + {% if sponsor.url and sponsor.logo_url %} + + {% if sponsor.logo_url %} + {{ sponsor.name }} + {% else %} + {{ sponsor.name }} + {% endif %} + + {% elif sponsor.logo_url %} {{ sponsor.name }} - {% else %} + {% else %} {{ sponsor.name }} - {% endif %} - - {% elif sponsor.logo_url %} - {{ sponsor.name }} - {% else %} - {{ sponsor.name }} + {% endif %} + {% endfor %} {% endif %} - {% endfor %} +
+ {% else %} +
+ CAPESCAPES + CNPqCNPq + FAPESPFAPESP + BVSBVS + BIREMEBIREME + FAP-UNIFESPFAP-UNIFESP +
{% endif %} -
diff --git a/opac/webapp/utils/handler_with_logo.py b/opac/webapp/utils/handler_with_logo.py new file mode 100644 index 000000000..669a52d02 --- /dev/null +++ b/opac/webapp/utils/handler_with_logo.py @@ -0,0 +1,43 @@ +import logging +import os +from urllib.parse import urlparse + +import requests +from flask import current_app + + +def handler_with_logo(logo_url, folder): + """ + Ex logo_url: https://core.scielo.org/media/original_images/av_glogo.gif + + """ + if not logo_url or logo_url == "null": + return {} + + # Extrai o nome do arquivo com extensão a partir da URL. + base_name = os.path.basename(urlparse(logo_url).path) + rel_path = os.path.join(folder, base_name) + # abs_path /app/opac/webapp/static/img/sponsors/screenshot_from_2024-10-14_11-20-16.png + abs_path = os.path.join(current_app.static_folder, rel_path) + os.makedirs(os.path.dirname(abs_path), exist_ok=True) + print(os.path.join(current_app.static_url_path, rel_path)) + try: + resp = requests.get(logo_url, stream=True, timeout=60) + resp.raise_for_status() + + with open(abs_path, "wb") as f: + for chunk in resp.iter_content(chunk_size=8192): + f.write(chunk) + logging.info(f"File {base_name} downloaded successfully") + except requests.exceptions.RequestException as e: + logging.error(f"Error downloading file {base_name}: {e}") + except OSError as e: + logging.error(f"Error saving file {base_name}: {e}") + + return { + 'abs_path': abs_path, + 'rel_path': os.path.join( + current_app.static_url_path, + rel_path + ) + } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 39fe5cdd4..85de160db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -97,3 +97,4 @@ tenacity==8.2.3 -e git+https://git@github.com/scieloorg/opac_schema@v2.9.0#egg=Opac_Schema -e git+https://git@github.com/scieloorg/packtools@4.11.20#egg=packtools -e git+https://github.com/scieloorg/scieloh5m5.git@1.9.5#egg=scieloh5m5 +cryptography==46.0.1 \ No newline at end of file