Add Olist V3 product structure sync
This commit is contained in:
174
src/services/olist-v3.service.ts
Normal file
174
src/services/olist-v3.service.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import axios from 'axios';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
type OlistV3Tokens = {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in?: number;
|
||||
expires_at?: string;
|
||||
};
|
||||
|
||||
type OlistV3AuthorizationState = {
|
||||
state: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const OAUTH_BASE_URL = 'https://accounts.tiny.com.br/realms/tiny/protocol/openid-connect';
|
||||
const TOKEN_URL = `${OAUTH_BASE_URL}/token`;
|
||||
const AUTHORIZE_URL = `${OAUTH_BASE_URL}/auth`;
|
||||
const DEFAULT_TOKEN_FILE = path.join(process.cwd(), 'data-runtime', 'olist_v3_tokens.json');
|
||||
const DEFAULT_STATE_FILE = path.join(process.cwd(), 'data-runtime', 'olist_v3_oauth_state.json');
|
||||
|
||||
function requiredEnv(name: string) {
|
||||
const value = process.env[name]?.trim();
|
||||
if (!value) throw new Error(`Missing ${name}.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function getTokenFile() {
|
||||
return process.env.OLIST_V3_TOKEN_FILE || DEFAULT_TOKEN_FILE;
|
||||
}
|
||||
|
||||
function getStateFile() {
|
||||
return process.env.OLIST_V3_OAUTH_STATE_FILE || DEFAULT_STATE_FILE;
|
||||
}
|
||||
|
||||
function ensureParentDirectory(filePath: string) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
}
|
||||
|
||||
function writeJson(filePath: string, value: unknown) {
|
||||
ensureParentDirectory(filePath);
|
||||
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), { mode: 0o600 });
|
||||
}
|
||||
|
||||
function readJson<T>(filePath: string): T | null {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T;
|
||||
}
|
||||
|
||||
function getClientConfig() {
|
||||
return {
|
||||
clientId: requiredEnv('OLIST_V3_CLIENT_ID'),
|
||||
clientSecret: requiredEnv('OLIST_V3_CLIENT_SECRET'),
|
||||
redirectUri: requiredEnv('OLIST_V3_REDIRECT_URI')
|
||||
};
|
||||
}
|
||||
|
||||
function saveTokens(tokens: OlistV3Tokens) {
|
||||
const expiresIn = Number(tokens.expires_in || 0);
|
||||
const expiresAt = Number.isFinite(expiresIn) && expiresIn > 0
|
||||
? new Date(Date.now() + Math.max(0, expiresIn - 60) * 1000).toISOString()
|
||||
: undefined;
|
||||
const previous = readJson<OlistV3Tokens>(getTokenFile());
|
||||
|
||||
writeJson(getTokenFile(), {
|
||||
...previous,
|
||||
...tokens,
|
||||
refresh_token: tokens.refresh_token || previous?.refresh_token,
|
||||
expires_at: expiresAt || previous?.expires_at
|
||||
});
|
||||
}
|
||||
|
||||
function tokenIsUsable(tokens: OlistV3Tokens) {
|
||||
if (!tokens.access_token) return false;
|
||||
if (!tokens.expires_at) return true;
|
||||
return new Date(tokens.expires_at).getTime() > Date.now();
|
||||
}
|
||||
|
||||
async function requestTokens(params: URLSearchParams) {
|
||||
const response = await axios.post<OlistV3Tokens>(TOKEN_URL, params, {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
timeout: 30000
|
||||
});
|
||||
if (!response.data?.access_token) {
|
||||
throw new Error('Olist V3 OAuth did not return an access token.');
|
||||
}
|
||||
|
||||
saveTokens(response.data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export function isOlistV3Configured() {
|
||||
return Boolean(
|
||||
process.env.OLIST_V3_CLIENT_ID?.trim()
|
||||
&& process.env.OLIST_V3_CLIENT_SECRET?.trim()
|
||||
&& process.env.OLIST_V3_REDIRECT_URI?.trim()
|
||||
);
|
||||
}
|
||||
|
||||
export function getOlistV3AuthorizationUrl() {
|
||||
const { clientId, redirectUri } = getClientConfig();
|
||||
const state = crypto.randomBytes(32).toString('hex');
|
||||
writeJson(getStateFile(), { state, createdAt: new Date().toISOString() } satisfies OlistV3AuthorizationState);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope: 'openid',
|
||||
response_type: 'code',
|
||||
state
|
||||
});
|
||||
return `${AUTHORIZE_URL}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export async function exchangeOlistV3AuthorizationCode(code: string, state: string) {
|
||||
const expected = readJson<OlistV3AuthorizationState>(getStateFile());
|
||||
const expectedState = Buffer.from(expected?.state || '');
|
||||
const receivedState = Buffer.from(state || '');
|
||||
if (!expected?.state || expectedState.length !== receivedState.length || !crypto.timingSafeEqual(expectedState, receivedState)) {
|
||||
throw new Error('Invalid OAuth state. Start the authorization flow again.');
|
||||
}
|
||||
|
||||
const createdAt = new Date(expected.createdAt).getTime();
|
||||
if (!Number.isFinite(createdAt) || Date.now() - createdAt > 10 * 60 * 1000) {
|
||||
throw new Error('OAuth state expired. Start the authorization flow again.');
|
||||
}
|
||||
|
||||
const { clientId, clientSecret, redirectUri } = getClientConfig();
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
code
|
||||
});
|
||||
|
||||
const tokens = await requestTokens(params);
|
||||
fs.rmSync(getStateFile(), { force: true });
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export async function getOlistV3AccessToken(forceRefresh = false) {
|
||||
const current = readJson<OlistV3Tokens>(getTokenFile());
|
||||
if (current && !forceRefresh && tokenIsUsable(current)) {
|
||||
return current.access_token;
|
||||
}
|
||||
|
||||
if (!current?.refresh_token) {
|
||||
throw new Error('Olist V3 is not authorized. Open /api/olist-v3/authorize first.');
|
||||
}
|
||||
|
||||
const { clientId, clientSecret } = getClientConfig();
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
refresh_token: current.refresh_token
|
||||
});
|
||||
const tokens = await requestTokens(params);
|
||||
return tokens.access_token;
|
||||
}
|
||||
|
||||
export function getOlistV3AuthStatus() {
|
||||
const tokens = readJson<OlistV3Tokens>(getTokenFile());
|
||||
return {
|
||||
configured: isOlistV3Configured(),
|
||||
authorized: Boolean(tokens?.access_token || tokens?.refresh_token),
|
||||
accessTokenUsable: Boolean(tokens && tokenIsUsable(tokens)),
|
||||
tokenExpiresAt: tokens?.expires_at || null,
|
||||
tokenFile: getTokenFile()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user