diff --git a/odex25_mobile/odex_web_app/__init__.py b/odex25_mobile/odex_web_app/__init__.py new file mode 100644 index 000000000..f7209b171 --- /dev/null +++ b/odex25_mobile/odex_web_app/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/odex25_mobile/odex_web_app/__manifest__.py b/odex25_mobile/odex_web_app/__manifest__.py new file mode 100644 index 000000000..814a39f1d --- /dev/null +++ b/odex25_mobile/odex_web_app/__manifest__.py @@ -0,0 +1,28 @@ +{ + 'name': 'Odex Web App API', + 'version': '1.0', + 'license': 'AGPL-3', + 'category': 'Odex25-Mobile/Odex25-Mobile', + 'author': 'Expert Co. Ltd.', + 'website': 'http://exp-sa.com', + 'summary': "All Mopile Web App Api and Configurations", + 'depends': [ + 'base', + 'attendances', + 'employee_requests', + 'website', + 'mass_mailing', + 'hr_holidays_public', + 'exp_payroll_custom', + 'hr_timesheet_sheet', + ], + 'external_dependencies': { + 'python': ['jwt', ], + }, + 'data': [ + 'views/attendance_zone_config_view.xml', + 'views/hr_employee_view.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/odex25_mobile/odex_web_app/__pycache__/__init__.cpython-38.pyc b/odex25_mobile/odex_web_app/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..e76a9d7ee Binary files /dev/null and b/odex25_mobile/odex_web_app/__pycache__/__init__.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/__pycache__/http_helper.cpython-38.pyc b/odex25_mobile/odex_web_app/__pycache__/http_helper.cpython-38.pyc new file mode 100644 index 000000000..443d53559 Binary files /dev/null and b/odex25_mobile/odex_web_app/__pycache__/http_helper.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/__pycache__/validator.cpython-38.pyc b/odex25_mobile/odex_web_app/__pycache__/validator.cpython-38.pyc new file mode 100644 index 000000000..09fc085ac Binary files /dev/null and b/odex25_mobile/odex_web_app/__pycache__/validator.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/controllers/__init__.py b/odex25_mobile/odex_web_app/controllers/__init__.py new file mode 100644 index 000000000..f9417ca5b --- /dev/null +++ b/odex25_mobile/odex_web_app/controllers/__init__.py @@ -0,0 +1,3 @@ +from . import authentication +from . import web + diff --git a/odex25_mobile/odex_web_app/controllers/__pycache__/__init__.cpython-38.pyc b/odex25_mobile/odex_web_app/controllers/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..d027d9eca Binary files /dev/null and b/odex25_mobile/odex_web_app/controllers/__pycache__/__init__.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/controllers/__pycache__/authentication.cpython-38.pyc b/odex25_mobile/odex_web_app/controllers/__pycache__/authentication.cpython-38.pyc new file mode 100644 index 000000000..6f28a5875 Binary files /dev/null and b/odex25_mobile/odex_web_app/controllers/__pycache__/authentication.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/controllers/__pycache__/web.cpython-38.pyc b/odex25_mobile/odex_web_app/controllers/__pycache__/web.cpython-38.pyc new file mode 100644 index 000000000..1059230ad Binary files /dev/null and b/odex25_mobile/odex_web_app/controllers/__pycache__/web.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/controllers/authentication.py b/odex25_mobile/odex_web_app/controllers/authentication.py new file mode 100644 index 000000000..1d9fb09af --- /dev/null +++ b/odex25_mobile/odex_web_app/controllers/authentication.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +import werkzeug +from odoo import http, tools +from odoo.http import request, Response +from odoo.addons.auth_signup.models.res_users import SignupError +from odoo.exceptions import UserError +import base64 +from ..validator import validator +from ..http_helper import http_helper +import json +import logging +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + +SENSITIVE_FIELDS = ['password', 'password_crypt', 'new_password', 'create_uid', 'write_uid'] + + +class AuthenticationController(http.Controller): + + @http.route('/rest_api/login', type='http', auth='none', csrf=False, cors='*', methods=['POST']) + def login_phone(self, **kw): + login = kw.get('login') + password = kw.get('password') + if not login: + return http_helper.response(code=400, message=_('username or email is missing'), success=False) + + if not password: + return http_helper.response(code=400, message=_('Password is missing'), success=False) + if not kw.get('device_id'): + return http_helper.response(code=400, message=_('Device id is missing'), success=False) + + # check fcm_token + if not kw.get('fcm_token'): + return http_helper.response(code=400, message=_('FCM Token is missing'), success=False) + + user = request.env['res.users'].sudo().search([('login', '=', login)], limit=1) + + if not user or not user.login: + return http_helper.response(code=400, message=_('User account with login {} not found').format(login), + success=False) + + uid = http_helper.is_authentic(login, password) + + if not uid: + return http_helper.errcode(code=400, message=_('Unable to Sign In. invalid user password')) + token = validator.create_token(request.env.user) + dic = request.env.user.to_dict(True) + employee = http.request.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1) + if employee and kw.get('device_id') and not employee.device_id: + employee.sudo().write({'device_id': kw.get('device_id')}) + + # write fcm_token and fcm_token_web in employee + if employee and (kw.get('fcm_token') or kw.get('fcm_token_web')): + employee.sudo().write({'fcm_token': kw.get('fcm_token'), 'fcm_token_web': kw.get('fcm_token_web')}) + + dic['token'] = token + http_helper.cleanup(); + return http_helper.response(data=dic, message=_("User log in successfully")) diff --git a/odex25_mobile/odex_web_app/controllers/web.py b/odex25_mobile/odex_web_app/controllers/web.py new file mode 100644 index 000000000..2c893e3dc --- /dev/null +++ b/odex25_mobile/odex_web_app/controllers/web.py @@ -0,0 +1,18 @@ +import odoo +from odoo import http +from odoo.http import request + + +class WebController(http.Controller): + @http.route('/web/session/authenticate', type='json', auth="none") + def authenticate(self, login, password, base_location=None): + db = odoo.tools.config.get('db_name') + if not db: + response_data = { + "error": "Database name should be specified in Conf File", + "status": 400 + } + return response_data + + request.session.authenticate(db, login, password) + return request.env['ir.http'].session_info() diff --git a/odex25_mobile/odex_web_app/http_helper.py b/odex25_mobile/odex_web_app/http_helper.py new file mode 100644 index 000000000..f744e3df7 --- /dev/null +++ b/odex25_mobile/odex_web_app/http_helper.py @@ -0,0 +1,128 @@ +from odoo import http +from odoo.http import request, Response +from .validator import validator +import simplejson as json +import logging +_logger = logging.getLogger(__name__) + +return_fields = ['id', 'login', 'name', 'company_id','state'] + + +class HttpHelper: + + def get_state(self): + return { + 'd': request.session.db + } + + def parse_request(self): + http_method = request.httprequest.method + try: + body = http.request.params + except Exception: + body = {} + + headers = dict(list(request.httprequest.headers.items())) + if 'wsgi.input' in headers: + del headers['wsgi.input'] + if 'wsgi.errors' in headers: + del headers['wsgi.errors'] + if 'HTTP_AUTHORIZATION' in headers: + headers['Authorization'] = headers['HTTP_AUTHORIZATION'] + + # extract token + token = '' + if 'Authorization' in headers: + try: + # Bearer token_string + token = headers['Authorization'].split(' ')[1] + except Exception: + pass + + return http_method, body, headers, token + + def date2str(self, d, f='%Y-%m-%d %H:%M:%S'): + """ + Convert datetime to string + :param self: + :param d: datetime object + :param f='%Y-%m-%d%H:%M:%S': string format + """ + try: + s = d.strftime(f) + except: + s = None + finally: + return s + + def response(self, success=True, message=None, data=None, code=200,errors=None): + """ + Create a HTTP Response for controller + :param success=True indicate this response is successful or not + :param message=None message string + :param data=None data to return + :param code=200 http status code + """ + payload = json.dumps({ + 'success': success, + 'message': message, + 'data': data, + 'code':code + }) + + return Response(payload, status=code, headers=[ + ('Content-Type', 'application/json'), + ]) + + def response_500(self, message='Internal Server Error', data=None): + return self.response(success=False, message=message, data=data, code=500) + + def response_404(self, message='404 Not Found', data=None): + return self.response(success=False, message=message, data=data, code=404) + + def response_403(self, message='403 Forbidden', data=None): + return self.response(success=False, message=message, data=data, code=403) + + def errcode(self, code, message=None): + return self.response(success=False, code=code, message=message) + + def is_authentic(self, login, password): + state = self.get_state() + name = login.strip() + pwd = password.strip() + return request.session.authenticate(state['d'], name, pwd) + + + + def do_login(self, login, password): + # get current db + state = self.get_state() + name = login.strip() + pwd = password.strip() + + uid = request.session.authenticate(state['d'], name, pwd) + if not uid: + return self.errcode(code=400, message='incorrect login') + # login success, generate token + token = validator.create_token(request.env.user) + dic = request.env.user.to_dict(True) + dic['token'] = token + + return self.response(data=dic) + + def do_logout(self, token): + request.session.logout() + request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]).unlink() + + def cleanup(self): + # Clean up things after success request + # use logout here to make request as stateless as possible + + + + request.session.logout() + + +http_helper = HttpHelper() diff --git a/odex25_mobile/odex_web_app/models/__init__.py b/odex25_mobile/odex_web_app/models/__init__.py new file mode 100644 index 000000000..d024c97e2 --- /dev/null +++ b/odex25_mobile/odex_web_app/models/__init__.py @@ -0,0 +1,3 @@ +from . import hr_employee +from . import attendence_zone_config +from . import mail_thread diff --git a/odex25_mobile/odex_web_app/models/__pycache__/__init__.cpython-38.pyc b/odex25_mobile/odex_web_app/models/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..544925438 Binary files /dev/null and b/odex25_mobile/odex_web_app/models/__pycache__/__init__.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/models/__pycache__/attendence_zone_config.cpython-38.pyc b/odex25_mobile/odex_web_app/models/__pycache__/attendence_zone_config.cpython-38.pyc new file mode 100644 index 000000000..defc2c6e1 Binary files /dev/null and b/odex25_mobile/odex_web_app/models/__pycache__/attendence_zone_config.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/models/__pycache__/hr_employee.cpython-38.pyc b/odex25_mobile/odex_web_app/models/__pycache__/hr_employee.cpython-38.pyc new file mode 100644 index 000000000..0f6fbca49 Binary files /dev/null and b/odex25_mobile/odex_web_app/models/__pycache__/hr_employee.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/models/__pycache__/mail_thread.cpython-38.pyc b/odex25_mobile/odex_web_app/models/__pycache__/mail_thread.cpython-38.pyc new file mode 100644 index 000000000..d4320ff51 Binary files /dev/null and b/odex25_mobile/odex_web_app/models/__pycache__/mail_thread.cpython-38.pyc differ diff --git a/odex25_mobile/odex_web_app/models/attendence_zone_config.py b/odex25_mobile/odex_web_app/models/attendence_zone_config.py new file mode 100644 index 000000000..19a610396 --- /dev/null +++ b/odex25_mobile/odex_web_app/models/attendence_zone_config.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +import datetime + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + web_fcm_server_key = fields.Char(string='Server Key:', related="company_id.web_fcm_server_key", readonly=False) + web_sender_id = fields.Char(string='Sender ID:', related="company_id.web_sender_id", readonly=False) + + +class ResCompany(models.Model): + _inherit = 'res.company' + + web_fcm_server_key = fields.Char(string='Server Key') + web_sender_id = fields.Char(string='Sender ID') diff --git a/odex25_mobile/odex_web_app/models/hr_employee.py b/odex25_mobile/odex_web_app/models/hr_employee.py new file mode 100644 index 000000000..2cb1d434e --- /dev/null +++ b/odex25_mobile/odex_web_app/models/hr_employee.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from datetime import datetime, timedelta +from odoo import models,fields,api,_ +from odoo.exceptions import ValidationError +import random +import json +import json, requests + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + fcm_token_web = fields.Char(string='FCM Web Token') + def user_push_notification(self, notification): + url = "https://fcm.googleapis.com/fcm/send" + header = { + 'Content-Type': 'application/json', + 'Authorization': 'key=%s' % (self.env.user.company_id.web_fcm_server_key) + } + body = json.dumps({ + "to": self.fcm_token_web, + "direct_boot_ok": True, + "notification": notification + }) + try: + respons = requests.post(url=url, data=body, headers=header) + return True + except Exception as e: + return False + + diff --git a/odex25_mobile/odex_web_app/models/mail_thread.py b/odex25_mobile/odex_web_app/models/mail_thread.py new file mode 100644 index 000000000..8929c1948 --- /dev/null +++ b/odex25_mobile/odex_web_app/models/mail_thread.py @@ -0,0 +1,31 @@ +from odoo import fields, models, api + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + @api.returns('mail.message', lambda value: value.id) + def message_post(self, **kwargs): + print(self.read(),"==") + print(self.env.user.partner_id,"=====") + print("CALLED,++++++++++++++++++++++++++++++++",kwargs) + return super(MailThread, self).message_post(**kwargs) + + @api.model + def message_notify(self, *, + partner_ids=False, parent_id=False, model=False, res_id=False, + author_id=None, email_from=None, body='', subject=False, **kwargs): + new_message = super(MailThread, self).message_notify( + partner_ids=partner_ids, parent_id=parent_id, model=model, res_id=res_id, + author_id=author_id, email_from=email_from, body=body, subject=subject, **kwargs + ) + print(new_message,"=============================") + + if partner_ids: + partners = self.env['res.partner'].browse(partner_ids) + for partner in partners: + print(partner.read(), "===============") + print(partner.employee.send_notification(subject, body), "===============") + # partner.send_notification(subject, body) + + return new_message diff --git a/odex25_mobile/odex_web_app/validator.py b/odex25_mobile/odex_web_app/validator.py new file mode 100644 index 000000000..5afba63db --- /dev/null +++ b/odex25_mobile/odex_web_app/validator.py @@ -0,0 +1,293 @@ +import logging +import jwt +import re +import datetime +import traceback +from odoo import http, service, registry, SUPERUSER_ID,_ +from odoo.http import request +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT +import phonenumbers +import math +from math import ceil +from odoo.service import security + +import logging +_logger = logging.getLogger(__name__) + +SECRET_KEY = "skjdfe48ueq739rihesdio*($U*WIO$u8" + +regex = r"^[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" + + +class Validator: + def get_page_pagination(self,page): + limit = 20 + page = int(page) + page = 1 if page<1 else page + offset = (page - 1) * limit + prev = False if page -1<=0 else page -1 + return page,offset,limit,prev + + def get_page_pagination_next(self,page,count): + page = int(page) + page = page+1 + next = math.ceil(count / 20) + if next 6 + + def is_valid_password(self, password): + return len (password) > 5 + + def is_valid_location(self, location): + try: + lat , lat = [float(x) for x in location.split(',') if x.replace('.','',1).isdigit()] + if lat and lat: + return True + except : + pass + return False + + def is_valid_email(self, email): + return re.search(regex, email) + + def is_valid_phone(self,phone): + try: + if not phone.startswith('+'): + phone = '+'+phone + number = phonenumbers.parse(phone, None) + isvalid = phonenumbers.is_valid_number(number) + return isvalid + except : + pass + return False + + def get_server_error(self,e,user): + x= False + if len(e.args) == 2: + x, y = e.args + x = re.sub('\n', '', x) + else: + x = e.args + text = "" + if user.lang == 'en_US': + text =(_("contact admin or edit it Manually")) + else: + text = "الرجاء التواصل مع مدير النظام او استخدام الموقع الالكترونى" + message = "%s, %s" % (x,text) + return message + + + def create_token(self, user): + try: + exp = datetime.datetime.utcnow() + datetime.timedelta(days=7) + payload = { + 'exp': exp, + 'iat': datetime.datetime.utcnow(), + 'sub': user['id'], + 'lgn': user['login'], + } + token = jwt.encode( + payload, + SECRET_KEY, + algorithm='HS256' + ) + # payload = jwt.decode(token, SECRET_KEY ,algorithms='HS256') + print(token) + self.save_token(token, user['id'], exp) + + return token + except Exception as ex: + _logger.error(ex) + raise + + def save_token(self, token, uid, exp): + request.env['jwt_provider.access_token'].sudo().create({ + 'user_id': uid, + 'expires': exp.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'token': token, + }) + + def verify(self, token): + record = request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]) + + if len(record) != 1: + _logger.info('not found %s' % token) + return False + + if record.is_expired: + return False + + record.set_env(request.env) + return record.user_id + + def verify_token(self, token): + try: + result = { + 'status': False, + 'message': None, + } + + record = request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]) + + if len(record) != 1: + result['message'] = 'Token not found' + result['code'] = 497 + return result + + if record.is_expired: + result['message'] = 'Token has expired' + result['code'] = 498 + return result + + payload = jwt.decode(token, SECRET_KEY ,algorithms='HS256') + + uid =self.verify(token) + user = uid + request.session.uid = user.id + request.session.login = user.login + request.session.session_token = user.id and security.compute_session_token( + request.session, request.env + ) + request.uid = user.id + request.disable_db = False + request.session.get_context() + + # Set user's context + user_context = request.env(request.cr, request.session.uid)['res.users'].context_get().copy() + request.session.context = request.context = user_context + request.env.user = uid + # uid = request.session.authenticate(request.session.db, uid=payload['sub'], password=token) + if not uid: + result['message'] = 'Token invalid or expired' + result['code'] = 498 + return result + + result['status'] = True + return result + except jwt.ExpiredSignatureError as ex : + result['code'] = 498 + result['message'] = 'Signature has expired' + _logger.error(traceback.format_exc()) + + except (jwt.InvalidTokenError, Exception) as e: + result['code'] = 497 + result['message'] = 'Token invalid or expired' + _logger.error(traceback.format_exc()) + return result + + def refresh_token(self, token): + try: + result = { + 'status': False, + 'message': None, + 'code':200 + } + record = request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]) + + if len(record) != 1 or not record: + result['message'] = 'Token not found' + result['code'] = 497 + return result + + payload = jwt.decode(token, SECRET_KEY ,algorithms='HS256') + user = request.env['res.users'].sudo().search([('id', '=',payload['sub'])], limit=1) + if not user: + result['message'] = 'Token for user not found' + result['code'] = 497 + return result + + request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]).unlink() + result['status'] = True + result['code'] == 200 + result['data'] = {'token':self.create_token(user)} + return result + except jwt.ExpiredSignatureError as ex : + request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]).unlink() + result['status'] = True + result['code'] == 200 + result['data'] = {'token':self.create_token(user)} + _logger.error(traceback.format_exc()) + + except (jwt.InvalidTokenError, Exception) as e: + result['code'] = 497 + result['message'] = 'Token invalid' + _logger.error(traceback.format_exc()) + return result + + + def validate_token(self, token): + try: + result = { + 'status': False, + 'message': None, + 'code':200 + } + record = request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]) + + if len(record) != 1 or not record: + result['message'] = 'Token not found' + result['code'] = 497 + return result + + payload = jwt.decode(token, SECRET_KEY ,algorithms='HS256') + user = request.env['res.users'].sudo().search([('id', '=',payload['sub'])], limit=1) + if not user: + result['message'] = 'Token for user not found' + result['code'] = 497 + return result + + uid =self.verify(token) #request.session.finalize() + # uid = request.session.authenticate(request.session.db, uid=payload['sub'], password=token) + if not uid: + result['message'] = 'Token invalid or expired' + result['code'] = 498 + return result + result['data'] = {} + return result + except jwt.ExpiredSignatureError as ex : + request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]).unlink() + result['message'] = 'Token invalid or expired' + result['code'] = 498 + _logger.error(traceback.format_exc()) + return result + + except (jwt.InvalidTokenError, Exception) as e: + result['code'] = 497 + result['message'] = 'Token invalid' + _logger.error(traceback.format_exc()) + return result + + +validator = Validator() diff --git a/odex25_mobile/odex_web_app/views/attendance_zone_config_view.xml b/odex25_mobile/odex_web_app/views/attendance_zone_config_view.xml new file mode 100644 index 000000000..5d64aff2f --- /dev/null +++ b/odex25_mobile/odex_web_app/views/attendance_zone_config_view.xml @@ -0,0 +1,38 @@ + + + + + + + + + mobile.res.config.settings.view.form + res.config.settings + + + +
+

WebMobile Configuration

+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/odex25_mobile/odex_web_app/views/hr_employee_view.xml b/odex25_mobile/odex_web_app/views/hr_employee_view.xml new file mode 100644 index 000000000..f0442f300 --- /dev/null +++ b/odex25_mobile/odex_web_app/views/hr_employee_view.xml @@ -0,0 +1,21 @@ + + + + + + + employee.web.mobile.inherited.form + hr.employee + + 200 + + + + + + + + + + + \ No newline at end of file