Skip to content
135 linesCodeBlameRaw
1import { Link, data, useFetcher, useSearchParams } from "react-router";
2
3import type { BypassRequest } from "@g1t/contracts";
4
5import type { Route } from "./+types/security-bypass";
6import { page } from "../../lib/meta";
7import { CARD, FilterSelect } from "../../components/security-suite";
8import { WorkspaceSecurityHeading, WorkspaceSecurityTabs } from "../../components/workspace-security-tabs";
9import { TimeAgo } from "../../components/ui";
10import { Badge } from "../../components/ui/badge";
11import { Button } from "../../components/ui/button";
12import { Card } from "../../components/ui/card";
13import { securitySuite } from "../../lib/services.server";
14import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
15
16export function meta({ params, ...args }: Route.MetaArgs) {
17 return page(args, { title: `Bypass requests · ${params.owner} · g1t` });
18}
19
20const STATES = new Set(["pending", "approved", "denied", "cancelled"]);
21
22export async function loader({ params, context, request }: Route.LoaderArgs) {
23 const viewer = getViewer(context);
24 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
25 const state = new URL(request.url).searchParams.get("state") ?? "pending";
26 const [list, pending] = await Promise.all([
27 securitySuite.bypassRequests(params.owner, viewer, STATES.has(state) ? state : null),
28 securitySuite.bypassRequests(params.owner, viewer, "pending"),
29 ]);
30 return { requests: unwrap(list), pending: pending.ok ? pending.value.length : 0, me: viewer?.username ?? "" };
31}
32
33export async function action({ params, context, request }: Route.ActionArgs) {
34 assertSameOrigin(request);
35 const user = requireUser(context, request);
36 const form = await request.formData();
37 const decision = String(form.get("decision") ?? "");
38 if (decision !== "approve" && decision !== "deny" && decision !== "cancel") return { ok: false, error: "Choose approve or deny." };
39 const done = await securitySuite.reviewBypass(user, params.owner, String(form.get("id") ?? ""), decision, String(form.get("comment") ?? "").trim().slice(0, 500));
40 return done.ok ? { ok: true } : { ok: false, error: done.error.message };
41}
42
43const STATE_TONE = { pending: "warn", approved: "accent", denied: "danger", cancelled: "neutral" } as const;
44
45function Review({ request, mine }: { request: BypassRequest; mine: boolean }) {
46 const fetcher = useFetcher<{ ok: boolean; error?: string }>();
47 const busy = fetcher.state !== "idle";
48 return (
49 <fetcher.Form method="post" className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center">
50 <input type="hidden" name="id" value={request.id} />
51 {!mine && (
52 <input
53 name="comment"
54 maxLength={500}
55 placeholder="Comment (optional)"
56 className="h-8 min-w-0 grow rounded-md border border-line bg-bg px-2.5 text-[0.8125rem] outline-none hover:border-line-strong focus:border-accent-dim"
57 />
58 )}
59 <div className="flex gap-2">
60 {mine ? (
61 <Button type="submit" name="decision" value="cancel" disabled={busy} variant="outline" size="inline" className="px-2.5 py-1 text-xs text-muted">
62 Cancel request
63 </Button>
64 ) : (
65 <>
66 <Button type="submit" name="decision" value="approve" disabled={busy} size="inline" className="px-3 py-1 text-xs">
67 Approve
68 </Button>
69 <Button type="submit" name="decision" value="deny" disabled={busy} variant="destructive" size="inline" className="px-3 py-1 text-xs">
70 Deny
71 </Button>
72 </>
73 )}
74 </div>
75 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
76 </fetcher.Form>
77 );
78}
79
80export default function BypassRequests({ loaderData, params }: Route.ComponentProps) {
81 const { requests, pending, me } = loaderData;
82 const [search, setSearch] = useSearchParams();
83 const state = search.get("state") ?? "pending";
84 return (
85 <div>
86 <WorkspaceSecurityHeading
87 title="Bypass requests"
88 about="With delegated bypass on, people who push a blocked secret ask to push it anyway, and the workspace's owners and the repository's admins decide. Each decision is recorded on the alert and in the audit log."
89 />
90 <WorkspaceSecurityTabs owner={params.owner} pending={pending} />
91 <div className="mb-4">
92 <FilterSelect
93 label="State"
94 value={STATES.has(state) ? state : "all"}
95 options={[["pending", "Pending"], ["approved", "Approved"], ["denied", "Denied"], ["cancelled", "Cancelled"], ["all", "All"]]}
96 onChange={(value) => setSearch(value === "pending" ? {} : { state: value }, { replace: true })}
97 />
98 </div>
99 {requests.length === 0 ? (
100 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-sm text-muted">
101 <p>
102 No {state === "all" ? "" : `${state} `}requests. Delegated bypass is turned on in this workspace's Security settings.
103 </p>
104 </Card>
105 ) : (
106 <ul className="space-y-3">
107 {requests.map((request) => (
108 <li key={request.id} className={`${CARD} p-4`}>
109 <div className="flex flex-wrap items-center gap-2">
110 <Link to={`/${request.workspace}/${request.repo}/security/secret-scanning/${request.secretId}`} className="text-sm font-medium first-letter:uppercase hover:underline">
111 {request.label}
112 </Link>
113 <Badge tone={STATE_TONE[request.state]}>{request.state}</Badge>
114 <span className="font-mono text-xs text-faint">
115 {request.repo} · {request.path}:{request.line}
116 </span>
117 </div>
118 <p className="mt-1.5 text-sm text-muted">
119 {request.requester} asked <TimeAgo at={request.createdAt} />: {request.reason.replaceAll("_", " ")}
120 {request.comment && <> · “{request.comment}”</>}
121 </p>
122 {request.reviewer && (
123 <p className="mt-1 text-xs text-faint">
124 {request.state} by {request.reviewer}
125 {request.reviewComment && <>: “{request.reviewComment}”</>}
126 </p>
127 )}
128 {request.state === "pending" && <Review request={request} mine={request.requester.toLowerCase() === me.toLowerCase()} />}
129 </li>
130 ))}
131 </ul>
132 )}
133 </div>
134 );
135}