From 75f453adec5f40cae1d76b81946bb28f99ba0e10 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 26 Jul 2021 13:07:37 +0200 Subject: [PATCH] [ADD] pms_api_rest: module created --- pms_api_rest/__init__.py | 5 + pms_api_rest/__manifest__.py | 19 +++ pms_api_rest/controllers/__init__.py | 2 + pms_api_rest/controllers/jwt_controller.py | 110 ++++++++++++++++++ pms_api_rest/controllers/pms_rest.py | 22 ++++ pms_api_rest/datamodels/__init__.py | 2 + .../pms_reservation_search_param.py | 10 ++ .../datamodels/pms_reservation_short_info.py | 16 +++ pms_api_rest/models/__init__.py | 2 + pms_api_rest/models/jwt_access_token.py | 32 +++++ pms_api_rest/models/res_users.py | 43 +++++++ pms_api_rest/security/ir.model.access.csv | 2 + pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/reservation_services.py | 51 ++++++++ pms_api_rest/static/description/icon.png | Bin 0 -> 4852 bytes 15 files changed, 317 insertions(+) create mode 100644 pms_api_rest/__init__.py create mode 100644 pms_api_rest/__manifest__.py create mode 100644 pms_api_rest/controllers/__init__.py create mode 100644 pms_api_rest/controllers/jwt_controller.py create mode 100644 pms_api_rest/controllers/pms_rest.py create mode 100644 pms_api_rest/datamodels/__init__.py create mode 100644 pms_api_rest/datamodels/pms_reservation_search_param.py create mode 100644 pms_api_rest/datamodels/pms_reservation_short_info.py create mode 100644 pms_api_rest/models/__init__.py create mode 100644 pms_api_rest/models/jwt_access_token.py create mode 100644 pms_api_rest/models/res_users.py create mode 100644 pms_api_rest/security/ir.model.access.csv create mode 100644 pms_api_rest/services/__init__.py create mode 100644 pms_api_rest/services/reservation_services.py create mode 100644 pms_api_rest/static/description/icon.png diff --git a/pms_api_rest/__init__.py b/pms_api_rest/__init__.py new file mode 100644 index 000000000..d9df8e2dc --- /dev/null +++ b/pms_api_rest/__init__.py @@ -0,0 +1,5 @@ +from . import controllers +from . import datamodels +from . import models +from . import services + diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py new file mode 100644 index 000000000..bf4ba876f --- /dev/null +++ b/pms_api_rest/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "API REST PMS", + "author": "Commit [Sun], Odoo Community Association (OCA)", + "website": "https://github.com/OCA/pms", + "category": "Generic Modules/Property Management System", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "depends": [ + "pms", + "base_rest", + "base_rest_datamodel", + "web", + "auth_signup", + ], + "external_dependencies": { + "python": ["jwt", "simplejson", "marshmallow"], + }, + "installable": True, +} diff --git a/pms_api_rest/controllers/__init__.py b/pms_api_rest/controllers/__init__.py new file mode 100644 index 000000000..edc7f8a32 --- /dev/null +++ b/pms_api_rest/controllers/__init__.py @@ -0,0 +1,2 @@ +from . import jwt_controller +from . import pms_rest diff --git a/pms_api_rest/controllers/jwt_controller.py b/pms_api_rest/controllers/jwt_controller.py new file mode 100644 index 000000000..a5c544712 --- /dev/null +++ b/pms_api_rest/controllers/jwt_controller.py @@ -0,0 +1,110 @@ +import logging + +import werkzeug + +from odoo import http +from odoo.http import request + +from odoo.addons.auth_signup.models.res_users import SignupError + +from ..lib.jwt_http import jwt_http +from ..lib.validator import validator + +_logger = logging.getLogger(__name__) + +SENSITIVE_FIELDS = [ + "password", + "password_crypt", + "new_password", + "create_uid", + "write_uid", +] + + +class JwtController(http.Controller): + # test route + @http.route("/api/info", auth="public", csrf=False, cors="*") + def index(self, **kw): + return "Hello, world" + + @http.route( + "/api/login", type="http", auth="public", csrf=False, cors="*", methods=["POST"] + ) + def login(self, email, password, **kw): + return jwt_http.do_login(email, password) + + @http.route("/api/me", type="http", auth="public", csrf=False, cors="*") + def me(self, **kw): + http_method, body, headers, token = jwt_http.parse_request() + result = validator.verify_token(token) + if not result["status"]: + return jwt_http.errcode(code=result["code"], message=result["message"]) + + return jwt_http.response(request.env.user.to_dict(True)) + + @http.route("/api/logout", type="http", auth="public", csrf=False, cors="*") + def logout(self, **kw): + http_method, body, headers, token = jwt_http.parse_request() + result = validator.verify_token(token) + if not result["status"]: + return jwt_http.errcode(code=result["code"], message=result["message"]) + + jwt_http.do_logout(token) + return jwt_http.response() + + @http.route( + "/api/register", + type="http", + auth="public", + csrf=False, + cors="*", + methods=["POST"], + ) + def register(self, email=None, name=None, password=None, **kw): + if not validator.is_valid_email(email): + return jwt_http.errcode(code=400, message="Invalid email address") + if not name: + return jwt_http.errcode(code=400, message="Name cannot be empty") + if not password: + return jwt_http.errcode(code=400, message="Password cannot be empty") + + # sign up + try: + self._signup_with_values(login=email, name=name, password=password) + except AttributeError: + return jwt_http.errcode(code=501, message="Signup is disabled") + except (SignupError, AssertionError) as e: + if request.env["res.users"].sudo().search([("login", "=", email)]): + return jwt_http.errcode( + code=400, message="Email address already exists" + ) + else: + _logger.error("%s", e) + return jwt_http.response_500() + except Exception as e: + _logger.error(str(e)) + return jwt_http.response_500() + # log the user in + return jwt_http.do_login(email, password) + + def _signup_with_values(self, **values): + request.env["res.users"].sudo().signup(values, None) + request.env.cr.commit() # as authenticate will use its + # own cursor we need to commit the current transaction + self.signup_email(values) + + def signup_email(self, values): + user_sudo = ( + request.env["res.users"] + .sudo() + .search([("login", "=", values.get("login"))]) + ) + template = request.env.ref( + "auth_signup.mail_template_user_signup_account_created", + raise_if_not_found=False, + ) + if user_sudo and template: + template.sudo().with_context( + lang=user_sudo.lang, + auth_login=werkzeug.url_encode({"auth_login": user_sudo.email}), + ).send_mail(user_sudo.id, force_send=True) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py new file mode 100644 index 000000000..1123144f8 --- /dev/null +++ b/pms_api_rest/controllers/pms_rest.py @@ -0,0 +1,22 @@ +from odoo.addons.base_rest.controllers import main + +from ..lib.jwt_http import jwt_http +from ..lib.validator import validator + + +class BaseRestDemoPublicApiController(main.RestController): + _root_path = "/api/" + _collection_name = "pms.reservation.service" + _default_auth = "public" + + # RestController OVERRIDE method + def _process_method(self, service_name, method_name, *args, params=None): + + http_method, body, headers, token = jwt_http.parse_request() + result = validator.verify_token(token) + if not result["status"]: + return jwt_http.errcode(code=result["code"], message=result["message"]) + else: + return super(BaseRestDemoPublicApiController, self)._process_method( + service_name, method_name, *args, params=params + ) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py new file mode 100644 index 000000000..f02015c30 --- /dev/null +++ b/pms_api_rest/datamodels/__init__.py @@ -0,0 +1,2 @@ +from . import pms_reservation_short_info +from . import pms_reservation_search_param diff --git a/pms_api_rest/datamodels/pms_reservation_search_param.py b/pms_api_rest/datamodels/pms_reservation_search_param.py new file mode 100644 index 000000000..ca2a5df10 --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_search_param.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReservationSearchParam(Datamodel): + _name = "pms.reservation.search.param" + + id = fields.Integer(required=False, allow_none=False) + name = fields.String(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_short_info.py new file mode 100644 index 000000000..3daa347ed --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_short_info.py @@ -0,0 +1,16 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReservationShortInfo(Datamodel): + _name = "pms.reservation.short.info" + + id = fields.Integer(required=True, allow_none=False) + partner = fields.String(required=True, allow_none=False) + checkin = fields.String(required=True, allow_none=False) + checkout = fields.String(required=True, allow_none=False) + preferred_room_id = fields.String(required=True, allow_none=False) + room_type_id = fields.String(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + partner_requests = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py new file mode 100644 index 000000000..eb1992899 --- /dev/null +++ b/pms_api_rest/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_users +from . import jwt_access_token diff --git a/pms_api_rest/models/jwt_access_token.py b/pms_api_rest/models/jwt_access_token.py new file mode 100644 index 000000000..54f9fdd36 --- /dev/null +++ b/pms_api_rest/models/jwt_access_token.py @@ -0,0 +1,32 @@ +from datetime import datetime + +from odoo import api, fields, models + + +class JwtAccessToken(models.Model): + _name = "jwt_provider.access_token" + _description = "Store user access token for one-time-login" + + token = fields.Char( + "Access Token", + required=True + ) + user_id = fields.Many2one( + comodel_name="res.users", + string="User", + required=True, + ondelete="cascade", + ) + expires = fields.Datetime( + "Expires", + required=True, + ) + + is_expired = fields.Boolean( + compute="_compute_is_expired", + ) + + @api.depends("expires") + def _compute_is_expired(self): + for token in self: + token.is_expired = datetime.now() > token.expires diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py new file mode 100644 index 000000000..65dd31fff --- /dev/null +++ b/pms_api_rest/models/res_users.py @@ -0,0 +1,43 @@ +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import AccessDenied, ValidationError + +from ..lib.validator import validator + +_logger = logging.getLogger(__name__) + + +class ResUsers(models.Model): + _inherit = "res.users" + + access_token_ids = fields.One2many( + string="Access Tokens", + comodel_name="jwt_provider.access_token", + inverse_name="user_id", + ) + + @classmethod + def _login(cls, db, login, password, user_agent_env): + user_id = super(ResUsers, cls)._login(db, login, password, user_agent_env) + if user_id: + return user_id + uid = validator.verify(password) + _logger.info(uid) + return uid + + @api.model + def _check_credentials(self, password, user_agent_env): + try: + super(ResUsers, self)._check_credentials(password, user_agent_env) + except AccessDenied: + if not validator.verify(password): + raise + + def to_dict(self, single=False): + res = [] + for u in self: + d = u.read(["email", "name", "company_id"])[0] + res.append(d) + + return res[0] if single else res diff --git a/pms_api_rest/security/ir.model.access.csv b/pms_api_rest/security/ir.model.access.csv new file mode 100644 index 000000000..b343baac4 --- /dev/null +++ b/pms_api_rest/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_jwt_access_token,Read jwt access token,model_jwt_provider_access_token,,1,0,0,0 \ No newline at end of file diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py new file mode 100644 index 000000000..46413cba0 --- /dev/null +++ b/pms_api_rest/services/__init__.py @@ -0,0 +1 @@ +from . import reservation_services diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py new file mode 100644 index 000000000..67536903c --- /dev/null +++ b/pms_api_rest/services/reservation_services.py @@ -0,0 +1,51 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsReservationService(Component): + _inherit = "base.rest.service" + _name = "pms.reservation.service" + _usage = "reservations" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET" + ) + ], + input_param=Datamodel("pms.reservation.search.param"), + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="public", + ) + def search(self, reservation_search_param): + domain = [] + if reservation_search_param.name: + domain.append(("name", "like", reservation_search_param.name)) + if reservation_search_param.id: + domain.append(("id", "=", reservation_search_param.id)) + res = [] + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + for reservation in self.env["pms.reservation"].sudo().search( + domain, + ): + res.append( + PmsReservationShortInfo( + id=reservation.id, + partner=reservation.partner_id.name, + checkin=str(reservation.checkin), + checkout=str(reservation.checkout), + preferred_room_id=reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + room_type_id=reservation.room_type_id.name + if reservation.room_type_id + else "", + name=reservation.name, + ) + ) + return res diff --git a/pms_api_rest/static/description/icon.png b/pms_api_rest/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bd2b188d63758e9984b128316e8ca6fbadc5d167 GIT binary patch literal 4852 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 aK>q`m&r85i|Lk=D0000