import { google } from "googleapis"; import fs from "fs"; import path from "path"; const TOKENS_PATH = path.join(process.cwd(), "data", "google-tokens.json"); const SCOPES = [ "https://www.googleapis.com/auth/spreadsheets.readonly", "https://www.googleapis.com/auth/userinfo.email", ]; function getRedirectUri(): string { // В iistwin redirect всегда на основной домен (https) const base = process.env.APP_BASE_URL || "https://iistwin.ru"; return `${base}/api/auth/google/callback`; } function createOAuth2Client() { return new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, getRedirectUri() ); } interface StoredTokens { access_token: string; refresh_token?: string; expiry_date?: number; token_type?: string; scope?: string; email?: string; } function loadTokens(): StoredTokens | null { try { if (fs.existsSync(TOKENS_PATH)) { return JSON.parse(fs.readFileSync(TOKENS_PATH, "utf-8")); } } catch {} return null; } function saveTokens(tokens: StoredTokens): void { const dir = path.dirname(TOKENS_PATH); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(TOKENS_PATH, JSON.stringify(tokens, null, 2)); } function clearTokens(): void { try { if (fs.existsSync(TOKENS_PATH)) fs.unlinkSync(TOKENS_PATH); } catch {} } export function getAuthUrl(): string { const client = createOAuth2Client(); return client.generateAuthUrl({ access_type: "offline", scope: SCOPES, prompt: "consent", }); } export async function handleCallback(code: string): Promise<{ email?: string }> { const client = createOAuth2Client(); const { tokens } = await client.getToken(code); client.setCredentials(tokens); let email: string | undefined; try { const oauth2 = google.oauth2({ version: "v2", auth: client }); const userInfo = await oauth2.userinfo.get(); email = userInfo.data.email || undefined; } catch {} saveTokens({ access_token: tokens.access_token!, refresh_token: tokens.refresh_token || undefined, expiry_date: tokens.expiry_date || undefined, token_type: tokens.token_type || undefined, scope: tokens.scope || undefined, email, }); return { email }; } export function getAuthStatus(): { loggedIn: boolean; email?: string } { const tokens = loadTokens(); if (!tokens?.access_token) return { loggedIn: false }; return { loggedIn: true, email: tokens.email }; } export function logout(): void { clearTokens(); } export async function getAuthenticatedClient() { const tokens = loadTokens(); if (!tokens?.access_token) throw new Error("Не авторизован в Google"); const client = createOAuth2Client(); client.setCredentials({ access_token: tokens.access_token, refresh_token: tokens.refresh_token, expiry_date: tokens.expiry_date, }); const needsRefresh = tokens.expiry_date ? tokens.expiry_date < Date.now() + 60000 : false; if (needsRefresh && tokens.refresh_token) { try { const { credentials } = await client.refreshAccessToken(); client.setCredentials(credentials); saveTokens({ ...tokens, access_token: credentials.access_token!, expiry_date: credentials.expiry_date || undefined, }); } catch (err) { clearTokens(); throw new Error("Токен Google истёк. Пожалуйста, войдите заново."); } } client.on("tokens", (newTokens) => { const current = loadTokens(); if (current && newTokens.access_token) { saveTokens({ ...current, access_token: newTokens.access_token, expiry_date: newTokens.expiry_date || current.expiry_date, refresh_token: newTokens.refresh_token || current.refresh_token, }); } }); return client; } export async function getSheetNames(spreadsheetId: string): Promise { const auth = await getAuthenticatedClient(); const sheets = google.sheets({ version: "v4", auth }); const res = await sheets.spreadsheets.get({ spreadsheetId, fields: "sheets.properties.title" }); return (res.data.sheets || []).map(s => s.properties?.title || "").filter(Boolean); } export async function getSheetData(spreadsheetId: string, range: string): Promise { const auth = await getAuthenticatedClient(); const sheets = google.sheets({ version: "v4", auth }); const res = await sheets.spreadsheets.values.get({ spreadsheetId, range }); return res.data.values || []; }