Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ opac/webapp/media/images/*.*

# nodejs/gulp/etc
node_modules/
data
Empty file removed data/db/.gitkeep
Empty file.
9 changes: 9 additions & 0 deletions jwt_public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg2CAMS9xU5wlcIgD+lHv
HjmeehX1FjB0T3oJl2e5XEnI7OvqrtQTnp8Y9PQd8tjUICgeFr17mzvHDHgwBhRd
A0h0oZp0EJTr7DdsBGleV5yjLpFl8rTW8DHwWrrj0Lz91Ym527qpiosyuoBKhFRV
bN9P5ZepUyYKVzEuxPAIb2R2NBde6Ggf5p7y3RtYDRZsK0tzy2KlQree2u8Dau6a
8uS7F66h9lxg8gz6iNe2zz1OFQL1efgR4pxMYryVH58Qo3VPcgCTbf1YUoXmvnaR
iXK/otcp3RemJtsz8u5gAi81Rk71co6YRGcUF9C+IyGjx+N4qzX4iC+CY/TsuBPm
xwIDAQAB
-----END PUBLIC KEY-----
9 changes: 9 additions & 0 deletions opac/webapp/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment on lines +695 to +696
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File operations at module level should include proper error handling. If the JWT public key file doesn't exist or is unreadable, this will cause the application to fail during import.

Suggested change
with open(JWT_PUBLIC_KEY_PATH, "rb") as f:
JWT_PUBLIC_KEY_PEM = f.read()
try:
with open(JWT_PUBLIC_KEY_PATH, "rb") as f:
JWT_PUBLIC_KEY_PEM = f.read()
except (FileNotFoundError, PermissionError, OSError) as e:
JWT_PUBLIC_KEY_PEM = None
import warnings
warnings.warn(
f"Could not read JWT public key from '{JWT_PUBLIC_KEY_PATH}': {e}. "
"JWT_PUBLIC_KEY_PEM is set to None."
)

Copilot uses AI. Check for mistakes.

Comment on lines +695 to +697
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opening the JWT public key file at import time will cause the application to fail if the file doesn't exist. This should be handled with proper error handling or moved to a function that's called when needed.

Suggested change
with open(JWT_PUBLIC_KEY_PATH, "rb") as f:
JWT_PUBLIC_KEY_PEM = f.read()
def get_jwt_public_key_pem():
"""
Reads the JWT public key from the configured path.
Returns the key contents as bytes.
Raises FileNotFoundError with a clear message if the file is missing.
"""
try:
with open(JWT_PUBLIC_KEY_PATH, "rb") as f:
return f.read()
except FileNotFoundError:
raise FileNotFoundError(
f"JWT public key file not found at '{JWT_PUBLIC_KEY_PATH}'. "
"Please ensure the file exists and the path is correct."
)
except Exception as e:
raise RuntimeError(
f"An error occurred while reading the JWT public key file at '{JWT_PUBLIC_KEY_PATH}': {e}"
)

Copilot uses AI. Check for mistakes.
JWT_ALG = "RS256"
JWT_ISS = os.environ.get("JWT_ISS", default="Scielo Core")
JWT_AUD = os.environ.get("JWT_AUD", default="opac_5")

159 changes: 143 additions & 16 deletions opac/webapp/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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:
Comment on lines +178 to +179
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring should be in English and has Portuguese text.

Suggested change
Atribuí os logos do modelo collection. (home_logo, logo_menu, header_logo)
Ex:
Assigns the logos to the collection model. (home_logo, logo_menu, header_logo)
Example:

Copilot uses AI. Check for mistakes.
"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
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import statement should be at the top of the file, not inside a function.

Copilot uses AI. Check for mistakes.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuelveigarangel isso pode fazer parte do dado registrado no core. Assim evita processamento excessivo e a ordem é garantida na origem.

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 --------


Expand Down
22 changes: 22 additions & 0 deletions opac/webapp/main/decorators.py
Original file line number Diff line number Diff line change
@@ -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))
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message should be in English to maintain consistency with codebase.

Suggested change
logging.error("Erro na validação JWT:", str(e))
logging.error("Error validating JWT:", str(e))

Copilot uses AI. Check for mistakes.
return jsonify({"detail": f"Invalid token: {str(e)}"}), 401
return f(*args, **kwargs)
return wrapper
33 changes: 33 additions & 0 deletions opac/webapp/main/helper.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import datetime
from functools import wraps

Expand Down Expand Up @@ -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"]},
Comment on lines +95 to +101
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code should be removed if not needed, or uncommented with proper explanation if required for JWT validation.

Suggested change
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"]},
# Enforce presence of standard claims for robust JWT validation
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"]},

Copilot uses AI. Check for mistakes.
leeway=30,
)
39 changes: 16 additions & 23 deletions opac/webapp/main/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# coding: utf-8
import re
import json
import logging
import mimetypes
Expand All @@ -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__)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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})
Loading