@nocobase/portal-template-default 1.0.4 → 1.0.6
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/README.MD +77 -0
- package/package.json +23 -17
- package/registry/nocobase-acl/demo/prompt-generator.tsx +6 -39
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +6 -42
- package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +1 -1
- package/registry/nocobase-ai/demo/prompt-card.tsx +7 -28
- package/registry/nocobase-ai/demo/prompt-generator.tsx +9 -30
- package/registry/nocobase-ai/demo/shortcut.tsx +1 -1
- package/registry/nocobase-ai/demo/tool-cards.tsx +10 -30
- package/registry/nocobase-auth-cas/README.md +6 -0
- package/registry/nocobase-auth-cas/cas-sign-in-button.tsx +41 -0
- package/registry/nocobase-auth-cas/demo.tsx +20 -0
- package/registry/nocobase-auth-cas/extension.tsx +46 -0
- package/registry/nocobase-auth-cas/index.ts +1 -0
- package/registry/nocobase-auth-cas/use-cas-sign-in.ts +29 -0
- package/registry/nocobase-auth-dingtalk/README.md +7 -0
- package/registry/nocobase-auth-dingtalk/auto-login-provider.tsx +154 -0
- package/registry/nocobase-auth-dingtalk/demo.tsx +23 -0
- package/registry/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +45 -0
- package/registry/nocobase-auth-dingtalk/extension.tsx +55 -0
- package/registry/nocobase-auth-dingtalk/index.ts +4 -0
- package/registry/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +64 -0
- package/registry/nocobase-auth-ldap/README.md +6 -0
- package/registry/nocobase-auth-ldap/demo.tsx +21 -0
- package/registry/nocobase-auth-ldap/extension.tsx +46 -0
- package/registry/nocobase-auth-ldap/index.ts +1 -0
- package/registry/nocobase-auth-ldap/ldap-sign-in-form.tsx +69 -0
- package/registry/nocobase-auth-ldap/use-ldap-sign-in.ts +5 -0
- package/registry/nocobase-auth-oidc/README.md +11 -0
- package/registry/nocobase-auth-oidc/auto-redirect-provider.tsx +13 -0
- package/registry/nocobase-auth-oidc/demo.tsx +20 -0
- package/registry/nocobase-auth-oidc/extension.tsx +51 -0
- package/registry/nocobase-auth-oidc/index.ts +1 -0
- package/registry/nocobase-auth-oidc/oidc-sign-in-button.tsx +44 -0
- package/registry/nocobase-auth-oidc/use-oidc-sign-in.ts +58 -0
- package/registry/nocobase-auth-saml/README.md +6 -0
- package/registry/nocobase-auth-saml/auto-redirect-provider.tsx +13 -0
- package/registry/nocobase-auth-saml/demo.tsx +20 -0
- package/registry/nocobase-auth-saml/extension.tsx +51 -0
- package/registry/nocobase-auth-saml/index.ts +1 -0
- package/registry/nocobase-auth-saml/saml-sign-in-button.tsx +43 -0
- package/registry/nocobase-auth-saml/use-saml-sign-in.ts +51 -0
- package/registry/nocobase-auth-sms/README.md +7 -0
- package/registry/nocobase-auth-sms/demo.tsx +25 -0
- package/registry/nocobase-auth-sms/extension.tsx +46 -0
- package/registry/nocobase-auth-sms/index.ts +1 -0
- package/registry/nocobase-auth-sms/sms-sign-in-form.tsx +107 -0
- package/registry/nocobase-auth-sms/use-sms-sign-in.ts +88 -0
- package/registry/nocobase-auth-wecom/README.md +6 -0
- package/registry/nocobase-auth-wecom/auto-login-provider.tsx +21 -0
- package/registry/nocobase-auth-wecom/demo.tsx +23 -0
- package/registry/nocobase-auth-wecom/extension.tsx +49 -0
- package/registry/nocobase-auth-wecom/index.ts +1 -0
- package/registry/nocobase-auth-wecom/use-wecom-sign-in.ts +62 -0
- package/registry/nocobase-auth-wecom/wecom-sign-in-button.tsx +70 -0
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +30 -54
- package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +18 -21
- package/registry.config.json +112 -0
- package/scripts/build-html.mjs +106 -0
- package/src/App.tsx +34 -8
- package/src/app/extension.ts +4 -0
- package/src/app/extensions.tsx +24 -1
- package/src/components/auth/authenticator-adapters.ts +20 -0
- package/src/components/auth/auto-redirect-provider.tsx +84 -0
- package/src/components/auth/basic-sign-in-form.tsx +101 -0
- package/src/components/auth/default-sign-in-page.tsx +20 -0
- package/src/components/auth/demo/auth-demo-page.tsx +236 -0
- package/src/components/auth/demo/auth-demo-prompt-generator.tsx +93 -0
- package/src/components/auth/demo/auth-demo-route.tsx +11 -0
- package/src/components/auth/demo/auth-method-demo.tsx +48 -0
- package/src/components/auth/demo/index.ts +4 -0
- package/src/components/auth/dynamic-sign-in.tsx +191 -0
- package/src/components/auth/forgot-password-form.tsx +13 -5
- package/src/components/auth/index.ts +13 -0
- package/src/components/auth/sign-in-form.tsx +1 -111
- package/src/components/auth/sign-up-form.tsx +14 -37
- package/src/components/auth/types.ts +38 -0
- package/src/components/auth/use-authenticator-sign-in.ts +14 -0
- package/src/components/auth/use-public-authenticators.ts +26 -0
- package/src/components/data-table/data-table-filter.tsx +107 -153
- package/src/components/demo/prompt-output.tsx +71 -0
- package/src/components/ui/alert-dialog.tsx +2 -0
- package/src/components/ui/attachment.tsx +207 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +87 -0
- package/src/components/ui/button.tsx +6 -6
- package/src/components/ui/calendar.tsx +0 -2
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/carousel.tsx +2 -0
- package/src/components/ui/combobox.tsx +297 -0
- package/src/components/ui/command.tsx +43 -41
- package/src/components/ui/dialog.tsx +0 -2
- package/src/components/ui/direction.tsx +4 -0
- package/src/components/ui/drawer.tsx +2 -0
- package/src/components/ui/dropdown-menu.tsx +0 -2
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/hover-card.tsx +2 -0
- package/src/components/ui/input-group.tsx +0 -2
- package/src/components/ui/input-otp.tsx +0 -2
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/item.tsx +201 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +2 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +92 -0
- package/src/components/ui/native-select.tsx +61 -0
- package/src/components/ui/popover.tsx +0 -2
- package/src/components/ui/progress.tsx +2 -0
- package/src/components/ui/radio-group.tsx +0 -2
- package/src/components/ui/resizable.tsx +2 -0
- package/src/components/ui/scroll-area.tsx +0 -2
- package/src/components/ui/select.tsx +6 -4
- package/src/components/ui/separator.tsx +0 -2
- package/src/components/ui/sheet.tsx +21 -22
- package/src/components/ui/sidebar.tsx +1 -3
- package/src/components/ui/sonner.tsx +2 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +0 -2
- package/src/components/ui/table.tsx +4 -2
- package/src/components/ui/tabs.tsx +0 -2
- package/src/components/ui/textarea.tsx +1 -1
- package/src/components/ui/toggle-group.tsx +2 -0
- package/src/components/ui/toggle.tsx +2 -0
- package/src/components/ui/tooltip.tsx +0 -2
- package/src/index.tsx +3 -0
- package/src/lib/nocobase/client.ts +32 -2
- package/src/lib/utils.ts +2 -1
- package/src/locales/en-US.ts +4 -2
- package/src/locales/zh-CN.ts +4 -2
- package/src/pages/login/index.tsx +2 -2
- package/src/pages/users/list.tsx +144 -16
- package/src/providers/auth-callback.ts +21 -0
- package/src/providers/auth.ts +108 -22
- package/src/providers/constants.ts +4 -1
- package/src/providers/runtime-config.ts +19 -0
- package/src/vite-env.d.ts +5 -0
- package/vite.config.ts +55 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LogIn } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
|
4
|
+
import {
|
|
5
|
+
Alert,
|
|
6
|
+
AlertDescription,
|
|
7
|
+
AlertTitle,
|
|
8
|
+
} from "@/components/ui/alert";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { resolveTranslatableText } from "@/lib/i18n";
|
|
11
|
+
|
|
12
|
+
import { useOidcSignIn } from "./use-oidc-sign-in";
|
|
13
|
+
|
|
14
|
+
export default function OidcSignInButton({
|
|
15
|
+
authenticator,
|
|
16
|
+
onSignIn,
|
|
17
|
+
}: AuthenticatorComponentProps & { onSignIn?: () => void }) {
|
|
18
|
+
const { signIn, isPending, error } = useOidcSignIn(authenticator);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-3">
|
|
22
|
+
{error && (
|
|
23
|
+
<Alert variant="destructive">
|
|
24
|
+
<AlertTitle>OIDC sign-in failed</AlertTitle>
|
|
25
|
+
<AlertDescription>{error.message}</AlertDescription>
|
|
26
|
+
</Alert>
|
|
27
|
+
)}
|
|
28
|
+
<Button
|
|
29
|
+
type="button"
|
|
30
|
+
variant="outline"
|
|
31
|
+
className="w-full"
|
|
32
|
+
disabled={!onSignIn && isPending}
|
|
33
|
+
onClick={onSignIn ?? signIn}
|
|
34
|
+
>
|
|
35
|
+
<LogIn />
|
|
36
|
+
{!onSignIn && isPending
|
|
37
|
+
? "Redirecting…"
|
|
38
|
+
: resolveTranslatableText(
|
|
39
|
+
authenticator.title || authenticator.authTypeTitle || "OIDC"
|
|
40
|
+
)}
|
|
41
|
+
</Button>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
|
+
|
|
4
|
+
import type { Authenticator } from "@/components/auth/types";
|
|
5
|
+
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
6
|
+
|
|
7
|
+
export function useOidcSignIn(authenticator: Authenticator) {
|
|
8
|
+
const [searchParams] = useSearchParams();
|
|
9
|
+
const [isPending, setIsPending] = useState(false);
|
|
10
|
+
const [error, setError] = useState<Error>();
|
|
11
|
+
const callbackError = useMemo(() => {
|
|
12
|
+
if (searchParams.get("authenticator") !== authenticator.name) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const message = searchParams.get("error");
|
|
16
|
+
return message ? new Error(message) : undefined;
|
|
17
|
+
}, [authenticator.name, searchParams]);
|
|
18
|
+
|
|
19
|
+
const signIn = useCallback(async () => {
|
|
20
|
+
setError(undefined);
|
|
21
|
+
setIsPending(true);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const redirect =
|
|
25
|
+
searchParams.get("to") || searchParams.get("redirect") || "/";
|
|
26
|
+
const authUrl = await nocobaseClient.action<string>(
|
|
27
|
+
"oidc",
|
|
28
|
+
"getAuthUrl",
|
|
29
|
+
{
|
|
30
|
+
method: "POST",
|
|
31
|
+
authenticator: authenticator.name,
|
|
32
|
+
includeRole: false,
|
|
33
|
+
withAclMeta: false,
|
|
34
|
+
body: { redirect },
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!authUrl) {
|
|
39
|
+
throw new Error("NocoBase did not return an OIDC authorization URL.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
window.location.replace(nocobaseClient.resolveUrl(authUrl));
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
setError(
|
|
45
|
+
cause instanceof Error
|
|
46
|
+
? cause
|
|
47
|
+
: new Error("Unable to start OIDC sign-in.")
|
|
48
|
+
);
|
|
49
|
+
setIsPending(false);
|
|
50
|
+
}
|
|
51
|
+
}, [authenticator.name, searchParams]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
signIn,
|
|
55
|
+
isPending,
|
|
56
|
+
error: error ?? callbackError,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
import { AuthAutoRedirectProvider } from "@/components/auth";
|
|
4
|
+
|
|
5
|
+
export default function SamlAutoRedirectProvider({
|
|
6
|
+
children,
|
|
7
|
+
}: PropsWithChildren) {
|
|
8
|
+
return (
|
|
9
|
+
<AuthAutoRedirectProvider resource="saml" action="checkRedirect">
|
|
10
|
+
{children}
|
|
11
|
+
</AuthAutoRedirectProvider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AuthMethodDemo } from "@/components/auth/demo";
|
|
2
|
+
import SamlSignInButton from "./saml-sign-in-button";
|
|
3
|
+
|
|
4
|
+
const authenticator = {
|
|
5
|
+
name: "company-saml",
|
|
6
|
+
authType: "SAML",
|
|
7
|
+
title: "Company SAML",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function SamlAuthDemoPage() {
|
|
11
|
+
return (
|
|
12
|
+
<AuthMethodDemo
|
|
13
|
+
authType="SAML"
|
|
14
|
+
methodName="SAML"
|
|
15
|
+
description="Start an identity-provider redirect and return through the Starter's shared authentication callback."
|
|
16
|
+
>
|
|
17
|
+
<SamlSignInButton authenticator={authenticator} onSignIn={() => {}} />
|
|
18
|
+
</AuthMethodDemo>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
import { BadgeCheck } from "lucide-react";
|
|
3
|
+
import { Route } from "react-router";
|
|
4
|
+
|
|
5
|
+
import type { AppExtension } from "@/app/extension";
|
|
6
|
+
import { AuthDemoRoute } from "@/components/auth/demo";
|
|
7
|
+
|
|
8
|
+
const SamlSignInButton = lazy(() => import("./saml-sign-in-button"));
|
|
9
|
+
const SamlAutoRedirectProvider = lazy(
|
|
10
|
+
() => import("./auto-redirect-provider")
|
|
11
|
+
);
|
|
12
|
+
const SamlAuthDemoPage = lazy(() =>
|
|
13
|
+
import("./demo").then((module) => ({ default: module.SamlAuthDemoPage }))
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const samlAuthExtension: AppExtension = {
|
|
17
|
+
id: "nocobase-auth-saml",
|
|
18
|
+
AuthRuntimeProvider: SamlAutoRedirectProvider,
|
|
19
|
+
authRuntimePriority: 20,
|
|
20
|
+
resources: [
|
|
21
|
+
{
|
|
22
|
+
name: "auth-saml-demo",
|
|
23
|
+
list: "/auth/saml",
|
|
24
|
+
meta: {
|
|
25
|
+
parent: "auth-components",
|
|
26
|
+
label: "SAML",
|
|
27
|
+
icon: <BadgeCheck />,
|
|
28
|
+
acl: { type: "authenticated" },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
routes: (
|
|
33
|
+
<Route
|
|
34
|
+
path="/auth/saml"
|
|
35
|
+
element={
|
|
36
|
+
<AuthDemoRoute>
|
|
37
|
+
<SamlAuthDemoPage />
|
|
38
|
+
</AuthDemoRoute>
|
|
39
|
+
}
|
|
40
|
+
/>
|
|
41
|
+
),
|
|
42
|
+
authAdapters: [
|
|
43
|
+
{
|
|
44
|
+
authType: "SAML",
|
|
45
|
+
placement: "button",
|
|
46
|
+
Component: SamlSignInButton,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default samlAuthExtension;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useSamlSignIn } from "./use-saml-sign-in";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LogIn } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
|
4
|
+
import {
|
|
5
|
+
Alert,
|
|
6
|
+
AlertDescription,
|
|
7
|
+
AlertTitle,
|
|
8
|
+
} from "@/components/ui/alert";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { resolveTranslatableText } from "@/lib/i18n";
|
|
11
|
+
|
|
12
|
+
import { useSamlSignIn } from "./use-saml-sign-in";
|
|
13
|
+
|
|
14
|
+
export default function SamlSignInButton({
|
|
15
|
+
authenticator,
|
|
16
|
+
onSignIn,
|
|
17
|
+
}: AuthenticatorComponentProps & { onSignIn?: () => void }) {
|
|
18
|
+
const { signIn, isPending, error } = useSamlSignIn(authenticator);
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-3">
|
|
21
|
+
{error && (
|
|
22
|
+
<Alert variant="destructive">
|
|
23
|
+
<AlertTitle>SAML sign-in failed</AlertTitle>
|
|
24
|
+
<AlertDescription>{error.message}</AlertDescription>
|
|
25
|
+
</Alert>
|
|
26
|
+
)}
|
|
27
|
+
<Button
|
|
28
|
+
type="button"
|
|
29
|
+
variant="outline"
|
|
30
|
+
className="w-full"
|
|
31
|
+
disabled={!onSignIn && isPending}
|
|
32
|
+
onClick={onSignIn ?? signIn}
|
|
33
|
+
>
|
|
34
|
+
<LogIn />
|
|
35
|
+
{!onSignIn && isPending
|
|
36
|
+
? "Redirecting…"
|
|
37
|
+
: resolveTranslatableText(
|
|
38
|
+
authenticator.title || authenticator.authTypeTitle || "SAML"
|
|
39
|
+
)}
|
|
40
|
+
</Button>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
|
+
|
|
4
|
+
import type { Authenticator } from "@/components/auth/types";
|
|
5
|
+
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
6
|
+
|
|
7
|
+
export function useSamlSignIn(authenticator: Authenticator) {
|
|
8
|
+
const [searchParams] = useSearchParams();
|
|
9
|
+
const [isPending, setIsPending] = useState(false);
|
|
10
|
+
const [error, setError] = useState<Error>();
|
|
11
|
+
const callbackError = useMemo(() => {
|
|
12
|
+
if (searchParams.get("authenticator") !== authenticator.name) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const message = searchParams.get("error");
|
|
16
|
+
return message ? new Error(message) : undefined;
|
|
17
|
+
}, [authenticator.name, searchParams]);
|
|
18
|
+
|
|
19
|
+
const signIn = useCallback(async () => {
|
|
20
|
+
setError(undefined);
|
|
21
|
+
setIsPending(true);
|
|
22
|
+
try {
|
|
23
|
+
const redirect =
|
|
24
|
+
searchParams.get("to") || searchParams.get("redirect") || "/";
|
|
25
|
+
const authUrl = await nocobaseClient.action<string>(
|
|
26
|
+
"saml",
|
|
27
|
+
"getAuthUrl",
|
|
28
|
+
{
|
|
29
|
+
method: "POST",
|
|
30
|
+
authenticator: authenticator.name,
|
|
31
|
+
includeRole: false,
|
|
32
|
+
withAclMeta: false,
|
|
33
|
+
body: { redirect },
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
if (!authUrl) {
|
|
37
|
+
throw new Error("NocoBase did not return a SAML authorization URL.");
|
|
38
|
+
}
|
|
39
|
+
window.location.replace(nocobaseClient.resolveUrl(authUrl));
|
|
40
|
+
} catch (cause) {
|
|
41
|
+
setError(
|
|
42
|
+
cause instanceof Error
|
|
43
|
+
? cause
|
|
44
|
+
: new Error("Unable to start SAML sign-in.")
|
|
45
|
+
);
|
|
46
|
+
setIsPending(false);
|
|
47
|
+
}
|
|
48
|
+
}, [authenticator.name, searchParams]);
|
|
49
|
+
|
|
50
|
+
return { signIn, isPending, error: error ?? callbackError };
|
|
51
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# NocoBase SMS authentication
|
|
2
|
+
|
|
3
|
+
Adds phone and verification-code sign-in to the dynamic login page. The
|
|
4
|
+
`useSmsSignIn` hook exposes code delivery, countdown state, and sign-in for
|
|
5
|
+
custom UI.
|
|
6
|
+
|
|
7
|
+
Installing the Registry also adds an SMS component Demo under Authentication.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AuthMethodDemo } from "@/components/auth/demo";
|
|
2
|
+
import SmsSignInForm from "./sms-sign-in-form";
|
|
3
|
+
|
|
4
|
+
const authenticator = {
|
|
5
|
+
name: "company-sms",
|
|
6
|
+
authType: "SMS",
|
|
7
|
+
title: "SMS verification code",
|
|
8
|
+
options: { autoSignup: true, verifier: "login-sms" },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function SmsAuthDemoPage() {
|
|
12
|
+
return (
|
|
13
|
+
<AuthMethodDemo
|
|
14
|
+
authType="SMS"
|
|
15
|
+
methodName="SMS verification code"
|
|
16
|
+
description="Collect a phone number and one-time code while keeping delivery and sign-in bound to the configured verifier."
|
|
17
|
+
>
|
|
18
|
+
<SmsSignInForm
|
|
19
|
+
authenticator={authenticator}
|
|
20
|
+
onSendCode={() => {}}
|
|
21
|
+
onSignIn={() => {}}
|
|
22
|
+
/>
|
|
23
|
+
</AuthMethodDemo>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
import { Smartphone } from "lucide-react";
|
|
3
|
+
import { Route } from "react-router";
|
|
4
|
+
|
|
5
|
+
import type { AppExtension } from "@/app/extension";
|
|
6
|
+
import { AuthDemoRoute } from "@/components/auth/demo";
|
|
7
|
+
|
|
8
|
+
const SmsSignInForm = lazy(() => import("./sms-sign-in-form"));
|
|
9
|
+
const SmsAuthDemoPage = lazy(() =>
|
|
10
|
+
import("./demo").then((module) => ({ default: module.SmsAuthDemoPage }))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const smsAuthExtension: AppExtension = {
|
|
14
|
+
id: "nocobase-auth-sms",
|
|
15
|
+
resources: [
|
|
16
|
+
{
|
|
17
|
+
name: "auth-sms-demo",
|
|
18
|
+
list: "/auth/sms",
|
|
19
|
+
meta: {
|
|
20
|
+
parent: "auth-components",
|
|
21
|
+
label: "SMS",
|
|
22
|
+
icon: <Smartphone />,
|
|
23
|
+
acl: { type: "authenticated" },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
routes: (
|
|
28
|
+
<Route
|
|
29
|
+
path="/auth/sms"
|
|
30
|
+
element={
|
|
31
|
+
<AuthDemoRoute>
|
|
32
|
+
<SmsAuthDemoPage />
|
|
33
|
+
</AuthDemoRoute>
|
|
34
|
+
}
|
|
35
|
+
/>
|
|
36
|
+
),
|
|
37
|
+
authAdapters: [
|
|
38
|
+
{
|
|
39
|
+
authType: "SMS",
|
|
40
|
+
placement: "form",
|
|
41
|
+
Component: SmsSignInForm,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default smsAuthExtension;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useSmsSignIn } from "./use-sms-sign-in";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
|
4
|
+
import {
|
|
5
|
+
Alert,
|
|
6
|
+
AlertDescription,
|
|
7
|
+
AlertTitle,
|
|
8
|
+
} from "@/components/ui/alert";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Input } from "@/components/ui/input";
|
|
11
|
+
import { Label } from "@/components/ui/label";
|
|
12
|
+
|
|
13
|
+
import { useSmsSignIn } from "./use-sms-sign-in";
|
|
14
|
+
|
|
15
|
+
export default function SmsSignInForm({
|
|
16
|
+
authenticator,
|
|
17
|
+
onSendCode,
|
|
18
|
+
onSignIn,
|
|
19
|
+
}: AuthenticatorComponentProps & {
|
|
20
|
+
onSendCode?: (phone: string) => void;
|
|
21
|
+
onSignIn?: (values: { phone: string; code: string }) => void;
|
|
22
|
+
}) {
|
|
23
|
+
const [phone, setPhone] = useState("");
|
|
24
|
+
const [code, setCode] = useState("");
|
|
25
|
+
const sms = useSmsSignIn(authenticator);
|
|
26
|
+
const autoSignup = authenticator.options?.autoSignup === true;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<form
|
|
30
|
+
className="space-y-5"
|
|
31
|
+
onSubmit={(event) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
if (onSignIn) {
|
|
34
|
+
onSignIn({ phone, code });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
void sms.signIn(phone, code);
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{sms.error && (
|
|
41
|
+
<Alert variant="destructive">
|
|
42
|
+
<AlertTitle>SMS sign-in failed</AlertTitle>
|
|
43
|
+
<AlertDescription>{sms.error.message}</AlertDescription>
|
|
44
|
+
</Alert>
|
|
45
|
+
)}
|
|
46
|
+
<div className="space-y-2">
|
|
47
|
+
<Label htmlFor={`${authenticator.name}-phone`}>Phone</Label>
|
|
48
|
+
<Input
|
|
49
|
+
id={`${authenticator.name}-phone`}
|
|
50
|
+
type="tel"
|
|
51
|
+
value={phone}
|
|
52
|
+
onChange={(event) => setPhone(event.target.value)}
|
|
53
|
+
autoComplete="tel"
|
|
54
|
+
required
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="space-y-2">
|
|
58
|
+
<Label htmlFor={`${authenticator.name}-code`}>
|
|
59
|
+
Verification code
|
|
60
|
+
</Label>
|
|
61
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
|
62
|
+
<Input
|
|
63
|
+
id={`${authenticator.name}-code`}
|
|
64
|
+
inputMode="numeric"
|
|
65
|
+
value={code}
|
|
66
|
+
onChange={(event) => setCode(event.target.value)}
|
|
67
|
+
autoComplete="one-time-code"
|
|
68
|
+
required
|
|
69
|
+
/>
|
|
70
|
+
<Button
|
|
71
|
+
type="button"
|
|
72
|
+
variant="outline"
|
|
73
|
+
disabled={
|
|
74
|
+
(!onSendCode && (sms.isSendingCode || sms.retryAfter > 0)) ||
|
|
75
|
+
!phone
|
|
76
|
+
}
|
|
77
|
+
onClick={() => {
|
|
78
|
+
if (onSendCode) {
|
|
79
|
+
onSendCode(phone);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
void sms.sendCode(phone).catch(() => undefined);
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{!onSendCode && sms.isSendingCode
|
|
86
|
+
? "Sending…"
|
|
87
|
+
: !onSendCode && sms.retryAfter > 0
|
|
88
|
+
? `Retry in ${sms.retryAfter}s`
|
|
89
|
+
: "Send code"}
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<Button
|
|
94
|
+
type="submit"
|
|
95
|
+
className="w-full"
|
|
96
|
+
disabled={!onSignIn && sms.isSigningIn}
|
|
97
|
+
>
|
|
98
|
+
{!onSignIn && sms.isSigningIn ? "Signing in…" : "Sign in"}
|
|
99
|
+
</Button>
|
|
100
|
+
{autoSignup && (
|
|
101
|
+
<p className="text-xs leading-5 text-muted-foreground">
|
|
102
|
+
An account will be created automatically if this phone number is new.
|
|
103
|
+
</p>
|
|
104
|
+
)}
|
|
105
|
+
</form>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { useAuthenticatorSignIn } from "@/components/auth";
|
|
4
|
+
import type { Authenticator } from "@/components/auth/types";
|
|
5
|
+
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
6
|
+
|
|
7
|
+
type SmsCodeResponse = {
|
|
8
|
+
expiresAt?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function useSmsSignIn(authenticator: Authenticator) {
|
|
12
|
+
const auth = useAuthenticatorSignIn(authenticator.name);
|
|
13
|
+
const [isSendingCode, setIsSendingCode] = useState(false);
|
|
14
|
+
const [sendError, setSendError] = useState<Error>();
|
|
15
|
+
const [expiresAt, setExpiresAt] = useState<number>();
|
|
16
|
+
const [now, setNow] = useState(Date.now());
|
|
17
|
+
const verifier =
|
|
18
|
+
typeof authenticator.options?.verifier === "string"
|
|
19
|
+
? authenticator.options.verifier
|
|
20
|
+
: "";
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!expiresAt || expiresAt <= Date.now()) return;
|
|
24
|
+
const timer = window.setInterval(() => setNow(Date.now()), 1_000);
|
|
25
|
+
return () => window.clearInterval(timer);
|
|
26
|
+
}, [expiresAt]);
|
|
27
|
+
|
|
28
|
+
const retryAfter = useMemo(
|
|
29
|
+
() =>
|
|
30
|
+
expiresAt ? Math.max(0, Math.ceil((expiresAt - now) / 1_000)) : 0,
|
|
31
|
+
[expiresAt, now]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const sendCode = useCallback(
|
|
35
|
+
async (phone: string) => {
|
|
36
|
+
if (!phone) throw new Error("Enter a phone number first.");
|
|
37
|
+
if (!verifier) {
|
|
38
|
+
throw new Error("The SMS verifier is not configured.");
|
|
39
|
+
}
|
|
40
|
+
if (retryAfter > 0) return;
|
|
41
|
+
|
|
42
|
+
setSendError(undefined);
|
|
43
|
+
setIsSendingCode(true);
|
|
44
|
+
try {
|
|
45
|
+
const result = await nocobaseClient.action<SmsCodeResponse>(
|
|
46
|
+
"smsOTP",
|
|
47
|
+
"publicCreate",
|
|
48
|
+
{
|
|
49
|
+
method: "POST",
|
|
50
|
+
authenticator: null,
|
|
51
|
+
includeRole: false,
|
|
52
|
+
withAclMeta: false,
|
|
53
|
+
body: {
|
|
54
|
+
action: "auth:signIn",
|
|
55
|
+
verifier,
|
|
56
|
+
uuid: phone,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const nextExpiry = result?.expiresAt
|
|
61
|
+
? Date.parse(result.expiresAt)
|
|
62
|
+
: Date.now() + 60_000;
|
|
63
|
+
setNow(Date.now());
|
|
64
|
+
setExpiresAt(nextExpiry);
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
const error =
|
|
67
|
+
cause instanceof Error
|
|
68
|
+
? cause
|
|
69
|
+
: new Error("Unable to send the verification code.");
|
|
70
|
+
setSendError(error);
|
|
71
|
+
throw error;
|
|
72
|
+
} finally {
|
|
73
|
+
setIsSendingCode(false);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[retryAfter, verifier]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
sendCode,
|
|
81
|
+
signIn: (phone: string, code: string) =>
|
|
82
|
+
auth.signIn({ uuid: phone, code }),
|
|
83
|
+
isSendingCode,
|
|
84
|
+
isSigningIn: auth.isPending,
|
|
85
|
+
retryAfter,
|
|
86
|
+
error: sendError ?? auth.error,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
import { AuthAutoRedirectProvider } from "@/components/auth";
|
|
4
|
+
|
|
5
|
+
import { isWecomBrowser } from "./use-wecom-sign-in";
|
|
6
|
+
|
|
7
|
+
export default function WecomAutoLoginProvider({
|
|
8
|
+
children,
|
|
9
|
+
}: PropsWithChildren) {
|
|
10
|
+
const insideWecom = isWecomBrowser();
|
|
11
|
+
return (
|
|
12
|
+
<AuthAutoRedirectProvider
|
|
13
|
+
resource="wecom"
|
|
14
|
+
action="checkLogin"
|
|
15
|
+
enabled={insideWecom}
|
|
16
|
+
query={{ isWeComBrowser: insideWecom }}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</AuthAutoRedirectProvider>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AuthMethodDemo } from "@/components/auth/demo";
|
|
2
|
+
import WecomSignInButton from "./wecom-sign-in-button";
|
|
3
|
+
|
|
4
|
+
const authenticator = {
|
|
5
|
+
name: "company-wecom",
|
|
6
|
+
authType: "wecom",
|
|
7
|
+
title: "Sign in via WeCom",
|
|
8
|
+
options: {
|
|
9
|
+
btnTooltip: "Use the account associated with your company directory.",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function WecomAuthDemoPage() {
|
|
14
|
+
return (
|
|
15
|
+
<AuthMethodDemo
|
|
16
|
+
authType="wecom"
|
|
17
|
+
methodName="WeCom"
|
|
18
|
+
description="Provide a WeCom sign-in button and support automatic login when the application is opened inside WeCom."
|
|
19
|
+
>
|
|
20
|
+
<WecomSignInButton authenticator={authenticator} onSignIn={() => {}} />
|
|
21
|
+
</AuthMethodDemo>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
import { Building2 } from "lucide-react";
|
|
3
|
+
import { Route } from "react-router";
|
|
4
|
+
|
|
5
|
+
import type { AppExtension } from "@/app/extension";
|
|
6
|
+
import { AuthDemoRoute } from "@/components/auth/demo";
|
|
7
|
+
|
|
8
|
+
const WecomSignInButton = lazy(() => import("./wecom-sign-in-button"));
|
|
9
|
+
const WecomAutoLoginProvider = lazy(() => import("./auto-login-provider"));
|
|
10
|
+
const WecomAuthDemoPage = lazy(() =>
|
|
11
|
+
import("./demo").then((module) => ({ default: module.WecomAuthDemoPage }))
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const wecomAuthExtension: AppExtension = {
|
|
15
|
+
id: "nocobase-auth-wecom",
|
|
16
|
+
AuthRuntimeProvider: WecomAutoLoginProvider,
|
|
17
|
+
authRuntimePriority: 10,
|
|
18
|
+
resources: [
|
|
19
|
+
{
|
|
20
|
+
name: "auth-wecom-demo",
|
|
21
|
+
list: "/auth/wecom",
|
|
22
|
+
meta: {
|
|
23
|
+
parent: "auth-components",
|
|
24
|
+
label: "WeCom",
|
|
25
|
+
icon: <Building2 />,
|
|
26
|
+
acl: { type: "authenticated" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
routes: (
|
|
31
|
+
<Route
|
|
32
|
+
path="/auth/wecom"
|
|
33
|
+
element={
|
|
34
|
+
<AuthDemoRoute>
|
|
35
|
+
<WecomAuthDemoPage />
|
|
36
|
+
</AuthDemoRoute>
|
|
37
|
+
}
|
|
38
|
+
/>
|
|
39
|
+
),
|
|
40
|
+
authAdapters: [
|
|
41
|
+
{
|
|
42
|
+
authType: "wecom",
|
|
43
|
+
placement: "button",
|
|
44
|
+
Component: WecomSignInButton,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default wecomAuthExtension;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isWecomBrowser, useWecomSignIn } from "./use-wecom-sign-in";
|