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;
|
|||
|
|
}
|
|||
|
|
}
|