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
8 changes: 0 additions & 8 deletions sbc_translation/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,6 @@
"license": "AGPL-3",
"website": "https://github.com/CompassionCH/compassion-modules",
"depends": ["sbc_compassion", "partner_contact_birthdate", "portal"],
"assets": {
"web.assets_frontend": [
"sbc_translation/static/src/frontend/**/*.css",
"sbc_translation/static/src/frontend/**/*.xml",
"sbc_translation/static/src/frontend/**/*.esm.js",
],
},
"data": [
"security/ir_groups.xml",
"security/ir.model.access.csv",
Expand All @@ -53,7 +46,6 @@
"data/mail_template.xml",
"data/update_translation_priority_cron.xml",
"data/queue_job.xml",
"templates/portal_templates.xml",
"views/translation_user_view.xml",
"views/correspondence_view.xml",
"views/translation_pool_view.xml",
Expand Down
84 changes: 20 additions & 64 deletions sbc_translation/controllers/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
##############################################################################
#
# Copyright (C) 2023 Compassion CH (http://www.compassion.ch)
# Copyright (C) 2023-2026 Compassion CH (http://www.compassion.ch)
# Releasing children from poverty in Jesus' name
#
# The licence is in the file __manifest__.py
Expand All @@ -11,75 +11,31 @@
from werkzeug.utils import redirect

from odoo import http
from odoo.http import request

from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.tools import file_open

_logger = logging.getLogger(__name__)


class TranslationPlatformController(CustomerPortal):
@http.route(
"/my/translation-platform",
type="http",
auth="user",
website=True,
)
def translation_platform_portal(self, **kwargs):
"""
Portal page for the Translation Platform OWL app.
Only accessible to authenticated users who belong to the
sbc_translation.group_user group.
"""
if not request.env.user.has_group("sbc_translation.group_user"):
return redirect("/my")
return request.render("sbc_translation.portal_translation_platform", {})

class TranslationPlatformController(http.Controller):
@http.route(
["/translation-platform", "/translation-platform/<path:page>"],
type="http",
auth="user",
website=True,
auth="public",
)
Comment on lines +19 to 24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Unauthenticated access to the translation platform

The previous controller required user-level authentication and an explicit translator-group check before rendering the page. The new route sets the auth parameter to "public", so unauthenticated visitors receive the SPA shell with no server-side gate. If the SPA handles login internally this is intentional, but it is a significant security posture change: any regression in the SPA client-side login logic would silently expose the platform to anonymous users. Consider keeping user-level auth so Odoo redirects unauthenticated visitors to the login page before serving the app shell.

def translation_platform_legacy(self, page="", **kwargs):
def translation_platform(self, page=""):
Comment thread
NoeBerdoz marked this conversation as resolved.
"""Serve the built translation-platform-web SPA from
`static/tp/`.

`static/tp/` is the destination for the `npm run build`
output of the external translation-platform-web repo: copy
the `dist/` folder there at release time. The webapp itself
does client-side routing; this controller only serves
`index.html` for app routes and redirects asset URLs into
`/sbc_translation/static/tp/...`.
"""
Legacy route: redirect old standalone-app URLs to the new portal page.
"""
return redirect("/my/translation-platform", 301)

def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
if not request.env.user.has_group("sbc_translation.group_user"):
return values
partner = request.env.user.partner_id
translator = request.env["translation.user"].search(
[("partner_id", "=", partner.id)]
)
if translator and "letters_to_translate" in counters:
if translator.translation_skills:
nb_letters = request.env["correspondence"].search_count(
[
("state", "=", "Global Partner translation queue"),
("translation_status", "=", "to do"),
("new_translator_id", "=", False),
(
"translation_competence_id.skill_ids",
"in",
translator.translation_skills.ids,
),
]
)
values["letters_to_translate"] = nb_letters
else:
values["letters_to_translate"] = 1
if translator and "letters_in_progress" in counters:
nb_letters = request.env["correspondence"].search_count(
[
("state", "=", "Global Partner translation queue"),
("translation_status", "!=", "done"),
("new_translator_id", "=", translator.id),
]
)
values["letters_in_progress"] = nb_letters
values["translator"] = translator
return values
if (
"assets" in page or page.endswith(".png") or page.endswith(".jpg")
):
return redirect(f"/sbc_translation/static/tp/{page}")
Comment thread
NoeBerdoz marked this conversation as resolved.
Comment on lines +36 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Fragile asset-detection heuristic breaks SPA routing

The condition "assets" in page is a substring check, so any SPA client-side route that happens to contain the word "assets" (e.g. /translation-platform/letters/assets-list) will be incorrectly redirected to a static-file URL instead of receiving index.html. The SPA's client-side router will never see those routes. Additionally, the redirect uses the raw page value without sanitising path-traversal sequences, so a crafted URL like /translation-platform/assets/../../../other_module/static/somefile could expose static files outside static/tp/.

A safer approach is to match only well-known asset extensions (and validate no .. segments are present) instead of a substring check on the directory name.

with file_open("sbc_translation/static/tp/index.html") as app:
return app.read()
Comment on lines +40 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 No error handling when built SPA assets are absent

If sbc_translation/static/tp/index.html does not exist (e.g. in a fresh checkout before running npm run build and copying the dist/ output), file_open will raise a FileNotFoundError, producing an unhandled 500. Adding a try/except that returns a friendly 503 or maintenance page would be safer for operators who haven't yet deployed the built assets.

40 changes: 37 additions & 3 deletions sbc_translation/models/correspondence.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,28 @@ def _compute_translation_priority_name(self):
)

def _compute_translation_url(self):
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url", "")
base_url = base_url.rstrip("/")
"""Build the link translators click to open this letter in
the webapp.

The base URL is read from the system parameter
`sbc_translation.webapp_base_url`. If unset, the webapp is
assumed to be served by Odoo itself at `/translation-platform`
(see `controllers/main.py`), and the link falls back to
`<web.base.url>/translation-platform`.

Set the parameter to point at an external host
(e.g. `http://localhost:5173` for `npm run dev`) when the
webapp is not served by Odoo.
"""
icp = self.env["ir.config_parameter"].sudo()
webapp_url = icp.get_param("sbc_translation.webapp_base_url")
if not webapp_url:
base_url = icp.get_param("web.base.url", "").rstrip("/")
webapp_url = f"{base_url}/translation-platform"
webapp_url = webapp_url.rstrip("/")
for letter in self:
letter.translation_url = (
f"{base_url}/odoo/translation-platform?letterId={letter.id}"
f"{webapp_url}/letters/letter-edit/{letter.id}"
)

def _compute_paragraph_ids(self):
Expand Down Expand Up @@ -564,6 +581,13 @@ def submit_translation(self, letter_elements, translator_id=None) -> bool:
return True

def action_approve_translation(self):
"""Manager-side approval of a letter in `to validate` status.

Marks the translator's skill for this letter's competence as
verified (if not already), clears any reported translation
issue, records the current user as supervisor, and runs the
post-processing step that ships the letter on.
"""
for letter in self:
skill_to_validate = letter.new_translator_id.translation_skills.filtered(
lambda s, _letter=letter: s.competence_id
Expand Down Expand Up @@ -619,6 +643,16 @@ def list_letters(self):
"""API call to fetch letters to translate"""
return [letter.get_letter_info() for letter in self.sorted("scanned_date")]

# Webapp-facing aliases for the action_* methods.
# translation-platform-web calls these by their unprefixed names;
# the action_* names stay for backend button bindings (Odoo
# convention).
def remove_local_translate(self):
return self.action_remove_local_translate()

def resubmit_to_translation(self):
return self.action_resubmit_to_translation()

def get_letter_info(self):
"""Translation Platform API for fetching letter data."""
self.ensure_one()
Expand Down
Binary file removed sbc_translation/static/img/menu_icon.png
Binary file not shown.
Binary file removed sbc_translation/static/img/menu_icon_small.png
Binary file not shown.

This file was deleted.

43 changes: 0 additions & 43 deletions sbc_translation/static/src/frontend/components/tp_child_modal.xml

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading