Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guides/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Comprehensive proxy configuration enforcement across the entire request pipeline

## 📧 Email Privacy Masking _(v3.5.6+)_

OAuth account emails are now masked in the provider dashboard (e.g. `di*****@g****.com`) to prevent accidental exposure when sharing screenshots or recording demos. The full email address remains accessible via hover tooltip (`title` attribute).
OAuth account emails are masked by default (e.g. `di*****@g****.com`) to prevent accidental exposure when sharing screenshots or recording demos. Use Settings → Appearance → Account email visibility to reveal or mask full account emails globally across providers, combos, logs, quota, and playground screens.

---

Expand Down
27 changes: 0 additions & 27 deletions src/app/(dashboard)/dashboard/combos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import Input from "@/shared/components/Input";
import Modal from "@/shared/components/Modal";
import Toggle from "@/shared/components/Toggle";
import Tooltip from "@/shared/components/Tooltip";
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { pickDisplayValue } from "@/shared/utils/maskEmail";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
Expand Down Expand Up @@ -976,32 +975,6 @@ export default function CombosPage() {
</div>

<div className="flex flex-wrap items-center gap-2">
<div className="inline-flex items-center gap-2 rounded-lg border border-black/8 dark:border-white/8 bg-black/[0.02] dark:bg-white/[0.02] px-2.5 py-1.5">
<span className="hidden lg:inline text-xs text-text-muted">
{getI18nOrFallback(
t,
"emailVisibilityHint",
"Account emails here follow the global privacy toggle."
)}
</span>
<Tooltip
position="bottom"
content={getI18nOrFallback(
t,
"emailVisibilityTooltip",
"Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens."
)}
>
<span className="inline-flex">
<EmailPrivacyToggle size="md" />
</span>
</Tooltip>
<span className="text-[11px] text-text-muted">
{emailsVisible
? getI18nOrFallback(t, "emailVisibilityStateOn", "Emails visible globally")
: getI18nOrFallback(t, "emailVisibilityStateOff", "Emails masked globally")}
</span>
</div>
{!showUsageGuide && (
<Button size="sm" variant="ghost" onClick={handleShowUsageGuide}>
{getI18nOrFallback(t, "usageGuideShow", "Show guide")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/shared/components";
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import { maskEmailLikeValue } from "@/shared/utils/maskEmail";
import type { QuotaPool } from "@/lib/quota/dimensions";
Expand Down Expand Up @@ -136,7 +135,6 @@ export default function QuotaSharePageClient() {
const [connections, setConnections] = useState<Connection[]>([]);
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [plans, setPlans] = useState<Record<string, PlanInfo>>({});
const [, setSideLoading] = useState(true);
const [createOpen, setCreateOpen] = useState(false);
const [editing, setEditing] = useState<QuotaPool | null>(null);

Expand All @@ -149,8 +147,9 @@ export default function QuotaSharePageClient() {

// ── Fetch side data once on mount ─────────────────────────────────────────

useMemo(() => {
setSideLoading(true);
useEffect(() => {
let cancelled = false;

Promise.all([
fetch("/api/providers/client")
.then((r) => (r.ok ? r.json() : null))
Expand All @@ -163,6 +162,8 @@ export default function QuotaSharePageClient() {
.catch(() => null),
])
.then(([connsData, keysData, plansData]) => {
if (cancelled) return;

const conns: Connection[] = Array.isArray(connsData?.connections)
? connsData.connections
: [];
Expand All @@ -177,36 +178,43 @@ export default function QuotaSharePageClient() {
dimensions: PlanDimension[];
source: "auto" | "manual";
}>) {
if (p.connectionId) planMap[p.connectionId] = { dimensions: p.dimensions, source: p.source };
if (p.connectionId)
planMap[p.connectionId] = { dimensions: p.dimensions, source: p.source };
}
setPlans(planMap);
}
})
.catch(() => {
// fail open — side data not critical
})
.finally(() => {
setSideLoading(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps

return () => {
cancelled = true;
};
}, []);

// ── Fetch groups ──────────────────────────────────────────────────────────

const fetchGroups = useCallback(async () => {
const fetchGroups = useCallback(async (options?: { signal?: AbortSignal }) => {
try {
const res = await fetch("/api/quota/groups");
const res = await fetch("/api/quota/groups", { signal: options?.signal });
if (res.ok) {
const data = (await res.json()) as { groups: QuotaGroup[] };
if (options?.signal?.aborted) return;
setGroups(Array.isArray(data.groups) ? data.groups : []);
}
} catch {
if (options?.signal?.aborted) return;
// fail open — groups list not critical
}
}, []);

useEffect(() => {
void fetchGroups();
const controller = new AbortController();
void Promise.resolve().then(() => fetchGroups({ signal: controller.signal }));
return () => {
controller.abort();
};
}, [fetchGroups]);

// ── Group actions ─────────────────────────────────────────────────────────
Expand All @@ -233,7 +241,10 @@ export default function QuotaSharePageClient() {
}, [newGroupInput, fetchGroups]);

const handleRenameGroup = useCallback(async () => {
const name = prompt(t("groupNamePrompt"), groups.find((g) => g.id === selectedGroupId)?.name ?? "");
const name = prompt(
t("groupNamePrompt"),
groups.find((g) => g.id === selectedGroupId)?.name ?? ""
);
if (!name?.trim()) return;
setRenaming(true);
try {
Expand Down Expand Up @@ -388,7 +399,6 @@ export default function QuotaSharePageClient() {
<p className="text-sm text-text-muted mt-0.5">{t("description")}</p>
</div>
<div className="flex items-center gap-2">
<EmailPrivacyToggle />
<Button variant="primary" size="sm" onClick={() => setCreateOpen(true)}>
<span className="material-symbols-outlined text-[14px] mr-1">add</span>
{t("newPool")}
Expand All @@ -398,7 +408,9 @@ export default function QuotaSharePageClient() {

{/* Beta banner — scoped to this page only */}
<div className="flex items-center gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-[12px] text-amber-700 dark:text-amber-200">
<span className="material-symbols-outlined text-[16px] text-amber-500 shrink-0">science</span>
<span className="material-symbols-outlined text-[16px] text-amber-500 shrink-0">
science
</span>
<span className="flex-1">
<span className="font-semibold">{t("betaTitle")}</span> — {t("betaText")}
</span>
Expand Down Expand Up @@ -439,7 +451,10 @@ export default function QuotaSharePageClient() {
onChange={(e) => setNewGroupInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") void handleCreateGroup();
if (e.key === "Escape") { setShowNewGroupInput(false); setNewGroupInput(""); }
if (e.key === "Escape") {
setShowNewGroupInput(false);
setNewGroupInput("");
}
}}
placeholder={t("groupNamePrompt")}
autoFocus
Expand All @@ -455,7 +470,10 @@ export default function QuotaSharePageClient() {
</button>
<button
type="button"
onClick={() => { setShowNewGroupInput(false); setNewGroupInput(""); }}
onClick={() => {
setShowNewGroupInput(false);
setNewGroupInput("");
}}
className="text-xs px-2 py-1 rounded border border-border text-text-muted hover:text-text-main transition-colors"
>
{t("cancel")}
Expand Down Expand Up @@ -541,16 +559,20 @@ export default function QuotaSharePageClient() {
{groupsToRender.length === 0 ? (
<div className="rounded-xl border border-dashed border-border bg-surface py-10 text-center">
<p className="text-sm text-text-muted">{t("emptyDescription")}</p>
<Button variant="primary" size="sm" className="mt-3" onClick={() => setCreateOpen(true)}>
<Button
variant="primary"
size="sm"
className="mt-3"
onClick={() => setCreateOpen(true)}
>
<span className="material-symbols-outlined text-[14px] mr-1">add</span>
{t("newPool")}
</Button>
</div>
) : (
groupsToRender.map((g) => {
const groupPools = pools.filter(
(p) =>
((p as unknown as { groupId?: string }).groupId ?? "group-demo") === g.id
(p) => ((p as unknown as { groupId?: string }).groupId ?? "group-demo") === g.id
);
return (
<div key={g.id} className="flex flex-col gap-3">
Expand Down Expand Up @@ -643,7 +665,9 @@ export default function QuotaSharePageClient() {
connections={connections}
apiKeys={apiKeys}
plans={plans}
existingPoolConnectionIds={new Set(pools.flatMap((p) => p.connectionIds ?? [p.connectionId]))}
existingPoolConnectionIds={
new Set(pools.flatMap((p) => p.connectionIds ?? [p.connectionId]))
}
connectionPoolName={connectionPoolName}
groups={groups}
selectedGroupId={selectedGroupId}
Expand Down
3 changes: 0 additions & 3 deletions src/app/(dashboard)/dashboard/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useState, useRef, useEffect } from "react";
import { ConfirmModal, RequestLoggerV2 } from "@/shared/components";
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
import { useTranslations } from "next-intl";

const TIME_RANGES = [
Expand Down Expand Up @@ -124,8 +123,6 @@ export default function LogsPage() {
<h2 className="text-lg font-semibold text-text-main">{t("requestLogs")}</h2>

<div className="flex items-center gap-2">
<EmailPrivacyToggle size="md" />

<button
id="clean-log-history-btn"
onClick={() => setShowCleanHistory(true)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// Phase 1t.1 extraction — Issue #3501
import Link from "next/link";
import ProviderIcon from "@/shared/components/ProviderIcon";
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
import { getHeaderIconProviderId } from "../providerPageHelpers";
import type { ProviderMessageTranslator } from "../providerPageHelpers";

Expand Down Expand Up @@ -78,7 +77,6 @@ export default function ProviderPageHeader({
<p className="text-text-muted">
{t("connectionCountLabel", { count: connectionsCount })}
</p>
<EmailPrivacyToggle size="md" />
{providerId === "adapta-web" && (
<button
onClick={onOpenTutorial}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,7 @@ export default function EditConnectionModal({
>
>({});
const [showAdvanced, setShowAdvanced] = useState(false);
const { emailsVisible: showEmail, toggleEmailVisibility: toggleShowEmail } =
useEmailPrivacyStore();
const showEmail = useEmailPrivacyStore((state) => state.emailsVisible);

const usesBaseUrl = isBaseUrlConfigurableProvider(connection?.provider);
const defaultBaseUrl = getProviderBaseUrlDefault(connection?.provider);
Expand Down Expand Up @@ -681,21 +680,9 @@ export default function EditConnectionModal({
{isOAuth && connection.email && (
<div className="bg-sidebar/50 p-3 rounded-lg">
<p className="text-sm text-text-muted mb-1">{t("email")}</p>
<div className="flex items-center gap-2">
<p className="font-medium" title={showEmail ? connection.email : undefined}>
{showEmail ? connection.email : maskEmail(connection.email)}
</p>
<button
type="button"
onClick={toggleShowEmail}
className="rounded p-1 text-text-muted hover:bg-sidebar hover:text-primary"
title={showEmail ? t("hideEmail") : t("showEmail")}
>
<span className="material-symbols-outlined text-sm">
{showEmail ? "visibility_off" : "visibility"}
</span>
</button>
</div>
<p className="font-medium" title={showEmail ? connection.email : undefined}>
{showEmail ? connection.email : maskEmail(connection.email)}
</p>
</div>
)}
{isOAuth && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { useTranslations } from "next-intl";
import { useShallow } from "zustand/react/shallow";
import { Toggle } from "@/shared/components";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";

export default function AccountEmailVisibilitySetting() {
const t = useTranslations("settings");
const { emailsVisible, setEmailsVisible } = useEmailPrivacyStore(
useShallow((state) => ({
emailsVisible: state.emailsVisible,
setEmailsVisible: state.setEmailsVisible,
}))
);

const label = (key: string, fallback: string) =>
typeof t.has === "function" && t.has(key) ? t(key) : fallback;

return (
<div className="pt-4 border-t border-border">
<div className="flex items-center justify-between gap-4">
<div>
<p className="font-medium">
{label("accountEmailVisibility", "Account email visibility")}
</p>
<p className="text-sm text-text-muted">
{label(
"accountEmailVisibilityDesc",
"Show full account emails across providers, combos, logs, quota, and playground screens. Turn this off to mask them by default."
)}
</p>
</div>
<Toggle checked={emailsVisible} onChange={setEmailsVisible} />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import Link from "next/link";
import { Button, Card, Toggle } from "@/shared/components";
import { useTheme } from "@/shared/hooks/useTheme";
import useThemeStore, { COLOR_THEMES } from "@/store/themeStore";
Expand All @@ -17,6 +16,8 @@ import {
PIN_PROVIDER_QUOTA_TO_HOME_KEY,
SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY,
} from "@/shared/constants/homeWidgets";
import AccountEmailVisibilitySetting from "./AccountEmailVisibilitySetting";
import SidebarVisibilitySetting from "./SidebarVisibilitySetting";

export default function AppearanceTab() {
const { theme, setTheme, isDark } = useTheme();
Expand Down Expand Up @@ -533,26 +534,9 @@ export default function AppearanceTab() {
</div>
</div>

<div className="pt-4 border-t border-border">
<div className="flex items-center justify-between gap-4">
<div>
<p className="font-medium">{t("sidebarVisibilityToggle")}</p>
<p className="text-sm text-text-muted">
{getSettingsLabel(
"sidebarCustomizeLink",
"Customize which items appear in the sidebar, their order, and apply role presets."
)}
</p>
</div>
<Link
href="/dashboard/settings/sidebar"
className="shrink-0 flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md border border-border hover:bg-surface/80 hover:border-primary/40 transition-colors text-text-main"
>
<span className="material-symbols-outlined text-[16px]">view_sidebar</span>
{getSettingsLabel("sidebarCustomizeLinkBtn", "Customize")}
</Link>
</div>
</div>
<AccountEmailVisibilitySetting />

<SidebarVisibilitySetting />

<div className="pt-4 border-t border-border">
<div className="flex items-center justify-between">
Expand Down
Loading