auth.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. // Zoho OAuth2 Authentication Module
  2. // EU data center: accounts.zoho.eu
  3. var ACCOUNTS_BASE = 'https://accounts.zoho.eu';
  4. var SCOPE = 'ZohoCliq.Chats.READ,ZohoCliq.Messages.READ,ZohoCliq.Webhooks.CREATE,ZohoCliq.Channels.READ,ZohoCliq.Users.READ';
  5. function setupAuth(grantToken) {
  6. var clientId = shadowman.config.value('client_id');
  7. var clientSecret = shadowman.config.value('client_secret');
  8. if (!clientId || !clientSecret) {
  9. return { error: 'Missing OAuth credentials. Set client_id and client_secret in plugin config.' };
  10. }
  11. if (!grantToken) {
  12. return { error: 'Missing grant_token parameter. Generate one from Zoho Developer Console (Self Client).' };
  13. }
  14. shadowman.log.info('Exchanging Zoho grant token for access + refresh tokens...');
  15. var body = 'grant_type=authorization_code' +
  16. '&client_id=' + encodeURIComponent(clientId) +
  17. '&client_secret=' + encodeURIComponent(clientSecret) +
  18. '&redirect_uri=oob' +
  19. '&code=' + encodeURIComponent(grantToken);
  20. var resp = shadowman.http.post(ACCOUNTS_BASE + '/oauth/v2/token', body, {
  21. headers: {
  22. 'Content-Type': 'application/x-www-form-urlencoded'
  23. }
  24. });
  25. if (resp.status !== 200) {
  26. shadowman.log.error('Token exchange failed: ' + resp.status + ' ' + resp.body);
  27. var errBody;
  28. try {
  29. errBody = JSON.parse(resp.body);
  30. } catch (e) {
  31. errBody = resp.body;
  32. }
  33. return { error: 'Token exchange failed: ' + (errBody.error || errBody.error_description || resp.body) };
  34. }
  35. var tokens;
  36. try {
  37. tokens = JSON.parse(resp.body);
  38. } catch (e) {
  39. return { error: 'Invalid response from Zoho: ' + resp.body };
  40. }
  41. shadowman.storage.set('tokens', {
  42. access_token: tokens.access_token,
  43. refresh_token: tokens.refresh_token,
  44. expires_at: Date.now() + (tokens.expires_in * 1000),
  45. api_domain: tokens.api_domain || 'https://cliq.zoho.eu',
  46. token_type: tokens.token_type || 'Bearer'
  47. });
  48. shadowman.log.info('Zoho OAuth tokens stored successfully. Refresh token does not expire.');
  49. var result = {
  50. success: true,
  51. message: 'Authentication successful. Refresh token stored permanently.',
  52. expires_in: tokens.expires_in
  53. };
  54. var convId = shadowman.storage.get('auth_conversation_id');
  55. if (convId) {
  56. shadowman.events.emit('message', {
  57. text: 'Zoho Cliq OAuth successful! You can now use chat tools.',
  58. conversationId: convId
  59. });
  60. shadowman.storage.del('auth_conversation_id');
  61. }
  62. return result;
  63. }
  64. function getStatus() {
  65. var tokens = shadowman.storage.get('tokens');
  66. if (!tokens) {
  67. return { authenticated: false, message: 'Not authenticated. Use setup_auth with a grant token.' };
  68. }
  69. if (Date.now() >= tokens.expires_at) {
  70. var newTokens = refreshTokenInternal();
  71. if (newTokens.error) {
  72. return { authenticated: false, message: 'Token expired and refresh failed. Re-run setup_auth.' };
  73. }
  74. return { authenticated: true, message: 'Authenticated (token refreshed)', domain: newTokens.api_domain };
  75. }
  76. var remaining = Math.round((tokens.expires_at - Date.now()) / 1000);
  77. return {
  78. authenticated: true,
  79. message: 'Authenticated',
  80. expires_in_seconds: remaining,
  81. domain: tokens.api_domain
  82. };
  83. }
  84. function getValidToken() {
  85. var tokens = shadowman.storage.get('tokens');
  86. if (!tokens) {
  87. return null;
  88. }
  89. if (Date.now() >= (tokens.expires_at - 300000)) {
  90. var newTokens = refreshTokenInternal();
  91. if (newTokens.error) {
  92. shadowman.log.error('Token refresh failed: ' + newTokens.error);
  93. if (Date.now() < tokens.expires_at) {
  94. return tokens.access_token;
  95. }
  96. return null;
  97. }
  98. return newTokens.access_token;
  99. }
  100. return tokens.access_token;
  101. }
  102. function refreshTokenInternal() {
  103. var tokens = shadowman.storage.get('tokens');
  104. if (!tokens || !tokens.refresh_token) {
  105. return { error: 'No refresh token available' };
  106. }
  107. var clientId = shadowman.config.value('client_id');
  108. var clientSecret = shadowman.config.value('client_secret');
  109. if (!clientId || !clientSecret) {
  110. return { error: 'OAuth client credentials missing' };
  111. }
  112. shadowman.log.info('Refreshing Zoho access token...');
  113. var body = 'grant_type=refresh_token' +
  114. '&client_id=' + encodeURIComponent(clientId) +
  115. '&client_secret=' + encodeURIComponent(clientSecret) +
  116. '&refresh_token=' + encodeURIComponent(tokens.refresh_token);
  117. var resp = shadowman.http.post(ACCOUNTS_BASE + '/oauth/v2/token', body, {
  118. headers: {
  119. 'Content-Type': 'application/x-www-form-urlencoded'
  120. }
  121. });
  122. if (resp.status !== 200) {
  123. shadowman.log.error('Token refresh failed: ' + resp.status + ' ' + resp.body);
  124. return { error: 'Refresh failed: ' + resp.body };
  125. }
  126. var newTokens;
  127. try {
  128. newTokens = JSON.parse(resp.body);
  129. } catch (e) {
  130. return { error: 'Invalid refresh response: ' + resp.body };
  131. }
  132. var updated = {
  133. access_token: newTokens.access_token,
  134. refresh_token: tokens.refresh_token,
  135. expires_at: Date.now() + (newTokens.expires_in * 1000),
  136. api_domain: newTokens.api_domain || tokens.api_domain,
  137. token_type: newTokens.token_type || 'Bearer'
  138. };
  139. shadowman.storage.set('tokens', updated);
  140. shadowman.log.info('Zoho token refreshed successfully');
  141. return updated;
  142. }
  143. module.exports = {
  144. setupAuth: setupAuth,
  145. getStatus: getStatus,
  146. getValidToken: getValidToken
  147. };