@workos-inc/widgets 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cjs/lib/organization-switcher.d.ts +10 -1
- package/dist/cjs/lib/organization-switcher.d.ts.map +1 -1
- package/dist/cjs/lib/organization-switcher.js +31 -3
- package/dist/cjs/lib/organization-switcher.js.map +1 -1
- package/dist/cjs/workos-widgets.client.d.ts +6 -0
- package/dist/cjs/workos-widgets.client.d.ts.map +1 -1
- package/dist/cjs/workos-widgets.client.js +23 -3
- package/dist/cjs/workos-widgets.client.js.map +1 -1
- package/dist/esm/lib/organization-switcher.d.ts +10 -1
- package/dist/esm/lib/organization-switcher.d.ts.map +1 -1
- package/dist/esm/lib/organization-switcher.js +31 -3
- package/dist/esm/lib/organization-switcher.js.map +1 -1
- package/dist/esm/workos-widgets.client.d.ts +6 -0
- package/dist/esm/workos-widgets.client.d.ts.map +1 -1
- package/dist/esm/workos-widgets.client.js +25 -5
- package/dist/esm/workos-widgets.client.js.map +1 -1
- package/package.json +40 -47
- package/src/api/api-provider.tsx +0 -158
- package/src/api/constants.ts +0 -1
- package/src/api/endpoint.ts +0 -3097
- package/src/api/errors.ts +0 -48
- package/src/api/index.ts +0 -2
- package/src/api/utils.ts +0 -42
- package/src/api/widgets-api-client.ts +0 -87
- package/src/card-list.tsx +0 -26
- package/src/index.ts +0 -9
- package/src/lib/add-mfa-dialog.tsx +0 -379
- package/src/lib/api/config.ts +0 -9
- package/src/lib/api/user.ts +0 -98
- package/src/lib/change-password-dialog.tsx +0 -290
- package/src/lib/constants.ts +0 -3
- package/src/lib/copy-button.tsx +0 -53
- package/src/lib/delete-user-dialog.tsx +0 -110
- package/src/lib/edit-user-profile-dialog.tsx +0 -181
- package/src/lib/edit-user-role-dialog.tsx +0 -178
- package/src/lib/elements.tsx +0 -428
- package/src/lib/elevated-access.tsx +0 -261
- package/src/lib/error-boundary.tsx +0 -166
- package/src/lib/errors.ts +0 -49
- package/src/lib/generic-error.tsx +0 -70
- package/src/lib/icon-panel.tsx +0 -26
- package/src/lib/icons.tsx +0 -21
- package/src/lib/invite-user-dialog.tsx +0 -327
- package/src/lib/logout-all-sessions-dialog.tsx +0 -82
- package/src/lib/logout-dialog.tsx +0 -85
- package/src/lib/marker.tsx +0 -39
- package/src/lib/oauth-icons.tsx +0 -138
- package/src/lib/organization-switcher.tsx +0 -156
- package/src/lib/otp-input.tsx +0 -276
- package/src/lib/resend-invite-dialog.tsx +0 -145
- package/src/lib/reset-mfa-dialog.tsx +0 -104
- package/src/lib/revoke-invite-dialog.tsx +0 -111
- package/src/lib/save-button.tsx +0 -113
- package/src/lib/search-provider.tsx +0 -51
- package/src/lib/set-password-dialog.tsx +0 -204
- package/src/lib/use-dialog-close.tsx +0 -19
- package/src/lib/use-is-hydrated.ts +0 -13
- package/src/lib/use-layout-effect.ts +0 -6
- package/src/lib/use-security-settings.tsx +0 -49
- package/src/lib/user-actions-dropdown.tsx +0 -157
- package/src/lib/user-profile.tsx +0 -227
- package/src/lib/user-security.tsx +0 -187
- package/src/lib/user-sessions.tsx +0 -204
- package/src/lib/users-filter.tsx +0 -62
- package/src/lib/users-management-context.tsx +0 -74
- package/src/lib/users-management-state.ts +0 -165
- package/src/lib/users-management.tsx +0 -594
- package/src/lib/users-search.tsx +0 -73
- package/src/lib/utils.ts +0 -131
- package/src/lib/widgets-context.ts +0 -29
- package/src/organization-switcher.client.tsx +0 -81
- package/src/user-profile.client.tsx +0 -55
- package/src/user-security.client.tsx +0 -55
- package/src/user-sessions.client.tsx +0 -100
- package/src/users-management.client.tsx +0 -73
- package/src/workos-widgets.client.tsx +0 -75
- /package/{src → dist/css}/base.css +0 -0
- /package/{src → dist/css}/lib/card-list.css +0 -0
- /package/{src → dist/css}/lib/marker.css +0 -0
- /package/{src → dist/css}/lib/save-button.css +0 -0
- /package/{src → dist/css}/styles.css +0 -0
- /package/{src → dist/css}/users-management.css +0 -0
package/src/api/errors.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
type QueryType = "query" | "mutation";
|
|
2
|
-
|
|
3
|
-
export class FetchError extends Error {
|
|
4
|
-
queryType: QueryType;
|
|
5
|
-
recordType?: string;
|
|
6
|
-
context: unknown;
|
|
7
|
-
constructor(args: {
|
|
8
|
-
message: string;
|
|
9
|
-
queryType: QueryType;
|
|
10
|
-
recordType?: string;
|
|
11
|
-
context?: unknown;
|
|
12
|
-
}) {
|
|
13
|
-
super(args.message);
|
|
14
|
-
this.name = "FetchError";
|
|
15
|
-
this.queryType = args.queryType;
|
|
16
|
-
this.recordType = args.recordType;
|
|
17
|
-
this.context = args.context;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class ApiError extends Error {
|
|
22
|
-
status: number;
|
|
23
|
-
queryType: QueryType;
|
|
24
|
-
recordType?: string;
|
|
25
|
-
context: unknown;
|
|
26
|
-
constructor(args: {
|
|
27
|
-
message: string;
|
|
28
|
-
queryType: QueryType;
|
|
29
|
-
recordType?: string;
|
|
30
|
-
status: number;
|
|
31
|
-
context?: unknown;
|
|
32
|
-
}) {
|
|
33
|
-
super(args.message);
|
|
34
|
-
this.name = "ApiError";
|
|
35
|
-
this.status = args.status;
|
|
36
|
-
this.queryType = args.queryType;
|
|
37
|
-
this.recordType = args.recordType;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class NoAuthTokenError extends Error {
|
|
42
|
-
context: unknown;
|
|
43
|
-
constructor(args?: { context?: unknown }) {
|
|
44
|
-
super("No auth token provided");
|
|
45
|
-
this.name = "NoAuthTokenError";
|
|
46
|
-
this.context = args?.context;
|
|
47
|
-
}
|
|
48
|
-
}
|
package/src/api/index.ts
DELETED
package/src/api/utils.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { WIDGETS_API_VERSION } from "./constants";
|
|
2
|
-
|
|
3
|
-
export function getAuthHeaders(authToken: string): HeadersInit {
|
|
4
|
-
return {
|
|
5
|
-
Authorization: `Bearer ${authToken}`,
|
|
6
|
-
"WorkOS-Widgets-Version": WIDGETS_API_VERSION,
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
11
|
-
return typeof value === "object" && value !== null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function isErrorLike(
|
|
15
|
-
value: unknown,
|
|
16
|
-
): value is Record<string, unknown> & { message: string } {
|
|
17
|
-
return isObjectLike(value) && typeof value.message === "string";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function parseErrorResponse(
|
|
21
|
-
response: Response,
|
|
22
|
-
): Promise<{ message: string; status: number }> {
|
|
23
|
-
try {
|
|
24
|
-
const json = await response.json();
|
|
25
|
-
if (!isObjectLike(json) || typeof json.message !== "string") {
|
|
26
|
-
return {
|
|
27
|
-
status: response.status,
|
|
28
|
-
message: response.statusText,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
return {
|
|
32
|
-
...json,
|
|
33
|
-
status: response.status,
|
|
34
|
-
message: json.message || response.statusText,
|
|
35
|
-
};
|
|
36
|
-
} catch {
|
|
37
|
-
return {
|
|
38
|
-
status: response.status,
|
|
39
|
-
message: response.statusText,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { QueryKey } from "@tanstack/react-query";
|
|
2
|
-
import { useApi } from "./api-provider";
|
|
3
|
-
import { ApiError, FetchError } from "./errors";
|
|
4
|
-
import { getAuthHeaders, parseErrorResponse } from "./utils";
|
|
5
|
-
|
|
6
|
-
type WidgetsApiClient<T> = (data: {
|
|
7
|
-
url: string;
|
|
8
|
-
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
9
|
-
params?: Record<string, any>;
|
|
10
|
-
headers?: Record<string, any>;
|
|
11
|
-
data?: BodyType<unknown>;
|
|
12
|
-
signal?: AbortSignal;
|
|
13
|
-
}) => Promise<T>;
|
|
14
|
-
|
|
15
|
-
export const useWidgetsApiClient = <T>(): WidgetsApiClient<T> => {
|
|
16
|
-
const { authToken, baseUrl, elevatedAccess, widgetType } = useApi();
|
|
17
|
-
|
|
18
|
-
return async ({ url: pathname, method, params, data }) => {
|
|
19
|
-
const url = new URL(baseUrl);
|
|
20
|
-
url.pathname = pathname;
|
|
21
|
-
|
|
22
|
-
if (params) {
|
|
23
|
-
const cleanParams = Object.fromEntries(
|
|
24
|
-
Object.entries(params).filter(([_, value]) => value !== undefined),
|
|
25
|
-
);
|
|
26
|
-
url.search = new URLSearchParams(cleanParams).toString();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const queryType = method.toLowerCase() === "get" ? "query" : "mutation";
|
|
30
|
-
const headers = {
|
|
31
|
-
...getAuthHeaders(await Promise.resolve(authToken)),
|
|
32
|
-
...(elevatedAccess && queryType === "mutation"
|
|
33
|
-
? { "x-elevated-access-token": elevatedAccess.token }
|
|
34
|
-
: {}),
|
|
35
|
-
"WorkOS-Widgets-Type": widgetType,
|
|
36
|
-
"Content-Type": "application/json",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const response = await fetch(url, {
|
|
40
|
-
method,
|
|
41
|
-
cache: "no-store",
|
|
42
|
-
headers,
|
|
43
|
-
...(data ? { body: JSON.stringify(data) } : {}),
|
|
44
|
-
}).catch((error) => {
|
|
45
|
-
throw new FetchError({
|
|
46
|
-
message: `Failed to ${method} ${url.toString()}`,
|
|
47
|
-
queryType,
|
|
48
|
-
context: { error },
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
const { message, status } = await parseErrorResponse(response);
|
|
54
|
-
|
|
55
|
-
throw new ApiError({
|
|
56
|
-
message,
|
|
57
|
-
status,
|
|
58
|
-
queryType: method.toLowerCase() === "get" ? "query" : "mutation",
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return response.json();
|
|
63
|
-
};
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export type ErrorType<ErrorData> = ErrorData;
|
|
67
|
-
|
|
68
|
-
export type BodyType<BodyData> = BodyData & { headers?: any };
|
|
69
|
-
|
|
70
|
-
export const useWidgetsApiQueryOptions = <
|
|
71
|
-
QueryOptions extends { queryKey: QK },
|
|
72
|
-
QK extends QueryKey,
|
|
73
|
-
>(
|
|
74
|
-
queryOptions: QueryOptions,
|
|
75
|
-
) => {
|
|
76
|
-
const { authTokenQueryKey, baseUrl } = useApi();
|
|
77
|
-
const queryKey = [
|
|
78
|
-
// initial query key needs to go first to allow invalidation of the query name to work
|
|
79
|
-
...queryOptions.queryKey,
|
|
80
|
-
{ authTokenQueryKey, baseUrl },
|
|
81
|
-
] as unknown as QK;
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
...queryOptions,
|
|
85
|
-
queryKey: queryKey as QK,
|
|
86
|
-
};
|
|
87
|
-
};
|
package/src/card-list.tsx
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Box, Card, CardProps, Inset } from "@radix-ui/themes";
|
|
2
|
-
|
|
3
|
-
export function Root({ children, ...props }: CardProps) {
|
|
4
|
-
return (
|
|
5
|
-
<Card size="2" {...props} className="woswidgets-card-list">
|
|
6
|
-
<Inset side="all" clip="padding-box">
|
|
7
|
-
{children}
|
|
8
|
-
</Inset>
|
|
9
|
-
</Card>
|
|
10
|
-
);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function Item({ children }: { children: React.ReactNode }) {
|
|
14
|
-
return (
|
|
15
|
-
<Inset
|
|
16
|
-
side="x"
|
|
17
|
-
px="current"
|
|
18
|
-
className="woswidgets-card-list-item"
|
|
19
|
-
clip="padding-box"
|
|
20
|
-
>
|
|
21
|
-
<Box px="4" py="4">
|
|
22
|
-
{children}
|
|
23
|
-
</Box>
|
|
24
|
-
</Inset>
|
|
25
|
-
);
|
|
26
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
WorkOsWidgets,
|
|
3
|
-
type WorkOsWidgetsProps,
|
|
4
|
-
} from "./workos-widgets.client";
|
|
5
|
-
export { OrganizationSwitcher } from "./organization-switcher.client";
|
|
6
|
-
export { UserProfile } from "./user-profile.client";
|
|
7
|
-
export { UserSecurity } from "./user-security.client";
|
|
8
|
-
export { UserSessions } from "./user-sessions.client";
|
|
9
|
-
export { UsersManagement } from "./users-management.client";
|
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Box,
|
|
5
|
-
Code,
|
|
6
|
-
Dialog,
|
|
7
|
-
Flex,
|
|
8
|
-
Grid,
|
|
9
|
-
Link,
|
|
10
|
-
Separator,
|
|
11
|
-
Text,
|
|
12
|
-
VisuallyHidden,
|
|
13
|
-
} from "@radix-ui/themes";
|
|
14
|
-
import * as React from "react";
|
|
15
|
-
import { type ReactNode } from "react";
|
|
16
|
-
import * as Otp from "./otp-input";
|
|
17
|
-
import { DialogContent, PrimaryButton, SecondaryButton } from "./elements";
|
|
18
|
-
import {
|
|
19
|
-
CreateTotpFactorResponse,
|
|
20
|
-
CreateTotpFactorResponseAuthenticationFactorAllOfTotpAnyOf,
|
|
21
|
-
useCreateTotpFactor,
|
|
22
|
-
useElevatedAccessToken,
|
|
23
|
-
useVerifyTotpFactor,
|
|
24
|
-
} from "../api";
|
|
25
|
-
import * as Form from "@radix-ui/react-form";
|
|
26
|
-
import { Marker } from "./marker";
|
|
27
|
-
import { CopyButton } from "./copy-button";
|
|
28
|
-
import { useSecuritySettings } from "./use-security-settings";
|
|
29
|
-
import { ElevatedAccess } from "./elevated-access";
|
|
30
|
-
import { SaveButton } from "./save-button";
|
|
31
|
-
import { useDialogClose } from "./use-dialog-close";
|
|
32
|
-
|
|
33
|
-
interface AddMfaDialogProps extends Dialog.RootProps {
|
|
34
|
-
children?: ReactNode;
|
|
35
|
-
onSuccess?: () => void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function AddMfaDialog({
|
|
39
|
-
children,
|
|
40
|
-
onSuccess,
|
|
41
|
-
...props
|
|
42
|
-
}: AddMfaDialogProps) {
|
|
43
|
-
const [open, setOpen] = React.useState(false);
|
|
44
|
-
const [manuallyTriggered, setManuallyTriggered] = React.useState(false);
|
|
45
|
-
const { elevatedAccess } = useElevatedAccessToken();
|
|
46
|
-
const createAuthFactor = useCreateTotpFactor();
|
|
47
|
-
|
|
48
|
-
const handleVerified = async () => {
|
|
49
|
-
await createAuthFactor.mutateAsync(undefined);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const onTriggerClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
53
|
-
if (elevatedAccess && !createAuthFactor.data) {
|
|
54
|
-
event.preventDefault();
|
|
55
|
-
setManuallyTriggered(true);
|
|
56
|
-
createAuthFactor.mutate(undefined, {
|
|
57
|
-
onSuccess: () => {
|
|
58
|
-
setOpen(true);
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
} else {
|
|
62
|
-
setManuallyTriggered(false);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const handleClose = React.useCallback(() => {
|
|
67
|
-
setOpen(false);
|
|
68
|
-
}, []);
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<Dialog.Root
|
|
72
|
-
{...props}
|
|
73
|
-
open={props.open || open}
|
|
74
|
-
onOpenChange={props.onOpenChange || setOpen}
|
|
75
|
-
>
|
|
76
|
-
<Dialog.Trigger
|
|
77
|
-
onClick={onTriggerClick}
|
|
78
|
-
// @ts-ignore I've to find a way to pass the loading state to the trigger
|
|
79
|
-
loading={manuallyTriggered && createAuthFactor.isPending}
|
|
80
|
-
>
|
|
81
|
-
{children}
|
|
82
|
-
</Dialog.Trigger>
|
|
83
|
-
|
|
84
|
-
<DialogContent maxWidth="480px">
|
|
85
|
-
<ElevatedAccess onVerified={handleVerified}>
|
|
86
|
-
<Content onClose={handleClose} totpFactor={createAuthFactor.data} />
|
|
87
|
-
</ElevatedAccess>
|
|
88
|
-
</DialogContent>
|
|
89
|
-
</Dialog.Root>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface ContentProps {
|
|
94
|
-
totpFactor?: CreateTotpFactorResponse;
|
|
95
|
-
onClose?: () => void;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function Content({ onClose, totpFactor }: ContentProps) {
|
|
99
|
-
const securitySettings = useSecuritySettings();
|
|
100
|
-
|
|
101
|
-
const [customError, setCustomError] = React.useState<string | null>(null);
|
|
102
|
-
const totp = hasQrCode(totpFactor)
|
|
103
|
-
? totpFactor.authenticationFactor.totp
|
|
104
|
-
: undefined;
|
|
105
|
-
|
|
106
|
-
const verifyTotp = useVerifyTotpFactor({
|
|
107
|
-
mutation: {
|
|
108
|
-
onSuccess: ({ success }) => {
|
|
109
|
-
if (!success) {
|
|
110
|
-
setCustomError("Invalid passcode");
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const serverError = verifyTotp.error || customError;
|
|
117
|
-
const isSuccess = verifyTotp.isSuccess && !serverError;
|
|
118
|
-
|
|
119
|
-
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
120
|
-
event.preventDefault();
|
|
121
|
-
setCustomError(null);
|
|
122
|
-
|
|
123
|
-
const formData = new FormData(event.currentTarget);
|
|
124
|
-
const otpCode = formData.get("otp-code")?.toString();
|
|
125
|
-
|
|
126
|
-
verifyTotp.mutate({
|
|
127
|
-
data: {
|
|
128
|
-
authenticationChallengeId: totpFactor?.authenticationChallenge.id ?? "",
|
|
129
|
-
code: otpCode ?? "",
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
useDialogClose(isSuccess, () => {
|
|
135
|
-
securitySettings.update("Mfa", true);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<>
|
|
140
|
-
<Dialog.Title mb="5">Set up an authenticator app</Dialog.Title>
|
|
141
|
-
|
|
142
|
-
<Form.Root onSubmit={handleSubmit}>
|
|
143
|
-
<Grid columns="auto 1fr" rows="repeat(4, auto)" gapX="3" gapY="1">
|
|
144
|
-
<Grid
|
|
145
|
-
rows="subgrid"
|
|
146
|
-
gridRow="span 4"
|
|
147
|
-
style={{ placeItems: "center" }}
|
|
148
|
-
>
|
|
149
|
-
<Marker>1</Marker>
|
|
150
|
-
<Separator
|
|
151
|
-
orientation="vertical"
|
|
152
|
-
size="4"
|
|
153
|
-
style={{ width: "2px" }}
|
|
154
|
-
/>
|
|
155
|
-
<Marker>2</Marker>
|
|
156
|
-
<div />
|
|
157
|
-
</Grid>
|
|
158
|
-
|
|
159
|
-
<Grid rows="subgrid" gridRow="span 4">
|
|
160
|
-
<Text as="p" size="3" weight="bold">
|
|
161
|
-
Scan the QR code
|
|
162
|
-
</Text>
|
|
163
|
-
<Flex direction="column" gap="1">
|
|
164
|
-
<Text as="p" size="2">
|
|
165
|
-
Use an authenticator app like{" "}
|
|
166
|
-
<Link
|
|
167
|
-
href="https://1password.com/"
|
|
168
|
-
rel="noopener noreferrer"
|
|
169
|
-
target="_blank"
|
|
170
|
-
>
|
|
171
|
-
1Password
|
|
172
|
-
</Link>
|
|
173
|
-
,{" "}
|
|
174
|
-
<Link
|
|
175
|
-
href="https://apps.apple.com/us/app/google-authenticator/id388497605"
|
|
176
|
-
rel="noopener noreferrer"
|
|
177
|
-
target="_blank"
|
|
178
|
-
>
|
|
179
|
-
Google Authenticator
|
|
180
|
-
</Link>
|
|
181
|
-
,{" "}
|
|
182
|
-
<Link
|
|
183
|
-
href="https://authy.com/"
|
|
184
|
-
rel="noopener noreferrer"
|
|
185
|
-
target="_blank"
|
|
186
|
-
>
|
|
187
|
-
Authy
|
|
188
|
-
</Link>
|
|
189
|
-
, or{" "}
|
|
190
|
-
<Link
|
|
191
|
-
href="https://www.microsoft.com/en-gb/security/mobile-authenticator-app"
|
|
192
|
-
rel="noopener noreferrer"
|
|
193
|
-
target="_blank"
|
|
194
|
-
>
|
|
195
|
-
Microsoft Authenticator
|
|
196
|
-
</Link>{" "}
|
|
197
|
-
to scan the QR code below.
|
|
198
|
-
</Text>
|
|
199
|
-
<Flex align="center" gap="5" my="5">
|
|
200
|
-
<Grid
|
|
201
|
-
position="relative"
|
|
202
|
-
width="160px"
|
|
203
|
-
height="160px"
|
|
204
|
-
p="2"
|
|
205
|
-
style={{
|
|
206
|
-
border: "1px solid var(--gray-7)",
|
|
207
|
-
borderRadius: "var(--radius-4)",
|
|
208
|
-
background: "var(--gray-2)",
|
|
209
|
-
overflow: "hidden",
|
|
210
|
-
}}
|
|
211
|
-
>
|
|
212
|
-
{totp?.qr_code && (
|
|
213
|
-
<Box asChild width="100%" height="auto">
|
|
214
|
-
<img
|
|
215
|
-
alt="Scan this QR code to enroll"
|
|
216
|
-
aria-describedby="secret-note"
|
|
217
|
-
height="160"
|
|
218
|
-
src={totp.qr_code}
|
|
219
|
-
style={{ userSelect: "none", background: "white" }}
|
|
220
|
-
width="160"
|
|
221
|
-
/>
|
|
222
|
-
</Box>
|
|
223
|
-
)}
|
|
224
|
-
</Grid>
|
|
225
|
-
|
|
226
|
-
<Flex direction="column" gap="1" align="start">
|
|
227
|
-
<Text color="gray" id="secret-note" size="2">
|
|
228
|
-
Can‘t scan the code?{" "}
|
|
229
|
-
</Text>
|
|
230
|
-
|
|
231
|
-
<SecretDialog setupKey={totp?.secret ?? ""}>
|
|
232
|
-
<SecondaryButton
|
|
233
|
-
size="1"
|
|
234
|
-
disabled={verifyTotp.isPending || isSuccess}
|
|
235
|
-
>
|
|
236
|
-
View setup key
|
|
237
|
-
</SecondaryButton>
|
|
238
|
-
</SecretDialog>
|
|
239
|
-
</Flex>
|
|
240
|
-
</Flex>
|
|
241
|
-
</Flex>
|
|
242
|
-
<Text as="p" size="3" weight="bold">
|
|
243
|
-
Get verification passcode
|
|
244
|
-
</Text>
|
|
245
|
-
<Flex direction="column" gap="4">
|
|
246
|
-
<Text as="p" size="2">
|
|
247
|
-
Enter the 6-digit passcode from your authenticator app.
|
|
248
|
-
</Text>
|
|
249
|
-
|
|
250
|
-
<Flex direction="column" gap="2">
|
|
251
|
-
<Otp.Root
|
|
252
|
-
autoSubmit
|
|
253
|
-
gap="3"
|
|
254
|
-
justify="start"
|
|
255
|
-
name="otp-code"
|
|
256
|
-
rows="48px"
|
|
257
|
-
columns="repeat(6, 48px)"
|
|
258
|
-
readOnly={verifyTotp.isPending || isSuccess}
|
|
259
|
-
>
|
|
260
|
-
<Otp.Input required autoFocus autoComplete="off" />
|
|
261
|
-
<Otp.Input required />
|
|
262
|
-
<Otp.Input required />
|
|
263
|
-
<Otp.Input required />
|
|
264
|
-
<Otp.Input required />
|
|
265
|
-
<Otp.Input required />
|
|
266
|
-
</Otp.Root>
|
|
267
|
-
|
|
268
|
-
{serverError && (
|
|
269
|
-
<Text size="2" color="red">
|
|
270
|
-
{getMutationErrorMessage(serverError)}
|
|
271
|
-
</Text>
|
|
272
|
-
)}
|
|
273
|
-
</Flex>
|
|
274
|
-
</Flex>
|
|
275
|
-
</Grid>
|
|
276
|
-
</Grid>
|
|
277
|
-
|
|
278
|
-
<Flex mt="5" gap="3" justify="end">
|
|
279
|
-
<Dialog.Close>
|
|
280
|
-
<SecondaryButton disabled={verifyTotp.isPending || isSuccess}>
|
|
281
|
-
Cancel
|
|
282
|
-
</SecondaryButton>
|
|
283
|
-
</Dialog.Close>
|
|
284
|
-
|
|
285
|
-
<SaveButton
|
|
286
|
-
asChild
|
|
287
|
-
loading={verifyTotp.isPending}
|
|
288
|
-
done={isSuccess}
|
|
289
|
-
onDone={onClose}
|
|
290
|
-
>
|
|
291
|
-
<PrimaryButton type="submit">Confirm</PrimaryButton>
|
|
292
|
-
</SaveButton>
|
|
293
|
-
</Flex>
|
|
294
|
-
</Form.Root>
|
|
295
|
-
|
|
296
|
-
{/* mirror errors in a live region */}
|
|
297
|
-
<VisuallyHidden asChild>
|
|
298
|
-
<section aria-live="polite">
|
|
299
|
-
{getMutationErrorMessage(serverError)}
|
|
300
|
-
</section>
|
|
301
|
-
</VisuallyHidden>
|
|
302
|
-
</>
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
interface SecretDialogProps extends React.PropsWithChildren {
|
|
307
|
-
setupKey: string;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function SecretDialog({ children, setupKey }: SecretDialogProps) {
|
|
311
|
-
return (
|
|
312
|
-
<Dialog.Root>
|
|
313
|
-
<Dialog.Trigger>{children}</Dialog.Trigger>
|
|
314
|
-
<Dialog.Content
|
|
315
|
-
maxWidth="90vw"
|
|
316
|
-
size={{ initial: "3", sm: "4" }}
|
|
317
|
-
minWidth="300px"
|
|
318
|
-
width="fit-content"
|
|
319
|
-
>
|
|
320
|
-
<Dialog.Title size="2" weight="regular">
|
|
321
|
-
Your setup key
|
|
322
|
-
</Dialog.Title>
|
|
323
|
-
<Dialog.Description>
|
|
324
|
-
<Text size="6">
|
|
325
|
-
<Code variant="ghost" style={{ letterSpacing: "0.15rem" }}>
|
|
326
|
-
{setupKey}
|
|
327
|
-
</Code>
|
|
328
|
-
</Text>
|
|
329
|
-
</Dialog.Description>
|
|
330
|
-
|
|
331
|
-
<Flex align="center" gap="3" justify="end" mt="5">
|
|
332
|
-
<Dialog.Close>
|
|
333
|
-
<CopyButton asChild value={setupKey}>
|
|
334
|
-
<PrimaryButton>Copy and close</PrimaryButton>
|
|
335
|
-
</CopyButton>
|
|
336
|
-
</Dialog.Close>
|
|
337
|
-
</Flex>
|
|
338
|
-
</Dialog.Content>
|
|
339
|
-
</Dialog.Root>
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function getMutationErrorMessage(error: unknown) {
|
|
344
|
-
let message = "Something went wrong, please try again";
|
|
345
|
-
|
|
346
|
-
if (typeof error === "string") {
|
|
347
|
-
message = error;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (error instanceof Error) {
|
|
351
|
-
message = error.message;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
typeof error === "object" &&
|
|
356
|
-
error !== null &&
|
|
357
|
-
"message" in error &&
|
|
358
|
-
typeof error.message === "string"
|
|
359
|
-
) {
|
|
360
|
-
message = error.message;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (message === "Invalid passcode") {
|
|
364
|
-
message = "Invalid passcode, please try again";
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return message;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function hasQrCode(
|
|
371
|
-
response?: CreateTotpFactorResponse,
|
|
372
|
-
): response is CreateTotpFactorResponse & {
|
|
373
|
-
authenticationFactor: {
|
|
374
|
-
totp: NonNullable<CreateTotpFactorResponseAuthenticationFactorAllOfTotpAnyOf>;
|
|
375
|
-
};
|
|
376
|
-
} {
|
|
377
|
-
const totp = response?.authenticationFactor.totp;
|
|
378
|
-
return totp != null && "qr_code" in totp;
|
|
379
|
-
}
|
package/src/lib/api/config.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_API_HOSTNAME = "api.workos.com";
|
|
2
|
-
export const API_ENDPOINTS = {
|
|
3
|
-
USERS: "_widgets/UserManagement/members",
|
|
4
|
-
USER_ROLES: "_widgets/UserManagement/roles",
|
|
5
|
-
USER_INVITE: "_widgets/UserManagement/invite-user",
|
|
6
|
-
USER_INVITES: "_widgets/UserManagement/invites",
|
|
7
|
-
USER_INVITES_RESEND: (userId: string) =>
|
|
8
|
-
`_widgets/UserManagement/invites/${userId}/resend`,
|
|
9
|
-
};
|
package/src/lib/api/user.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getMembersQueryKey,
|
|
3
|
-
useInviteMember,
|
|
4
|
-
useMembers,
|
|
5
|
-
useRemoveMember,
|
|
6
|
-
useResendInvite,
|
|
7
|
-
useRevokeInvite,
|
|
8
|
-
useUpdateMember,
|
|
9
|
-
} from "../../api";
|
|
10
|
-
import { keepPreviousData, useQueryClient } from "@tanstack/react-query";
|
|
11
|
-
import { USER_ROW_LIMIT } from "../constants";
|
|
12
|
-
import {
|
|
13
|
-
useUsersManagementContext,
|
|
14
|
-
type UsersManagementContextType,
|
|
15
|
-
} from "../users-management-context";
|
|
16
|
-
|
|
17
|
-
export const useUsers = (context?: UsersManagementContextType) => {
|
|
18
|
-
const {
|
|
19
|
-
state: { pagination, role, searchQuery },
|
|
20
|
-
} = useUsersManagementContext(context);
|
|
21
|
-
const limit = USER_ROW_LIMIT;
|
|
22
|
-
const { before, after } = pagination || {};
|
|
23
|
-
|
|
24
|
-
return useMembers(
|
|
25
|
-
{
|
|
26
|
-
after: after ?? undefined,
|
|
27
|
-
before: before ?? undefined,
|
|
28
|
-
limit: limit.toString(),
|
|
29
|
-
role: role ?? undefined,
|
|
30
|
-
search: searchQuery ?? undefined,
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
query: {
|
|
34
|
-
placeholderData: keepPreviousData,
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const useUpdateUserRole: typeof useUpdateMember = () => {
|
|
41
|
-
const queryClient = useQueryClient();
|
|
42
|
-
|
|
43
|
-
return useUpdateMember({
|
|
44
|
-
mutation: {
|
|
45
|
-
onSuccess: () => {
|
|
46
|
-
queryClient.invalidateQueries({ queryKey: getMembersQueryKey() });
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const useInviteUser: typeof useInviteMember = () => {
|
|
53
|
-
const queryClient = useQueryClient();
|
|
54
|
-
|
|
55
|
-
return useInviteMember({
|
|
56
|
-
mutation: {
|
|
57
|
-
onSuccess: () => {
|
|
58
|
-
queryClient.invalidateQueries({ queryKey: getMembersQueryKey() });
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export const useDeleteUser = () => {
|
|
65
|
-
const queryClient = useQueryClient();
|
|
66
|
-
|
|
67
|
-
return useRemoveMember({
|
|
68
|
-
mutation: {
|
|
69
|
-
onSuccess: () => {
|
|
70
|
-
queryClient.invalidateQueries({ queryKey: getMembersQueryKey() });
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export const useRevokeUserInvite = () => {
|
|
77
|
-
const queryClient = useQueryClient();
|
|
78
|
-
|
|
79
|
-
return useRevokeInvite({
|
|
80
|
-
mutation: {
|
|
81
|
-
onSuccess: () => {
|
|
82
|
-
queryClient.invalidateQueries({ queryKey: getMembersQueryKey() });
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const useResendUserInvite = () => {
|
|
89
|
-
const queryClient = useQueryClient();
|
|
90
|
-
|
|
91
|
-
return useResendInvite({
|
|
92
|
-
mutation: {
|
|
93
|
-
onSuccess: () => {
|
|
94
|
-
queryClient.invalidateQueries({ queryKey: getMembersQueryKey() });
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
};
|