@nocobase/portal-template-default 1.0.13 → 1.0.14

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.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/src/extensions/nocobase-auth-cas/README.md +6 -0
  3. package/src/extensions/nocobase-auth-cas/cas-sign-in-button.tsx +41 -0
  4. package/src/extensions/nocobase-auth-cas/demo.tsx +20 -0
  5. package/src/extensions/nocobase-auth-cas/extension.tsx +48 -0
  6. package/src/extensions/nocobase-auth-cas/index.ts +1 -0
  7. package/src/extensions/nocobase-auth-cas/use-cas-sign-in.ts +31 -0
  8. package/src/extensions/nocobase-auth-dingtalk/README.md +7 -0
  9. package/src/extensions/nocobase-auth-dingtalk/auto-login-provider.tsx +185 -0
  10. package/src/extensions/nocobase-auth-dingtalk/demo.tsx +23 -0
  11. package/src/extensions/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +45 -0
  12. package/src/extensions/nocobase-auth-dingtalk/extension.tsx +53 -0
  13. package/src/extensions/nocobase-auth-dingtalk/index.ts +4 -0
  14. package/src/extensions/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +66 -0
  15. package/src/extensions/nocobase-auth-ldap/README.md +6 -0
  16. package/src/extensions/nocobase-auth-ldap/demo.tsx +21 -0
  17. package/src/extensions/nocobase-auth-ldap/extension.tsx +48 -0
  18. package/src/extensions/nocobase-auth-ldap/index.ts +1 -0
  19. package/src/extensions/nocobase-auth-ldap/ldap-sign-in-form.tsx +69 -0
  20. package/src/extensions/nocobase-auth-ldap/use-ldap-sign-in.ts +5 -0
  21. package/src/extensions/nocobase-auth-oidc/README.md +11 -0
  22. package/src/extensions/nocobase-auth-oidc/auto-redirect-provider.tsx +13 -0
  23. package/src/extensions/nocobase-auth-oidc/demo.tsx +20 -0
  24. package/src/extensions/nocobase-auth-oidc/extension.tsx +51 -0
  25. package/src/extensions/nocobase-auth-oidc/index.ts +1 -0
  26. package/src/extensions/nocobase-auth-oidc/oidc-sign-in-button.tsx +44 -0
  27. package/src/extensions/nocobase-auth-oidc/use-oidc-sign-in.ts +60 -0
  28. package/src/extensions/nocobase-auth-saml/README.md +6 -0
  29. package/src/extensions/nocobase-auth-saml/auto-redirect-provider.tsx +13 -0
  30. package/src/extensions/nocobase-auth-saml/demo.tsx +20 -0
  31. package/src/extensions/nocobase-auth-saml/extension.tsx +51 -0
  32. package/src/extensions/nocobase-auth-saml/index.ts +1 -0
  33. package/src/extensions/nocobase-auth-saml/saml-sign-in-button.tsx +43 -0
  34. package/src/extensions/nocobase-auth-saml/use-saml-sign-in.ts +53 -0
  35. package/src/extensions/nocobase-auth-sms/README.md +7 -0
  36. package/src/extensions/nocobase-auth-sms/demo.tsx +25 -0
  37. package/src/extensions/nocobase-auth-sms/extension.tsx +48 -0
  38. package/src/extensions/nocobase-auth-sms/index.ts +1 -0
  39. package/src/extensions/nocobase-auth-sms/sms-sign-in-form.tsx +107 -0
  40. package/src/extensions/nocobase-auth-sms/use-sms-sign-in.ts +88 -0
  41. package/src/extensions/nocobase-auth-wecom/README.md +6 -0
  42. package/src/extensions/nocobase-auth-wecom/auto-login-provider.tsx +21 -0
  43. package/src/extensions/nocobase-auth-wecom/demo.tsx +23 -0
  44. package/src/extensions/nocobase-auth-wecom/extension.tsx +51 -0
  45. package/src/extensions/nocobase-auth-wecom/index.ts +1 -0
  46. package/src/extensions/nocobase-auth-wecom/use-wecom-sign-in.ts +64 -0
  47. package/src/extensions/nocobase-auth-wecom/wecom-sign-in-button.tsx +70 -0
  48. package/src/extensions/nocobase-i18n/README.md +44 -0
  49. package/src/extensions/nocobase-i18n/components/index.ts +1 -0
  50. package/src/extensions/nocobase-i18n/components/language-switcher.tsx +138 -0
  51. package/src/extensions/nocobase-i18n/demo/index.tsx +209 -0
  52. package/src/extensions/nocobase-i18n/demo/prompt-generator.tsx +132 -0
  53. package/src/extensions/nocobase-i18n/extension.tsx +48 -0
  54. package/src/extensions/nocobase-i18n/index.ts +7 -0
  55. package/src/extensions/nocobase-i18n/locales/en-US.ts +39 -0
  56. package/src/extensions/nocobase-i18n/locales/index.ts +8 -0
  57. package/src/extensions/nocobase-i18n/locales/zh-CN.ts +35 -0
  58. package/src/extensions/nocobase-i18n/provider.tsx +44 -0
  59. package/src/extensions/nocobase-i18n/server-resources.ts +166 -0
  60. package/src/extensions/nocobase-i18n/tests/i18n-regression.mjs +51 -0
@@ -0,0 +1,69 @@
1
+ import { useState } from "react";
2
+
3
+ import { InputPassword } from "@/components/auth/input-password";
4
+ import type { AuthenticatorComponentProps } from "@/components/auth/types";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+
9
+ import { useLdapSignIn } from "./use-ldap-sign-in";
10
+
11
+ export default function LdapSignInForm({
12
+ authenticator,
13
+ onSignIn,
14
+ }: AuthenticatorComponentProps & {
15
+ onSignIn?: (values: { account: string; password: string }) => void;
16
+ }) {
17
+ const [account, setAccount] = useState("");
18
+ const [password, setPassword] = useState("");
19
+ const { signIn, isPending } = useLdapSignIn(authenticator.name);
20
+ const autoSignup = authenticator.options?.autoSignup === true;
21
+
22
+ return (
23
+ <form
24
+ className="space-y-5"
25
+ onSubmit={(event) => {
26
+ event.preventDefault();
27
+ if (onSignIn) {
28
+ onSignIn({ account, password });
29
+ return;
30
+ }
31
+ void signIn({ account, password });
32
+ }}
33
+ >
34
+ <div className="space-y-2">
35
+ <Label htmlFor={`${authenticator.name}-ldap-account`}>Account</Label>
36
+ <Input
37
+ id={`${authenticator.name}-ldap-account`}
38
+ value={account}
39
+ onChange={(event) => setAccount(event.target.value)}
40
+ autoComplete="username"
41
+ required
42
+ />
43
+ </div>
44
+ <div className="space-y-2">
45
+ <Label htmlFor={`${authenticator.name}-ldap-password`}>Password</Label>
46
+ <InputPassword
47
+ id={`${authenticator.name}-ldap-password`}
48
+ value={password}
49
+ onChange={(event) => setPassword(event.target.value)}
50
+ autoComplete="current-password"
51
+ required
52
+ />
53
+ </div>
54
+ <Button
55
+ type="submit"
56
+ className="w-full"
57
+ disabled={!onSignIn && isPending}
58
+ >
59
+ {!onSignIn && isPending ? "Signing in…" : "Sign in"}
60
+ </Button>
61
+ {autoSignup && (
62
+ <p className="text-xs leading-5 text-muted-foreground">
63
+ An account will be created automatically after the first successful
64
+ LDAP sign-in.
65
+ </p>
66
+ )}
67
+ </form>
68
+ );
69
+ }
@@ -0,0 +1,5 @@
1
+ import { useAuthenticatorSignIn } from "@/components/auth";
2
+
3
+ export function useLdapSignIn(authenticator: string) {
4
+ return useAuthenticatorSignIn(authenticator);
5
+ }
@@ -0,0 +1,11 @@
1
+ # NocoBase OIDC authentication
2
+
3
+ This Registry adds the default OIDC sign-in button to the Starter's dynamic
4
+ authentication page. It also exports `useOidcSignIn` so applications can keep
5
+ the NocoBase OIDC protocol while replacing the button UI.
6
+
7
+ Installing the Registry also adds an OIDC component Demo under Authentication.
8
+
9
+ The authenticator instance must be enabled in the connected NocoBase app. SSO
10
+ callbacks currently require the Portal and NocoBase public endpoints to share
11
+ an origin or use a compatible reverse proxy.
@@ -0,0 +1,13 @@
1
+ import type { PropsWithChildren } from "react";
2
+
3
+ import { AuthAutoRedirectProvider } from "@/components/auth";
4
+
5
+ export default function OidcAutoRedirectProvider({
6
+ children,
7
+ }: PropsWithChildren) {
8
+ return (
9
+ <AuthAutoRedirectProvider resource="oidc" action="checkRedirect">
10
+ {children}
11
+ </AuthAutoRedirectProvider>
12
+ );
13
+ }
@@ -0,0 +1,20 @@
1
+ import { AuthMethodDemo } from "@/components/auth/demo";
2
+ import OidcSignInButton from "./oidc-sign-in-button";
3
+
4
+ const authenticator = {
5
+ name: "company-oidc",
6
+ authType: "OIDC",
7
+ title: "Company OIDC",
8
+ };
9
+
10
+ export function OidcAuthDemoPage() {
11
+ return (
12
+ <AuthMethodDemo
13
+ authType="OIDC"
14
+ methodName="OpenID Connect"
15
+ description="Redirect users to an OpenID Connect provider while the Starter handles callback tokens and the active authenticator."
16
+ >
17
+ <OidcSignInButton authenticator={authenticator} onSignIn={() => {}} />
18
+ </AuthMethodDemo>
19
+ );
20
+ }
@@ -0,0 +1,51 @@
1
+ import { lazy } from "react";
2
+ import { LogIn } 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 OidcSignInButton = lazy(() => import("./oidc-sign-in-button"));
9
+ const OidcAutoRedirectProvider = lazy(() => import("./auto-redirect-provider"));
10
+ const OidcAuthDemoPage = lazy(() =>
11
+ import("./demo").then((module) => ({ default: module.OidcAuthDemoPage }))
12
+ );
13
+
14
+ const oidcAuthExtension: AppExtension = {
15
+ id: "nocobase-auth-oidc",
16
+ AuthRuntimeProvider: OidcAutoRedirectProvider,
17
+ authRuntimePriority: 20,
18
+ dev: {
19
+ resources: [
20
+ {
21
+ name: "auth-oidc-demo",
22
+ list: "auth/oidc",
23
+ meta: {
24
+ parent: "auth-components",
25
+ label: "OIDC",
26
+ icon: <LogIn />,
27
+ acl: { type: "authenticated" },
28
+ },
29
+ },
30
+ ],
31
+ routes: (
32
+ <Route
33
+ path="auth/oidc"
34
+ element={
35
+ <AuthDemoRoute>
36
+ <OidcAuthDemoPage />
37
+ </AuthDemoRoute>
38
+ }
39
+ />
40
+ ),
41
+ },
42
+ authAdapters: [
43
+ {
44
+ authType: "OIDC",
45
+ placement: "button",
46
+ Component: OidcSignInButton,
47
+ },
48
+ ],
49
+ };
50
+
51
+ export default oidcAuthExtension;
@@ -0,0 +1 @@
1
+ export { useOidcSignIn } from "./use-oidc-sign-in";
@@ -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,60 @@
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
+ import { resolvePortalUrl } from "@/providers/runtime-config";
7
+
8
+ export function useOidcSignIn(authenticator: Authenticator) {
9
+ const [searchParams] = useSearchParams();
10
+ const [isPending, setIsPending] = useState(false);
11
+ const [error, setError] = useState<Error>();
12
+ const callbackError = useMemo(() => {
13
+ if (searchParams.get("authenticator") !== authenticator.name) {
14
+ return undefined;
15
+ }
16
+ const message = searchParams.get("error");
17
+ return message ? new Error(message) : undefined;
18
+ }, [authenticator.name, searchParams]);
19
+
20
+ const signIn = useCallback(async () => {
21
+ setError(undefined);
22
+ setIsPending(true);
23
+
24
+ try {
25
+ const redirect = resolvePortalUrl(
26
+ searchParams.get("to") || searchParams.get("redirect") || "/"
27
+ );
28
+ const authUrl = await nocobaseClient.action<string>(
29
+ "oidc",
30
+ "getAuthUrl",
31
+ {
32
+ method: "POST",
33
+ authenticator: authenticator.name,
34
+ includeRole: false,
35
+ withAclMeta: false,
36
+ body: { redirect },
37
+ }
38
+ );
39
+
40
+ if (!authUrl) {
41
+ throw new Error("NocoBase did not return an OIDC authorization URL.");
42
+ }
43
+
44
+ window.location.replace(nocobaseClient.resolveUrl(authUrl));
45
+ } catch (cause) {
46
+ setError(
47
+ cause instanceof Error
48
+ ? cause
49
+ : new Error("Unable to start OIDC sign-in.")
50
+ );
51
+ setIsPending(false);
52
+ }
53
+ }, [authenticator.name, searchParams]);
54
+
55
+ return {
56
+ signIn,
57
+ isPending,
58
+ error: error ?? callbackError,
59
+ };
60
+ }
@@ -0,0 +1,6 @@
1
+ # NocoBase SAML authentication
2
+
3
+ Adds dynamic SAML sign-in, callback error handling, automatic redirect support,
4
+ and the `useSamlSignIn` hook for custom login UI.
5
+
6
+ Installing the Registry also adds a SAML component Demo under Authentication.
@@ -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(() => import("./auto-redirect-provider"));
10
+ const SamlAuthDemoPage = lazy(() =>
11
+ import("./demo").then((module) => ({ default: module.SamlAuthDemoPage }))
12
+ );
13
+
14
+ const samlAuthExtension: AppExtension = {
15
+ id: "nocobase-auth-saml",
16
+ AuthRuntimeProvider: SamlAutoRedirectProvider,
17
+ authRuntimePriority: 20,
18
+ dev: {
19
+ resources: [
20
+ {
21
+ name: "auth-saml-demo",
22
+ list: "auth/saml",
23
+ meta: {
24
+ parent: "auth-components",
25
+ label: "SAML",
26
+ icon: <BadgeCheck />,
27
+ acl: { type: "authenticated" },
28
+ },
29
+ },
30
+ ],
31
+ routes: (
32
+ <Route
33
+ path="auth/saml"
34
+ element={
35
+ <AuthDemoRoute>
36
+ <SamlAuthDemoPage />
37
+ </AuthDemoRoute>
38
+ }
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,53 @@
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
+ import { resolvePortalUrl } from "@/providers/runtime-config";
7
+
8
+ export function useSamlSignIn(authenticator: Authenticator) {
9
+ const [searchParams] = useSearchParams();
10
+ const [isPending, setIsPending] = useState(false);
11
+ const [error, setError] = useState<Error>();
12
+ const callbackError = useMemo(() => {
13
+ if (searchParams.get("authenticator") !== authenticator.name) {
14
+ return undefined;
15
+ }
16
+ const message = searchParams.get("error");
17
+ return message ? new Error(message) : undefined;
18
+ }, [authenticator.name, searchParams]);
19
+
20
+ const signIn = useCallback(async () => {
21
+ setError(undefined);
22
+ setIsPending(true);
23
+ try {
24
+ const redirect = resolvePortalUrl(
25
+ searchParams.get("to") || searchParams.get("redirect") || "/"
26
+ );
27
+ const authUrl = await nocobaseClient.action<string>(
28
+ "saml",
29
+ "getAuthUrl",
30
+ {
31
+ method: "POST",
32
+ authenticator: authenticator.name,
33
+ includeRole: false,
34
+ withAclMeta: false,
35
+ body: { redirect },
36
+ }
37
+ );
38
+ if (!authUrl) {
39
+ throw new Error("NocoBase did not return a SAML authorization URL.");
40
+ }
41
+ window.location.replace(nocobaseClient.resolveUrl(authUrl));
42
+ } catch (cause) {
43
+ setError(
44
+ cause instanceof Error
45
+ ? cause
46
+ : new Error("Unable to start SAML sign-in.")
47
+ );
48
+ setIsPending(false);
49
+ }
50
+ }, [authenticator.name, searchParams]);
51
+
52
+ return { signIn, isPending, error: error ?? callbackError };
53
+ }
@@ -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/auth-method-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,48 @@
1
+ import { lazy } from "react";
2
+ import { Smartphone } from "lucide-react";
3
+ import { Route } from "react-router";
4
+
5
+ import type { AppExtension } from "@/app/extensions";
6
+ import { AuthDemoRoute } from "@/components/auth/demo/auth-demo-route";
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
+ dev: {
16
+ resources: [
17
+ {
18
+ name: "auth-sms-demo",
19
+ list: "auth/sms",
20
+ meta: {
21
+ parent: "auth-components",
22
+ label: "SMS",
23
+ icon: <Smartphone />,
24
+ acl: { type: "authenticated" },
25
+ },
26
+ },
27
+ ],
28
+ routes: (
29
+ <Route
30
+ path="auth/sms"
31
+ element={
32
+ <AuthDemoRoute>
33
+ <SmsAuthDemoPage />
34
+ </AuthDemoRoute>
35
+ }
36
+ />
37
+ ),
38
+ },
39
+ authAdapters: [
40
+ {
41
+ authType: "SMS",
42
+ placement: "form",
43
+ Component: SmsSignInForm,
44
+ },
45
+ ],
46
+ };
47
+
48
+ 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
+ }