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:
Semih
2026-05-18 09:53:54 +03:00
parent 3621c0801f
commit 631ca6b4c4
5 changed files with 5 additions and 110 deletions

View File

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

View File

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

View File

@@ -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>
) : (

View File

@@ -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>

View File

@@ -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();