diff --git a/base_rest/README.rst b/base_rest/README.rst index 921157c84..2191d8146 100644 --- a/base_rest/README.rst +++ b/base_rest/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ========= Base Rest ========= @@ -17,7 +13,7 @@ Base Rest .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github diff --git a/base_rest/__manifest__.py b/base_rest/__manifest__.py index 6ed1c7c0d..3d627d35b 100644 --- a/base_rest/__manifest__.py +++ b/base_rest/__manifest__.py @@ -9,7 +9,7 @@ "version": "18.0.1.1.2", "development_status": "Beta", "license": "LGPL-3", - "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", + "author": "ACSONE SA/NV, Odoo Community Association (OCA)", "maintainers": [], "website": "https://github.com/OCA/rest-framework", "depends": ["component", "web"], @@ -20,8 +20,7 @@ "assets": { "web.assets_frontend": [ "base_rest/static/src/scss/base_rest.scss", - "base_rest/static/src/js/swagger_ui.js", - "base_rest/static/src/js/swagger.js", + "base_rest/static/src/js/components/swagger_ui.js", ], }, "external_dependencies": { diff --git a/base_rest/static/description/index.html b/base_rest/static/description/index.html index d5fc38403..50145ecf5 100644 --- a/base_rest/static/description/index.html +++ b/base_rest/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Base Rest -
+
+

Base Rest

- - -Odoo Community Association - -
-

Base Rest

-

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This addon is deprecated and not fully supported anymore from Odoo 16. Please migrate to the FastAPI migration module. See https://github.com/OCA/rest-framework/pull/291.

@@ -414,7 +409,7 @@

Base Rest

-

Configuration

+

Configuration

If an error occurs when calling a method of a service (ie missing parameter, ..) the system returns only a general description of the problem without details. This is done on purpose to ensure maximum @@ -436,7 +431,7 @@

Configuration

mode in production.

-

Usage

+

Usage

To add your own REST service you must provides at least 2 classes.

  • A Component providing the business logic of your service,
  • @@ -683,7 +678,7 @@

    Usage

    evaluation context.

-

Changelog

+

Changelog

-

16.0.1.0.2 (2023-10-07)

+

16.0.1.0.2 (2023-10-07)

Features

-

12.0.2.0.1

+

12.0.2.0.1

  • validator_…() methods can now return a cerberus Validator object instead of a schema dictionnary, for additional flexibility @@ -715,20 +710,20 @@

    12.0.2.0.1

-

12.0.2.0.0

+

12.0.2.0.0

  • Licence changed from AGPL-3 to LGPL-3
-

12.0.1.0.1

+

12.0.1.0.1

  • Fix issue when rendering the jsonapi documentation if no documentation is provided on a method part of the REST api.
-

12.0.1.0.0

+

12.0.1.0.0

First official version. The addon has been incubated into the Shopinvader repository from @@ -736,7 +731,7 @@

12.0.1.0.0

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -744,22 +739,22 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • ACSONE SA/NV
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -772,6 +767,5 @@

Maintainers

-
diff --git a/fastapi_auth_partner/README.rst b/fastapi_auth_partner/README.rst new file mode 100644 index 000000000..735b14942 --- /dev/null +++ b/fastapi_auth_partner/README.rst @@ -0,0 +1,148 @@ +==================== +Fastapi Auth Partner +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2ebd9377ca7b035ab9fb0383513aacb5ca8645f69d5d85c171883b40b439017e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/18.0/fastapi_auth_partner + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-18-0/rest-framework-18-0-fastapi_auth_partner + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is the FastAPI implementation of +`auth_partner <../auth_partner>`__ it provides all the routes to manage +the authentication of partners. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +First you have to add the auth router to your FastAPI endpoint and the +authentication dependency to your app dependencies: + +.. code:: python + + from odoo.addons.fastapi import dependencies + from odoo.addons.fastapi_auth_partner.dependencies import ( + auth_partner_authenticated_partner, + ) + from odoo.addons.fastapi_auth_partner.routers.auth import auth_router + + class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + def _get_fastapi_routers(self): + if self.app == "myapp": + return [ + auth_router, + ] + return super()._get_fastapi_routers() + + def _get_app_dependencies_overrides(self): + res = super()._get_app_dependencies_overrides() + if self.app == "myapp": + res.update( + { + dependencies.authenticated_partner_impl: auth_partner_authenticated_partner, + } + ) + return res + +Next you can manage your authenticable partners and directories in the +Odoo interface: + +FastAPI > Authentication > Partner + +and + +FastAPI > Authentication > Directory + +Next you must set the directory used for the authentication in the +FastAPI endpoint: + +FastAPI > FastAPI Endpoint > myapp > Directory + +Then you can use the auth router to authenticate your requests: + +- POST /auth/register to register a partner +- POST /auth/login to authenticate a partner +- POST /auth/logout to unauthenticate a partner +- POST /auth/validate_email to validate a partner email +- POST /auth/request_reset_password to request a password reset +- POST /auth/set_password to set a new password +- GET /auth/profile to get the partner profile +- GET /auth/impersonate to impersonate a partner + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- `Akretion `__: + + - Sébastien Beau + - Florian Mounier + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_auth_partner/__init__.py b/fastapi_auth_partner/__init__.py new file mode 100644 index 000000000..3f274f8d1 --- /dev/null +++ b/fastapi_auth_partner/__init__.py @@ -0,0 +1,4 @@ +from . import models +from . import routers +from . import schemas +from . import wizards diff --git a/fastapi_auth_partner/__manifest__.py b/fastapi_auth_partner/__manifest__.py new file mode 100644 index 000000000..343ec9400 --- /dev/null +++ b/fastapi_auth_partner/__manifest__.py @@ -0,0 +1,34 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Fastapi Auth Partner", + "summary": """This provides an implementation of auth_partner for FastAPI""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/rest-framework", + "depends": [ + "extendable_fastapi", + "auth_partner", + ], + "data": [ + "security/res_group.xml", + "security/ir_rule.xml", + "security/ir.model.access.csv", + "views/auth_partner_view.xml", + "views/auth_directory_view.xml", + "views/fastapi_endpoint_view.xml", + "wizards/wizard_auth_partner_impersonate_view.xml", + "wizards/wizard_auth_partner_reset_password_view.xml", + ], + "demo": [ + "demo/fastapi_endpoint_demo.xml", + ], + "maintainers": ["paradoxxxzero"], + "external_dependencies": { + "python": ["itsdangerous"], + }, +} diff --git a/fastapi_auth_partner/demo/fastapi_endpoint_demo.xml b/fastapi_auth_partner/demo/fastapi_endpoint_demo.xml new file mode 100644 index 000000000..ff53c707c --- /dev/null +++ b/fastapi_auth_partner/demo/fastapi_endpoint_demo.xml @@ -0,0 +1,29 @@ + + + + Fastapi Auth Partner Demo Endpoint + + demo + /fastapi_auth_partner_demo + auth_partner + + + https://api.example.com/ + https://www.example.com/ + + + + + + diff --git a/fastapi_auth_partner/dependencies.py b/fastapi_auth_partner/dependencies.py new file mode 100644 index 000000000..22ad03067 --- /dev/null +++ b/fastapi_auth_partner/dependencies.py @@ -0,0 +1,68 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +from typing import Annotated, Any + +from itsdangerous import URLSafeTimedSerializer +from starlette.status import HTTP_401_UNAUTHORIZED + +from odoo.api import Environment + +from odoo.addons.base.models.res_partner import Partner +from odoo.addons.fastapi.dependencies import fastapi_endpoint, odoo_env +from odoo.addons.fastapi.models import FastapiEndpoint + +from fastapi import Cookie, Depends, HTTPException, Request, Response + +_logger = logging.getLogger(__name__) + + +Payload = dict[str, Any] + + +class AuthPartner: + def __init__(self, allow_unauthenticated: bool = False): + self.allow_unauthenticated = allow_unauthenticated + + def __call__( + self, + request: Request, + response: Response, + env: Annotated[ + Environment, + Depends(odoo_env), + ], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + fastapi_auth_partner: Annotated[str | None, Cookie()] = None, + ) -> Partner: + if not fastapi_auth_partner and self.allow_unauthenticated: + return env["res.partner"].with_user(env.ref("base.public_user")).browse() + + elif fastapi_auth_partner: + directory = endpoint.sudo().directory_id + try: + vals = URLSafeTimedSerializer( + directory.cookie_secret_key or directory.secret_key + ).loads(fastapi_auth_partner, max_age=directory.cookie_duration * 60) + except Exception as e: + _logger.error("Invalid cookies error %s", e) + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED) from e + if vals["did"] == directory.id and vals["pid"]: + partner = env["res.partner"].browse(vals["pid"]).exists() + if partner: + auth_partner = partner._get_auth_partner_for_directory(directory) + if auth_partner: + if directory.sliding_session: + helper = env["fastapi.auth.service"].new( + {"endpoint_id": endpoint} + ) + helper._set_auth_cookie(auth_partner, request, response) + return partner + _logger.info("Could not determine partner from 'fastapi_auth_partner' cookie.") + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED) + + +auth_partner_authenticated_partner = AuthPartner() +auth_partner_optionally_authenticated_partner = AuthPartner(allow_unauthenticated=True) diff --git a/fastapi_auth_partner/i18n/fastapi_auth_partner.pot b/fastapi_auth_partner/i18n/fastapi_auth_partner.pot new file mode 100644 index 000000000..fd3569e94 --- /dev/null +++ b/fastapi_auth_partner/i18n/fastapi_auth_partner.pot @@ -0,0 +1,263 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fastapi_auth_partner +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__app +msgid "App" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_auth_directory +msgid "Auth Directory" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_partner_id +msgid "Auth Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.ui.menu,name:fastapi_auth_partner.fastapi_auth +msgid "Authentication" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__demo_auth_method +msgid "Authentication method" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Cancel" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_duration +msgid "Cookie Duration" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_secret_key +msgid "Cookie Secret Key" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_uid +msgid "Created by" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_date +msgid "Created on" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__app__demo +msgid "Demo Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__directory_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__directory_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_directory_id +#: model:ir.ui.menu,name:fastapi_auth_partner.auth_directory_menu +msgid "Directory" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__display_name +msgid "Display Name" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__endpoint_id +msgid "Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_fastapi_endpoint +msgid "FastAPI Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__fastapi_endpoint_ids +msgid "FastAPI Endpoints" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_fastapi_auth_service +msgid "Fastapi Auth Service" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__fastapi_endpoint_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_reset_password__fastapi_endpoint_id +msgid "Fastapi Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__id +msgid "ID" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.actions.act_window,name:fastapi_auth_partner.auth_partner_action_impersonate +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Impersonation successful" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_duration +msgid "In minute, default 525600 minutes => 1 year" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner +msgid "Is Auth Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Label" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_partner_view_form +msgid "Local Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/routers/auth.py:0 +#, python-format +msgid "No cookie secret key defined" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Only admin can impersonate locally" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.ui.menu,name:fastapi_auth_partner.auth_partner_menu +msgid "Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__demo_auth_method__auth_partner +msgid "Partner Auth" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Please choose an endpoint:" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Please install base_future_response for local impersonate to work" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_api_url +msgid "Public Api Url" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_url +msgid "Public Url" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_directory_view_form +msgid "Regenerate cookie secret key" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__sliding_session +msgid "Sliding Session" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner +msgid "Technical field to know if the auth method is partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_api_url +msgid "" +"The public URL of the API.\n" +"This URL is used in impersonation to set the cookie on the right API domain if you use a reverse proxy to serve the API.\n" +"Defaults to the public_url if not set or the odoo url if not set either." +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_url +msgid "" +"The public URL of the site.\n" +"This URL is used for the impersonation final redirect. And can also be used in the mail template to construct links.\n" +"Default to the public_api_url if not set or the odoo url if not set either." +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_secret_key +msgid "The secret key used to sign the cookie" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_impersonate +msgid "Wizard Partner Auth Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_reset_password +msgid "Wizard Partner Auth Reset Password" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "" +"You are now impersonating %s\n" +"%%s" +msgstr "" diff --git a/fastapi_auth_partner/i18n/it.po b/fastapi_auth_partner/i18n/it.po new file mode 100644 index 000000000..91674ad11 --- /dev/null +++ b/fastapi_auth_partner/i18n/it.po @@ -0,0 +1,278 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fastapi_auth_partner +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-12-10 11:42+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__app +msgid "App" +msgstr "Applicazione" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_auth_directory +msgid "Auth Directory" +msgstr "Cartella autorizzazione" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_partner_id +msgid "Auth Partner" +msgstr "Partner autorizzazione" + +#. module: fastapi_auth_partner +#: model:ir.ui.menu,name:fastapi_auth_partner.fastapi_auth +msgid "Authentication" +msgstr "Autenticazione" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__demo_auth_method +msgid "Authentication method" +msgstr "Metodo autenticazione" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Cancel" +msgstr "Annulla" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_duration +msgid "Cookie Duration" +msgstr "Durata cookie" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_secret_key +msgid "Cookie Secret Key" +msgstr "Chiave segreta cookie" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fastapi_auth_partner +#: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__app__demo +msgid "Demo Endpoint" +msgstr "Endpoint esempio" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__directory_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__directory_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_directory_id +#: model:ir.ui.menu,name:fastapi_auth_partner.auth_directory_menu +msgid "Directory" +msgstr "Cartella" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__endpoint_id +msgid "Endpoint" +msgstr "Endpoint" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_fastapi_endpoint +msgid "FastAPI Endpoint" +msgstr "Endpoint FastAPI" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__fastapi_endpoint_ids +msgid "FastAPI Endpoints" +msgstr "Endpoint FastAPI" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_fastapi_auth_service +msgid "Fastapi Auth Service" +msgstr "Servizio autenticazione FastAPI" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__fastapi_endpoint_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_reset_password__fastapi_endpoint_id +msgid "Fastapi Endpoint" +msgstr "Endopoint FastAPI" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__id +msgid "ID" +msgstr "ID" + +#. module: fastapi_auth_partner +#: model:ir.actions.act_window,name:fastapi_auth_partner.auth_partner_action_impersonate +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Impersonate" +msgstr "Imita" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Impersonation successful" +msgstr "Imitazione riuscita" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_duration +msgid "In minute, default 525600 minutes => 1 year" +msgstr "In minuti, predefinito minuti => 1 anno" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner +msgid "Is Auth Partner" +msgstr "È partner autorizzazione" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Label" +msgstr "Etichetta" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_partner_view_form +msgid "Local Impersonate" +msgstr "Imitazione locale" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/routers/auth.py:0 +#, python-format +msgid "No cookie secret key defined" +msgstr "Nessuna chiave segreta cookie definita" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Only admin can impersonate locally" +msgstr "Solo l'amministratore può imitare localmente" + +#. module: fastapi_auth_partner +#: model:ir.ui.menu,name:fastapi_auth_partner.auth_partner_menu +msgid "Partner" +msgstr "Partner" + +#. module: fastapi_auth_partner +#: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__demo_auth_method__auth_partner +msgid "Partner Auth" +msgstr "Autorizzazione partner" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Please choose an endpoint:" +msgstr "Scegliere un endpoint:" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Please install base_future_response for local impersonate to work" +msgstr "Installare base_future_response per far funzionare l'imitazione locale" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_api_url +msgid "Public Api Url" +msgstr "URL API pubblico" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_url +msgid "Public Url" +msgstr "URL pubblico" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_directory_view_form +msgid "Regenerate cookie secret key" +msgstr "Rigenera chiave segreta cookie" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__sliding_session +msgid "Sliding Session" +msgstr "Sessione scorrevole" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner +msgid "Technical field to know if the auth method is partner" +msgstr "Campo tecnico per sapere se il metodo di autorizzazione è partner" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_api_url +msgid "" +"The public URL of the API.\n" +"This URL is used in impersonation to set the cookie on the right API domain if you use a reverse proxy to serve the API.\n" +"Defaults to the public_url if not set or the odoo url if not set either." +msgstr "" +"URL pubblico dell'API.\n" +"Questo URL viene utilizzato nell'imitazione per impostare il cookie sul " +"dominio API corretto se si utilizza un reverse proxy per servire l'API.\n" +"Il valore predefinito è public_url se non impostato, oppure l'URL di Odoo se " +"non impostato." + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_url +msgid "" +"The public URL of the site.\n" +"This URL is used for the impersonation final redirect. And can also be used in the mail template to construct links.\n" +"Default to the public_api_url if not set or the odoo url if not set either." +msgstr "" +"URL pubblico del sito.\n" +"Questo URL viene utilizzato per il reindirizzamento finale dell'imitazione. " +"Può anche essere utilizzato nel modello di posta per creare link.\n" +"Impostato di default su public_api_url se non impostato, oppure su Odoo URL " +"se non impostato." + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_secret_key +msgid "The secret key used to sign the cookie" +msgstr "La chiave segreta usata per firmare il cookie" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_impersonate +msgid "Wizard Partner Auth Impersonate" +msgstr "Procedura guidata imitazione autorizzazione partner" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_reset_password +msgid "Wizard Partner Auth Reset Password" +msgstr "Procedura guidata reset password autorizzazione partner" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "" +"You are now impersonating %s\n" +"%%s" +msgstr "" +"Osa si sta imitando %s\n" +"%%s" diff --git a/fastapi_auth_partner/models/__init__.py b/fastapi_auth_partner/models/__init__.py new file mode 100644 index 000000000..526f7a263 --- /dev/null +++ b/fastapi_auth_partner/models/__init__.py @@ -0,0 +1,3 @@ +from . import auth_directory +from . import auth_partner +from . import fastapi_endpoint diff --git a/fastapi_auth_partner/models/auth_directory.py b/fastapi_auth_partner/models/auth_directory.py new file mode 100644 index 000000000..b671bf96d --- /dev/null +++ b/fastapi_auth_partner/models/auth_directory.py @@ -0,0 +1,51 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class AuthDirectory(models.Model): + _inherit = "auth.directory" + + fastapi_endpoint_ids = fields.One2many( + "fastapi.endpoint", + "directory_id", + string="FastAPI Endpoints", + ) + + cookie_secret_key = fields.Char( + groups="base.group_system", + help="The secret key used to sign the cookie", + required=True, + default=lambda self: self._generate_default_secret_key(), + ) + cookie_duration = fields.Integer( + default=525600, + help="In minute, default 525600 minutes => 1 year", + required=True, + ) + sliding_session = fields.Boolean() + + def action_regenerate_cookie_secret_key(self): + self.ensure_one() + self.cookie_secret_key = self._generate_default_secret_key() + + def _prepare_mail_context(self, context): + rv = super()._prepare_mail_context(context) + endpoint_id = self.env.context.get("_fastapi_endpoint_id") + + if endpoint_id: + endpoint = self.env["fastapi.endpoint"].browse(endpoint_id) + rv["public_url"] = endpoint.public_url or endpoint.public_api_url + + return rv + + @property + def _server_env_fields(self): + return { + **super()._server_env_fields, + "cookie_secret_key": {}, + } diff --git a/fastapi_auth_partner/models/auth_partner.py b/fastapi_auth_partner/models/auth_partner.py new file mode 100644 index 000000000..54018d1ad --- /dev/null +++ b/fastapi_auth_partner/models/auth_partner.py @@ -0,0 +1,84 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.exceptions import AccessDenied, UserError +from odoo.http import request + + +class AuthPartner(models.Model): + _inherit = "auth.partner" + + def local_impersonate(self): + """Local impersonate for dev mode""" + self.ensure_one() + + if not self.env.user._is_admin(): + raise AccessDenied(self.env._("Only admin can impersonate locally")) + + if not hasattr(request, "future_response"): + raise UserError( + self.env._( + "Please install base_future_response for local impersonate to work" + ) + ) + + for endpoint in self.directory_id.fastapi_endpoint_ids: + helper = self.env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + helper._set_auth_cookie(self, request.httprequest, request.future_response) + + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": self.env._("Impersonation successful"), + "message": self.env._("You are now impersonating %s\n%%s") % self.login, + "links": [ + { + "label": f"{endpoint.app.title()} api docs", + "url": endpoint.docs_url, + } + for endpoint in self.directory_id.fastapi_endpoint_ids + ], + "type": "success", + "sticky": False, + }, + } + + def _get_impersonate_url(self, token, **kwargs): + endpoint = kwargs.get("endpoint") + if not endpoint: + return super()._get_impersonate_url(token, **kwargs) + + base = ( + endpoint.public_api_url + or endpoint.public_url + or ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url") + + endpoint.root_path + ) + ) + return f"{base.rstrip('/')}/auth/impersonate/{token}" + + def _get_impersonate_action(self, token, **kwargs): + # Get the endpoint from a wizard + endpoint_id = self.env.context.get("fastapi_endpoint_id") + endpoint = None + + if endpoint_id: + endpoint = self.env["fastapi.endpoint"].browse(endpoint_id) + + if not endpoint: + endpoints = self.directory_id.fastapi_endpoint_ids + if len(endpoints) == 1: + endpoint = endpoints + else: + wizard = self.env["ir.actions.act_window"]._for_xml_id( + "fastapi_auth_partner.auth_partner_action_impersonate" + ) + wizard["context"] = {"default_auth_partner_id": self.id} + return wizard + + return super()._get_impersonate_action(token, endpoint=endpoint, **kwargs) diff --git a/fastapi_auth_partner/models/fastapi_endpoint.py b/fastapi_auth_partner/models/fastapi_endpoint.py new file mode 100644 index 000000000..8fa9491ba --- /dev/null +++ b/fastapi_auth_partner/models/fastapi_endpoint.py @@ -0,0 +1,54 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + +from fastapi import APIRouter + +from ..routers.auth import auth_router + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + app = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + demo_auth_method = fields.Selection( + selection_add=[ + ("auth_partner", "Partner Auth"), + ], + string="Authentication method", + ) + directory_id = fields.Many2one("auth.directory") + + is_auth_partner = fields.Boolean( + compute="_compute_is_auth_partner", + help="Technical field to know if the auth method is partner", + ) + public_api_url: str = fields.Char( + help="The public URL of the API.\n" + "This URL is used in impersonation to set the cookie on the right API " + "domain if you use a reverse proxy to serve the API.\n" + "Defaults to the public_url if not set or the odoo url if not set either." + ) + # More info in https://github.com/OCA/rest-framework/pull/438/files + public_url: str = fields.Char( + help="The public URL of the site.\n" + "This URL is used for the impersonation final redirect. " + "And can also be used in the mail template to construct links.\n" + "Default to the public_api_url if not set or the odoo url if not set either." + ) + + def _get_fastapi_routers(self) -> list[APIRouter]: + routers = super()._get_fastapi_routers() + if self.app == "demo" and self.demo_auth_method == "auth_partner": + routers.append(auth_router) + return routers + + def _compute_is_auth_partner(self): + for rec in self: + rec.is_auth_partner = auth_router in rec._get_fastapi_routers() diff --git a/fastapi_auth_partner/pyproject.toml b/fastapi_auth_partner/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/fastapi_auth_partner/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fastapi_auth_partner/readme/CONTRIBUTORS.md b/fastapi_auth_partner/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..6079ca504 --- /dev/null +++ b/fastapi_auth_partner/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Akretion](https://www.akretion.com): + - Sébastien Beau + - Florian Mounier diff --git a/fastapi_auth_partner/readme/DESCRIPTION.md b/fastapi_auth_partner/readme/DESCRIPTION.md new file mode 100644 index 000000000..8d4456e52 --- /dev/null +++ b/fastapi_auth_partner/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module is the FastAPI implementation of +[auth_partner](../auth_partner) it provides all the routes to manage the +authentication of partners. diff --git a/fastapi_auth_partner/readme/USAGE.md b/fastapi_auth_partner/readme/USAGE.md new file mode 100644 index 000000000..fc6bc9024 --- /dev/null +++ b/fastapi_auth_partner/readme/USAGE.md @@ -0,0 +1,55 @@ +First you have to add the auth router to your FastAPI endpoint and the +authentication dependency to your app dependencies: + +``` python +from odoo.addons.fastapi import dependencies +from odoo.addons.fastapi_auth_partner.dependencies import ( + auth_partner_authenticated_partner, +) +from odoo.addons.fastapi_auth_partner.routers.auth import auth_router + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + def _get_fastapi_routers(self): + if self.app == "myapp": + return [ + auth_router, + ] + return super()._get_fastapi_routers() + + def _get_app_dependencies_overrides(self): + res = super()._get_app_dependencies_overrides() + if self.app == "myapp": + res.update( + { + dependencies.authenticated_partner_impl: auth_partner_authenticated_partner, + } + ) + return res +``` + +Next you can manage your authenticable partners and directories in the +Odoo interface: + +FastAPI \> Authentication \> Partner + +and + +FastAPI \> Authentication \> Directory + +Next you must set the directory used for the authentication in the +FastAPI endpoint: + +FastAPI \> FastAPI Endpoint \> myapp \> Directory + +Then you can use the auth router to authenticate your requests: + +- POST /auth/register to register a partner +- POST /auth/login to authenticate a partner +- POST /auth/logout to unauthenticate a partner +- POST /auth/validate_email to validate a partner email +- POST /auth/request_reset_password to request a password reset +- POST /auth/set_password to set a new password +- GET /auth/profile to get the partner profile +- GET /auth/impersonate to impersonate a partner diff --git a/fastapi_auth_partner/routers/__init__.py b/fastapi_auth_partner/routers/__init__.py new file mode 100644 index 000000000..582cb2cd7 --- /dev/null +++ b/fastapi_auth_partner/routers/__init__.py @@ -0,0 +1 @@ +from .auth import auth_router diff --git a/fastapi_auth_partner/routers/auth.py b/fastapi_auth_partner/routers/auth.py new file mode 100644 index 000000000..8ec37845f --- /dev/null +++ b/fastapi_auth_partner/routers/auth.py @@ -0,0 +1,257 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta, timezone +from typing import Annotated + +from itsdangerous import URLSafeTimedSerializer + +from odoo import fields, models, tools +from odoo.api import Environment +from odoo.exceptions import ValidationError + +from odoo.addons.base.models.res_partner import Partner +from odoo.addons.fastapi.dependencies import fastapi_endpoint, odoo_env +from odoo.addons.fastapi.models import FastapiEndpoint + +from fastapi import APIRouter, Depends, Request, Response +from fastapi.responses import RedirectResponse + +from ..dependencies import auth_partner_authenticated_partner +from ..schemas import ( + AuthForgetPasswordInput, + AuthLoginInput, + AuthPartnerResponse, + AuthRegisterInput, + AuthSetPasswordInput, + AuthValidateEmailInput, +) + +COOKIE_AUTH_NAME = "fastapi_auth_partner" + +auth_router = APIRouter(tags=["auth"]) + + +@auth_router.post("/auth/register", status_code=201) +def register( + data: AuthRegisterInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + request: Request, + response: Response, +) -> AuthPartnerResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._signup(data) + helper._set_auth_cookie(auth_partner, request, response) + return AuthPartnerResponse.from_auth_partner(auth_partner) + + +@auth_router.post("/auth/login") +def login( + data: AuthLoginInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + request: Request, + response: Response, +) -> AuthPartnerResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._login(data) + helper._set_auth_cookie(auth_partner, request, response) + return AuthPartnerResponse.from_auth_partner(auth_partner) + + +@auth_router.post("/auth/logout", status_code=205) +def logout( + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + response: Response, +): + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + helper._logout() + helper._clear_auth_cookie(response) + return {} + + +@auth_router.post("/auth/validate_email") +def validate_email( + data: AuthValidateEmailInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], +): + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + helper._validate_email(data) + return {} + + +@auth_router.post("/auth/request_reset_password") +def request_reset_password( + data: AuthForgetPasswordInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], +): + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + helper._request_reset_password(data) + return {} + + +@auth_router.post("/auth/set_password") +def set_password( + data: AuthSetPasswordInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + request: Request, + response: Response, +) -> AuthPartnerResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._set_password(data) + helper._set_auth_cookie(auth_partner, request, response) + return AuthPartnerResponse.from_auth_partner(auth_partner) + + +@auth_router.get("/auth/profile") +def profile( + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + partner: Annotated[Partner, Depends(auth_partner_authenticated_partner)], +) -> AuthPartnerResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._get_auth_from_partner(partner) + return AuthPartnerResponse.from_auth_partner(auth_partner) + + +@auth_router.get("/auth/impersonate/{token}") +def impersonate( + token: str, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + request: Request, +) -> RedirectResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._impersonate(token) + base = ( + endpoint.public_url + or endpoint.public_api_url + or ( + env["ir.config_parameter"].sudo().get_param("web.base.url") + + endpoint.root_path + ) + ) + response = RedirectResponse(url=base) + helper._set_auth_cookie(auth_partner, request, response) + return response + + +class AuthService(models.AbstractModel): + _name = "fastapi.auth.service" + _description = "Fastapi Auth Service" + + endpoint_id = fields.Many2one("fastapi.endpoint", required=True) + directory_id = fields.Many2one("auth.directory") + + def new(self, vals, **kwargs): + # As we are in an abstract model, we need to bypass cache since it's + # based on the record id (and there is none) + endpoint = vals.pop("endpoint_id") + + rec = super().new(vals, **kwargs) + # Can't have computed / related field in AbstractModel + rec.endpoint_id = endpoint + rec.directory_id = rec.endpoint_id.directory_id + # Auto add endpoint context for mail context + return rec.with_context(_fastapi_endpoint_id=endpoint.id) + + def _get_auth_from_partner(self, partner): + return partner._get_auth_partner_for_directory(self.directory_id) + + def _signup(self, data): + auth_partner = ( + self.env["auth.partner"] + .sudo() + ._signup(self.directory_id, **data.model_dump()) + ) + return auth_partner + + def _login(self, data): + return ( + self.env["auth.partner"] + .sudo() + ._login(self.directory_id, **data.model_dump()) + ) + + def _impersonate(self, token): + return self.env["auth.partner"].sudo()._impersonating(self.directory_id, token) + + def _logout(self): + pass + + def _set_password(self, data): + return ( + self.env["auth.partner"] + .sudo() + ._set_password(self.directory_id, data.token, data.password) + ) + + def _request_reset_password(self, data): + # There can be only one auth_partner per login per directory + auth_partner = ( + self.env["auth.partner"] + .sudo() + .search( + [ + ("directory_id", "=", self.directory_id.id), + ("login", "=", data.login.lower()), + ] + ) + ) + + if not auth_partner: + # do not leak information, no partner no mail sent + return + + return auth_partner.sudo()._request_reset_password() + + def _validate_email(self, data): + return ( + self.env["auth.partner"] + .sudo() + ._validate_email(self.directory_id, data.token) + ) + + def _prepare_cookie_payload(self, partner): + # use short key to reduce cookie size + return { + "did": self.directory_id.id, + "pid": partner.id, + } + + def _prepare_cookie(self, partner): + secret = self.directory_id.cookie_secret_key or self.directory_id.secret_key + if not secret: + raise ValidationError(self.env._("No cookie secret key defined")) + payload = self._prepare_cookie_payload(partner) + value = URLSafeTimedSerializer(secret).dumps(payload) + exp = ( + datetime.now(timezone.utc) + + timedelta(minutes=self.directory_id.cookie_duration) + ).timestamp() + vals = { + "value": value, + "expires": exp, + "httponly": True, + "secure": True, + "samesite": "strict", + } + if tools.config.get("test_enable"): + # do not force https for test + vals["secure"] = False + return vals + + def _set_auth_cookie(self, auth_partner, request, response): + response.set_cookie( + COOKIE_AUTH_NAME, **self.sudo()._prepare_cookie(auth_partner.partner_id) + ) + + def _clear_auth_cookie(self, response): + response.set_cookie(COOKIE_AUTH_NAME, max_age=0) diff --git a/fastapi_auth_partner/schemas.py b/fastapi_auth_partner/schemas.py new file mode 100644 index 000000000..27bec5f05 --- /dev/null +++ b/fastapi_auth_partner/schemas.py @@ -0,0 +1,40 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from extendable_pydantic import StrictExtendableBaseModel + + +class AuthLoginInput(StrictExtendableBaseModel): + login: str + password: str + + +class AuthRegisterInput(StrictExtendableBaseModel): + name: str + login: str + password: str + + +class AuthForgetPasswordInput(StrictExtendableBaseModel): + login: str + + +class AuthSetPasswordInput(StrictExtendableBaseModel): + token: str + password: str + + +class AuthValidateEmailInput(StrictExtendableBaseModel): + token: str + + +class AuthPartnerResponse(StrictExtendableBaseModel): + login: str + mail_verified: bool + + @classmethod + def from_auth_partner(cls, odoo_rec): + return cls.model_construct( + login=odoo_rec.login, mail_verified=odoo_rec.mail_verified + ) diff --git a/fastapi_auth_partner/security/ir.model.access.csv b/fastapi_auth_partner/security/ir.model.access.csv new file mode 100644 index 000000000..b52ae0751 --- /dev/null +++ b/fastapi_auth_partner/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +api_access_fastapi_wizard_auth_partner_impersonate,fastapi_wizard_auth_partner_impersonate,model_wizard_auth_partner_impersonate,auth_partner.group_auth_partner_manager,1,1,1,1 diff --git a/fastapi_auth_partner/security/ir_rule.xml b/fastapi_auth_partner/security/ir_rule.xml new file mode 100644 index 000000000..3d599bd0f --- /dev/null +++ b/fastapi_auth_partner/security/ir_rule.xml @@ -0,0 +1,26 @@ + + + + Auth API (res_partner) + + + [('id','=', authenticated_partner_id)] + + + + + + + + Auth API (auth_partner) + + + [('partner_id','=', authenticated_partner_id)] + + + + + + diff --git a/fastapi_auth_partner/security/res_group.xml b/fastapi_auth_partner/security/res_group.xml new file mode 100644 index 000000000..e1879c2f3 --- /dev/null +++ b/fastapi_auth_partner/security/res_group.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/fastapi_auth_partner/static/description/icon.png b/fastapi_auth_partner/static/description/icon.png new file mode 100644 index 000000000..1dcc49c24 Binary files /dev/null and b/fastapi_auth_partner/static/description/icon.png differ diff --git a/fastapi_auth_partner/static/description/index.html b/fastapi_auth_partner/static/description/index.html new file mode 100644 index 000000000..ee8504325 --- /dev/null +++ b/fastapi_auth_partner/static/description/index.html @@ -0,0 +1,483 @@ + + + + + +Fastapi Auth Partner + + + +
+

Fastapi Auth Partner

+ + +

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This module is the FastAPI implementation of +auth_partner it provides all the routes to manage +the authentication of partners.

+

Table of contents

+ +
+

Usage

+

First you have to add the auth router to your FastAPI endpoint and the +authentication dependency to your app dependencies:

+
+from odoo.addons.fastapi import dependencies
+from odoo.addons.fastapi_auth_partner.dependencies import (
+  auth_partner_authenticated_partner,
+)
+from odoo.addons.fastapi_auth_partner.routers.auth import auth_router
+
+class FastapiEndpoint(models.Model):
+    _inherit = "fastapi.endpoint"
+
+    def _get_fastapi_routers(self):
+      if self.app == "myapp":
+          return [
+              auth_router,
+          ]
+      return super()._get_fastapi_routers()
+
+    def _get_app_dependencies_overrides(self):
+        res = super()._get_app_dependencies_overrides()
+        if self.app == "myapp":
+            res.update(
+                {
+                    dependencies.authenticated_partner_impl: auth_partner_authenticated_partner,
+                }
+            )
+        return res
+
+

Next you can manage your authenticable partners and directories in the +Odoo interface:

+

FastAPI > Authentication > Partner

+

and

+

FastAPI > Authentication > Directory

+

Next you must set the directory used for the authentication in the +FastAPI endpoint:

+

FastAPI > FastAPI Endpoint > myapp > Directory

+

Then you can use the auth router to authenticate your requests:

+
    +
  • POST /auth/register to register a partner
  • +
  • POST /auth/login to authenticate a partner
  • +
  • POST /auth/logout to unauthenticate a partner
  • +
  • POST /auth/validate_email to validate a partner email
  • +
  • POST /auth/request_reset_password to request a password reset
  • +
  • POST /auth/set_password to set a new password
  • +
  • GET /auth/profile to get the partner profile
  • +
  • GET /auth/impersonate to impersonate a partner
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+
    +
  • Akretion:
      +
    • Sébastien Beau
    • +
    • Florian Mounier
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

paradoxxxzero

+

This module is part of the OCA/rest-framework project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/fastapi_auth_partner/tests/__init__.py b/fastapi_auth_partner/tests/__init__.py new file mode 100644 index 000000000..021c23763 --- /dev/null +++ b/fastapi_auth_partner/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_auth +from . import test_fastapi_auth_partner_demo diff --git a/fastapi_auth_partner/tests/test_auth.py b/fastapi_auth_partner/tests/test_auth.py new file mode 100644 index 000000000..fa5686320 --- /dev/null +++ b/fastapi_auth_partner/tests/test_auth.py @@ -0,0 +1,236 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager +from functools import partial + +from requests import Response + +from odoo.tests.common import tagged +from odoo.tools import mute_logger + +from odoo.addons.auth_partner.tests.common import CommonTestAuthPartner +from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase +from odoo.addons.fastapi.dependencies import fastapi_endpoint + +from fastapi import status + +from ..routers.auth import auth_router + + +class CommonTestAuth(FastAPITransactionCase): + @contextmanager + def _create_test_client(self, **kwargs): + self.env.invalidate_all() + with mute_logger("httpx"): + with super()._create_test_client(**kwargs) as test_client: + yield test_client + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.demo_app = cls.env.ref("fastapi_auth_partner.fastapi_endpoint_demo") + cls.env = cls.env(context=dict(cls.env.context, queue_job__no_delay=True)) + cls.default_fastapi_router = auth_router + cls.default_fastapi_app = cls.demo_app._get_app() + cls.default_fastapi_dependency_overrides = { + fastapi_endpoint: partial(lambda a: a, cls.demo_app) + } + cls.default_fastapi_odoo_env = cls.env + cls.default_fastapi_running_user = cls.demo_app.user_id + + def _register_partner(self): + with self._create_test_client() as test_client, self.new_mails() as new_mails: + response: Response = test_client.post( + "/auth/register", + json={ + "name": "Loriot", + "login": "loriot@example.org", + "password": "supersecret", + }, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) + return response, new_mails + + def _login(self, test_client, password="supersecret"): + response: Response = test_client.post( + "/auth/login", + json={ + "login": "loriot@example.org", + "password": password, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + return response + + +@tagged("post_install", "-at_install") +class TestFastapiAuthPartner(CommonTestAuth, CommonTestAuthPartner): + def test_register(self): + response, new_mails = self._register_partner() + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) + self.assertEqual( + response.json(), {"login": "loriot@example.org", "mail_verified": False} + ) + self.assertEqual(len(new_mails), 1) + self.assertIn( + "please click on the following link to verify your email", + str(new_mails.body), + ) + + def test_login(self): + self._register_partner() + with self._create_test_client() as test_client: + response = self._login(test_client) + self.assertEqual( + response.json(), {"login": "loriot@example.org", "mail_verified": False} + ) + + def test_logout(self): + self._register_partner() + with self._create_test_client() as test_client: + response: Response = test_client.post("/auth/logout") + self.assertEqual( + response.status_code, status.HTTP_205_RESET_CONTENT, response.text + ) + + def test_request_reset_password(self): + self._register_partner() + with self._create_test_client() as test_client, self.new_mails() as new_mails: + response: Response = test_client.post( + "/auth/request_reset_password", + json={"login": "loriot@example.org"}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + self.assertFalse( + self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .mail_verified, + ) + self.assertEqual(len(new_mails), 1) + self.assertIn( + "Click on the following link to reset your password", + str(new_mails.body), + ) + token = str(new_mails.body).split("token=")[1].split('">')[0] + response: Response = test_client.post( + "/auth/set_password", + json={ + "password": "megasecret", + "token": token, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + + self.assertTrue( + self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .mail_verified, + ) + response = self._login(test_client, password="megasecret") + self.assertEqual( + response.json(), {"login": "loriot@example.org", "mail_verified": True} + ) + + def test_validate_email(self): + self._register_partner() + mail = self.env["mail.mail"].search([], limit=1, order="id desc") + self.assertIn( + "please click on the following link to verify your email", str(mail.body) + ) + self.assertFalse( + self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .mail_verified, + ) + token = str(mail.body).split("token=")[1].split('">')[0] + with self._create_test_client() as test_client: + response: Response = test_client.post( + "/auth/validate_email", + json={"token": token}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + + self.assertTrue( + self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .mail_verified, + ) + + def test_impersonate(self): + self.demo_app.public_url = self.demo_app.public_api_url = False + self._register_partner() + auth_partner = self.env["auth.partner"].search( + [("login", "=", "loriot@example.org")] + ) + self.assertEqual(len(auth_partner), 1) + action = auth_partner.with_user(self.env.ref("base.user_admin")).impersonate() + url = action["url"].split("fastapi_auth_partner_demo", 1)[1] + + with self._create_test_client() as test_client: + response: Response = test_client.get(url, follow_redirects=False) + self.assertEqual(response.status_code, status.HTTP_307_TEMPORARY_REDIRECT) + self.assertTrue( + response.headers["location"].endswith("/fastapi_auth_partner_demo") + ) + self.assertIn("fastapi_auth_partner", response.cookies) + + def test_impersonate_api_url(self): + self._register_partner() + auth_partner = self.env["auth.partner"].search( + [("login", "=", "loriot@example.org")] + ) + self.assertEqual(len(auth_partner), 1) + action = auth_partner.with_user(self.env.ref("base.user_admin")).impersonate() + self.assertTrue( + action["url"].startswith("https://api.example.com/auth/impersonate/") + ) + action["url"].split("auth/impersonate/", 1)[1] + + def test_wizard_auth_partner_impersonate(self): + self._register_partner() + action = ( + self.env["wizard.auth.partner.impersonate"] + .create( + { + "auth_partner_id": self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .id, + "fastapi_endpoint_id": self.demo_app.id, + } + ) + .with_user(self.env.ref("base.user_admin")) + .action_impersonate() + ) + self.assertTrue( + action["url"].startswith("https://api.example.com/auth/impersonate/") + ) + + def test_wizard_auth_partner_reset_password(self): + self._register_partner() + + template = self.env.ref("auth_partner.email_reset_password") + template.body_html = template.body_html.replace( + "https://example.org/", "{{ object.env.context['public_url'] }}" + ) + with self.new_mails() as new_mails: + self.env["wizard.auth.partner.reset.password"].create( + { + "delay": "2-days", + "template_id": template.id, + "fastapi_endpoint_id": self.demo_app.id, + } + ).with_context( + active_ids=self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .ids + ).action_reset_password() + + self.assertEqual(len(new_mails), 1) + self.assertIn( + "Click on the following link to reset your password", str(new_mails.body) + ) + self.assertIn( + "https://www.example.com/password/reset?token=", str(new_mails.body) + ) diff --git a/fastapi_auth_partner/tests/test_fastapi_auth_partner_demo.py b/fastapi_auth_partner/tests/test_fastapi_auth_partner_demo.py new file mode 100644 index 000000000..73005c89c --- /dev/null +++ b/fastapi_auth_partner/tests/test_fastapi_auth_partner_demo.py @@ -0,0 +1,90 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +from typing import Annotated + +from odoo import tests +from odoo.tools import mute_logger + +from odoo.addons.base.models.res_partner import Partner +from odoo.addons.fastapi_auth_partner.dependencies import AuthPartner +from odoo.addons.fastapi_auth_partner.routers.auth import auth_router +from odoo.addons.fastapi_auth_partner.schemas import AuthPartnerResponse + +from fastapi import Depends, status + + +@auth_router.get("/auth/whoami-public-or-partner") +def whoami_public_or_partner( + partner: Annotated[ + Partner, + Depends(AuthPartner(allow_unauthenticated=True)), + ], +) -> AuthPartnerResponse: + if partner: + return AuthPartnerResponse.from_auth_partner(partner.auth_partner_ids) + return AuthPartnerResponse(login="no-one", mail_verified=False) + + +@tests.tagged("post_install", "-at_install") +class TestEndToEnd(tests.HttpCase): + def setUp(self): + super().setUp() + endpoint = self.env.ref("fastapi_auth_partner.fastapi_endpoint_demo") + endpoint._handle_registry_sync() + + self.fastapi_demo_app = self.env.ref("fastapi.fastapi_endpoint_demo") + self.fastapi_demo_app._handle_registry_sync() + + def _register_partner(self): + return self.url_open( + "/fastapi_auth_partner_demo/auth/register", + timeout=1000, + headers={"Content-Type": "application/json"}, + data=json.dumps( + { + "name": "Loriot", + "login": "loriot@example.org", + "password": "supersecret", + } + ), + ) + + def test_register(self): + response = self._register_partner() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual( + response.json(), {"login": "loriot@example.org", "mail_verified": False} + ) + self.assertIn("fastapi_auth_partner", response.cookies) + + def test_profile(self): + self._register_partner() + resp = self.url_open("/fastapi_auth_partner_demo/auth/profile") + resp.raise_for_status() + data = resp.json() + self.assertEqual( + data, + {"login": "loriot@example.org", "mail_verified": False}, + ) + + @mute_logger("odoo.http", "odoo.addons.base.models.assetsbundle") + def test_profile_forbidden(self): + """A end-to-end test with negative authentication.""" + resp = self.url_open("/fastapi_auth_partner_demo/auth/profile") + self.assertEqual(resp.status_code, 401) + + def test_public(self): + """A end-to-end test for anonymous/public access.""" + resp = self.url_open("/fastapi_auth_partner_demo/auth/whoami-public-or-partner") + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), {"login": "no-one", "mail_verified": False}) + + self._register_partner() + resp = self.url_open("/fastapi_auth_partner_demo/auth/whoami-public-or-partner") + self.assertEqual( + resp.json(), {"login": "loriot@example.org", "mail_verified": False} + ) diff --git a/fastapi_auth_partner/views/auth_directory_view.xml b/fastapi_auth_partner/views/auth_directory_view.xml new file mode 100644 index 000000000..fd115531c --- /dev/null +++ b/fastapi_auth_partner/views/auth_directory_view.xml @@ -0,0 +1,29 @@ + + + + auth.directory + + +
+
+ + + + + + +
+ + +
diff --git a/fastapi_auth_partner/views/auth_partner_view.xml b/fastapi_auth_partner/views/auth_partner_view.xml new file mode 100644 index 000000000..0417e1f32 --- /dev/null +++ b/fastapi_auth_partner/views/auth_partner_view.xml @@ -0,0 +1,31 @@ + + + + auth.partner + + + + + + + + + diff --git a/fastapi_auth_partner/views/fastapi_endpoint_view.xml b/fastapi_auth_partner/views/fastapi_endpoint_view.xml new file mode 100644 index 000000000..c7c3f66ac --- /dev/null +++ b/fastapi_auth_partner/views/fastapi_endpoint_view.xml @@ -0,0 +1,22 @@ + + + + fastapi.endpoint + + + + + + + + + + + + + + diff --git a/fastapi_auth_partner/wizards/__init__.py b/fastapi_auth_partner/wizards/__init__.py new file mode 100644 index 000000000..adc3f5233 --- /dev/null +++ b/fastapi_auth_partner/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import wizard_auth_partner_impersonate +from . import wizard_auth_partner_reset_password diff --git a/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate.py b/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate.py new file mode 100644 index 000000000..8d04cef3c --- /dev/null +++ b/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate.py @@ -0,0 +1,29 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class WizardAuthPartnerImpersonate(models.TransientModel): + _name = "wizard.auth.partner.impersonate" + _description = "Wizard Partner Auth Impersonate" + + auth_partner_id = fields.Many2one( + "auth.partner", + required=True, + ) + auth_directory_id = fields.Many2one( + "auth.directory", + related="auth_partner_id.directory_id", + ) + fastapi_endpoint_id = fields.Many2one( + "fastapi.endpoint", + required=True, + ) + + def action_impersonate(self): + return self.auth_partner_id.with_context( + fastapi_endpoint_id=self.fastapi_endpoint_id.id + ).impersonate() diff --git a/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate_view.xml b/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate_view.xml new file mode 100644 index 000000000..bfe5201ec --- /dev/null +++ b/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate_view.xml @@ -0,0 +1,40 @@ + + + + wizard.auth.partner.impersonate + +
+ Please choose an endpoint: + + + + +
+
+ +
+
+
+ + + Impersonate + wizard.auth.partner.impersonate + ir.actions.act_window + form + new + + +
diff --git a/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password.py b/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password.py new file mode 100644 index 000000000..5b3eeecd2 --- /dev/null +++ b/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password.py @@ -0,0 +1,18 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class WizardAuthPartnerResetPassword(models.TransientModel): + _inherit = "wizard.auth.partner.reset.password" + + fastapi_endpoint_id = fields.Many2one( + "fastapi.endpoint", + ) + + def action_reset_password(self): + if self.fastapi_endpoint_id: + self = self.with_context(_fastapi_endpoint_id=self.fastapi_endpoint_id.id) + return super().action_reset_password() diff --git a/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password_view.xml b/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password_view.xml new file mode 100644 index 000000000..49e2edf1d --- /dev/null +++ b/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password_view.xml @@ -0,0 +1,15 @@ + + + + wizard.auth.partner.reset.password + + + + + + + + diff --git a/requirements.txt b/requirements.txt index b37f64671..e70ed9964 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ cryptography extendable-pydantic>=1.2.0 extendable>=0.0.4 fastapi>=0.110.0 +itsdangerous parse-accept-language pydantic>=2.0.0 pyjwt diff --git a/test-requirements.txt b/test-requirements.txt index cf108f84e..bf5c9c46e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,3 @@ +odoo-addon-auth-partner @ git+https://github.com/OCA/rest-framework.git@refs/pull/580/head#subdirectory=auth_partner odoo_test_helper httpx