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 000000000..bd2b188d6 Binary files /dev/null and b/pms_api_rest/static/description/icon.png differ