res_users_settings.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from datetime import timedelta
  4. import logging
  5. from odoo import api, fields, models, _
  6. from odoo.exceptions import UserError
  7. _logger = logging.getLogger(__name__)
  8. class ResUsersSettings(models.Model):
  9. _inherit = "res.users.settings"
  10. # Google API tokens and synchronization information
  11. google_rtoken = fields.Char('Google Refresh Token', copy=False, groups='base.group_system')
  12. google_token = fields.Char('Google User Token', copy=False, groups='base.group_system')
  13. google_token_validity = fields.Datetime('Google Token Validity', copy=False, groups='base.group_system')
  14. google_email = fields.Char('Google Email', copy=False, groups='base.group_system')
  15. google_sync_enabled = fields.Boolean('Google Sync Enabled', default=False, copy=False, groups='base.group_system')
  16. # CRM Meets Files Configuration
  17. google_crm_meets_folder_id = fields.Char(
  18. string='Archivos Meets CRM',
  19. help='ID de la carpeta en Google Drive donde se almacenan archivos de meets para sincronización con CRM',
  20. copy=False,
  21. groups='base.group_system'
  22. )
  23. @api.model
  24. def _get_fields_blacklist(self):
  25. """Get list of google drive fields that won't be formatted in session_info."""
  26. google_fields_blacklist = [
  27. 'google_rtoken',
  28. 'google_token',
  29. 'google_token_validity',
  30. 'google_email',
  31. 'google_sync_enabled',
  32. 'google_crm_meets_folder_id'
  33. ]
  34. return super()._get_fields_blacklist() + google_fields_blacklist
  35. def _set_google_auth_tokens(self, access_token, refresh_token, expires_at):
  36. """Set Google authentication tokens for the user"""
  37. self.sudo().write({
  38. 'google_rtoken': refresh_token,
  39. 'google_token': access_token,
  40. 'google_token_validity': expires_at if expires_at else False,
  41. })
  42. def _google_authenticated(self):
  43. """Check if user is authenticated with Google"""
  44. self.ensure_one()
  45. # Verificar que tenemos tanto refresh token como access token
  46. return bool(self.sudo().google_rtoken and self.sudo().google_token)
  47. def _is_google_token_valid(self):
  48. """Check if Google token is still valid"""
  49. self.ensure_one()
  50. return (self.sudo().google_token_validity and
  51. self.sudo().google_token_validity >= (fields.Datetime.now() + timedelta(minutes=1)))
  52. def _refresh_google_token(self):
  53. """Refresh Google access token using refresh token with custom credentials"""
  54. self.ensure_one()
  55. try:
  56. # Get Google API credentials from system settings
  57. config = self.env['ir.config_parameter'].sudo()
  58. client_id = config.get_param('google_api.client_id', '')
  59. client_secret = config.get_param('google_api.client_secret', '')
  60. refresh_token = self.sudo().google_rtoken
  61. if not all([client_id, client_secret, refresh_token]):
  62. _logger.error(f"Missing credentials for token refresh for user {self.user_id.name}")
  63. return False
  64. # Exchange refresh token for new access token using our custom credentials
  65. import requests
  66. token_url = 'https://oauth2.googleapis.com/token'
  67. data = {
  68. 'client_id': client_id,
  69. 'client_secret': client_secret,
  70. 'refresh_token': refresh_token,
  71. 'grant_type': 'refresh_token'
  72. }
  73. response = requests.post(token_url, data=data, timeout=30)
  74. if response.status_code == 200:
  75. token_data = response.json()
  76. new_access_token = token_data.get('access_token')
  77. expires_in = token_data.get('expires_in', 3600)
  78. if new_access_token:
  79. # Calculate expiration time
  80. expires_at = fields.Datetime.now() + timedelta(seconds=expires_in)
  81. self.sudo().write({
  82. 'google_token': new_access_token,
  83. 'google_token_validity': expires_at,
  84. })
  85. _logger.info(f"Google token refreshed for user {self.user_id.name}")
  86. return True
  87. else:
  88. _logger.error(f"No access token in response for user {self.user_id.name}")
  89. return False
  90. else:
  91. _logger.error(f"Token refresh failed for user {self.user_id.name}: {response.status_code} - {response.text}")
  92. return False
  93. except Exception as e:
  94. _logger.error(f"Failed to refresh Google token for user {self.user_id.name}: {str(e)}")
  95. # Solo eliminar tokens si es definitivamente un error de credenciales inválidas
  96. if 'invalid_grant' in str(e) or 'invalid_token' in str(e) or 'invalid_client' in str(e):
  97. _logger.warning(f"Invalid credentials for user {self.user_id.name}, deleting tokens")
  98. self.env.cr.rollback()
  99. self.sudo()._set_google_auth_tokens(False, False, False)
  100. self.env.cr.commit()
  101. return False
  102. else:
  103. # Para otros errores (timeout, red, etc.), mantener los tokens y reintentar después
  104. _logger.warning(f"Temporary error refreshing token for user {self.user_id.name}, keeping tokens for retry")
  105. return False
  106. def _get_google_access_token(self):
  107. """Get a valid Google access token, refreshing if necessary"""
  108. self.ensure_one()
  109. if not self._google_authenticated():
  110. return None
  111. # Si el token no es válido, intentar renovarlo
  112. if not self._is_google_token_valid():
  113. _logger.info(f"Token expired for user {self.user_id.name}, attempting refresh")
  114. # Intentar renovar el token
  115. if not self._refresh_google_token():
  116. _logger.warning(f"Token refresh failed for user {self.user_id.name}")
  117. # Si falla la renovación, verificar si aún tenemos refresh token
  118. if not self.sudo().google_rtoken:
  119. _logger.error(f"No refresh token available for user {self.user_id.name}")
  120. return None
  121. # Intentar una vez más con un pequeño delay
  122. import time
  123. time.sleep(1)
  124. _logger.info(f"Retrying token refresh for user {self.user_id.name}")
  125. if not self._refresh_google_token():
  126. _logger.error(f"Final token refresh attempt failed for user {self.user_id.name}")
  127. return None
  128. # Verificar que tenemos un token válido
  129. token = self.sudo().google_token
  130. if not token:
  131. _logger.warning(f"No access token available for user {self.user_id.name}")
  132. return None
  133. return token
  134. def _clean_invalid_tokens(self):
  135. """Clean invalid tokens when they can't be refreshed"""
  136. self.ensure_one()
  137. _logger.warning(f"Cleaning invalid tokens for user {self.user_id.name}")
  138. self.sudo()._set_google_auth_tokens(False, False, False)
  139. self.sudo().write({
  140. 'google_email': False,
  141. 'google_sync_enabled': False,
  142. })
  143. def action_connect_google(self):
  144. """Connect user's Google account"""
  145. self.ensure_one()
  146. # Get Google API credentials from system settings
  147. config = self.env['ir.config_parameter'].sudo()
  148. client_id = config.get_param('google_api.client_id', '')
  149. client_secret = config.get_param('google_api.client_secret', '')
  150. if not client_id or not client_secret:
  151. _logger.error("Google API credentials not configured.")
  152. raise UserError(_('Google API credentials not configured. Please contact your administrator.'))
  153. # Build OAuth authorization URL
  154. base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
  155. redirect_uri = f"{base_url}/web/google_oauth_callback"
  156. scope = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/calendar'
  157. # Generate the authorization URL using our own credentials
  158. authorize_uri = self._get_google_authorize_uri(
  159. client_id=client_id,
  160. redirect_uri=redirect_uri,
  161. scope=scope,
  162. state=str(self.id) # Pass user settings ID as state
  163. )
  164. return {
  165. 'type': 'ir.actions.act_url',
  166. 'url': authorize_uri,
  167. 'target': 'new',
  168. }
  169. def action_disconnect_google(self):
  170. """Disconnect user's Google account"""
  171. self.ensure_one()
  172. self.sudo()._set_google_auth_tokens(False, False, False)
  173. self.sudo().write({
  174. 'google_email': False,
  175. 'google_sync_enabled': False,
  176. })
  177. return {
  178. 'type': 'ir.actions.client',
  179. 'tag': 'display_notification',
  180. 'params': {
  181. 'title': _('Success'),
  182. 'message': _('Google account disconnected successfully.'),
  183. 'type': 'success',
  184. 'sticky': False,
  185. }
  186. }
  187. def action_test_google_connection(self):
  188. """Test Google connection for the user"""
  189. self.ensure_one()
  190. if not self._google_authenticated():
  191. raise UserError(_('Google account not connected. Please connect your account first.'))
  192. access_token = self._get_google_access_token()
  193. if not access_token:
  194. raise UserError(_('Could not obtain valid access token. Please reconnect your Google account.'))
  195. try:
  196. # Test Google Drive API access
  197. headers = {
  198. 'Authorization': f'Bearer {access_token}',
  199. 'Content-Type': 'application/json'
  200. }
  201. response = self.env['google.drive.service']._do_request(
  202. '/drive/v3/about',
  203. params={'fields': 'user'},
  204. headers=headers
  205. )
  206. if response and 'user' in response:
  207. user_email = response['user'].get('emailAddress', 'Unknown')
  208. self.sudo().write({'google_email': user_email})
  209. return {
  210. 'type': 'ir.actions.client',
  211. 'tag': 'display_notification',
  212. 'params': {
  213. 'title': _('Success'),
  214. 'message': _('Google connection successful! Connected as: %s') % user_email,
  215. 'type': 'success',
  216. 'sticky': False,
  217. }
  218. }
  219. else:
  220. raise UserError(_('Could not retrieve user information from Google.'))
  221. except Exception as e:
  222. _logger.error(f"Google connection test failed for user {self.user_id.name}: {str(e)}")
  223. raise UserError(_('Google connection test failed: %s') % str(e))
  224. def _get_google_authorize_uri(self, client_id, redirect_uri, scope, state):
  225. """Generate Google OAuth authorization URI using our own credentials"""
  226. import urllib.parse
  227. params = {
  228. 'response_type': 'code',
  229. 'client_id': client_id,
  230. 'redirect_uri': redirect_uri,
  231. 'scope': scope,
  232. 'state': state,
  233. 'access_type': 'offline',
  234. 'prompt': 'consent select_account', # Force account selection
  235. 'hd': '', # Allow any hosted domain
  236. 'include_granted_scopes': 'true'
  237. }
  238. base_url = 'https://accounts.google.com/o/oauth2/auth'
  239. query_string = urllib.parse.urlencode(params)
  240. return f"{base_url}?{query_string}"
  241. def _exchange_authorization_code_for_tokens(self, code, client_id, client_secret, redirect_uri):
  242. """Exchange authorization code for access and refresh tokens"""
  243. import requests
  244. token_url = 'https://oauth2.googleapis.com/token'
  245. data = {
  246. 'code': code,
  247. 'client_id': client_id,
  248. 'client_secret': client_secret,
  249. 'redirect_uri': redirect_uri,
  250. 'grant_type': 'authorization_code'
  251. }
  252. response = requests.post(token_url, data=data)
  253. if response.status_code != 200:
  254. _logger.error(f"Token exchange failed: {response.status_code} - {response.text}")
  255. raise UserError(_('Failed to exchange authorization code for tokens: %s') % response.text)
  256. token_data = response.json()
  257. access_token = token_data.get('access_token')
  258. refresh_token = token_data.get('refresh_token')
  259. expires_in = token_data.get('expires_in', 3600)
  260. if not access_token:
  261. raise UserError(_('No access token received from Google'))
  262. # Calculate expiration time
  263. from datetime import datetime, timedelta
  264. expires_at = datetime.now() + timedelta(seconds=expires_in)
  265. return access_token, refresh_token, expires_at