// Zoho OAuth2 Authentication Module // EU data center: accounts.zoho.eu var ACCOUNTS_BASE = 'https://accounts.zoho.eu'; var SCOPE = 'ZohoProjects.tasks.ALL,ZohoProjects.projects.READ,ZohoProjects.tasklists.READ'; function setupAuth(grantToken) { var clientId = shadowman.config.value('client_id'); var clientSecret = shadowman.config.value('client_secret'); if (!clientId || !clientSecret) { return { error: 'Missing OAuth credentials. Set client_id and client_secret in plugin config.' }; } if (!grantToken) { return { error: 'Missing grant_token parameter. Generate one from Zoho Developer Console (Self Client).' }; } shadowman.log.info('Exchanging Zoho grant token for access + refresh tokens...'); // Exchange grant token for access + refresh tokens // POST /oauth/v2/token with grant_type=authorization_code var body = 'grant_type=authorization_code' + '&client_id=' + encodeURIComponent(clientId) + '&client_secret=' + encodeURIComponent(clientSecret) + '&redirect_uri=oob' + '&code=' + encodeURIComponent(grantToken); var resp = shadowman.http.post(ACCOUNTS_BASE + '/oauth/v2/token', body, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (resp.status !== 200) { shadowman.log.error('Token exchange failed: ' + resp.status + ' ' + resp.body); var errBody; try { errBody = JSON.parse(resp.body); } catch (e) { errBody = resp.body; } return { error: 'Token exchange failed: ' + (errBody.error || errBody.error_description || resp.body) }; } var tokens; try { tokens = JSON.parse(resp.body); } catch (e) { return { error: 'Invalid response from Zoho: ' + resp.body }; } // Store tokens with expiry calculation shadowman.storage.set('tokens', { access_token: tokens.access_token, refresh_token: tokens.refresh_token, expires_at: Date.now() + (tokens.expires_in * 1000), api_domain: tokens.api_domain || 'https://projectsapi.zoho.eu', token_type: tokens.token_type || 'Bearer' }); shadowman.log.info('Zoho OAuth tokens stored successfully. Refresh token does not expire.'); // Notify conversation var convId = shadowman.storage.get('auth_conversation_id'); if (convId) { shadowman.events.emit('message', { text: 'Zoho OAuth sikeres! A refresh token nem jár le, az access token automatikusan frissül. Most már használhatod a Zoho Tasks funkciókat.', conversationId: convId }); shadowman.storage.del('auth_conversation_id'); } return { success: true, message: 'Authentication successful. Refresh token stored permanently.', expires_in: tokens.expires_in }; } function getStatus() { var tokens = shadowman.storage.get('tokens'); if (!tokens) { return { authenticated: false, message: 'Not authenticated. Use setup_auth with a grant token.' }; } if (Date.now() >= tokens.expires_at) { // Token expired, try refresh var newTokens = refreshTokenInternal(); if (newTokens.error) { return { authenticated: false, message: 'Token expired and refresh failed. Re-run setup_auth.' }; } return { authenticated: true, message: 'Authenticated (token refreshed)', domain: newTokens.api_domain }; } var remaining = Math.round((tokens.expires_at - Date.now()) / 1000); return { authenticated: true, message: 'Authenticated', expires_in_seconds: remaining, domain: tokens.api_domain }; } function getValidToken() { var tokens = shadowman.storage.get('tokens'); if (!tokens) { return null; } // Check if token needs refresh (5 minute buffer) if (Date.now() >= (tokens.expires_at - 300000)) { var newTokens = refreshTokenInternal(); if (newTokens.error) { shadowman.log.error('Token refresh failed: ' + newTokens.error); return null; } return newTokens.access_token; } return tokens.access_token; } function refreshTokenInternal() { var tokens = shadowman.storage.get('tokens'); if (!tokens || !tokens.refresh_token) { return { error: 'No refresh token available' }; } var clientId = shadowman.config.value('client_id'); var clientSecret = shadowman.config.value('client_secret'); if (!clientId || !clientSecret) { return { error: 'OAuth client credentials missing' }; } shadowman.log.info('Refreshing Zoho access token...'); var resp = shadowman.http.post(ACCOUNTS_BASE + '/oauth/v2/token', null, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'grant_type=refresh_token' + '&client_id=' + encodeURIComponent(clientId) + '&client_secret=' + encodeURIComponent(clientSecret) + '&refresh_token=' + encodeURIComponent(tokens.refresh_token) }); if (resp.status !== 200) { shadowman.log.error('Token refresh failed: ' + resp.status + ' ' + resp.body); return { error: 'Refresh failed: ' + resp.body }; } var newTokens; try { newTokens = JSON.parse(resp.body); } catch (e) { return { error: 'Invalid refresh response: ' + resp.body }; } var updated = { access_token: newTokens.access_token, refresh_token: tokens.refresh_token, // Keep original - Zoho refresh tokens don't expire expires_at: Date.now() + (newTokens.expires_in * 1000), api_domain: newTokens.api_domain || tokens.api_domain, token_type: newTokens.token_type || 'Bearer' }; shadowman.storage.set('tokens', updated); shadowman.log.info('Zoho token refreshed successfully'); return updated; } module.exports = { setupAuth: setupAuth, getStatus: getStatus, getValidToken: getValidToken };