Skip to content

Commit 86b9a6e

Browse files
atrakhConvex, Inc.
authored andcommitted
Add ability to renew sso certificate (#42899)
GitOrigin-RevId: 7821b3d12de8a8380247aad1eebcaf6f25dc7c9b
1 parent 74ca5c7 commit 86b9a6e

File tree

4 files changed

+121
-52
lines changed

4 files changed

+121
-52
lines changed

crates/workos_client/src/lib.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ pub struct WorkOSAPIKeyResponse {
116116

117117
#[derive(Debug, Deserialize)]
118118
pub struct WorkOSErrorResponse {
119-
pub code: String,
119+
pub code: Option<String>,
120120
pub message: String,
121121
}
122122

@@ -182,6 +182,7 @@ pub struct WorkOSPortalLinkResponse {
182182
pub enum WorkOSPortalIntent {
183183
Sso,
184184
DomainVerification,
185+
CertificateRenewal,
185186
}
186187

187188
#[async_trait]
@@ -500,6 +501,7 @@ impl WorkOSClient for MockWorkOSClient {
500501
let intent_str = match intent {
501502
WorkOSPortalIntent::Sso => "sso",
502503
WorkOSPortalIntent::DomainVerification => "domain_verification",
504+
WorkOSPortalIntent::CertificateRenewal => "certificate_renewal",
503505
};
504506
Ok(WorkOSPortalLinkResponse {
505507
link: format!(
@@ -857,7 +859,7 @@ where
857859
let response_body = response.into_body();
858860

859861
if let Ok(error_response) = serde_json::from_slice::<WorkOSErrorResponse>(&response_body)
860-
&& error_response.code == "user_already_exists"
862+
&& error_response.code == Some("user_already_exists".to_string())
861863
{
862864
// This will be special-cased in scripts.
863865
anyhow::bail!(ErrorMetadata::bad_request(
@@ -1372,6 +1374,21 @@ where
13721374
if !response.status().is_success() {
13731375
let status = response.status();
13741376
let response_body = response.into_body();
1377+
1378+
// Handle 400 Bad Request specially to forward the error message
1379+
if status == http::StatusCode::BAD_REQUEST {
1380+
println!("Hello! Bad request");
1381+
if let Ok(error_response) =
1382+
serde_json::from_slice::<WorkOSErrorResponse>(&response_body)
1383+
{
1384+
println!("Error message: {}", error_response.message);
1385+
return Err(anyhow::anyhow!(ErrorMetadata::bad_request(
1386+
"WorkOSPortalLinkError",
1387+
error_response.message
1388+
)));
1389+
}
1390+
}
1391+
13751392
anyhow::bail!(format_workos_error(
13761393
"generate portal link",
13771394
status,

npm-packages/dashboard/dashboard-management-openapi.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5237,7 +5237,8 @@
52375237
"type": "string",
52385238
"enum": [
52395239
"sso",
5240-
"domainVerification"
5240+
"domainVerification",
5241+
"certificateRenewal"
52415242
]
52425243
},
52435244
"SerializedAccessToken": {

npm-packages/dashboard/src/components/teamSettings/TeamSSO.tsx

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export function TeamSSO({ team }: { team: Team }) {
3838
const [isSubmitting, setIsSubmitting] = useState(false);
3939
const [isGeneratingDomainsLink, setIsGeneratingDomainsLink] = useState(false);
4040
const [isGeneratingSSOLink, setIsGeneratingSSOLink] = useState(false);
41+
const [
42+
isGeneratingCertificateRenewalLink,
43+
setIsGeneratingCertificateRenewalLink,
44+
] = useState(false);
4145
const [showDisableConfirmation, setShowDisableConfirmation] = useState(false);
4246
const [disableError, setDisableError] = useState<string>();
4347
const [requireSsoLoginValue, setRequireSsoLoginValue] = useState(false);
@@ -277,8 +281,34 @@ export function TeamSSO({ team }: { team: Team }) {
277281
</div>
278282
)}
279283
<div className="flex gap-2">
284+
<ManageSSOConfigurationButton
285+
loading={isGeneratingSSOLink}
286+
onClick={async () => {
287+
setIsGeneratingSSOLink(true);
288+
try {
289+
const result = await generateSSOConfigurationLink({
290+
intent: "sso",
291+
});
292+
if (result?.link) {
293+
window.open(result.link, "_blank");
294+
}
295+
} finally {
296+
setIsGeneratingSSOLink(false);
297+
}
298+
}}
299+
disabled={
300+
isSubmitting ||
301+
!hasAdminPermissions ||
302+
!hasVerifiedDomain ||
303+
isGeneratingAnyLink
304+
}
305+
tooltip={
306+
!hasVerifiedDomain
307+
? "You must verify at least one domain before managing the SSO configuration."
308+
: undefined
309+
}
310+
/>
280311
<ManageDomainsButton
281-
variant="neutral"
282312
loading={isGeneratingDomainsLink}
283313
onClick={async () => {
284314
setIsGeneratingDomainsLink(true);
@@ -299,20 +329,19 @@ export function TeamSSO({ team }: { team: Team }) {
299329
isGeneratingAnyLink
300330
}
301331
/>
302-
<ManageSSOConfigurationButton
303-
variant="primary"
304-
loading={isGeneratingSSOLink}
332+
<CertificateRenewalButton
333+
loading={isGeneratingCertificateRenewalLink}
305334
onClick={async () => {
306-
setIsGeneratingSSOLink(true);
307335
try {
336+
setIsGeneratingCertificateRenewalLink(true);
308337
const result = await generateSSOConfigurationLink({
309-
intent: "sso",
338+
intent: "certificateRenewal",
310339
});
311-
if (result?.link) {
340+
if (result) {
312341
window.open(result.link, "_blank");
313342
}
314343
} finally {
315-
setIsGeneratingSSOLink(false);
344+
setIsGeneratingCertificateRenewalLink(false);
316345
}
317346
}}
318347
disabled={
@@ -328,35 +357,40 @@ export function TeamSSO({ team }: { team: Team }) {
328357
}
329358
/>
330359
</div>
331-
</div>
332-
<hr />
333-
<h4 className="text-sm font-semibold text-content-primary">
334-
Additional Options
335-
</h4>
336-
<div className="space-y-2">
337-
<label className="ml-1 flex items-center gap-3 text-sm text-content-primary">
338-
<Checkbox
339-
checked={requireSsoLoginValue}
340-
disabled={requireSsoLoginDisabled}
341-
onChange={() => {
342-
if (requireSsoLoginDisabled) {
343-
return;
344-
}
345-
setRequireSsoLoginValue(!requireSsoLoginValue);
346-
}}
347-
/>
348-
<span className="flex items-center gap-2">
349-
Require SSO to access team
350-
<Tooltip
351-
tip="Require that team members log in with SSO to access the team."
352-
side="right"
353-
>
354-
<QuestionMarkCircledIcon className="h-4 w-4 text-content-secondary" />
355-
</Tooltip>
356-
</span>
357-
</label>
360+
<hr />
361+
<h4 className="text-sm font-semibold text-content-primary">
362+
Additional Options
363+
</h4>
364+
<Tooltip
365+
tip={
366+
!hasAdminPermissions
367+
? "You do not have permission to change SSO settings."
368+
: !ssoEnabled
369+
? "SSO is not available on your plan."
370+
: undefined
371+
}
372+
>
373+
<label className="ml-px flex items-center gap-2">
374+
<Checkbox
375+
checked={requireSsoLoginValue}
376+
disabled={requireSsoLoginDisabled}
377+
onChange={() => {
378+
setRequireSsoLoginValue(!requireSsoLoginValue);
379+
}}
380+
/>
381+
<span className="ml-px flex items-center gap-2">
382+
Require SSO to access team
383+
<Tooltip
384+
tip="Require that team members log in with SSO to access the team."
385+
side="right"
386+
>
387+
<QuestionMarkCircledIcon className="h-4 w-4 text-content-secondary" />
388+
</Tooltip>
389+
</span>
390+
</label>
391+
</Tooltip>
358392

359-
<div className="mt-4 flex">
393+
<div className="flex">
360394
<Button
361395
type="button"
362396
variant="primary"
@@ -468,16 +502,14 @@ function ManageDomainsButton({
468502
onClick,
469503
disabled,
470504
loading,
471-
variant,
472505
}: {
473506
onClick: () => Promise<void>;
474507
disabled: boolean;
475508
loading: boolean;
476-
variant: "primary" | "neutral";
477509
}) {
478510
return (
479511
<Button
480-
variant={variant}
512+
variant="neutral"
481513
className="w-fit"
482514
size="sm"
483515
loading={loading}
@@ -493,31 +525,50 @@ function ManageSSOConfigurationButton({
493525
onClick,
494526
disabled,
495527
loading,
496-
variant,
497528
tooltip,
498529
}: {
499530
onClick: () => Promise<void>;
500531
disabled: boolean;
501532
loading: boolean;
502-
variant: "primary" | "neutral";
503533
tooltip?: string;
504534
}) {
505-
const button = (
535+
return (
506536
<Button
507-
variant={variant}
537+
variant="primary"
508538
className="w-fit"
509539
size="sm"
510540
loading={loading}
511541
onClick={onClick}
512542
disabled={disabled}
543+
tip={tooltip}
513544
>
514545
Manage SSO configuration
515546
</Button>
516547
);
548+
}
517549

518-
if (tooltip) {
519-
return <Tooltip tip={tooltip}>{button}</Tooltip>;
520-
}
521-
522-
return button;
550+
function CertificateRenewalButton({
551+
onClick,
552+
disabled,
553+
loading,
554+
tooltip,
555+
}: {
556+
onClick: () => Promise<void>;
557+
disabled: boolean;
558+
loading: boolean;
559+
tooltip?: string;
560+
}) {
561+
return (
562+
<Button
563+
variant="neutral"
564+
className="w-fit"
565+
size="sm"
566+
loading={loading}
567+
onClick={onClick}
568+
disabled={disabled}
569+
tip={tooltip}
570+
>
571+
Renew Certificate
572+
</Button>
573+
);
523574
}

npm-packages/dashboard/src/generatedApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2271,7 +2271,7 @@ export interface components {
22712271
requireSsoLogin: boolean;
22722272
};
22732273
/** @enum {string} */
2274-
SSOPortalIntent: "sso" | "domainVerification";
2274+
SSOPortalIntent: "sso" | "domainVerification" | "certificateRenewal";
22752275
/** @description ConvexAccessToken is our own internal notion of authorization.
22762276
* It is versioned.
22772277
*

0 commit comments

Comments
 (0)