92 lines
2.7 KiB
TypeScript
92 lines
2.7 KiB
TypeScript
import { z } from "zod";
|
|
import { AppError } from "@/server/lib/errors";
|
|
import { getRequiredEnvValue } from "@/server/lib/runtime-env";
|
|
|
|
const API_BASE = "https://api.dataforseo.com";
|
|
|
|
const userDataResponseSchema = z
|
|
.object({
|
|
status_code: z.number().optional(),
|
|
tasks: z
|
|
.array(
|
|
z
|
|
.object({
|
|
status_code: z.number().optional(),
|
|
result: z
|
|
.array(
|
|
z
|
|
.object({
|
|
backlinks_subscription_expiry_date: z
|
|
.string()
|
|
.nullable()
|
|
.optional(),
|
|
llm_mentions_subscription_expiry_date: z
|
|
.string()
|
|
.nullable()
|
|
.optional(),
|
|
})
|
|
.passthrough(),
|
|
)
|
|
.nullable()
|
|
.optional(),
|
|
})
|
|
.passthrough(),
|
|
)
|
|
.optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
type DataforseoAccountState = {
|
|
backlinksSubscriptionExpiryDate: string | null;
|
|
llmMentionsSubscriptionExpiryDate: string | null;
|
|
};
|
|
|
|
export function hasActiveDataforseoSubscription(
|
|
expiryDate: string | null,
|
|
): boolean {
|
|
if (!expiryDate) return false;
|
|
|
|
const expiryTime = Date.parse(expiryDate);
|
|
return Number.isFinite(expiryTime) && expiryTime > Date.now();
|
|
}
|
|
|
|
export async function fetchDataforseoAccountState(): Promise<DataforseoAccountState | null> {
|
|
const apiKey = await getRequiredEnvValue("DATAFORSEO_API_KEY");
|
|
const response = await fetch(`${API_BASE}/v3/appendix/user_data`, {
|
|
method: "GET",
|
|
headers: { Authorization: `Basic ${apiKey}` },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// 401/403 here means the API key itself is invalid or missing — surface a
|
|
// clear, actionable message instead of a generic "unexpected error".
|
|
if (response.status === 401 || response.status === 403) {
|
|
throw new AppError(
|
|
"DATAFORSEO_AUTH_FAILED",
|
|
`DataForSEO HTTP ${response.status} on /v3/appendix/user_data`,
|
|
);
|
|
}
|
|
throw new AppError(
|
|
"INTERNAL_ERROR",
|
|
`DataForSEO HTTP ${response.status} on /v3/appendix/user_data`,
|
|
);
|
|
}
|
|
|
|
const raw = await response.json();
|
|
const parsed = userDataResponseSchema.safeParse(raw);
|
|
if (!parsed.success || parsed.data.status_code !== 20000) return null;
|
|
|
|
const task = parsed.data.tasks?.[0];
|
|
if (!task || task.status_code !== 20000) return null;
|
|
|
|
const result = task.result?.[0];
|
|
if (!result) return null;
|
|
|
|
return {
|
|
backlinksSubscriptionExpiryDate:
|
|
result.backlinks_subscription_expiry_date ?? null,
|
|
llmMentionsSubscriptionExpiryDate:
|
|
result.llm_mentions_subscription_expiry_date ?? null,
|
|
};
|
|
}
|