Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

131 linesCodeBlameRaw
1import { Link } from "react-router";
2
3import type { SsoSettings } from "@g1t/contracts";
4
5import { Badge, EmptyState, Notice, Section, When } from "~/components/ui";
6
7const PROTOCOLS = { oidc: "OpenID Connect", oauth2: "OAuth 2.0", saml: "SAML 2.0" } as const;
8
9/** Which provider a connection is, by its own address. */
10function provider(sso: NonNullable<SsoSettings["connection"]>): string {
11 const config = sso.config;
12 return config.issuer ?? config.idp_entity_id ?? config.authorization_url ?? "—";
13}
14
15/**
16 * A workspace's single sign-on, as its owners see it: the provider, whether
17 * it is required, and who is linked. Read only: owners change it on the
18 * site, and are never held out, so staff never need to.
19 */
20export function SsoSection({ sso, unavailable }: { sso: SsoSettings | null; unavailable: string | null }) {
21 const connection = sso?.connection ?? null;
22 return (
23 <Section
24 id="sso"
25 title="Single sign-on"
26 description="The workspace's own identity provider. Owners set it up and require it at /-/sso; they are never held out."
27 className="mt-6"
28 >
29 {unavailable ? (
30 <Notice tone="error">Single sign-on could not be read: {unavailable}</Notice>
31 ) : !connection ? (
32 <EmptyState title="Not set up" />
33 ) : (
34 <div className="space-y-5">
35 <dl className="grid gap-x-6 gap-y-3 text-sm sm:grid-cols-2">
36 <div>
37 <dt className="text-xs text-faint">Protocol</dt>
38 <dd className="mt-0.5 flex flex-wrap items-center gap-1.5">
39 {PROTOCOLS[connection.protocol]}
40 {sso?.required ? <Badge tone="lavender">Required of members</Badge> : <Badge>Not required</Badge>}
41 {connection.allow_join && <Badge tone="info">Provider's people join</Badge>}
42 </dd>
43 </div>
44 <div>
45 <dt className="text-xs text-faint">Provider</dt>
46 <dd className="mt-0.5 font-mono text-xs break-all">{provider(connection)}</dd>
47 </div>
48 <div>
49 <dt className="text-xs text-faint">Tested</dt>
50 <dd className="mt-0.5">
51 {connection.tested_at ? (
52 <>
53 By{" "}
54 {connection.tested_by ? (
55 <Link to={`/users/${connection.tested_by}`} className="font-mono hover:underline">
56 {connection.tested_by}
57 </Link>
58 ) : (
59 "someone"
60 )}{" "}
61 <When at={connection.tested_at} />
62 </>
63 ) : (
64 <Badge tone="warn">Not yet</Badge>
65 )}
66 </dd>
67 </div>
68 <div>
69 <dt className="text-xs text-faint">Set up</dt>
70 <dd className="mt-0.5">
71 <When at={connection.created_at} />, changed <When at={connection.updated_at} />
72 </dd>
73 </div>
74 {connection.protocol !== "saml" && (
75 <div>
76 <dt className="text-xs text-faint">Client id</dt>
77 <dd className="mt-0.5 font-mono text-xs break-all">
78 {connection.config.client_id ?? "—"} {connection.has_client_secret ? "(secret kept)" : "(no secret)"}
79 </dd>
80 </div>
81 )}
82 {connection.certificates.length > 0 && (
83 <div>
84 <dt className="text-xs text-faint">Signing certificates, SHA-256</dt>
85 <dd className="mt-0.5 space-y-0.5 font-mono text-xs break-all">
86 {connection.certificates.map((certificate) => (
87 <p key={certificate.fingerprint}>{certificate.fingerprint}</p>
88 ))}
89 </dd>
90 </div>
91 )}
92 </dl>
93 {sso!.identities.length === 0 ? (
94 <p className="text-sm text-muted">No one has signed in through it yet.</p>
95 ) : (
96 <div className="-mx-4 -mb-4 overflow-x-auto border-t border-line sm:-mx-5 sm:-mb-5">
97 <table className="w-full min-w-120 text-sm">
98 <thead>
99 <tr className="border-b border-line text-left text-xs text-muted">
100 <th className="px-4 py-2 font-medium sm:pl-5">Person</th>
101 <th className="px-4 py-2 font-medium">At the provider</th>
102 <th className="px-4 py-2 font-medium">Linked</th>
103 <th className="px-4 py-2 font-medium sm:pr-5">Last signed in</th>
104 </tr>
105 </thead>
106 <tbody>
107 {sso!.identities.map((identity) => (
108 <tr key={identity.username} className="border-b border-line last:border-0">
109 <td className="px-4 py-2.5 font-mono sm:pl-5">
110 <Link to={`/users/${identity.username}`} className="hover:underline">
111 {identity.username}
112 </Link>
113 </td>
114 <td className="px-4 py-2.5 break-all text-muted">{identity.email ?? identity.subject}</td>
115 <td className="px-4 py-2.5 text-xs whitespace-nowrap text-muted">
116 <When at={identity.linked_at} />
117 </td>
118 <td className="px-4 py-2.5 text-xs whitespace-nowrap text-muted sm:pr-5">
119 <When at={identity.last_used_at} />
120 </td>
121 </tr>
122 ))}
123 </tbody>
124 </table>
125 </div>
126 )}
127 </div>
128 )}
129 </Section>
130 );
131}