| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- // 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,ZohoProjects.timesheets.ALL,ZohoProjects.statuses.READ,ZohoProjects.tasklists.CREATE,ZohoProjects.tasklists.UPDATE,ZohoProjects.portals.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...');
- 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 };
- }
- 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.');
- // Auto-resolve portal if portal_url config is set
- var portalUrl = shadowman.config.value('portal_url');
- var resolveResult = null;
- if (portalUrl) {
- resolveResult = resolvePortalInternal(tokens.access_token, portalUrl, null);
- }
- var result = {
- success: true,
- message: 'Authentication successful. Refresh token stored permanently.',
- expires_in: tokens.expires_in
- };
- if (resolveResult && resolveResult.success) {
- result.portal_id = resolveResult.portal_id;
- result.portal_name = resolveResult.portal_name;
- result.message += ' Portal auto-resolved: ' + resolveResult.portal_name + ' (ID: ' + resolveResult.portal_id + ')';
- } else if (portalUrl) {
- result.message += ' Portal auto-resolution failed. Run resolve_portal manually.';
- } else {
- result.message += ' Run resolve_portal with your portal URL to auto-configure.';
- }
- // Notify conversation
- var convId = shadowman.storage.get('auth_conversation_id');
- if (convId) {
- var msg = 'Zoho OAuth sikeres!';
- if (resolveResult && resolveResult.success) {
- msg += ' Portal automatikusan beállítva: ' + resolveResult.portal_name;
- } else {
- msg += ' Futtasd a resolve_portal parancsot a portal URL-eddel.';
- }
- shadowman.events.emit('message', {
- text: msg,
- conversationId: convId
- });
- shadowman.storage.del('auth_conversation_id');
- }
- return result;
- }
- function resolvePortalInternal(token, portalUrl, portalName) {
- var apiBase = 'https://projectsapi.zoho.eu';
- // Method 1: /api/v3/portals — returns all portals with numeric ID, name, slug
- var resp = shadowman.http.request(apiBase + '/api/v3/portals', {
- method: 'GET',
- headers: {
- 'Authorization': 'Zoho-oauthtoken ' + token
- }
- });
- if (resp.status === 200) return processPortalsResponse(resp.body, portalUrl, portalName);
- // Method 2: Fallback — use v1 API with slug, extract numeric ID from response
- var slug = portalUrl ? extractSlug(portalUrl) : '';
- if (!slug) {
- var existingId = shadowman.config.value('portal_id');
- if (existingId) {
- return { success: true, portal_id: existingId, note: 'Using existing portal_id. Re-run setup_auth with ZohoProjects.portals.READ scope to enable /api/v3/portals.' };
- }
- return { error: 'Portal URL not provided and /api/v3/portals failed (scope?). Run setup_auth again with a fresh grant token.' };
- }
- var testResp = shadowman.http.request(apiBase + '/restapi/portal/' + slug + '/projects/', {
- method: 'GET',
- headers: {
- 'Authorization': 'Zoho-oauthtoken ' + token
- }
- });
- if (testResp.status === 200) {
- try {
- var testData = JSON.parse(testResp.body);
- var projects = testData.projects || [];
- if (projects.length > 0) {
- var url = (projects[0].link || {}).self || {};
- var urlStr = url.url || '';
- var urlMatch = urlStr.match(/\/portal\/(\d+)\//);
- if (urlMatch) {
- var numericId = urlMatch[1];
- shadowman.config.set('portal_id', numericId);
- shadowman.log.info('Portal resolved (v1 fallback): ' + slug + ' -> ID: ' + numericId);
- return { success: true, portal_id: numericId, portal_slug: slug };
- }
- }
- } catch (e) { /* ignore */ }
- shadowman.config.set('portal_id', slug);
- shadowman.log.info('Portal configured with slug (no numeric ID found): ' + slug);
- return { success: true, portal_id: slug, portal_slug: slug, note: 'Using slug. Timesheet API may require numeric ID.' };
- }
- return { error: 'Portal "' + slug + '" not found. Check the URL and your access.' };
- }
- function processPortalsResponse(body, portalUrl, portalName) {
- var data;
- try {
- data = JSON.parse(body);
- } catch (e) {
- return { error: 'Invalid portal list response: ' + body };
- }
- var portals = data.portals || [];
- if (portals.length === 0) {
- return { error: 'No portals found for this account.', portals: [] };
- }
- if (portals.length === 1 && !portalUrl && !portalName) {
- var p = portals[0];
- shadowman.config.set('portal_id', String(p.id));
- shadowman.log.info('Portal auto-configured: ' + p.name + ' (ID: ' + p.id + ')');
- return { success: true, portal_id: String(p.id), portal_name: p.name, portal_slug: p.portal_slug };
- }
- var match = null;
- if (portalUrl) {
- var slug = extractSlug(portalUrl);
- for (var i = 0; i < portals.length; i++) {
- if (portals[i].portal_slug && portals[i].portal_slug.toLowerCase() === slug.toLowerCase()) {
- match = portals[i];
- break;
- }
- if (portals[i].name && portals[i].name.toLowerCase() === slug.toLowerCase()) {
- match = portals[i];
- break;
- }
- }
- } else if (portalName) {
- for (var j = 0; j < portals.length; j++) {
- if (portals[j].name && portals[j].name.toLowerCase() === portalName.toLowerCase()) {
- match = portals[j];
- break;
- }
- }
- }
- if (match) {
- shadowman.config.set('portal_id', String(match.id));
- shadowman.log.info('Portal configured: ' + match.name + ' (ID: ' + match.id + ')');
- return { success: true, portal_id: String(match.id), portal_name: match.name, portal_slug: match.portal_slug };
- }
- var list = [];
- for (var k = 0; k < portals.length; k++) {
- list.push({
- id: String(portals[k].id),
- name: portals[k].name,
- slug: portals[k].portal_slug
- });
- }
- return { error: 'Portal not found. Available portals:', portals: list };
- }
- function extractSlug(url) {
- if (!url) return '';
- var slug = url;
- var slashIdx = slug.lastIndexOf('/');
- if (slashIdx !== -1) slug = slug.substring(slashIdx + 1);
- if (slug === 'portal' || slug === '') return '';
- return slug;
- }
- function resolvePortal(token, args) {
- return resolvePortalInternal(token, args.portal_url || null, args.portal_name || null);
- }
- 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);
- // Refresh failed but token might still be valid
- if (Date.now() < tokens.expires_at) {
- return tokens.access_token;
- }
- 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 body = 'grant_type=refresh_token' +
- '&client_id=' + encodeURIComponent(clientId) +
- '&client_secret=' + encodeURIComponent(clientSecret) +
- '&refresh_token=' + encodeURIComponent(tokens.refresh_token);
- 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 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,
- resolvePortal: resolvePortal
- };
|