"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import type { User } from "@supabase/supabase-js";
import type { LucideIcon } from "lucide-react";
import {
Bot,
CheckCircle2,
ChevronLeft,
Clock,
Copy,
Crown,
Fingerprint,
Hash,
Loader2,
LogIn,
LogOut,
Mail,
RefreshCw,
Shield,
User as UserIcon,
UserCheck,
} from "lucide-react";
import {
getSupabaseBrowserClient,
hasSupabasePublicEnv,
} from "@/lib/supabase/client";
type AuthMeResponse = {
authenticated?: boolean;
user_id?: string | null;
email?: string | null;
entitlement_mode?: string | null;
auth_required?: boolean;
subscription_required?: boolean;
subscription_active?: boolean | null;
};
type InfoItemProps = {
icon: LucideIcon;
label: string;
value: string;
status?: "default" | "primary";
};
function formatTime(value: string | undefined | null, locale: string) {
if (!value) return "--";
try {
const dt = new Date(value);
if (Number.isNaN(dt.getTime())) return "--";
return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(dt);
} catch {
return "--";
}
}
function normalizeProvider(user: User | null) {
const provider = String(user?.app_metadata?.provider || "").trim().toLowerCase();
if (provider) return provider;
const providers = user?.app_metadata?.providers;
if (Array.isArray(providers) && providers.length) {
return String(providers[0] || "").trim().toLowerCase();
}
return "";
}
function InfoItem({ icon: Icon, label, value, status = "default" }: InfoItemProps) {
return (
);
}
export function AccountCenter() {
const router = useRouter();
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [errorText, setErrorText] = useState("");
const [copied, setCopied] = useState(false);
const [updatedAt, setUpdatedAt] = useState("");
const [user, setUser] = useState(null);
const [backend, setBackend] = useState(null);
const supabaseReady = hasSupabasePublicEnv();
const loadSnapshot = useCallback(async () => {
setErrorText("");
try {
const userPromise = supabaseReady
? getSupabaseBrowserClient().auth.getUser()
: Promise.resolve({ data: { user: null as User | null } });
const backendPromise = fetch("/api/auth/me", { cache: "no-store" });
const [userResult, backendResult] = await Promise.all([
userPromise,
backendPromise,
]);
setUser(userResult.data?.user ?? null);
if (!backendResult.ok) {
const raw = (await backendResult.text()).slice(0, 260);
throw new Error(`HTTP ${backendResult.status} ${raw}`.trim());
}
const backendJson = (await backendResult.json()) as AuthMeResponse;
setBackend(backendJson);
setUpdatedAt(new Date().toISOString());
} catch (error) {
setErrorText(String(error));
}
}, [supabaseReady]);
useEffect(() => {
let cancelled = false;
const run = async () => {
setLoading(true);
await loadSnapshot();
if (!cancelled) setLoading(false);
};
void run();
return () => {
cancelled = true;
};
}, [loadSnapshot]);
const onRefresh = async () => {
setRefreshing(true);
await loadSnapshot();
setRefreshing(false);
};
const onSignOut = async () => {
if (supabaseReady) {
try {
await getSupabaseBrowserClient().auth.signOut();
} catch {}
}
router.replace("/");
};
const userId = backend?.user_id || user?.id || "";
const isAuthenticated = Boolean(userId);
const email = backend?.email || user?.email || "";
const displayName =
String(user?.user_metadata?.full_name || "").trim() ||
(email ? String(email).split("@")[0] : "") ||
"PolyWeather 用户";
const initials = (displayName.slice(0, 2) || "PW").toUpperCase();
const providerRaw = normalizeProvider(user);
const provider = providerRaw ? providerRaw.toUpperCase() : "--";
const lastSignIn = formatTime(
user?.last_sign_in_at,
typeof navigator !== "undefined" ? navigator.language : "zh-CN",
);
const updatedAtLabel = formatTime(
updatedAt,
typeof navigator !== "undefined" ? navigator.language : "zh-CN",
);
const modeLabel = useMemo(() => {
const mode = String(backend?.entitlement_mode || "").trim().toLowerCase();
if (mode === "supabase_required") return "Supabase 强制登录";
if (mode === "supabase_optional") return "Supabase 可选登录";
if (mode === "legacy_token") return "Legacy Token 鉴权";
if (mode === "disabled") return "未启用鉴权";
return "未知模式";
}, [backend?.entitlement_mode]);
const backendStatus = useMemo(() => {
if (backend?.authenticated) return "通过";
if (backend?.auth_required) return "未登录";
return "游客模式";
}, [backend?.authenticated, backend?.auth_required]);
const subscriptionRequirement = backend?.subscription_required
? "已启用订阅校验"
: "当前未强制订阅";
const subscriptionResult = !backend?.subscription_required
? "当前未强制订阅"
: backend?.subscription_active
? "有效订阅"
: "无有效订阅";
const bindCommand = userId
? `/bind ${userId}${email ? ` ${email}` : ""}`
: "/bind ";
const copyBindCommand = async () => {
try {
await navigator.clipboard.writeText(bindCommand);
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
} catch {}
};
return (
返回看板
{isAuthenticated ? (
) : (
登录 / 注册
)}
{errorText ? (
加载失败: {errorText}
) : null}
{!supabaseReady ? (
NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY 未配置。
) : null}
最近同步: {updatedAtLabel}
{displayName}
{isAuthenticated ? "已登录" : "游客模式"}
{email || "--"}
Bot 绑定
将下面命令发送至 Telegram Bot,即可把网页账户与机器人权限绑定,实现全平台气象推送。
{bindCommand}
);
}