const axios = require('axios'); // MCCS is on Zoho's US data centre; users on another DC would need accounts.zoho. const ACCOUNTS_URL = 'https://accounts.zoho.com'; // profile.READ is only for showing who's signed in; the dashboard itself needs requests.READ const SCOPES = 'SDPOnDemand.requests.READ,AaaServer.profile.READ'; // zoho reports most failures as a 200 with an error field, not an http error function zohoError(data, fallback) { const err = new Error('Zoho: ' + (data?.error || fallback)); err.zoho = true; return err; } function createZohoAuth({ clientId, clientSecret, redirectUri }) { // the page Zoho shows to approve the dashboard; state ties the callback to this browser function authorizeUrl(state) { const params = new URLSearchParams({ client_id: clientId, response_type: 'code', scope: SCOPES, redirect_uri: redirectUri, state, // offline is what gets us a refresh token; consent makes zoho issue one every time access_type: 'offline', prompt: 'consent' }); return `${ACCOUNTS_URL}/oauth/v2/auth?${params}`; } async function tokenRequest(fields) { const params = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, ...fields }); const res = await axios.post(`${ACCOUNTS_URL}/oauth/v2/token`, params); if (!res.data?.access_token) throw zohoError(res.data, 'no access token'); return res.data; } async function exchangeCode(code) { const data = await tokenRequest({ grant_type: 'authorization_code', redirect_uri: redirectUri, code }); if (!data.refresh_token) throw zohoError(data, 'no refresh token'); return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date.now() + (data.expires_in || 3600) * 1000 }; } // access tokens last about an hour; the refresh token lasts until revoked async function refreshAccessToken(refreshToken) { const data = await tokenRequest({ grant_type: 'refresh_token', refresh_token: refreshToken }); return { accessToken: data.access_token, expiresAt: Date.now() + (data.expires_in || 3600) * 1000 }; } // zoho keeps at most 20 refresh tokens per user per client and silently drops the oldest, // so revoke on sign-out rather than leaving them to pile up async function revokeToken(refreshToken) { await axios.post(`${ACCOUNTS_URL}/oauth/v2/token/revoke`, new URLSearchParams({ token: refreshToken })); } async function getUser(accessToken) { try { const res = await axios.get(`${ACCOUNTS_URL}/oauth/user/info`, { headers: { Authorization: `Zoho-oauthtoken ${accessToken}` } }); return { name: res.data.Display_Name || [res.data.First_Name, res.data.Last_Name].filter(Boolean).join(' ') || null, email: res.data.Email || null }; } catch (err) { // only cosmetic; the page just says "Signed in" without a name console.warn('Could not read Zoho profile:', err.response?.data || err.message); return { name: null, email: null }; } } return { authorizeUrl, exchangeCode, refreshAccessToken, revokeToken, getUser }; } module.exports = { createZohoAuth };