Files
iistwin/server/finance-di2/google-auth.ts
Ильяс Султанов 254950f151 DI2 внедрение, таск 1: серверная интеграция Data-Insight2 (server/finance-di2)
- Копия DI2-сервера в server/finance-di2/ с правками: schema/db-client/cache/table-config/override-tables/google-auth/audit-agent/routes
- db-client — обёртка над существующим пулом server/finance/db-client (второй пул не создаётся, добавлен экспорт isConnectionError)
- ai-config.ts — чистое IO конфигов из ai-agent.ts без Telegram-поллинга; роуты /api/ai/toggle и /api/ai/status удалены, без compression/startAuditScheduler/autoStartIfEnabled
- Обёртка registerDi2Routes с auth-gate (authenticateToken + finance.manage), регистрация строго перед registerFinanceRoutes
- Статика /di2 из dist/public-di2 перед веткой vite/static (dev и prod)
- Фикс предсуществующего бага DI2: buildGlobalExclusionConditions без cats в /api/profitability
2026-09-06 19:27:43 +03:00

156 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string[]> {
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<any[][]> {
const auth = await getAuthenticatedClient();
const sheets = google.sheets({ version: "v4", auth });
const res = await sheets.spreadsheets.values.get({ spreadsheetId, range });
return res.data.values || [];
}