diff --git a/rest_api_odoo/Postman Collections/Odoo REST Api.postman_collection.zip b/rest_api_odoo/Postman Collections/Odoo REST Api.postman_collection.zip deleted file mode 100644 index d2a8392f5..000000000 Binary files a/rest_api_odoo/Postman Collections/Odoo REST Api.postman_collection.zip and /dev/null differ diff --git a/rest_api_odoo/__manifest__.py b/rest_api_odoo/__manifest__.py index 76914b0e7..694df2e04 100644 --- a/rest_api_odoo/__manifest__.py +++ b/rest_api_odoo/__manifest__.py @@ -33,7 +33,7 @@ "depends": ['base', 'web'], "data": [ 'security/ir.model.access.csv', - 'views/res_users_views.xml', + 'security/res_api_odoo_security.xml', 'views/connection_api_views.xml' ], 'images': ['static/description/banner.png'], diff --git a/rest_api_odoo/controllers/main.py b/rest_api_odoo/controllers/main.py index 37169cd3b..f5892d604 100644 --- a/rest_api_odoo/controllers/main.py +++ b/rest_api_odoo/controllers/main.py @@ -22,222 +22,244 @@ import json import logging -from odoo import http -from odoo.http import request + +from datetime import datetime +from odoo import http, models +from odoo.http import request, Response +from ast import literal_eval _logger = logging.getLogger(__name__) -class RestApi(http.Controller): - """This is a controller which is used to generate responses based on the - api requests""" +class IrHttp(models.AbstractModel): + """This model is used to authenticate the api-key when sending a request""" + _inherit = "ir.http" - def auth_api_key(self, api_key): + @classmethod + def _auth_method_rest_api(cls): """This function is used to authenticate the api-key when sending a request""" - user_id = request.env['res.users'].search([('api_key', '=', api_key)]) - if api_key is not None and user_id: - response = True - elif not user_id: - response = ('

Invalid API Key ' - '!

') - else: - response = ("

No API Key Provided " - "!

") + try: + api_key = request.httprequest.headers.get("Authorization").lstrip("Bearer ") + if not api_key: + raise PermissionError("Invalid API key provided.") + + user_id = request.env["res.users.apikeys"]._check_credentials( + scope="rpc", key=api_key + ) + request.update_env(user_id) + except Exception as e: + _logger.error(e) + return Response( + json.dumps({"message": e.args[0]}), + status=401, + content_type="application/json", + ) + + +class RestApi(http.Controller): + """This is a controller which is used to generate responses based on the + api requests""" + + def simplest_type(self, input): + """Try cast input into native Python class, otherwise return as string""" + try: + return literal_eval(input) + except Exception: + # Handle lowercase booleans + if input == "true": + return True + if input == "false": + return False + return input - return response + def sanitize_records(self, records): + """Sanitize records for response""" + for record in records: + for key, value in record.items(): + # Manually convert datetime fields to string format + if isinstance(value, datetime): + record[key] = value.isoformat() + return records - def generate_response(self, method, model, rec_id): + def generate_response(self, method, **query): """This function is used to generate the response based on the type of request and the parameters given""" - option = request.env['connection.api'].search( - [('model_id', '=', model)], limit=1) - model_name = option.model_id.model - - if method != 'DELETE': - data = json.loads(request.httprequest.data) - else: - data = {} - fields = [] - if data: - for field in data['fields']: - fields.append(field) - if not fields and method != 'DELETE': - return ("

No fields selected for the model" - "

") - if not option: - return ("

No Record Created for the model" - "

") try: - if method == 'GET': - fields = [] - for field in data['fields']: + model = query.pop("model") + option = request.env["connection.api"].search( + [("model_id", "=", model)], limit=1 + ) + model_name = option.model_id.model + model_display_name = option.model_id.name + + try: + data = json.loads(request.httprequest.data) + except Exception: + data = {} + + fields = [] + if data: + for field in data["fields"]: fields.append(field) + + # Return records' ID by default if not specified + if not fields: + fields.append("id") + + # Get all model's fields if wildcard is used + if "*" in fields: + fields = [] + record_fields = request.env[str(model_name)].fields_get( + [], attributes=["type"] + ) + for field, value in record_fields.items(): + value_type = value.get("type") + if not (value_type == "binary"): + fields.append(field) + if not option: + raise NotImplementedError("No Record Created for the model. ") + if method == "GET": if not option.is_get: - return ("

Method Not Allowed" - "

") - else: - datas = [] - if rec_id != 0: - partner_records = request.env[ - str(model_name)].search_read( - domain=[('id', '=', rec_id)], - fields=fields - ) - data = json.dumps({ - 'records': partner_records - }) - datas.append(data) - return request.make_response(data=datas) - else: - partner_records = request.env[ - str(model_name)].search_read( - domain=[], - fields=fields - ) - data = json.dumps({ - 'records': partner_records - }) - datas.append(data) - return request.make_response(data=datas) - except: - return ("

Invalid JSON Data" - "

") - if method == 'POST': - if not option.is_post: - return ("

Method Not Allowed" - "

") - else: - try: - data = json.loads(request.httprequest.data) - datas = [] - new_resource = request.env[str(model_name)].create( - data['values']) - partner_records = request.env[ - str(model_name)].search_read( - domain=[('id', '=', new_resource.id)], - fields=fields - ) - new_data = json.dumps({'New resource': partner_records, }) - datas.append(new_data) - return request.make_response(data=datas) - except: - return ("

Invalid JSON Data" - "

") - if method == 'PUT': - if not option.is_put: - return ("

Method Not Allowed" - "

") - else: - if rec_id == 0: - return ("

No ID Provided" - "

") - else: - resource = request.env[str(model_name)].browse( - int(rec_id)) - if not resource.exists(): - return ("

Resource not found" - "

") - else: - try: - datas = [] - data = json.loads(request.httprequest.data) - resource.write(data['values']) - partner_records = request.env[ - str(model_name)].search_read( - domain=[('id', '=', resource.id)], - fields=fields - ) - new_data = json.dumps( - {'Updated resource': partner_records, - }) - datas.append(new_data) - return request.make_response(data=datas) - - except: - return ("

Invalid JSON Data " - "!

") - if method == 'DELETE': - if not option.is_delete: - return ("

Method Not Allowed" - "

") - else: - if rec_id == 0: - return ("

No ID Provided" - "

") - else: - resource = request.env[str(model_name)].browse( - int(rec_id)) - if not resource.exists(): - return ("

Resource not found" - "

") - else: - - records = request.env[ - str(model_name)].search_read( - domain=[('id', '=', resource.id)], - fields=['id', 'display_name'] - ) - remove = json.dumps( - {"Resource deleted": records, - }) - resource.unlink() - return request.make_response(data=remove) - - @http.route(['/send_request'], type='http', - auth='none', - methods=['GET', 'POST', 'PUT', 'DELETE'], csrf=False) + raise NameError() + limit = 0 + if query.get("limit"): + limit = int(str(query.get("limit"))) + offset = 0 + if query.get("offset"): + offset = int(str(query.get("offset"))) + + domains = [] + for key, value in query.items(): + if not (key == "limit" or key == "offset"): + domains.append((key, "=", self.simplest_type(value))) + partner_records = request.env[str(model_name)].search_read( + domains, fields, limit=limit, offset=offset + ) + + return Response( + json.dumps({"records": self.sanitize_records(partner_records)}), + content_type="application/json", + ) + if method == "POST": + if not option.is_post: + raise NotImplementedError() + if not data or "values" not in data: + raise ValueError("No Data Provided") + + data = json.loads(request.httprequest.data) + new_resource = request.env[str(model_name)].create(data["values"]) + partner_records = request.env[str(model_name)].search_read( + [("id", "=", new_resource.id)], fields + ) + return Response( + json.dumps({"new_record": self.sanitize_records(partner_records)}), + status=201, + content_type="application/json", + ) + if method == "PUT": + if not option.is_put: + raise NotImplementedError() + + if "id" not in query: + raise ValueError("No ID Provided") + if not data or "values" not in data: + raise ValueError("No Data Provided") + + resource_id = str(query.get("id")) + resource = request.env[str(model_name)].browse(int(resource_id)) + if not resource.exists(): + raise ValueError("Resource not found") + + data = json.loads(request.httprequest.data) + resource.write(data["values"]) + partner_records = request.env[str(model_name)].search_read( + [("id", "=", resource.id)], fields + ) + return Response( + json.dumps( + {"updated_record": self.sanitize_records(partner_records)} + ), + content_type="application/json", + ) + if method == "DELETE": + if not option.is_delete: + raise NotImplementedError() + + if "id" not in query: + raise ValueError("No ID Provided") + + resource_id = str(query.get("id")) + resource = request.env[str(model_name)].browse(int(resource_id)) + if not resource.exists(): + raise ValueError("Resource not found") + + partner_records = request.env[str(model_name)].search_read( + [("id", "=", resource.id)], fields + ) + resource.unlink() + return Response( + json.dumps( + { + "message": "Resource deleted", + "data": self.sanitize_records(partner_records), + } + ), + status=202, + content_type="application/json", + ) + + # If not using any method above, simply return an error + raise NotImplementedError() + except ValueError as e: + return Response( + json.dumps({"message": e.args[0]}), + status=403, + content_type="application/json", + ) + except NotImplementedError as e: + return Response( + json.dumps( + { + "message": f"Method not allowed. {e.args[0]}Please contact your admininstrator to enable {method} method for {model_display_name or 'this'} record." + } + ), + status=405, + content_type="application/json", + ) + except Exception as e: + _logger.error(e) + return Response( + json.dumps({"message": f"Internal server error. {e.args[0]}"}), + status=500, + content_type="application/json", + ) + + @http.route( + ["/send_request"], + type="http", + auth="rest_api", + methods=["GET", "POST", "PUT", "DELETE"], + csrf=False, + ) def fetch_data(self, **kw): """This controller will be called when sending a request to the specified url, and it will authenticate the api-key and then will generate the result""" http_method = request.httprequest.method - api_key = request.httprequest.headers.get('api-key') - auth_api = self.auth_api_key(api_key) - model = kw.get('model') - username = request.httprequest.headers.get('login') - password = request.httprequest.headers.get('password') - request.session.authenticate(request.session.db, username, - password) - model_id = request.env['ir.model'].search( - [('model', '=', model)]) + model = kw.pop("model") + model_id = request.env["ir.model"].search([("model", "=", model)]) if not model_id: - return ("

Invalid model, check spelling or maybe " - "the related " - "module is not installed" - "

") - - if auth_api == True: - if not kw.get('Id'): - rec_id = 0 - else: - rec_id = int(kw.get('Id')) - result = self.generate_response(http_method, model_id.id, rec_id) - return result - else: - return auth_api - - @http.route(['/odoo_connect'], type="http", auth="none", csrf=False, - methods=['GET']) - def odoo_connect(self, **kw): - """This is the controller which initializes the api transaction by - generating the api-key for specific user and database""" - - username = request.httprequest.headers.get('login') - password = request.httprequest.headers.get('password') - db = request.httprequest.headers.get('db') - try: - request.session.update(http.get_default_session(), db=db) - auth = request.session.authenticate(request.session.db, username, - password) - user = request.env['res.users'].browse(auth) - api_key = request.env.user.generate_api(username) - datas = json.dumps({"Status": "auth successful", - "User": user.name, - "api-key": api_key}) - return request.make_response(data=datas) - except: - return ("

wrong login credentials" - "

") + return Response( + json.dumps( + { + "message": "Invalid model, check spelling or maybe the related module is not installed" + } + ), + status=403, + content_type="application/json", + ) + + return self.generate_response(http_method, model=model_id.id, **kw) diff --git a/rest_api_odoo/models/__init__.py b/rest_api_odoo/models/__init__.py index e9bf22051..27f8df7c3 100644 --- a/rest_api_odoo/models/__init__.py +++ b/rest_api_odoo/models/__init__.py @@ -21,4 +21,3 @@ ############################################################################# from . import connection_api -from . import res_users diff --git a/rest_api_odoo/models/res_users.py b/rest_api_odoo/models/res_users.py deleted file mode 100644 index a2d861f36..000000000 --- a/rest_api_odoo/models/res_users.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding:utf-8 -*- -############################################################################# -# -# Cybrosys Technologies Pvt. Ltd. -# -# Copyright (C) 2023-TODAY Cybrosys Technologies() -# Author: Cybrosys Techno Solutions() -# -# You can modify it under the terms of the GNU LESSER -# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. -# -# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE -# (LGPL v3) along with this program. -# If not, see . -# -############################################################################# - -import uuid -from odoo import fields, models - - -class UserLogin(models.Model): - """This class is used to inherit users and add api key generation""" - _inherit = 'res.users' - - api_key = fields.Char(string="API Key", readonly=True, - help="Api key for connecting with the " - "Database.The key will be " - "generated when authenticating " - "rest api.") - - def generate_api(self, username): - """This function is used to generate api-key for each user""" - users = self.env['res.users'].sudo().search([('login', '=', username)]) - if not users.api_key: - users.api_key = str(uuid.uuid4()) - key = users.api_key - else: - key = users.api_key - return key diff --git a/rest_api_odoo/security/res_api_odoo_security.xml b/rest_api_odoo/security/res_api_odoo_security.xml new file mode 100644 index 000000000..545fd5132 --- /dev/null +++ b/rest_api_odoo/security/res_api_odoo_security.xml @@ -0,0 +1,32 @@ + + + + + + REST API + Helps you manage your REST API records. + 17 + + + + Manager + + + + + + + + + + All REST APIs + + [(1,'=',1)] + + + + + \ No newline at end of file diff --git a/rest_api_odoo/static/description/assets/screenshots/screenshot_2.png b/rest_api_odoo/static/description/assets/screenshots/screenshot_2.png index c892dd068..94edbd0e0 100644 Binary files a/rest_api_odoo/static/description/assets/screenshots/screenshot_2.png and b/rest_api_odoo/static/description/assets/screenshots/screenshot_2.png differ diff --git a/rest_api_odoo/static/description/index.html b/rest_api_odoo/static/description/index.html index bf41854b7..fff5c7eb1 100644 --- a/rest_api_odoo/static/description/index.html +++ b/rest_api_odoo/static/description/index.html @@ -23,7 +23,7 @@

- Odoo rest API

+ Odoo Rest API @@ -138,7 +138,7 @@
- Authentication using the generated api key. + Authentication using the generated API key.
@@ -217,29 +217,21 @@
  • First of all, we have to add a new parameter in odoo conf. file.
  • -
  • server_wide_modules = web, base, rest_api_odoo
    +
  • server_wide_modules = web, base, rest_api_odoo
    - This will allow us to send request to server without selecting database first.
    - Incase if you have to uninstall the module , you have to remove this parameter.
    - Next we can install the module.
  • -
  • After installing the Rest api app we can see a new api key +
  • After installing the Rest API app we can see a new API key field in users.
  • - - Next we have to generate the api-key for the current + - Next we have to generate the API-key for the current user.
    -
  • You can import the postman collections provided in the app - folder for authentication and interacting with database in - various methods. -
  • -
    -
    -
    @@ -248,27 +240,8 @@

      -
    • Next you have to select the database and login.
    • -
    • We have attached Postman collections through which - you can - authenticate rest api. -
    • -
    • First, extract the zip file. Then, you will obtain the JSON-format file, which you can directly import into POSTMAN.
    • -
    • The url format will be like this - http://cybrosys:8016/odoo_connect - Replace 'cybrosys:8016' with your localhost port number. -
    • -
    • You have to provide database name, username and password - through the headers while sending request. -
    • -
    • If the authentication is successful , an api key will be - generated for the current user. -
    • -
    • This key will be used when sending api requests to - database. -
    • -
    • The response will be like this - {"Status": "auth - successful", "User": "Mitchell Admin", "api-key": - "66c2ebab-d4dc-42f0-87d0-d1646e887569"}.
    • +
    • Next you have to generate the API key under My Profile > Account Security.
    • +
    • Set the name of the API key according to your needs.

    - Create records in Rest api app + Create records in Rest API app

      -
    • After rest api authentication, we can create records in the - rest api app. +
    • After creating the API key, we can create records in the + Rest API app.
    • Here we can choose the model, and also we can choose the http methods.
    • -
    • The api response will be based on these records.
    • +
    • The API response will be based on these records.

      -
    • You can send GET request to retrieve data from the +
    • You can send GET request to retrieve data from the database.
    • -
    • The postman collection has been provided with app files for - sending request from postman. -
    • -
    • You have to provide username, password and api key through - the header. +
    • You have to provide the API key in the header: Authorization: Bearer {API_KEY} + where {API_KEY} should be replaced with the actual API key.
    • -
    • Model can be passed as argument as the technical name , and - also if you want - specific record you can provide the id as well. +
    • Model can be passed as argument as the technical name, and + also if you want to get a specific record you can provide any + of the related fields to the module as well.
    • -
    • The format for GET method will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=10. +
    • The format for GET method will be like this - http://cybrosys:8016/send_request?model=res.partner&id=10.
    • We can specify the fields inside the JSON data, and it will - be like this - {"fields": ["name", "email"]}.
    • -
    • This is the format of api response - {"records": [{"id": + be like this - {"fields": ["name", "email"]}. + If no fields are passed , it will returns just the record's ID. + To get all of the fields, set the fields to wildcard - {"fields": ["*"]}. + Pagination can also be used by adding {"limit": 10, "offset": 0} + to the query parameter above. +
    • +
    • This is the format of API response - {"records": [{"id": 10, "email": "deco.addict82@example.com", "name": "Deco - Addict"}]}. + Addict"}]}.
    @@ -338,31 +313,28 @@

      -
    • Using POST method , you can create new records in the +
    • Using POST method , you can create new records in the database.
    • -
    • Just make sure you enabled POST method for the model record - in rest api app , otherwise you will get 'method not +
    • Just make sure you enabled POST method for the model record + in Rest API app , otherwise you will get 'method not allowed' message.
    • For creating record you have to provide the JSON data along with the model.
    • -
    • You can make use of the postman collection that we have - added with app files. -
    • -
    • The format for sending POST request will be like this - http://cybrosys:8016/send_request?model=res.partner. +
    • The format for sending POST request will be like this - http://cybrosys:8016/send_request?model=res.partner.
    • -
    • This is the format for JSON data - { +
    • This is the format for JSON data - { "fields" :["name", "phone"] , "values": {"name": "abc", "phone":"55962441552" - } }. + } }.
    • Make sure the data entered in correct format otherwise you will get 'Invalid JSON data' message.
    • -
    • Response will be in this format - {"New resource": - [{"id": 51, "name": "abc", "phone": "55962441552"}]}. +
    • Response will be in this format - {"New resource": + [{"id": 51, "name": "abc", "phone": "55962441552"}]}.

    @@ -373,24 +345,20 @@ Update Records
      -
    • Updation of records in the database can be done with PUT +
    • Updation of records in the database can be done with PUT method.
    • You have to provide the model and also the id or the record that you want to update.
    • -
    • You can use the Postman collection that we have provided and , you - will be always have to send request with your login - credentials. Otherwise, it will be showing access denied. -
    • -
    • The format for sending PUT request will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=46. +
    • The format for sending PUT request will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=46.
    • Here too you have to provide the JSON data through which the updates will be done.
    • -
    • The response format will be like this - {"Updated +
    • The response format will be like this - {"Updated resource": [{"id": 46, "email": "abc@example.com", "name": - "Toni"}]}.
    • + "Toni"}]}.

    @@ -401,20 +369,20 @@

      -
    • Database records can be deleted by sending DELETE method +
    • Database records can be deleted by sending DELETE method request.
    • For the deletion we have to provide the Model and the record id that we want to delete.
    • Make sure you have permission to delete files for the - selected model in the rest api record. + selected model in the Rest API record.
    • -
    • The delete request format will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=46. +
    • The delete request format will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=46.
    • -
    • The response after successful deletion will be - +
    • The response after successful deletion will be - {"Resource deleted": [{"id": 46, "email": "abc@example.com", - "name": "Toni"}]}.
    • + "name": "Toni"}]}.

    @@ -843,7 +811,7 @@