chore(sase): show emails in plain text — drop masking + reveal flow
User decision: emails should display openly on the panel (Tailscale-only, single founder). The reveal-with-audit pattern was overkill for this trust model and added a click per inspection. - List page: u****@x.com → u.email - Detail header: <EmailReveal/> → plain <span>{user.email}</span> - Delete _email-reveal.tsx + /api/sase/users/[id]/reveal-email route - Delete maskEmail() helper (no remaining callers) Audit log no longer captures per-email reveals; the audit tab now only shows lifecycle/billing/impersonation actions, which is what we actually care about. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { saseDb } from "@/lib/db-sase";
|
||||
import { writeAudit } from "@/lib/audit";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(
|
||||
_req: Request,
|
||||
ctx: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, error: "unauthenticated" }, { status: 401 });
|
||||
}
|
||||
const { id } = await ctx.params;
|
||||
|
||||
const user = await saseDb.user.findUnique({
|
||||
where: { id },
|
||||
select: { email: true },
|
||||
});
|
||||
if (!user) {
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint: `/api/sase/users/${id}/reveal-email`,
|
||||
method: "POST",
|
||||
responseStatus: 404,
|
||||
});
|
||||
return NextResponse.json({ ok: false, error: "not_found" }, { status: 404 });
|
||||
}
|
||||
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint: `/api/sase/users/${id}/reveal-email`,
|
||||
method: "POST",
|
||||
responseStatus: 200,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, email: user.email });
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function EmailReveal({
|
||||
userId,
|
||||
masked,
|
||||
}: {
|
||||
userId: string;
|
||||
masked: string;
|
||||
}) {
|
||||
const [revealed, setRevealed] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
function onReveal() {
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
const res = await fetch(`/api/sase/users/${userId}/reveal-email`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
setError(data.error ?? `HTTP ${res.status}`);
|
||||
return;
|
||||
}
|
||||
const data = (await res.json()) as { email: string };
|
||||
setRevealed(data.email);
|
||||
});
|
||||
}
|
||||
|
||||
if (revealed) {
|
||||
return (
|
||||
<span className="font-mono text-sm" title="Audit'li reveal">
|
||||
{revealed}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm text-muted-foreground">{masked}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={onReveal}
|
||||
disabled={pending}
|
||||
>
|
||||
{pending ? "..." : "Aç"}
|
||||
</Button>
|
||||
{error && <span className="text-xs text-destructive">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,13 +23,12 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { getUser, listPlans, maskEmail } from "@/lib/sase/users";
|
||||
import { getUser, listPlans } from "@/lib/sase/users";
|
||||
import {
|
||||
getUserUsageStats,
|
||||
getUserTimeline,
|
||||
getUserAuditTrail,
|
||||
} from "@/lib/sase/user-detail";
|
||||
import { EmailReveal } from "./_email-reveal";
|
||||
import { ImpersonateButton } from "./_impersonate-button";
|
||||
import { NotesTab } from "./_notes";
|
||||
import { LifecycleButtons, LifecycleStatusBadge } from "./_lifecycle-buttons";
|
||||
@@ -68,7 +67,7 @@ export default async function SaseUserDetailPage({
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-xl">{user.name}</CardTitle>
|
||||
<CardDescription className="flex flex-wrap items-center gap-2">
|
||||
<EmailReveal userId={user.id} masked={maskEmail(user.email)} />
|
||||
<span className="font-mono text-sm">{user.email}</span>
|
||||
{user.emailVerified ? (
|
||||
<Badge variant="secondary">verified</Badge>
|
||||
) : (
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { listUsers, listPlans, maskEmail } from "@/lib/sase/users";
|
||||
import { listUsers, listPlans } from "@/lib/sase/users";
|
||||
import { UsersFilterBar } from "./_filter-bar";
|
||||
import { Pager } from "./_pager";
|
||||
import { buildHref, type UsersSearchParams } from "./_query";
|
||||
@@ -101,9 +101,9 @@ export default async function SaseUsersPage({
|
||||
) : (
|
||||
result.rows.map((u) => (
|
||||
<TableRow key={u.id}>
|
||||
<TableCell className="font-mono text-xs" title={u.email}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<Link href={`/projects/sase/users/${u.id}`} className="hover:underline">
|
||||
{maskEmail(u.email)}
|
||||
{u.email}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{u.name}</TableCell>
|
||||
|
||||
@@ -271,13 +271,6 @@ export async function listPlans() {
|
||||
});
|
||||
}
|
||||
|
||||
export function maskEmail(email: string): string {
|
||||
const [local, domain] = email.split("@");
|
||||
if (!local || !domain) return email;
|
||||
if (local.length <= 1) return `${local}***@${domain}`;
|
||||
return `${local[0]}${"*".repeat(Math.min(local.length - 1, 4))}@${domain}`;
|
||||
}
|
||||
|
||||
function tierFromPlan(name: string | null, brandCount: number | null): string | null {
|
||||
if (!name) return null;
|
||||
const n = name.toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user