- Копия 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
156 lines
4.4 KiB
TypeScript
156 lines
4.4 KiB
TypeScript
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 || [];
|
||
}
|