diff --git a/auth_partner/README.rst b/auth_partner/README.rst new file mode 100644 index 000000000..c0d848686 --- /dev/null +++ b/auth_partner/README.rst @@ -0,0 +1,109 @@ +============ +Partner Auth +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:33a8bc75dc8127331753aa9a54fe3a5b56f7d51a23cc7e9eb0000cc55f78c689 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/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-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 adds to the partners the ability to authenticate through +directories. + +This module does not implement any routing, it only provides the basic +mechanisms in a directory for: + + - Registering a partner and sending an welcome email (to validate + email address): \_signup + - Authenticating a partner: \_login + - Validating a partner email using a token: \_validate_email + - Impersonating: \_impersonate, \_impersonating + - Resetting the password with a unique token sent by mail: + \_request_reset_password, \_set_password + - Sending an invite mail when registering a partner from odoo + interface for the partner to enter a password: \_send_invite, + \_set_password + +For a routing implementation, see the +`fastapi_auth_partner <../fastapi_auth_partner>`__ module. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This module isn't meant to be used standalone but you can still see the +directories and authenticable partners in: + +Settings > Technical > Partner Authentication > Partner + +and + +Settings > Technical > Partner Authentication > Directory + +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. + +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/auth_partner/__init__.py b/auth_partner/__init__.py new file mode 100644 index 000000000..aee8895e7 --- /dev/null +++ b/auth_partner/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/auth_partner/__manifest__.py b/auth_partner/__manifest__.py new file mode 100644 index 000000000..6a18a5580 --- /dev/null +++ b/auth_partner/__manifest__.py @@ -0,0 +1,37 @@ +# 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": "Partner Auth", + "summary": "Implements the base features for a authenticable partner", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/rest-framework", + "depends": [ + "auth_signup", + "mail", + "queue_job", + "server_environment", + ], + "data": [ + "security/res_group.xml", + "security/ir.model.access.csv", + "data/email_data.xml", + "wizards/wizard_auth_partner_force_set_password_view.xml", + "wizards/wizard_auth_partner_reset_password_view.xml", + "views/auth_partner_view.xml", + "views/auth_directory_view.xml", + "views/res_partner_view.xml", + ], + "demo": [ + "demo/res_partner_demo.xml", + "demo/auth_directory_demo.xml", + "demo/auth_partner_demo.xml", + ], + "external_dependencies": { + "python": ["itsdangerous", "pyjwt"], + }, +} diff --git a/auth_partner/data/email_data.xml b/auth_partner/data/email_data.xml new file mode 100644 index 000000000..09cdbf737 --- /dev/null +++ b/auth_partner/data/email_data.xml @@ -0,0 +1,66 @@ + + + + Auth Directory: Reset Password + noreply@example.org + Reset Password + {{object.partner_id.id}} + + + {{object.partner_id.lang}} + +
+ Hi + Click on the following link to reset your password + Reset Password +
+
+
+ + + Auth Directory: Set Password + noreply@example.org + Welcome + {{object.partner_id.id}} + + + {{object.partner_id.lang}} + +
+ Hi + Welcome, your account have been created + Click on the following link to set your password + Set Password +
+
+
+ + + Auth Directory: Validate Email + noreply@example.org + Welcome + {{object.partner_id.id}} + + + {{object.partner_id.lang}} + +
+ Hi + Welcome to the site, please click on the following link to verify your email + Validate Email +
+
+
+
diff --git a/auth_partner/demo/auth_directory_demo.xml b/auth_partner/demo/auth_directory_demo.xml new file mode 100644 index 000000000..81708b69a --- /dev/null +++ b/auth_partner/demo/auth_directory_demo.xml @@ -0,0 +1,9 @@ + + + + Demo Auth Directory + + + + + diff --git a/auth_partner/demo/auth_partner_demo.xml b/auth_partner/demo/auth_partner_demo.xml new file mode 100644 index 000000000..93dda262c --- /dev/null +++ b/auth_partner/demo/auth_partner_demo.xml @@ -0,0 +1,8 @@ + + + + + + Super-secret$1 + + diff --git a/auth_partner/demo/res_partner_demo.xml b/auth_partner/demo/res_partner_demo.xml new file mode 100644 index 000000000..43a063524 --- /dev/null +++ b/auth_partner/demo/res_partner_demo.xml @@ -0,0 +1,7 @@ + + + + Demo auth partner + partner-auth@example.org + + diff --git a/auth_partner/i18n/auth_partner.pot b/auth_partner/i18n/auth_partner.pot new file mode 100644 index 000000000..9f79e2fb7 --- /dev/null +++ b/auth_partner/i18n/auth_partner.pot @@ -0,0 +1,554 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * 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: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__14-days +msgid "14 Days" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__2-days +msgid "2-days" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__6-hours +msgid "6 Hours" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__7-days +msgid "7 Days" +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_reset_password +msgid "" +"
\n" +" Hi \n" +" Click on the following link to reset your password\n" +" Reset Password\n" +"
\n" +" " +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_validate_email +msgid "" +"
\n" +" Hi \n" +" Welcome to the site, please click on the following link to verify your email\n" +" Validate Email\n" +"
\n" +" " +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_set_password +msgid "" +"
\n" +" Hi \n" +" Welcome, your account have been created\n" +" Click on the following link to set your password\n" +" Set Password\n" +"
\n" +" " +msgstr "" + +#. module: auth_partner +#: model:res.groups,name:auth_partner.group_auth_partner_api +msgid "API Partner Auth Access" +msgstr "" + +#. module: auth_partner +#: model:res.groups,name:auth_partner.group_auth_partner_manager +msgid "API Partner Auth Manager" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.view_partner_form +msgid "Account" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "" +"An email will be send with a token to each customer, you can specify the " +"date until the link is valid" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_auth_directory +msgid "Auth Directory" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_reset_password +msgid "Auth Directory: Reset Password" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_set_password +msgid "Auth Directory: Set Password" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_validate_email +msgid "Auth Directory: Validate Email" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Auth Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_count +#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_count +msgid "Auth Partner Count" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__auth_partner_ids +msgid "Auth Partners" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Cancel" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password_confirm +msgid "Confirm Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_res_partner +msgid "Contact" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__count_partner +msgid "Count Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_uid +msgid "Created by" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_date +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_date +msgid "Created on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_impersonation +msgid "Date Last Impersonation" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_request_reset_pwd +msgid "Date Last Request Reset Pwd" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd +msgid "Date Last Sucessfull Reset Pwd" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__date_validity +msgid "Date Validity" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_request_reset_pwd +msgid "Date of the last password reset request" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_impersonation +msgid "Date of the last sucessfull impersonation" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd +msgid "Date of the last sucessfull password reset" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__delay +msgid "Delay" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_directory_action +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__directory_id +#: model:ir.ui.menu,name:auth_partner.auth_directory_menu +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Directory" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_search +msgid "Directory Auth" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__display_name +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__display_name +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__display_name +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__display_name +msgid "Display Name" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "" +"Email address not validated. Validate your email address by clicking on the " +"link in the email sent to you or request a new password. " +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__encrypted_password +msgid "Encrypted Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__force_verified_email +msgid "Force Verified Email" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Group By" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__id +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__id +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__id +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__id +msgid "ID" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__force_verified_email +msgid "If checked, email must be verified to be able to log in" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +msgid "Impersonate" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_token_duration +msgid "Impersonating Token Duration" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_user_ids +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__impersonating_user_ids +msgid "Impersonating Users" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__set_password_token_duration +msgid "In minute, default 1440 minutes => 24h" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_token_duration +msgid "In seconds, default 60 seconds" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Invalid Login or Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#: code:addons/auth_partner/models/auth_directory.py:0 +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "Invalid Token" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "Invalid token" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Label" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory____last_update +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner____last_update +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password____last_update +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password____last_update +msgid "Last Modified on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_date +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_date +msgid "Last Updated on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__login +msgid "Login" +msgstr "" + +#. module: auth_partner +#: model:ir.model.constraint,message:auth_partner.constraint_auth_partner_directory_login_uniq +msgid "Login must be uniq per directory !" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__template_id +msgid "Mail Template" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__reset_password_template_id +msgid "Mail Template Forget Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_template_id +msgid "Mail Template New Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__validate_email_template_id +msgid "Mail Template Validate Email" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__mail_verified +msgid "Mail Verified" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__manually +msgid "Manually" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__name +msgid "Name" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__nbr_pending_reset_sent +msgid "Nbr Pending Reset Sent" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "No id in context" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "No email template defined for %(template)s in %(directory)s" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__nbr_pending_reset_sent +msgid "" +"Number of pending reset sent from your customer.This field is usefull when " +"after a migration from an other system you ask all you customer to reset " +"their password and you senddifferent mail depending on the number of " +"reminder" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_action +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__partner_id +#: model:ir.ui.menu,name:auth_partner.auth_partner_menu +msgid "Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_ids +#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_ids +msgid "Partner Auth" +msgstr "" + +#. module: auth_partner +#: model:ir.ui.menu,name:auth_partner.auth +msgid "Partner Authentication" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__password +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password +msgid "Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "Password and Confirm Password must be the same" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +msgid "Regenerate secret key" +msgstr "" + +#. module: auth_partner +#: model:mail.template,subject:auth_partner.email_reset_password +msgid "Reset Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key +msgid "Secret Key" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_default +msgid "Secret Key Env Default" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_is_editable +msgid "Secret Key Env Is Editable" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +msgid "Send Invite" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Send Reset Password" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_action_reset_password +msgid "Send Reset Password Instruction" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_force_set_password_action +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +msgid "Set Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_token_duration +msgid "Set Password Token Duration" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__user_can_impersonate +msgid "Technical field to check if the user can impersonate" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_user_ids +#: model:ir.model.fields,help:auth_partner.field_auth_partner__impersonating_user_ids +msgid "These odoo users can impersonate any partner of this directory" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__mail_verified +msgid "" +"This field is set to True when the user has clicked on the link sent by " +"email" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "This wizard can only be used on auth.partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__user_can_impersonate +msgid "User Can Impersonate" +msgstr "" + +#. module: auth_partner +#: model:mail.template,subject:auth_partner.email_set_password +#: model:mail.template,subject:auth_partner.email_validate_email +msgid "Welcome" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_wizard_auth_partner_force_set_password +#: model:ir.model,name:auth_partner.model_wizard_auth_partner_reset_password +msgid "Wizard Partner Auth Reset Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "You are not allowed to impersonate this user" +msgstr "" diff --git a/auth_partner/i18n/it.po b/auth_partner/i18n/it.po new file mode 100644 index 000000000..d94b32952 --- /dev/null +++ b/auth_partner/i18n/it.po @@ -0,0 +1,598 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_partner +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-06-25 09:25+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: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__14-days +msgid "14 Days" +msgstr "14 Giorni" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__2-days +msgid "2-days" +msgstr "2 Giorni" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__6-hours +msgid "6 Hours" +msgstr "6 Ore" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__7-days +msgid "7 Days" +msgstr "7 Giorni" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_reset_password +msgid "" +"
\n" +" Hi \n" +" Click on the following link to reset your password\n" +" Reset Password\n" +"
\n" +" " +msgstr "" +"
\n" +" Salve \n" +" fare clic sul seguente collegamento per resettare la passwrod\n" +" Resetta " +"password\n" +"
\n" +" " + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_validate_email +msgid "" +"
\n" +" Hi \n" +" Welcome to the site, please click on the following link to verify your email\n" +" Validate Email\n" +"
\n" +" " +msgstr "" +"
\n" +" Salve \n" +" Benvenuto sul sito, clicca sul seguente collegamento per " +"verificare la tua e-mail\n" +" Valida " +"e-mail\n" +"
\n" +" " + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_set_password +msgid "" +"
\n" +" Hi \n" +" Welcome, your account have been created\n" +" Click on the following link to set your password\n" +" Set Password\n" +"
\n" +" " +msgstr "" +"
\n" +" Salve \n" +" Benvenuto, il tuo account è stato creato\n" +" Clicca sul collegamento seguente per impostare la tua password\n" +" Imposta " +"password\n" +"
\n" +" " + +#. module: auth_partner +#: model:res.groups,name:auth_partner.group_auth_partner_api +msgid "API Partner Auth Access" +msgstr "Autorizzazione accesso API del partner" + +#. module: auth_partner +#: model:res.groups,name:auth_partner.group_auth_partner_manager +msgid "API Partner Auth Manager" +msgstr "Responsabile autorizzazione API del partner" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.view_partner_form +msgid "Account" +msgstr "Conto" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "" +"An email will be send with a token to each customer, you can specify the " +"date until the link is valid" +msgstr "" +"Verrà inviata una e-mail con un token ad ogni cliente, si può indicare la " +"data entro cui il collegamento è valido" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_auth_directory +msgid "Auth Directory" +msgstr "Cartella autorizzazione" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_reset_password +msgid "Auth Directory: Reset Password" +msgstr "Cartella autorizzazione: reimposta password" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_set_password +msgid "Auth Directory: Set Password" +msgstr "Cartella autorizzazione: imposta password" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_validate_email +msgid "Auth Directory: Validate Email" +msgstr "Cartella autorizzazione: valida e-mail" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Auth Partner" +msgstr "Partner autorizzazione" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_count +#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_count +msgid "Auth Partner Count" +msgstr "Conteggio partner autorizzazione" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__auth_partner_ids +msgid "Auth Partners" +msgstr "Partner autorizzazione" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Cancel" +msgstr "Annulla" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password_confirm +msgid "Confirm Password" +msgstr "Conferma password" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_res_partner +msgid "Contact" +msgstr "Contatto" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__count_partner +msgid "Count Partner" +msgstr "Conteggio partner" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_date +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_impersonation +msgid "Date Last Impersonation" +msgstr "Data ultima imitazione" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_request_reset_pwd +msgid "Date Last Request Reset Pwd" +msgstr "Data ultima richiesta reimpostazione password" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd +msgid "Date Last Sucessfull Reset Pwd" +msgstr "Data ultima reimpostazione password riuscita" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__date_validity +msgid "Date Validity" +msgstr "Validità data" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_request_reset_pwd +msgid "Date of the last password reset request" +msgstr "Data ultima richiesta reimpostazione password" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_impersonation +msgid "Date of the last sucessfull impersonation" +msgstr "Data ultima imitazione riuscita" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd +msgid "Date of the last sucessfull password reset" +msgstr "Data ultima reimpostazione password riuscita" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__delay +msgid "Delay" +msgstr "Ritardo" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_directory_action +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__directory_id +#: model:ir.ui.menu,name:auth_partner.auth_directory_menu +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Directory" +msgstr "Cartella" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_search +msgid "Directory Auth" +msgstr "Autorizzazione cartella" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__display_name +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__display_name +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__display_name +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "" +"Email address not validated. Validate your email address by clicking on the " +"link in the email sent to you or request a new password. " +msgstr "" +"Indirizzo e-mail non validato. Validare il proprio indirizzo e-mail facendo " +"click sul collegamento nella e-mail inviata per la richiesta di una nuova " +"password. " + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__encrypted_password +msgid "Encrypted Password" +msgstr "Password criptata" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__force_verified_email +msgid "Force Verified Email" +msgstr "Forza e-mail verificata" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Group By" +msgstr "Raggruppa per" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__id +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__id +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__id +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__id +msgid "ID" +msgstr "ID" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__force_verified_email +msgid "If checked, email must be verified to be able to log in" +msgstr "" +"Se selezionata, l'e-mail deve essere verificata per consentire l'accesso" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +msgid "Impersonate" +msgstr "Imita" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_token_duration +msgid "Impersonating Token Duration" +msgstr "Durata token imitazione" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_user_ids +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__impersonating_user_ids +msgid "Impersonating Users" +msgstr "Utenti imitazione" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__set_password_token_duration +msgid "In minute, default 1440 minutes => 24h" +msgstr "In minuti,predefinito 1440 minuti => 24 ore" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_token_duration +msgid "In seconds, default 60 seconds" +msgstr "In secondi, predefinito 60 secondi" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Invalid Login or Password" +msgstr "Nome o password errati" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#: code:addons/auth_partner/models/auth_directory.py:0 +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "Invalid Token" +msgstr "Token non valido" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "Invalid token" +msgstr "Token non valido" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Label" +msgstr "Etichetta" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory____last_update +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner____last_update +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password____last_update +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_date +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__login +msgid "Login" +msgstr "Login" + +#. module: auth_partner +#: model:ir.model.constraint,message:auth_partner.constraint_auth_partner_directory_login_uniq +msgid "Login must be uniq per directory !" +msgstr "La login deve essere univoca per cartella!" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__template_id +msgid "Mail Template" +msgstr "Modello e-mail" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__reset_password_template_id +msgid "Mail Template Forget Password" +msgstr "Modello e-mail password dimenticata" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_template_id +msgid "Mail Template New Password" +msgstr "Modello e-mail nuova password" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__validate_email_template_id +msgid "Mail Template Validate Email" +msgstr "Modello e-mail validazione e-mail" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__mail_verified +msgid "Mail Verified" +msgstr "E-mail verificata" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__manually +msgid "Manually" +msgstr "Manualmente" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__name +msgid "Name" +msgstr "Nome" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__nbr_pending_reset_sent +msgid "Nbr Pending Reset Sent" +msgstr "N° reset inviati in attesa" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "No id in context" +msgstr "Manca id nel context" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "No email template defined for %(template)s in %(directory)s" +msgstr "Modello e-mail non definito per %(template)s in %(directory)s" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__nbr_pending_reset_sent +msgid "" +"Number of pending reset sent from your customer.This field is usefull when " +"after a migration from an other system you ask all you customer to reset " +"their password and you senddifferent mail depending on the number of " +"reminder" +msgstr "" +"Numero di reimpostazioni in sospeso inviate dal cliente. Questo campo è " +"utile quando, dopo una migrazione da un altro sistema, si chiede a tutti i " +"tuoi clienti di reimpostare la propria password e si inviano e-mail diverse " +"a seconda del numero di promemoria" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_action +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__partner_id +#: model:ir.ui.menu,name:auth_partner.auth_partner_menu +msgid "Partner" +msgstr "Partner" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_ids +#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_ids +msgid "Partner Auth" +msgstr "Autorizzazione partner" + +#. module: auth_partner +#: model:ir.ui.menu,name:auth_partner.auth +msgid "Partner Authentication" +msgstr "Autenticazione partner" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__password +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password +msgid "Password" +msgstr "Password" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "Password and Confirm Password must be the same" +msgstr "La password e la conferma devono essere uguali" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +msgid "Regenerate secret key" +msgstr "Rigenera chiave segreta" + +#. module: auth_partner +#: model:mail.template,subject:auth_partner.email_reset_password +msgid "Reset Password" +msgstr "Resetta password" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key +msgid "Secret Key" +msgstr "Chiave segreta" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_default +msgid "Secret Key Env Default" +msgstr "Chiave segreta ambiente predefinita" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_is_editable +msgid "Secret Key Env Is Editable" +msgstr "La chiave segreta è modificabile" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +msgid "Send Invite" +msgstr "Invia invito" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Send Reset Password" +msgstr "Invia reset password" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_action_reset_password +msgid "Send Reset Password Instruction" +msgstr "Invia istruzioni reset password" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__server_env_defaults +msgid "Server Env Defaults" +msgstr "Predefiniti ambiente server" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_force_set_password_action +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +msgid "Set Password" +msgstr "Imposta password" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_token_duration +msgid "Set Password Token Duration" +msgstr "Durata token impostazione password" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__user_can_impersonate +msgid "Technical field to check if the user can impersonate" +msgstr "Campo tecnico per controllare se l'utente può imitare" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_user_ids +#: model:ir.model.fields,help:auth_partner.field_auth_partner__impersonating_user_ids +msgid "These odoo users can impersonate any partner of this directory" +msgstr "Questi utenti Odoo possono imitare qualsiasi partner in questa cartella" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__mail_verified +msgid "" +"This field is set to True when the user has clicked on the link sent by " +"email" +msgstr "" +"Questo campo è impostato a true quando l'utente ha cliccato nel collegamento " +"inviato per e-mail" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "This wizard can only be used on auth.partner" +msgstr "Questa procedura guidata può essere usata solo su auth.partner" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__user_can_impersonate +msgid "User Can Impersonate" +msgstr "L'utente può imitare" + +#. module: auth_partner +#: model:mail.template,subject:auth_partner.email_set_password +#: model:mail.template,subject:auth_partner.email_validate_email +msgid "Welcome" +msgstr "Benvenuto" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_wizard_auth_partner_force_set_password +#: model:ir.model,name:auth_partner.model_wizard_auth_partner_reset_password +msgid "Wizard Partner Auth Reset Password" +msgstr "Procedura guidata reset password autorizzazione partner" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "You are not allowed to impersonate this user" +msgstr "Non si è autorizzati a imitare questo utente" diff --git a/auth_partner/models/__init__.py b/auth_partner/models/__init__.py new file mode 100644 index 000000000..6259e6d10 --- /dev/null +++ b/auth_partner/models/__init__.py @@ -0,0 +1,3 @@ +from . import auth_directory +from . import auth_partner +from . import res_partner diff --git a/auth_partner/models/auth_directory.py b/auth_partner/models/auth_directory.py new file mode 100644 index 000000000..07a9dde3e --- /dev/null +++ b/auth_partner/models/auth_directory.py @@ -0,0 +1,211 @@ +# 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, timezone +from secrets import token_urlsafe + +import jwt + +from odoo import fields, models +from odoo.exceptions import UserError + +from odoo.addons.queue_job.delay import chain + + +class AuthDirectory(models.Model): + _name = "auth.directory" + _description = "Auth Directory" + _inherit = "server.env.mixin" + + name = fields.Char(required=True) + auth_partner_ids = fields.One2many("auth.partner", "directory_id", "Auth Partners") + set_password_token_duration = fields.Integer( + default=1440, help="In minute, default 1440 minutes => 24h", required=True + ) + impersonating_token_duration = fields.Integer( + default=60, help="In seconds, default 60 seconds", required=True + ) + reset_password_template_id = fields.Many2one( + "mail.template", + "Mail Template Forget Password", + required=True, + default=lambda self: self.env.ref( + "auth_partner.email_reset_password", + raise_if_not_found=False, + ), + ) + set_password_template_id = fields.Many2one( + "mail.template", + "Mail Template New Password", + required=True, + default=lambda self: self.env.ref( + "auth_partner.email_set_password", + raise_if_not_found=False, + ), + ) + validate_email_template_id = fields.Many2one( + "mail.template", + "Mail Template Validate Email", + required=True, + default=lambda self: self.env.ref( + "auth_partner.email_validate_email", + raise_if_not_found=False, + ), + ) + secret_key = fields.Char( + groups="base.group_system", + required=True, + default=lambda self: self._generate_default_secret_key(), + ) + count_partner = fields.Integer(compute="_compute_count_partner") + + impersonating_user_ids = fields.Many2many( + "res.users", + "auth_directory_impersonating_user_rel", + "directory_id", + "user_id", + string="Impersonating Users", + help="These odoo users can impersonate any partner of this directory", + default=lambda self: ( + self.env.ref("base.user_root") | self.env.ref("base.user_admin") + ).ids, + groups="auth_partner.group_auth_partner_manager", + ) + force_verified_email = fields.Boolean( + help="If checked, email must be verified to be able to log in" + ) + + def _generate_default_secret_key(self): + # generate random ~64 chars secret key + return token_urlsafe(64) + + def action_regenerate_secret_key(self): + self.ensure_one() + self.secret_key = self._generate_default_secret_key() + + def _compute_count_partner(self): + data = self.env["auth.partner"]._read_group( + [ + ("directory_id", "in", self.ids), + ], + ["directory_id"], + aggregates=["__count"], + ) + mapped_data = {auth_partner.id: count for auth_partner, count in data} + + for record in self: + record.count_partner = mapped_data.get(record.id, 0) + + def _get_template(self, type_or_template): + if isinstance(type_or_template, str): + return getattr(self, type_or_template + "_template_id", None) + return type_or_template + + def _prepare_mail_context(self, context): + return context or {} + + def _send_mail_background( + self, type_or_template, auth_partner, callback_job=None, **context + ): + """ + Send an email asynchronously to the auth_partner + using the template defined in the directory + """ + self.ensure_one() + auth_partner.ensure_one() + # Load context synchronously + context = self._prepare_mail_context(context) + + job = self.delayable()._send_mail_impl( + type_or_template, auth_partner, **context + ) + if callback_job: + job = chain(job, callback_job) + return job.delay() + + def _send_mail(self, type_or_template, auth_partner, **context): + """Send an email to the auth_partner using the template defined + in the directory""" + self.ensure_one() + auth_partner.ensure_one() + context = self._prepare_mail_context(context) + + self._send_mail_impl(type_or_template, auth_partner, **context) + + def _send_mail_impl(self, type_or_template, auth_partner, **context): + template = self.sudo()._get_template(type_or_template) + if not template: + raise UserError( + self.env._( + "No email template defined for %(template)s in %(directory)s" + ) + % {"template": type_or_template, "directory": self.name} + ) + template.sudo().with_context(**context).send_mail( + auth_partner.id, force_send=True, raise_exception=True + ) + + return f"Mail {template.name} sent to {auth_partner.login}" + + def _generate_token(self, action, auth_partner, expiration_delta, key_salt=""): + # We need to sudo here as secret_key is a protected field + self = self.sudo() + return jwt.encode( + { + "exp": datetime.now(tz=timezone.utc) + expiration_delta, + "aud": str(self.id), + "action": action, + "ap": auth_partner.id, + }, + self.secret_key + key_salt, + algorithm="HS256", + ) + + def _decode_token( + self, + token, + action, + key_salt=None, + ): + # We need to sudo here as secret_key is a protected field + self = self.sudo() + key = self.secret_key + if key_salt: + try: + obj = jwt.decode( + token, algorithms=["HS256"], options={"verify_signature": False} + ) + except jwt.PyJWTError as e: + raise UserError(self.env._("Invalid Token")) from e + probable_auth_partner = self.env["auth.partner"].browse(obj["ap"]) + if not probable_auth_partner: + raise UserError(self.env._("Invalid Token")) + key += key_salt(probable_auth_partner) + + try: + obj = jwt.decode( + token, + key, + audience=str(self.id), + options={"require": ["exp", "aud", "ap", "action"]}, + algorithms=["HS256"], + ) + except jwt.PyJWTError as e: + raise UserError(self.env._("Invalid Token")) from e + + auth_partner = self.env["auth.partner"].browse(obj["ap"]) + + if ( + obj["action"] != action + or not auth_partner.exists() + or auth_partner.directory_id != self + ): + raise UserError(self.env._("Invalid token")) + + return auth_partner + + @property + def _server_env_fields(self): + return {"secret_key": {}} diff --git a/auth_partner/models/auth_partner.py b/auth_partner/models/auth_partner.py new file mode 100644 index 000000000..02847bd05 --- /dev/null +++ b/auth_partner/models/auth_partner.py @@ -0,0 +1,313 @@ +# 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). + +import logging +from datetime import timedelta + +import passlib + +from odoo import api, fields, models +from odoo.exceptions import AccessDenied + +# please read passlib great documentation +# https://passlib.readthedocs.io +# https://passlib.readthedocs.io/en/stable/narr/quickstart.html#choosing-a-hash +# be carefull odoo requirements use an old version of passlib +DEFAULT_CRYPT_CONTEXT = passlib.context.CryptContext(["pbkdf2_sha512"]) + +_logger = logging.getLogger(__name__) + + +class AuthPartner(models.Model): + _name = "auth.partner" + _description = "Auth Partner" + _rec_name = "login" + + partner_id = fields.Many2one( + "res.partner", "Partner", required=True, ondelete="cascade", index=True + ) + directory_id = fields.Many2one( + "auth.directory", "Directory", required=True, index=True + ) + user_can_impersonate = fields.Boolean( + compute="_compute_user_can_impersonate", + help="Technical field to check if the user can impersonate", + ) + impersonating_user_ids = fields.Many2many( + related="directory_id.impersonating_user_ids", + ) + login = fields.Char( + compute="_compute_login", + store=True, + required=True, + index=True, + precompute=True, + ) + password = fields.Char(compute="_compute_password", inverse="_inverse_password") + encrypted_password = fields.Char(index=True) + nbr_pending_reset_sent = fields.Integer( + index=True, + help=( + "Number of pending reset sent from your customer." + "This field is usefull when after a migration from an other system " + "you ask all you customer to reset their password and you send" + "different mail depending on the number of reminder" + ), + ) + date_last_request_reset_pwd = fields.Datetime( + help="Date of the last password reset request" + ) + date_last_sucessfull_reset_pwd = fields.Datetime( + help="Date of the last sucessfull password reset" + ) + date_last_impersonation = fields.Datetime( + help="Date of the last sucessfull impersonation" + ) + + mail_verified = fields.Boolean( + help="This field is set to True when the user has clicked on the link " + "sent by email" + ) + + _sql_constraints = [ + ( + "directory_login_uniq", + "unique (directory_id, login)", + "Login must be uniq per directory !", + ), + ] + + @api.depends("partner_id.email") + def _compute_login(self): + for record in self: + record.login = record.partner_id.email + + def _crypt_context(self): + return DEFAULT_CRYPT_CONTEXT + + def _check_no_empty(self, login, password): + # double check by security but calling this through a service should + # already have check this + if not ( + isinstance(password, str) and password and isinstance(login, str) and login + ): + _logger.warning("Invalid login/password for sign in") + raise AccessDenied() + + def _get_hashed_password(self, directory, login): + self.flush_model() + self.env.cr.execute( + """ + SELECT id, COALESCE(encrypted_password, '') + FROM auth_partner + WHERE login=%s AND directory_id=%s""", + (login, directory.id), + ) + hashed = self.env.cr.fetchone() + if hashed and hashed[1]: + # ensure that we have a auth.partner and this partner have a password set + return hashed + else: + raise AccessDenied() + + def _compute_password(self): + for record in self: + record.password = "" + + def _inverse_password(self): + for record in self: + ctx = record._crypt_context() + hash_ = getattr(ctx, "hash", ctx.encrypt) + record.encrypted_password = hash_(record.password) + record.password = "" + + def _prepare_partner_auth_signup(self, directory, vals): + return { + "login": vals["login"].lower(), + "password": vals["password"], + "directory_id": directory.id, + } + + def _prepare_partner_signup(self, directory, vals): + return { + "name": vals["name"], + "email": vals["login"].lower(), + "auth_partner_ids": [ + (0, 0, self._prepare_partner_auth_signup(directory, vals)) + ], + } + + @api.model + def _signup(self, directory, **kwargs): + partner = self.env["res.partner"].create( + [ + self._prepare_partner_signup(directory, kwargs), + ] + ) + auth_partner = partner.auth_partner_ids + directory._send_mail_background( + "validate_email", + auth_partner, + token=auth_partner._generate_validate_email_token(), + ) + return auth_partner + + @api.model + def _login(self, directory, login, password, **kwargs): + self._check_no_empty(login, password) + login = login.lower() + try: + _id, hashed = self._get_hashed_password(directory, login) + valid, replacement = self._crypt_context().verify_and_update( + password, hashed + ) + + auth_partner = valid and self.browse(_id) + except AccessDenied: + # We do not want to leak information about the login, + # always raise the same exception + auth_partner = None + + if not auth_partner or not auth_partner.partner_id.active: + raise AccessDenied(self.env._("Invalid Login or Password")) + + if directory.sudo().force_verified_email and not auth_partner.mail_verified: + raise AccessDenied( + self.env._( + "Email address not validated. Validate your email address by " + "clicking on the link in the email sent to you or request a new " + "password. " + ) + ) + + if replacement is not None: + auth_partner.encrypted_password = replacement + + return auth_partner + + @api.model + def _validate_email(self, directory, token): + auth_partner = directory._decode_token(token, "validate_email") + auth_partner.write({"mail_verified": True}) + return auth_partner + + def _get_impersonate_url(self, token, **kwargs): + # You should override this method according to the impersonation url + # your framework is using + + base = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + url = f"{base}/auth/impersonate/{token}" + return url + + def _get_impersonate_action(self, token, **kwargs): + return { + "type": "ir.actions.act_url", + "url": self._get_impersonate_url(token, **kwargs), + "target": "new", + } + + def impersonate(self): + self.ensure_one() + if self.env.user not in self.impersonating_user_ids: + raise AccessDenied( + self.env._("You are not allowed to impersonate this user") + ) + + token = self._generate_impersonating_token() + return self._get_impersonate_action(token) + + @api.depends_context("uid") + def _compute_user_can_impersonate(self): + for record in self: + record.user_can_impersonate = self.env.user in record.impersonating_user_ids + + @api.model + def _impersonating(self, directory, token): + partner_auth = directory._decode_token( + token, + "impersonating", + key_salt=lambda auth_partner: ( + auth_partner.date_last_impersonation.isoformat() + if auth_partner.date_last_impersonation + else "never" + ), + ) + partner_auth.date_last_impersonation = fields.Datetime.now() + return partner_auth + + def _on_reset_password_sent(self): + self.ensure_one() + self.date_last_request_reset_pwd = fields.Datetime.now() + self.date_last_sucessfull_reset_pwd = None + self.nbr_pending_reset_sent += 1 + + def _send_invite(self): + self.ensure_one() + self.directory_id._send_mail_background( + "set_password", + self, + callback_job=self.delayable()._on_reset_password_sent(), + token=self._generate_set_password_token(), + ) + + def send_invite(self): + for rec in self: + rec._send_invite() + + def _request_reset_password(self): + return self.directory_id._send_mail_background( + "reset_password", + self, + callback_job=self.delayable()._on_reset_password_sent(), + token=self._generate_set_password_token(), + ) + + def _set_password(self, directory, token, password): + auth_partner = directory._decode_token( + token, + "set_password", + # See `_generate_set_password_token` for the key_salt + key_salt=lambda auth_partner: auth_partner.encrypted_password or "empty", + ) + auth_partner.write( + { + "password": password, + "mail_verified": True, + } + ) + auth_partner.date_last_sucessfull_reset_pwd = fields.Datetime.now() + auth_partner.nbr_pending_reset_sent = 0 + return auth_partner + + def _generate_set_password_token(self, expiration_delta=None): + # Here we use the current encrypted_password as key_salt to ensure that + # the token will be used to reset the password only once. + return self.directory_id._generate_token( + "set_password", + self, + expiration_delta + or timedelta(minutes=self.directory_id.set_password_token_duration), + key_salt=self.encrypted_password or "empty", + ) + + def _generate_validate_email_token(self): + return self.directory_id._generate_token( + # 30 days seem to be a good value, no need for configuration + "validate_email", + self, + timedelta(days=30), + ) + + def _generate_impersonating_token(self): + return self.directory_id._generate_token( + "impersonating", + self, + timedelta(minutes=self.directory_id.impersonating_token_duration), + key_salt=( + self.date_last_impersonation.isoformat() + if self.date_last_impersonation + else "never" + ), + ) diff --git a/auth_partner/models/res_partner.py b/auth_partner/models/res_partner.py new file mode 100644 index 000000000..06265a105 --- /dev/null +++ b/auth_partner/models/res_partner.py @@ -0,0 +1,33 @@ +# 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 ResPartner(models.Model): + _inherit = "res.partner" + + auth_partner_ids = fields.One2many("auth.partner", "partner_id", "Partner Auth") + auth_partner_count = fields.Integer( + compute="_compute_auth_partner_count", compute_sudo=True + ) + + def _compute_auth_partner_count(self): + data = self.env["auth.partner"]._read_group( + [ + ("partner_id", "in", self.ids), + ], + ["partner_id"], + aggregates=["__count"], + ) + mapped_data = {auth_partner.id: count for auth_partner, count in data} + + for record in self: + record.auth_partner_count = mapped_data.get(record.id, 0) + + def _get_auth_partner_for_directory(self, directory): + return self.sudo().auth_partner_ids.filtered( + lambda r: r.directory_id == directory + ) diff --git a/auth_partner/pyproject.toml b/auth_partner/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/auth_partner/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/auth_partner/readme/CONTRIBUTORS.md b/auth_partner/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..6079ca504 --- /dev/null +++ b/auth_partner/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Akretion](https://www.akretion.com): + - Sébastien Beau + - Florian Mounier diff --git a/auth_partner/readme/DESCRIPTION.md b/auth_partner/readme/DESCRIPTION.md new file mode 100644 index 000000000..71610a4cb --- /dev/null +++ b/auth_partner/readme/DESCRIPTION.md @@ -0,0 +1,19 @@ +This module adds to the partners the ability to authenticate through +directories. + +This module does not implement any routing, it only provides the basic +mechanisms in a directory for: + +> - Registering a partner and sending an welcome email (to validate +> email address): \_signup +> - Authenticating a partner: \_login +> - Validating a partner email using a token: \_validate_email +> - Impersonating: \_impersonate, \_impersonating +> - Resetting the password with a unique token sent by mail: +> \_request_reset_password, \_set_password +> - Sending an invite mail when registering a partner from odoo +> interface for the partner to enter a password: \_send_invite, +> \_set_password + +For a routing implementation, see the +[fastapi_auth_partner](../fastapi_auth_partner) module. diff --git a/auth_partner/readme/USAGE.md b/auth_partner/readme/USAGE.md new file mode 100644 index 000000000..455106eb3 --- /dev/null +++ b/auth_partner/readme/USAGE.md @@ -0,0 +1,8 @@ +This module isn't meant to be used standalone but you can still see the +directories and authenticable partners in: + +Settings \> Technical \> Partner Authentication \> Partner + +and + +Settings \> Technical \> Partner Authentication \> Directory diff --git a/auth_partner/security/ir.model.access.csv b/auth_partner/security/ir.model.access.csv new file mode 100644 index 000000000..cdcead759 --- /dev/null +++ b/auth_partner/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_auth_directory,auth_directory_system,model_auth_directory,base.group_system,1,1,1,1 +access_auth_directory_read,auth_directory_manager,model_auth_directory,group_auth_partner_manager,1,0,0,0 +access_auth_partner,auth_partner_manager,model_auth_partner,group_auth_partner_manager,1,1,1,1 +api_access_auth_partner,auth_partner_api,model_auth_partner,group_auth_partner_api,1,1,0,0 +api_access_res_partner,res_partner_api,base.model_res_partner,group_auth_partner_api,1,0,0,0 +api_access_wizard_auth_partner_reset_password,wizard_auth_partner_reset_password,model_wizard_auth_partner_reset_password,group_auth_partner_manager,1,1,1,1 +api_access_wizard_auth_partner_force_set_password,wizard_auth_partner_force_set_password,model_wizard_auth_partner_force_set_password,group_auth_partner_manager,1,1,1,1 diff --git a/auth_partner/security/res_group.xml b/auth_partner/security/res_group.xml new file mode 100644 index 000000000..a912c7d2f --- /dev/null +++ b/auth_partner/security/res_group.xml @@ -0,0 +1,16 @@ + + + + API Partner Auth Manager + + + + + + API Partner Auth Access + + + diff --git a/auth_partner/static/description/icon.png b/auth_partner/static/description/icon.png new file mode 100644 index 000000000..1dcc49c24 Binary files /dev/null and b/auth_partner/static/description/icon.png differ diff --git a/auth_partner/static/description/index.html b/auth_partner/static/description/index.html new file mode 100644 index 000000000..eb1137e3b --- /dev/null +++ b/auth_partner/static/description/index.html @@ -0,0 +1,455 @@ + + + + + +Partner Auth + + + +
+

Partner Auth

+ + +

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

+

This module adds to the partners the ability to authenticate through +directories.

+

This module does not implement any routing, it only provides the basic +mechanisms in a directory for:

+
+
    +
  • Registering a partner and sending an welcome email (to validate +email address): _signup
  • +
  • Authenticating a partner: _login
  • +
  • Validating a partner email using a token: _validate_email
  • +
  • Impersonating: _impersonate, _impersonating
  • +
  • Resetting the password with a unique token sent by mail: +_request_reset_password, _set_password
  • +
  • Sending an invite mail when registering a partner from odoo +interface for the partner to enter a password: _send_invite, +_set_password
  • +
+
+

For a routing implementation, see the +fastapi_auth_partner module.

+

Table of contents

+ +
+

Usage

+

This module isn’t meant to be used standalone but you can still see the +directories and authenticable partners in:

+

Settings > Technical > Partner Authentication > Partner

+

and

+

Settings > Technical > Partner Authentication > Directory

+
+
+

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.

+

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/auth_partner/tests/__init__.py b/auth_partner/tests/__init__.py new file mode 100644 index 000000000..ee9a639f6 --- /dev/null +++ b/auth_partner/tests/__init__.py @@ -0,0 +1 @@ +from . import test_auth_partner diff --git a/auth_partner/tests/common.py b/auth_partner/tests/common.py new file mode 100644 index 000000000..fa53b6f39 --- /dev/null +++ b/auth_partner/tests/common.py @@ -0,0 +1,67 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager +from typing import Any + +from odoo.tests.common import TransactionCase + +from odoo.addons.mail.tests.common import MockEmail + + +class CommonTestAuthPartner(TransactionCase, MockEmail): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, + queue_job__no_delay=True, + tracking_disable=True, + ) + ) + + cls.partner = cls.env.ref("auth_partner.res_partner_auth_demo") + cls.other_partner = cls.partner.copy( + {"name": "Other Partner", "email": "other-partner-auth@example.org"} + ) + cls.auth_partner = cls.partner.auth_partner_ids + + cls.directory = cls.env.ref("auth_partner.demo_directory") + cls.directory.impersonating_user_ids = cls.env.ref("base.user_admin") + + cls.other_auth_partner = cls.env["auth.partner"].create( + { + "login": cls.other_partner.email, + "password": "Super-secret3", + "directory_id": cls.directory.id, + "partner_id": cls.other_partner.id, + } + ) + cls.other_directory = cls.directory.copy({"name": "Other Directory"}) + + @contextmanager + def new_mails(self): + mailmail = self.env["mail.mail"] + + class MailsProxy(mailmail.__class__): + __slots__ = ["_prev", "__weakref__"] + + def __init__(self, env, ids, prefetch_ids): + super().__init__(env, ids, prefetch_ids) + object.__setattr__(self, "_prev", mailmail.search([])) + + def __getattribute__(self, name: str) -> Any: + mails = mailmail.search([]) - object.__getattribute__(self, "_prev") + return object.__getattribute__(mails, name) + + new_mails = MailsProxy(self.env, [], []) + with self.mock_mail_gateway(): + yield new_mails + + @contextmanager + def assert_no_new_mail(self): + with self.new_mails() as new_mails: + yield + self.assertFalse(new_mails) diff --git a/auth_partner/tests/test_auth_partner.py b/auth_partner/tests/test_auth_partner.py new file mode 100644 index 000000000..40d9e8478 --- /dev/null +++ b/auth_partner/tests/test_auth_partner.py @@ -0,0 +1,357 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager +from datetime import datetime, timedelta + +from freezegun import freeze_time + +from odoo.exceptions import AccessDenied, UserError + +from .common import CommonTestAuthPartner + + +class TestAuthPartner(CommonTestAuthPartner): + @contextmanager + def assert_no_new_mail(self): + with self.new_mails() as new_mails: + yield + self.assertFalse(new_mails) + + def test_default_secret_key(self): + self.assertGreaterEqual(len(self.directory.secret_key), 64) + + def test_login_ok(self): + with self.assert_no_new_mail(): + auth_partner = self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + self.assertTrue(auth_partner) + + def test_login_inactive_partner(self): + self.partner.active = False + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + + def test_login_no_auth(self): + self.auth_partner.unlink() + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + + def test_login_wrong_password(self): + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="wrong" + ) + + def test_login_mail_not_verified(self): + self.directory.force_verified_email = True + with self.assertRaisesRegex(AccessDenied, "Email address not validated"): + self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + + def test_login_wrong_login(self): + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.com", + password="Super-secret$1", + ) + + def test_login_wrong_directory(self): + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.other_directory, + login="partner-auth@example.com", + password="Super-secret$1", + ) + + def test_signup(self): + with self.new_mails() as new_mails: + new_auth_partner = self.env["auth.partner"]._signup( + self.directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret", + ) + self.assertTrue(new_auth_partner) + # Ensure we can't read the password + self.assertNotEqual(new_auth_partner.password, "NewSecret") + + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Welcome") + self.assertIn("Welcome to the site, please", new_mails.body) + + auth_partner = self.env["auth.partner"]._login( + self.directory, login="new-partner-auth@example.org", password="NewSecret" + ) + self.assertTrue(auth_partner) + self.assertEqual(auth_partner, new_auth_partner) + + def test_signup_wrong_directory(self): + new_auth_partner = self.env["auth.partner"]._signup( + self.other_directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret", + ) + self.assertTrue(new_auth_partner) + + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="new-partner-auth@example.org", + password="NewSecret", + ) + + def test_signup_same_login_other_directory(self): + new_auth_partner = self.env["auth.partner"]._signup( + self.directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret", + ) + self.assertTrue(new_auth_partner) + new_auth_partner_2 = self.env["auth.partner"]._signup( + self.other_directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret2", + ) + self.assertTrue(new_auth_partner_2) + self.assertNotEqual(new_auth_partner, new_auth_partner_2) + + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="new-partner-auth@example.org", + password="NewSecret2", + ) + + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.other_directory, + login="new-partner-auth@example.org", + password="NewSecret", + ) + + def test_validate_email_ok(self): + self.assertFalse(self.auth_partner.mail_verified) + token = self.auth_partner._generate_validate_email_token() + self.auth_partner._validate_email(self.directory, token) + self.assertTrue(self.auth_partner.mail_verified) + + def test_validate_email_required_login(self): + self.directory.force_verified_email = True + token = self.auth_partner._generate_validate_email_token() + self.auth_partner._validate_email(self.directory, token) + with self.assert_no_new_mail(): + auth_partner = self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + self.assertTrue(auth_partner) + + def test_validate_email_wrong_token(self): + self.assertFalse(self.auth_partner.mail_verified) + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._validate_email(self.directory, "wrong") + self.assertFalse(self.auth_partner.mail_verified) + + def test_validate_email_token(self): + with self.new_mails() as new_mails: + new_auth_partner = self.env["auth.partner"]._signup( + self.directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret", + ) + self.assertFalse(new_auth_partner.mail_verified) + token = new_mails.body.split("token=")[1].split('">')[0] + new_auth_partner._validate_email(self.directory, token) + self.assertTrue(new_auth_partner.mail_verified) + + def test_impersonate_ok(self): + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + auth_partner = self.env["auth.partner"]._impersonating(self.directory, token) + self.assertEqual(auth_partner, self.auth_partner) + + def test_impersonate_once(self): + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + self.env["auth.partner"]._impersonating(self.directory, token) + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.env["auth.partner"]._impersonating(self.directory, token) + + def test_impersonate_wrong_directory(self): + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.env["auth.partner"]._impersonating(self.other_directory, token) + + def test_impersonate_wrong_user(self): + with self.assertRaisesRegex(AccessDenied, "not allowed to impersonate"): + self.auth_partner.with_user(self.env.ref("base.default_user")).impersonate() + + def test_impersonate_not_expired_token(self): + self.directory.impersonating_token_duration = 100 + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + with freeze_time(datetime.now() + timedelta(hours=1)): + self.env["auth.partner"]._impersonating(self.directory, token) + + def test_impersonate_expired_token(self): + self.directory.impersonating_token_duration = 100 + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + with ( + freeze_time(datetime.now() + timedelta(hours=2)), + self.assertRaisesRegex(UserError, "Invalid Token"), + ): + self.env["auth.partner"]._impersonating(self.directory, token) + + def test_set_password_ok(self): + self.auth_partner._set_password( + self.directory, + self.auth_partner._generate_set_password_token(), + "ResetSecret", + ) + auth_partner = self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="ResetSecret" + ) + self.assertTrue(auth_partner) + + def test_set_password_wrong_token(self): + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._set_password(self.directory, "wrong", "ResetSecret") + + def test_set_password_once(self): + token = self.auth_partner._generate_set_password_token() + self.auth_partner._set_password(self.directory, token, "ResetSecret") + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + def test_set_password_not_expired_token(self): + self.directory.set_password_token_duration = 100 + token = self.auth_partner._generate_set_password_token() + + with freeze_time(datetime.now() + timedelta(hours=1)): + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + auth_partner = self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="ResetSecret" + ) + self.assertTrue(auth_partner) + + def test_set_password_expired_token(self): + self.directory.set_password_token_duration = 100 + token = self.auth_partner._generate_set_password_token() + + with ( + freeze_time(datetime.now() + timedelta(hours=2)), + self.assertRaisesRegex(UserError, "Invalid Token"), + ): + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + def test_reset_password_ok(self): + with self.new_mails() as new_mails: + self.auth_partner._request_reset_password() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Reset Password") + self.assertIn( + "Click on the following link to reset your password", new_mails.body + ) + + token = new_mails.body.split("token=")[1].split('">')[0] + self.auth_partner._set_password(self.directory, token, "ResetSecret") + auth_partner = self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="ResetSecret" + ) + self.assertTrue(auth_partner) + + def test_reset_password_wrong_partner(self): + with self.new_mails() as new_mails: + self.auth_partner._request_reset_password() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Reset Password") + self.assertIn( + "Click on the following link to reset your password", new_mails.body + ) + + token = new_mails.body.split("token=")[1].split('">')[0] + # This should probably raise instead of reseting the auth_partner password + self.other_auth_partner._set_password(self.directory, token, "ResetSecret") + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="other-partner-auth@example.org", + password="ResetSecret", + ) + + def test_reset_password_once(self): + with self.new_mails() as new_mails: + self.auth_partner._request_reset_password() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Reset Password") + token = new_mails.body.split("token=")[1].split('">')[0] + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._set_password(self.directory, token, "ResetSecret2") + + def test_send_invite_set_password_ok(self): + with self.new_mails() as new_mails: + self.auth_partner._send_invite() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Welcome") + self.assertIn("your account have been created", new_mails.body) + token = new_mails.body.split("token=")[1].split('">')[0] + + self.auth_partner._set_password(self.directory, token, "ResetSecret") + auth_partner = self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="ResetSecret" + ) + self.assertTrue(auth_partner) + + def test_send_invite_set_password_once(self): + with self.new_mails() as new_mails: + self.auth_partner._send_invite() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Welcome") + + token = new_mails.body.split("token=")[1].split('">')[0] + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._set_password(self.directory, token, "ResetSecret2") diff --git a/auth_partner/views/auth_directory_view.xml b/auth_partner/views/auth_directory_view.xml new file mode 100644 index 000000000..ad6ee28c5 --- /dev/null +++ b/auth_partner/views/auth_directory_view.xml @@ -0,0 +1,96 @@ + + + + auth.directory + + + + + + + + + auth.directory + +
+
+
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + +
+
+
+
+ + + auth.directory + + + + + + + + + Directory + ir.actions.act_window + auth.directory + list,form + + [] + {} + + + +
diff --git a/auth_partner/views/auth_partner_view.xml b/auth_partner/views/auth_partner_view.xml new file mode 100644 index 000000000..b0bb45977 --- /dev/null +++ b/auth_partner/views/auth_partner_view.xml @@ -0,0 +1,97 @@ + + + + auth.partner + + + + + + + + + + + + + + + auth.partner + +
+
+
+ +
+

+ +

+
+ + + + + + + +
+
+
+
+ + + auth.partner + + + + + + + + + + + + + Partner + ir.actions.act_window + auth.partner + list,form + + [] + {} + + + + +
diff --git a/auth_partner/views/res_partner_view.xml b/auth_partner/views/res_partner_view.xml new file mode 100644 index 000000000..30740ac8f --- /dev/null +++ b/auth_partner/views/res_partner_view.xml @@ -0,0 +1,26 @@ + + + + res.partner + + +
+ +
+
+
+
diff --git a/auth_partner/wizards/__init__.py b/auth_partner/wizards/__init__.py new file mode 100644 index 000000000..2f8025a36 --- /dev/null +++ b/auth_partner/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import wizard_auth_partner_reset_password +from . import wizard_auth_partner_force_set_password diff --git a/auth_partner/wizards/wizard_auth_partner_force_set_password.py b/auth_partner/wizards/wizard_auth_partner_force_set_password.py new file mode 100644 index 000000000..3e0c8b18c --- /dev/null +++ b/auth_partner/wizards/wizard_auth_partner_force_set_password.py @@ -0,0 +1,35 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class WizardAuthPartnerForceSetPassword(models.TransientModel): + _name = "wizard.auth.partner.force.set.password" + _description = "Wizard Partner Auth Reset Password" + + password = fields.Char(required=True) + password_confirm = fields.Char(string="Confirm Password", required=True) + + @api.constrains("password", "password_confirm") + def _check_password(self): + for wizard in self: + if wizard.password != wizard.password_confirm: + raise ValidationError( + self.env._("Password and Confirm Password must be the same") + ) + + def action_force_set_password(self): + self.ensure_one() + auth_partner_id = self.env.context.get("id") + if not auth_partner_id: + raise UserError(self.env._("No id in context")) + + auth_partner = self.env["auth.partner"].browse(auth_partner_id) + + auth_partner.write({"password": self.password}) + + return {"type": "ir.actions.act_window_close"} diff --git a/auth_partner/wizards/wizard_auth_partner_force_set_password_view.xml b/auth_partner/wizards/wizard_auth_partner_force_set_password_view.xml new file mode 100644 index 000000000..e83a5e13e --- /dev/null +++ b/auth_partner/wizards/wizard_auth_partner_force_set_password_view.xml @@ -0,0 +1,35 @@ + + + + + wizard.auth.partner.force.set.password + +
+ + + + +
+
+
+
+
+ + + Set Password + wizard.auth.partner.force.set.password + form + new + +
diff --git a/auth_partner/wizards/wizard_auth_partner_reset_password.py b/auth_partner/wizards/wizard_auth_partner_reset_password.py new file mode 100644 index 000000000..e69610877 --- /dev/null +++ b/auth_partner/wizards/wizard_auth_partner_reset_password.py @@ -0,0 +1,59 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo import api, fields, models + + +class WizardAuthPartnerResetPassword(models.TransientModel): + _name = "wizard.auth.partner.reset.password" + _description = "Wizard Partner Auth Reset Password" + + delay = fields.Selection( + [ + ("manually", "Manually"), + ("6-hours", "6 Hours"), + ("2-days", "2-days"), + ("7-days", "7 Days"), + ("14-days", "14 Days"), + ], + default="6-hours", + required=True, + ) + template_id = fields.Many2one( + "mail.template", + "Mail Template", + required=True, + domain=[("model_id", "=", "auth.partner")], + ) + date_validity = fields.Datetime( + compute="_compute_date_validity", store=True, readonly=False + ) + + @api.depends("delay") + def _compute_date_validity(self): + for record in self: + if record.delay != "manually": + duration, key = record.delay.split("-") + record.date_validity = datetime.now() + timedelta( + **{key: float(duration)} + ) + + def action_reset_password(self): + expiration_delta = None + if self.delay != "manually": + duration, key = self.delay.split("-") + expiration_delta = timedelta(**{key: float(duration)}) + + for auth_partner in self.env["auth.partner"].browse( + self._context["active_ids"] + ): + auth_partner.directory_id._send_mail_background( + self.template_id, + auth_partner, + callback_job=auth_partner.delayable()._on_reset_password_sent(), + token=auth_partner._generate_set_password_token(expiration_delta), + ) diff --git a/auth_partner/wizards/wizard_auth_partner_reset_password_view.xml b/auth_partner/wizards/wizard_auth_partner_reset_password_view.xml new file mode 100644 index 000000000..4fafa5574 --- /dev/null +++ b/auth_partner/wizards/wizard_auth_partner_reset_password_view.xml @@ -0,0 +1,40 @@ + + + + wizard.auth.partner.reset.password + +
+ An email will be send with a token to each customer, you can specify the date until the link is valid + + + + + +
+
+ +
+
+
+ + + Send Reset Password Instruction + wizard.auth.partner.reset.password + ir.actions.act_window + form + new + + + +
diff --git a/requirements.txt b/requirements.txt index 109483dfa..7fae249f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ contextvars cryptography extendable>=0.0.4 fastapi>=0.110.0 +itsdangerous parse-accept-language pydantic>=2.0.0 pyjwt