97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
import type { Setting } from "@prisma/client";
|
||
|
||
export function normalizeThreadsAppId(value: string | null | undefined): string | undefined {
|
||
const trimmed = value?.trim();
|
||
if (!trimmed) return undefined;
|
||
if (!/^\d+$/.test(trimmed)) return undefined;
|
||
return trimmed;
|
||
}
|
||
|
||
/** Meta App 憑證僅從部署層 .env 讀取(THREADS_APP_ID / THREADS_APP_SECRET)。 */
|
||
export function getThreadsAppId(): string | undefined {
|
||
return normalizeThreadsAppId(process.env.THREADS_APP_ID);
|
||
}
|
||
|
||
export function getThreadsAppSecret(): string | undefined {
|
||
return process.env.THREADS_APP_SECRET?.trim() || undefined;
|
||
}
|
||
|
||
export function isThreadsAppConfigured(): boolean {
|
||
return !!(getThreadsAppId() && getThreadsAppSecret());
|
||
}
|
||
|
||
type ThreadsAccountAuth = {
|
||
threadsAccessToken: string | null;
|
||
threadsUserId: string | null;
|
||
};
|
||
|
||
export function accountHasThreadsToken(account: ThreadsAccountAuth | null): boolean {
|
||
return !!(account?.threadsAccessToken?.trim() && account?.threadsUserId?.trim());
|
||
}
|
||
|
||
export function getThreadsCredentials(
|
||
settings: Setting,
|
||
account: ThreadsAccountAuth | null,
|
||
overrideToken?: string | null
|
||
) {
|
||
const appId = getThreadsAppId();
|
||
const appSecret = getThreadsAppSecret();
|
||
const accessToken = (overrideToken ?? account?.threadsAccessToken)?.trim();
|
||
const userId = account?.threadsUserId?.trim();
|
||
|
||
if (!accessToken || !userId) return null;
|
||
|
||
return {
|
||
appId,
|
||
appSecret,
|
||
accessToken,
|
||
userId,
|
||
};
|
||
}
|
||
|
||
export function maskToken(token?: string | null): string | null {
|
||
if (!token) return null;
|
||
if (token.length <= 8) return "••••";
|
||
return `••••${token.slice(-6)}`;
|
||
}
|
||
|
||
export function normalizeAppUrl(value: string | null | undefined): string | undefined {
|
||
const trimmed = value?.trim();
|
||
if (!trimmed) return undefined;
|
||
try {
|
||
const url = new URL(trimmed);
|
||
if (!["http:", "https:"].includes(url.protocol)) return undefined;
|
||
return `${url.protocol}//${url.host}`.replace(/\/$/, "");
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
export function getAppBaseUrl(
|
||
request?: Request,
|
||
appUrlOverride?: string | null
|
||
): string {
|
||
const fromSettings = normalizeAppUrl(appUrlOverride);
|
||
if (fromSettings) return fromSettings;
|
||
|
||
const fromEnv = normalizeAppUrl(process.env.APP_URL) || normalizeAppUrl(process.env.NEXT_PUBLIC_APP_URL);
|
||
if (fromEnv) return fromEnv;
|
||
|
||
if (request) {
|
||
const host = request.headers.get("x-forwarded-host") || request.headers.get("host");
|
||
const proto = request.headers.get("x-forwarded-proto") || "http";
|
||
if (host) return `${proto}://${host}`;
|
||
}
|
||
|
||
return "http://localhost:3000";
|
||
}
|
||
|
||
export function isPubliclyReachableUrl(url: string): boolean {
|
||
try {
|
||
const { hostname } = new URL(url);
|
||
return !["localhost", "127.0.0.1", "0.0.0.0"].includes(hostname);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|