diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a14f254b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# sphinx build directories +_build/ + +# dotfiles +.* +!.gitignore +!.github +!.mailmap +# compiled python files +*.py[co] +__pycache__/ +# setup.py egg_info +*.egg-info +# emacs backup files +*~ +# hg stuff +*.orig +status +# odoo filestore +odoo/filestore +# maintenance migration scripts +odoo/addons/base/maintenance + +# generated for windows installer? +install/win32/*.bat +install/win32/meta.py + +# needed only when building for win32 +setup/win32/static/less/ +setup/win32/static/wkhtmltopdf/ +setup/win32/static/postgresql*.exe + +# js tooling +node_modules +jsconfig.json +tsconfig.json +package-lock.json +package.json +.husky + +# various virtualenv +/bin/ +/build/ +/dist/ +/include/ +/lib/ +/man/ +/share/ +/src/ diff --git a/odex25_mobile/odex_mobile/controllers/authentication.py b/odex25_mobile/odex_mobile/controllers/authentication.py index b1964bcfc..a69d9d6fd 100644 --- a/odex25_mobile/odex_mobile/controllers/authentication.py +++ b/odex25_mobile/odex_mobile/controllers/authentication.py @@ -185,7 +185,6 @@ class AuthenticationController(http.Controller): 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) diff --git a/odex25_mobile/odex_mobile/controllers/rest_api_v2/attendance.py b/odex25_mobile/odex_mobile/controllers/rest_api_v2/attendance.py index 70375b037..9ad2f66f0 100644 --- a/odex25_mobile/odex_mobile/controllers/rest_api_v2/attendance.py +++ b/odex25_mobile/odex_mobile/controllers/rest_api_v2/attendance.py @@ -314,10 +314,12 @@ class AttendanceController(http.Controller): "You are not allowed to perform this operation. please check with one of your team admins"), success=False) try: + print("******************employee.message_sent:",employee.message_sent) if json.loads(body['in_zone']): records = employee.attendance_log_ids.sudo().filtered(lambda r: str(r.date) == str(datetime.today().date()) and r.old == False) for r in records: r.old = True + employee.message_sent = False return http_helper.response(message="Old Record Done", data={'status': True}) else: @@ -326,7 +328,6 @@ class AttendanceController(http.Controller): limit=1) if attendance.action == 'sign_in': records = employee.attendance_log_ids.sudo().filtered(lambda r: r.old == False and str(r.date) == str(datetime.today().date())) - # records = employee.attendance_log_ids.sudo().filtered(lambda r: r.old == False and r.date == str(datetime.today().date())) if records: n = len(records) last = records[n - 1] @@ -335,37 +336,43 @@ class AttendanceController(http.Controller): if now > last: diff = now - last diff = diff.seconds / 60 - auto = request.env.user.company_id.auto_checkout if request.env.user.company_id.auto_checkout > 0 else 5 + zone = http.request.env['attendance.zone'].search([('employee_ids', 'in', employee.id)],limit=1) + zone_general = http.request.env['attendance.zone'].search([('general', '=', True)],limit=1) + auto = zone.auto_checkout or zone_general.auto_checkout or request.env.user.company_id.auto_checkout or 20 if diff >= auto: attendance = http.request.env['attendance.attendance'].create({ 'employee_id': employee.id, - 'action':'sign_out', + 'action': 'sign_out', 'action_type': 'auto', 'name': fields.datetime.now(), - # 'device_id': body.get('device_id'), 'zone': "%s,%s" % (body.get('longitude'), body.get('latitude')), 'longitude': body.get('longitude'), 'latitude': body.get('latitude'), }) - msg = (_("Auto Checkout successfully")) - subject = (_("Auto Checkout")) + msg = _("Auto Checkout successfully") + subject = _("Auto Checkout") self.send_msg(employee, msg, subject) records = employee.attendance_log_ids.sudo().filtered( lambda r: str(r.date) == str(datetime.today().date()) and r.old == False) for r in records: r.old = True - return http_helper.response(message="Auto Checkout successfully", data={'status': True}) + employee.message_sent = False + return http_helper.response(message="Auto Checkout successfully", data={'status': True}) else: - msg = (_("You are out of attendance zone you will be auto sin out ")) - subject = (_("Auto Sign out")) - self.send_msg(employee, msg, subject) - return http_helper.response(message="Auto Checkout Fail and Send", data={'status': False}) + if not employee.message_sent: + msg = _("You are out of attendance zone you will be auto sign out") + subject = _("Auto Sign out") + self.send_msg(employee, msg, subject) + employee.message_sent = True + return http_helper.response(message="Auto Checkout Fail and Send", data={'status': False}) else: self.create_log(employee, body.get('longitude'), body.get('latitude')) - msg = (_("You are out of attendance zone you will be auto sin out ")) - subject = (_("Auto Sign out")) - self.send_msg(employee, msg, subject) - return http_helper.response(message="Auto Checkout Fail and Send", data={'status': False}) + if not employee.message_sent: + msg = _("You are out of attendance zone you will be auto sign out") + subject = _("Auto Sign out") + self.send_msg(employee, msg, subject) + employee.message_sent = True + return http_helper.response(message="Auto Checkout Fail and Send", data={'status': False}) else: return http_helper.response(message="You are not Checked in yet", data={'status': True}) except Exception as e: @@ -375,6 +382,7 @@ class AttendanceController(http.Controller): return http_helper.errcode(code=403, message=message) def send_msg(self, emp, msg, subject): + print("*****************************send") if emp.user_id.partner_id: partner_id = emp.user_id.partner_id partner_id.send_notification(subject, msg, data=None, all_device=True) diff --git a/odex25_mobile/odex_mobile/controllers/rest_api_v2/authentication.py b/odex25_mobile/odex_mobile/controllers/rest_api_v2/authentication.py index 8aa5f6f30..d2e5f3c84 100644 --- a/odex25_mobile/odex_mobile/controllers/rest_api_v2/authentication.py +++ b/odex25_mobile/odex_mobile/controllers/rest_api_v2/authentication.py @@ -186,7 +186,6 @@ class AuthenticationController(http.Controller): 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) diff --git a/odex25_mobile/odex_mobile/models/access_token.py b/odex25_mobile/odex_mobile/models/access_token.py index dfff1df61..8ed97b058 100644 --- a/odex25_mobile/odex_mobile/models/access_token.py +++ b/odex25_mobile/odex_mobile/models/access_token.py @@ -25,7 +25,10 @@ class JwtAccessToken(models.Model): token.is_expired = datetime.now() > token.expires def access_token_cron(self): - self.search([("is_expired", "=", True)]).unlink() + # self.search([("is_expired", "=", True)]).unlink() + current_time = datetime.now() + expired_tokens = self.search([('expires', '<', current_time)]) + expired_tokens.unlink() return True def set_env(self,env): diff --git a/odex25_mobile/odex_mobile/models/attendence_zone_config.py b/odex25_mobile/odex_mobile/models/attendence_zone_config.py index 4feae0abd..eeddb14ec 100644 --- a/odex25_mobile/odex_mobile/models/attendence_zone_config.py +++ b/odex25_mobile/odex_mobile/models/attendence_zone_config.py @@ -26,6 +26,8 @@ class AttendanceZone(models.Model): loc_ch_dist = fields.Integer('Location Change Distance - Meter', default=100) srv_ch_tmout = fields.Integer('Services Change Timeout - Minutes', default=5) + auto_checkout = fields.Integer(string="Auto Checkout After" ,default=10) + @api.constrains('start','end') def start_end(self): for rec in self: diff --git a/odex25_mobile/odex_mobile/models/hr_employee.py b/odex25_mobile/odex_mobile/models/hr_employee.py index 03bc31389..bfb7ec31e 100644 --- a/odex25_mobile/odex_mobile/models/hr_employee.py +++ b/odex25_mobile/odex_mobile/models/hr_employee.py @@ -13,7 +13,7 @@ class HrEmployee(models.Model): device_id = fields.Char(string="Employee Device ") fcm_token = fields.Char(string='FCM Token') attendance_log_ids = fields.One2many('attendance.log','employee_id',string="Attendance Log") - + message_sent = fields.Boolean(string="Message Sent", default=False) def user_push_notification(self, notification): url = "https://fcm.googleapis.com/fcm/send" header = { diff --git a/odex25_mobile/odex_mobile/models/res_users.py b/odex25_mobile/odex_mobile/models/res_users.py index 7b514f227..fde67073e 100644 --- a/odex25_mobile/odex_mobile/models/res_users.py +++ b/odex25_mobile/odex_mobile/models/res_users.py @@ -97,3 +97,22 @@ class Users(models.Model): groups.append("group_department_manager") return groups + + + @api.model + def create(self, vals): + res = super(Users, self).create(vals) + if 'password' in vals or vals.get('active') is False: + self._invalidate_tokens(res) + return res + + def write(self, vals): + result = super(Users, self).write(vals) + if 'password' in vals or 'active' in vals and not vals['active']: + self._invalidate_tokens(self) + return result + + def _invalidate_tokens(self, users): + token_model = self.env['jwt_provider.access_token'] + for user in users: + token_model.search([('user_id', '=', user.id)]).unlink() diff --git a/odex25_mobile/odex_mobile/validator.py b/odex25_mobile/odex_mobile/validator.py index 5afba63db..179427f5a 100644 --- a/odex25_mobile/odex_mobile/validator.py +++ b/odex25_mobile/odex_mobile/validator.py @@ -192,9 +192,17 @@ class Validator: _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()) + record = request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]) + if not record: + result['message'] = 'Token not found' + result['code'] = 497 + return result + else: + result['code'] = 498 + result['message'] = 'Token invalid' + _logger.error(traceback.format_exc()) return result def refresh_token(self, token): @@ -237,9 +245,17 @@ class Validator: _logger.error(traceback.format_exc()) except (jwt.InvalidTokenError, Exception) as e: - result['code'] = 497 - result['message'] = 'Token invalid' - _logger.error(traceback.format_exc()) + record = request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]) + if not record: + result['message'] = 'Token not found' + result['code'] = 497 + return result + else: + result['code'] = 498 + result['message'] = 'Token invalid' + _logger.error(traceback.format_exc()) return result @@ -284,9 +300,17 @@ class Validator: return result except (jwt.InvalidTokenError, Exception) as e: - result['code'] = 497 - result['message'] = 'Token invalid' - _logger.error(traceback.format_exc()) + record = request.env['jwt_provider.access_token'].sudo().search([ + ('token', '=', token) + ]) + if not record: + result['message'] = 'Token not found' + result['code'] = 497 + return result + else: + result['code'] = 498 + result['message'] = 'Token invalid' + _logger.error(traceback.format_exc()) return result diff --git a/odex25_mobile/odex_mobile/views/attendance_zone_config_view.xml b/odex25_mobile/odex_mobile/views/attendance_zone_config_view.xml index 5ce8b9065..4b41912a7 100644 --- a/odex25_mobile/odex_mobile/views/attendance_zone_config_view.xml +++ b/odex25_mobile/odex_mobile/views/attendance_zone_config_view.xml @@ -15,10 +15,10 @@ - + - - + + @@ -29,8 +29,9 @@ - - + + + @@ -70,11 +71,7 @@ tree,form - + 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..1f175239b --- /dev/null +++ b/odex25_mobile/odex_web_app/__manifest__.py @@ -0,0 +1,20 @@ +{ + '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': ['hr'], + 'external_dependencies': { + 'python': ['jwt', ], + }, + 'data': [ + 'security/ir.model.access.csv', + '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..30dc47f3d 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..09f0ddcdf 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..9f94059b9 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..4374fcef9 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..d5f1a2bd9 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..5ddbe49fd 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..9a69130e9 --- /dev/null +++ b/odex25_mobile/odex_web_app/controllers/authentication.py @@ -0,0 +1,142 @@ +# -*- 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/web/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_web'): + 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 + fcm_token_web = kw.get('fcm_token_web') + if employee and fcm_token_web: + employee.sudo().write({'fcm_token_web': fcm_token_web}) + + dic['token'] = token + http_helper.cleanup(); + return http_helper.response(data=dic, message=_("User log in successfully")) + + + @http.route('/rest_api/web/validate',type='http', auth='none', csrf=False, cors='*',methods=['POST']) + def validate_token(self, **kw): + http_method, body, headers, token = http_helper.parse_request() + + result = validator.validate_token(token) + if result['code'] == 497 or result['code'] == 498: + return http_helper.errcode(code=result['code'], message=result['message']) + + return http_helper.response(message="uploaded success",data=result['data']) + + @http.route('/rest_api/web/refresh',type='http', auth='none', csrf=False, cors='*',methods=['POST']) + def refresh_token(self, **kw): + http_method, body, headers, token = http_helper.parse_request() + + result = validator.refresh_token(token) + if result['code'] == 497: + return http_helper.errcode(code=result['code'], message=result['message']) + + return http_helper.response(message="uploaded success",data=result['data']) + + # Reet password with email + @http.route(['/rest_api/web/reset'], type='http', auth='none', csrf=False, methods=['POST']) + def reset_email(self, **kw): + http_method, body, headers, token = http_helper.parse_request() + if not body.get('email'): + return http_helper.response(code=400, message="Email must not be empty", success=False) + user = http.request.env['res.users'].sudo().search([('login', '=', kw.get('email'))]) + if user: + user.sudo().action_reset_password() + return http_helper.response(message=_("A verification link has been sent to you email account"), data={}) + else: + return http_helper.errcode(code=403, message="Password reset failed") + + @http.route('/rest_api/web/users/password',type='http', auth='none', csrf=False, cors='*',methods=['PUT']) + def change_password(self, **kw): + http_method, body, headers, token = http_helper.parse_request() + if not body.get('old_password') or not body.get('new_password'): + return http_helper.errcode(code=400, message='Password must not be empty') + + result = validator.verify_token(token) + + if not result['status']: + return http_helper.errcode(code=400, message='Invalid passwords') + + user = validator.verify(token) + if not user: + return http_helper.errcode(code=400, message=_("You are not allowed to perform this operation. please check with one of your team admins")) + + if not http_helper.is_authentic(user.login, body.get('old_password')): + return http_helper.errcode(code=400, message='Invalid passwords') + + request.env.user.write({ + 'password':str(body.get('new_password')).strip() + }) + request.session.logout() + + + return http_helper.response(message=_("password changed successfully"),data={'id':user.id}) + + @http.route('/rest_api/web/logout', type='http', auth='none', csrf=False, cors='*', methods=['POST']) + def logout(self, **kw): + http_method, body, headers, token = http_helper.parse_request() + result = validator.verify_token(token) + if not result['status']: + return http_helper.errcode(code=result['code'], message=result['message']) + + http_helper.do_logout(token) + return http_helper.response() + + @http.route('/rest_api/web/users', type='http', auth='none', csrf=False, cors='*', methods=['GET']) + def info(self, **kw): + http_method, body, headers, token = http_helper.parse_request() + result = validator.verify_token(token) + if not result['status']: + return http_helper.errcode(code=result['code'], message=result['message']) + user = validator.verify(token) + if not user: + return http_helper.response(code=400, message=_("You are not allowed to perform this operation. please check with one of your team admins"), success=False) + + return http_helper.response(data=user.to_dict(True)) 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..87567c3ab --- /dev/null +++ b/odex25_mobile/odex_web_app/models/__init__.py @@ -0,0 +1,5 @@ +from . import hr_employee +from . import attendence_zone_config +from . import mail_thread +from . import access_token +from . import res_users 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..d300be7ae 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..564d9601c 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..e3a21c38a 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..87a915c26 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/access_token.py b/odex25_mobile/odex_web_app/models/access_token.py new file mode 100644 index 000000000..8ed97b058 --- /dev/null +++ b/odex25_mobile/odex_web_app/models/access_token.py @@ -0,0 +1,35 @@ +from odoo import models, fields, api +from datetime import datetime, timedelta +from dateutil import parser +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + +import logging +_logger = logging.getLogger(__name__) +str_fmt = '%d/%m/%Y %H:%M:%S' + +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('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): + ctr = datetime.now().strftime(str_fmt) + _logger.info(ctr) + for token in self: + token.is_expired = datetime.now() > token.expires + + def access_token_cron(self): + # self.search([("is_expired", "=", True)]).unlink() + current_time = datetime.now() + expired_tokens = self.search([('expires', '<', current_time)]) + expired_tokens.unlink() + return True + + def set_env(self,env): + self.env = env 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..863dc1cbe --- /dev/null +++ b/odex25_mobile/odex_web_app/models/hr_employee.py @@ -0,0 +1,37 @@ +# -*- 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' + + device_id = fields.Char(string="Employee Device ") + fcm_token_web = fields.Char(string='FCM Web Token') + + + def user_push_notification_web(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": { + "title": "Message", + "body": 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..2f36083da --- /dev/null +++ b/odex25_mobile/odex_web_app/models/mail_thread.py @@ -0,0 +1,22 @@ +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, *, message_type='notification', **kwargs): + if self._name in ['mail.channel']: + notification_body = kwargs.get('body', '') + attachments = len(kwargs.get('attachment_ids', [])) + if notification_body and attachments: + notification_body += '\n{} File(s)'.format(attachments) + elif attachments: + notification_body = '{} File(s)'.format(attachments) + + partners_to_notify = self.channel_partner_ids.filtered(lambda r: r.id != self.env.user.partner_id.id) + for employee_id in self.env['hr.employee'].sudo().search([('user_id', 'in', partners_to_notify.user_ids.ids)]): + push_notify = employee_id.user_push_notification_web(notification_body) + + return super(MailThread, self).message_post(message_type=message_type, **kwargs) \ No newline at end of file diff --git a/odex25_mobile/odex_web_app/models/res_users.py b/odex25_mobile/odex_web_app/models/res_users.py new file mode 100644 index 000000000..973f79683 --- /dev/null +++ b/odex25_mobile/odex_web_app/models/res_users.py @@ -0,0 +1,99 @@ +import werkzeug + +from odoo.exceptions import AccessDenied +from odoo import api, models, fields, SUPERUSER_ID + +import logging + +_logger = logging.getLogger(__name__) + +from ..validator import validator + + +class Users(models.Model): + _inherit = "res.users" + + access_token_ids = fields.One2many( + string="Access Tokens", + comodel_name="jwt_provider.access_token", + inverse_name="user_id", + ) + + avatar = fields.Char(compute="_compute_avatar") + # is_verified = fields.Boolean("Verified" , default=False) + + @classmethod + def _login(cls, db, login, password, user_agent_env): + user_id = super(Users, cls)._login( + db, login, password, user_agent_env=user_agent_env + ) + if user_id: + return user_id + uid = validator.verify(password) + return uid + + @api.model + def check_credentials(self, password): + try: + super(Users, self).check_credentials(password) + except AccessDenied: + # verify password as token + if not validator.verify(password): + raise + + @api.depends("image_1024") + def _compute_avatar(self): + base = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + for u in self: + u.avatar = werkzeug.urls.url_join(base, "rest_api/web/avatar/%d" % u.id) + + # @api.multi + def to_dict(self, single=False): + res = [] + for u in self: + d = u.read(["email", "name", "avatar", "mobile", "phone", "partner_id"])[0] + d["user_id"] = self.id + d["partner_id"] = self.partner_id.id + d["lang"] = self.partner_id.lang + groups = self.user_groups() + d["groups"] = groups + employee = ( + self.env["hr.employee"] + .sudo() + .search([("user_id", "=", self.id)], limit=1) + ) + # attendance_status = validator.get_attendance_check(employee) + d["job"] = employee.job_id.name if employee and employee.job_id else None + d["employe_id"] = employee.id if employee and employee.id else None + # d["attendance_status"] = attendance_status if attendance_status else None + + res.append(d) + + return res[0] if single else res + + def user_groups(self): + groups = [] + if self.has_group("base.group_user"): + groups.append("group_user") + if self.has_group("hr_base.group_division_manager"): + groups.append("group_division_manager") + if self.has_group("hr.group_hr_manager"): + groups.append("group_hr_manager") + if self.has_group("hr_base.group_executive_manager"): + groups.append("group_executive_manager") + if self.has_group("hr_loans_salary_advance.group_loan_user"): + groups.append("group_loan_user") + if self.has_group("hr_base.group_general_manager"): + groups.append("group_general_manager") + if self.has_group("hr_base.group_account_manager"): + groups.append("group_account_manager") + if self.has_group("hr.group_hr_user"): + groups.append("group_hr_user") + if self.has_group("hr_timesheet.group_timesheet_manager"): + groups.append("group_timesheet_manager") + if self.has_group("hr_holidays.group_hr_holidays_user"): + groups.append("group_hr_holidays_user") + if self.has_group("hr_base.group_department_manager"): + groups.append("group_department_manager") + + return groups diff --git a/odex25_mobile/odex_web_app/security/ir.model.access.csv b/odex25_mobile/odex_web_app/security/ir.model.access.csv new file mode 100644 index 000000000..4a82f77dc --- /dev/null +++ b/odex25_mobile/odex_web_app/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 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 diff --git a/odex25_mobile/odoo_dynamic_workflow_mobile/controllers/controllers.py b/odex25_mobile/odoo_dynamic_workflow_mobile/controllers/controllers.py index c559fd1cb..4cdcc0b88 100644 --- a/odex25_mobile/odoo_dynamic_workflow_mobile/controllers/controllers.py +++ b/odex25_mobile/odoo_dynamic_workflow_mobile/controllers/controllers.py @@ -245,9 +245,12 @@ class RestApi(Controller): context = request.env.context.copy() context.update({"active_model": btn.model}) + reject_reason = kw.get('reason_msg') + if reject_reason: + context.update({"reject_reason": reject_reason}) context.update({"active_id": int(active_id)}) request.env.context = context - btn._run_code(active_id, btn.model, request.env) + btn.with_context(context)._run_code(active_id, btn.model, request.env) res = obj.read(["id", "state"])[0] state = res["state"] btn_new = (