OpenVuln / src /components /AuthButton.tsx
zRzRzRzRzRzRzR
feat: sync latest OpenVuln frontend
d22337b
Raw
History Blame Contribute Delete
4.42 kB
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Github, LogOut, ChevronDown, FolderGit2 } from "lucide-react";
import { navigateToLogin, navigateToLoginPopup, isEmbedded } from "../shared/api/client";
import { useLogout, useMe } from "../features/auth/useAuth";
/**
* 顶栏登录态:未登录 = Sign in with GitHub;已登录 = 头像 + 下拉(Logout)。
* 浅色体系统一样式;appearance prop 仅为调用方兼容保留。
*/
export function AuthButton({ appearance = "light" }: { appearance?: "light" | "dark" }) {
void appearance;
const meQ = useMe();
const logoutM = useLogout();
const nav = useNavigate();
const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const onDocDown = (e: MouseEvent) => {
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("mousedown", onDocDown);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onDocDown);
document.removeEventListener("keydown", onKey);
};
}, [open]);
// 加载中/后端未接入 auth:按未登录渲染(不阻塞页面)
const user = meQ.data?.authenticated ? meQ.data.user : null;
if (!user) {
return (
<button
type="button"
onClick={() => {
if (isEmbedded()) {
navigateToLoginPopup();
window.dispatchEvent(new Event("ov-oauth-popup-opened"));
} else {
navigateToLogin();
}
}}
className="inline-flex h-9 items-center gap-2 rounded-full bg-ink px-4 text-[13px] font-medium text-surface transition hover:bg-white focus-ring"
>
<Github size={14} />
Sign in
</button>
);
}
return (
<div ref={rootRef} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-haspopup="menu"
aria-label={`Signed in as ${user.login}`}
className="flex h-9 items-center gap-1.5 rounded-full border border-line bg-surface-raised p-1 pr-2 transition-colors hover:border-line-strong hover:bg-surface-sunken focus-ring"
>
{user.avatar_url ? (
<img
src={user.avatar_url}
alt=""
className="h-7 w-7 rounded-full object-cover"
referrerPolicy="no-referrer"
/>
) : (
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-accent-100 text-xs font-semibold text-accent-700">
{user.login.slice(0, 1).toUpperCase()}
</span>
)}
<ChevronDown
size={13}
className={`text-ink-tertiary transition-transform ${open ? "rotate-180" : ""}`}
/>
</button>
{open && (
<div
role="menu"
className="absolute right-0 top-full z-40 mt-2 w-44 overflow-hidden rounded-xl border border-line bg-surface-raised shadow-[0_16px_48px_-12px_rgba(0,0,0,0.7)]"
>
<div className="border-b border-line px-3.5 py-2.5 text-[13px] font-medium text-ink">
{user.login}
</div>
<a
role="menuitem"
href="/my"
onClick={(e) => {
e.preventDefault();
setOpen(false);
nav("/my");
}}
className="flex w-full items-center gap-2 px-3.5 py-2.5 text-left text-[13px] text-ink-secondary transition-colors hover:bg-surface-sunken hover:text-ink"
>
<FolderGit2 size={14} />
My submissions
</a>
<button
type="button"
role="menuitem"
onClick={() => {
setOpen(false);
logoutM.mutate();
}}
disabled={logoutM.isPending}
className="flex w-full items-center gap-2 px-3.5 py-2.5 text-left text-[13px] text-ink-secondary transition-colors hover:bg-surface-sunken hover:text-ink"
>
<LogOut size={14} />
{logoutM.isPending ? "Signing out…" : "Sign out"}
</button>
</div>
)}
</div>
);
}