@nocobase/portal-template-default 1.0.12 → 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.
- package/package.json +1 -1
- package/scripts/build-html.mjs +14 -0
- package/src/extensions/nocobase-auth-cas/README.md +6 -0
- package/src/extensions/nocobase-auth-cas/cas-sign-in-button.tsx +41 -0
- package/src/extensions/nocobase-auth-cas/demo.tsx +20 -0
- package/src/extensions/nocobase-auth-cas/extension.tsx +48 -0
- package/src/extensions/nocobase-auth-cas/index.ts +1 -0
- package/src/extensions/nocobase-auth-cas/use-cas-sign-in.ts +31 -0
- package/src/extensions/nocobase-auth-dingtalk/README.md +7 -0
- package/src/extensions/nocobase-auth-dingtalk/auto-login-provider.tsx +185 -0
- package/src/extensions/nocobase-auth-dingtalk/demo.tsx +23 -0
- package/src/extensions/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +45 -0
- package/src/extensions/nocobase-auth-dingtalk/extension.tsx +53 -0
- package/src/extensions/nocobase-auth-dingtalk/index.ts +4 -0
- package/src/extensions/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +66 -0
- package/src/extensions/nocobase-auth-ldap/README.md +6 -0
- package/src/extensions/nocobase-auth-ldap/demo.tsx +21 -0
- package/src/extensions/nocobase-auth-ldap/extension.tsx +48 -0
- package/src/extensions/nocobase-auth-ldap/index.ts +1 -0
- package/src/extensions/nocobase-auth-ldap/ldap-sign-in-form.tsx +69 -0
- package/src/extensions/nocobase-auth-ldap/use-ldap-sign-in.ts +5 -0
- package/src/extensions/nocobase-auth-oidc/README.md +11 -0
- package/src/extensions/nocobase-auth-oidc/auto-redirect-provider.tsx +13 -0
- package/src/extensions/nocobase-auth-oidc/demo.tsx +20 -0
- package/src/extensions/nocobase-auth-oidc/extension.tsx +51 -0
- package/src/extensions/nocobase-auth-oidc/index.ts +1 -0
- package/src/extensions/nocobase-auth-oidc/oidc-sign-in-button.tsx +44 -0
- package/src/extensions/nocobase-auth-oidc/use-oidc-sign-in.ts +60 -0
- package/src/extensions/nocobase-auth-saml/README.md +6 -0
- package/src/extensions/nocobase-auth-saml/auto-redirect-provider.tsx +13 -0
- package/src/extensions/nocobase-auth-saml/demo.tsx +20 -0
- package/src/extensions/nocobase-auth-saml/extension.tsx +51 -0
- package/src/extensions/nocobase-auth-saml/index.ts +1 -0
- package/src/extensions/nocobase-auth-saml/saml-sign-in-button.tsx +43 -0
- package/src/extensions/nocobase-auth-saml/use-saml-sign-in.ts +53 -0
- package/src/extensions/nocobase-auth-sms/README.md +7 -0
- package/src/extensions/nocobase-auth-sms/demo.tsx +25 -0
- package/src/extensions/nocobase-auth-sms/extension.tsx +48 -0
- package/src/extensions/nocobase-auth-sms/index.ts +1 -0
- package/src/extensions/nocobase-auth-sms/sms-sign-in-form.tsx +107 -0
- package/src/extensions/nocobase-auth-sms/use-sms-sign-in.ts +88 -0
- package/src/extensions/nocobase-auth-wecom/README.md +6 -0
- package/src/extensions/nocobase-auth-wecom/auto-login-provider.tsx +21 -0
- package/src/extensions/nocobase-auth-wecom/demo.tsx +23 -0
- package/src/extensions/nocobase-auth-wecom/extension.tsx +51 -0
- package/src/extensions/nocobase-auth-wecom/index.ts +1 -0
- package/src/extensions/nocobase-auth-wecom/use-wecom-sign-in.ts +64 -0
- package/src/extensions/nocobase-auth-wecom/wecom-sign-in-button.tsx +70 -0
- package/src/extensions/nocobase-i18n/README.md +44 -0
- package/src/extensions/nocobase-i18n/components/index.ts +1 -0
- package/src/extensions/nocobase-i18n/components/language-switcher.tsx +138 -0
- package/src/extensions/nocobase-i18n/demo/index.tsx +209 -0
- package/src/extensions/nocobase-i18n/demo/prompt-generator.tsx +132 -0
- package/src/extensions/nocobase-i18n/extension.tsx +48 -0
- package/src/extensions/nocobase-i18n/index.ts +7 -0
- package/src/extensions/nocobase-i18n/locales/en-US.ts +39 -0
- package/src/extensions/nocobase-i18n/locales/index.ts +8 -0
- package/src/extensions/nocobase-i18n/locales/zh-CN.ts +35 -0
- package/src/extensions/nocobase-i18n/provider.tsx +44 -0
- package/src/extensions/nocobase-i18n/server-resources.ts +166 -0
- package/src/extensions/nocobase-i18n/tests/i18n-regression.mjs +51 -0
package/package.json
CHANGED
package/scripts/build-html.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { loadEnv } from "vite";
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const rootDir = path.resolve(__dirname, "..");
|
|
@@ -11,6 +12,17 @@ const indexPath = path.join(distDir, "index.html");
|
|
|
11
12
|
const startMarker = "<!-- nocobase-runtime-config:start -->";
|
|
12
13
|
const endMarker = "<!-- nocobase-runtime-config:end -->";
|
|
13
14
|
|
|
15
|
+
const loadBuildHtmlEnv = () => {
|
|
16
|
+
const mode = process.env.MODE || "production";
|
|
17
|
+
const env = loadEnv(mode, rootDir, "");
|
|
18
|
+
|
|
19
|
+
for (const [key, value] of Object.entries(env)) {
|
|
20
|
+
if (process.env[key] === undefined) {
|
|
21
|
+
process.env[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
14
26
|
const normalizePortalBase = (base) => {
|
|
15
27
|
const normalized = String(base || "/").trim();
|
|
16
28
|
if (!normalized || normalized === "/") return "/";
|
|
@@ -63,6 +75,8 @@ const stripExistingRuntimeConfig = (html) => {
|
|
|
63
75
|
return html.replace(pattern, "");
|
|
64
76
|
};
|
|
65
77
|
|
|
78
|
+
loadBuildHtmlEnv();
|
|
79
|
+
|
|
66
80
|
const portalBase = normalizePortalBase(process.env.NOCOBASE_PORTAL_BASE);
|
|
67
81
|
const apiUrl = String(process.env.NOCOBASE_API_URL || "/api").trim() || "/api";
|
|
68
82
|
const storagePrefix =
|
|
@@ -0,0 +1,41 @@
|
|
|
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 { useCasSignIn } from "./use-cas-sign-in";
|
|
13
|
+
|
|
14
|
+
export default function CasSignInButton({
|
|
15
|
+
authenticator,
|
|
16
|
+
onSignIn,
|
|
17
|
+
}: AuthenticatorComponentProps & { onSignIn?: () => void }) {
|
|
18
|
+
const { signIn, error } = useCasSignIn(authenticator);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-3">
|
|
22
|
+
{error && (
|
|
23
|
+
<Alert variant="destructive">
|
|
24
|
+
<AlertTitle>CAS 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
|
+
onClick={onSignIn ?? signIn}
|
|
33
|
+
>
|
|
34
|
+
<LogIn />
|
|
35
|
+
{resolveTranslatableText(
|
|
36
|
+
authenticator.title || authenticator.authTypeTitle || "CAS"
|
|
37
|
+
)}
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AuthMethodDemo } from "@/components/auth/demo";
|
|
2
|
+
import CasSignInButton from "./cas-sign-in-button";
|
|
3
|
+
|
|
4
|
+
const authenticator = {
|
|
5
|
+
name: "company-cas",
|
|
6
|
+
authType: "CAS",
|
|
7
|
+
title: "Company CAS",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function CasAuthDemoPage() {
|
|
11
|
+
return (
|
|
12
|
+
<AuthMethodDemo
|
|
13
|
+
authType="CAS"
|
|
14
|
+
methodName="Central Authentication Service"
|
|
15
|
+
description="Redirect users to a CAS server and capture the returned NocoBase token without coupling the page UI to CAS."
|
|
16
|
+
>
|
|
17
|
+
<CasSignInButton authenticator={authenticator} onSignIn={() => {}} />
|
|
18
|
+
</AuthMethodDemo>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
import { KeyRound } 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 CasSignInButton = lazy(() => import("./cas-sign-in-button"));
|
|
9
|
+
const CasAuthDemoPage = lazy(() =>
|
|
10
|
+
import("./demo").then((module) => ({ default: module.CasAuthDemoPage }))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const casAuthExtension: AppExtension = {
|
|
14
|
+
id: "nocobase-auth-cas",
|
|
15
|
+
dev: {
|
|
16
|
+
resources: [
|
|
17
|
+
{
|
|
18
|
+
name: "auth-cas-demo",
|
|
19
|
+
list: "auth/cas",
|
|
20
|
+
meta: {
|
|
21
|
+
parent: "auth-components",
|
|
22
|
+
label: "CAS",
|
|
23
|
+
icon: <KeyRound />,
|
|
24
|
+
acl: { type: "authenticated" },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
routes: (
|
|
29
|
+
<Route
|
|
30
|
+
path="auth/cas"
|
|
31
|
+
element={
|
|
32
|
+
<AuthDemoRoute>
|
|
33
|
+
<CasAuthDemoPage />
|
|
34
|
+
</AuthDemoRoute>
|
|
35
|
+
}
|
|
36
|
+
/>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
authAdapters: [
|
|
40
|
+
{
|
|
41
|
+
authType: "CAS",
|
|
42
|
+
placement: "button",
|
|
43
|
+
Component: CasSignInButton,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default casAuthExtension;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useCasSignIn } from "./use-cas-sign-in";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useCallback, useMemo } 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 useCasSignIn(authenticator: Authenticator) {
|
|
9
|
+
const [searchParams] = useSearchParams();
|
|
10
|
+
const callbackError = useMemo(() => {
|
|
11
|
+
if (searchParams.get("authenticator") !== authenticator.name) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const message = searchParams.get("error");
|
|
15
|
+
return message ? new Error(message) : undefined;
|
|
16
|
+
}, [authenticator.name, searchParams]);
|
|
17
|
+
|
|
18
|
+
const signIn = useCallback(() => {
|
|
19
|
+
const redirect = resolvePortalUrl(
|
|
20
|
+
searchParams.get("to") || searchParams.get("redirect") || "/"
|
|
21
|
+
);
|
|
22
|
+
const url = nocobaseClient.buildUrl("cas:login", {
|
|
23
|
+
authenticator: authenticator.name,
|
|
24
|
+
__appName: nocobaseClient.getAppName(),
|
|
25
|
+
redirect,
|
|
26
|
+
});
|
|
27
|
+
window.location.replace(url.toString());
|
|
28
|
+
}, [authenticator.name, searchParams]);
|
|
29
|
+
|
|
30
|
+
return { signIn, error: callbackError };
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# NocoBase DingTalk authentication
|
|
2
|
+
|
|
3
|
+
Adds DingTalk OAuth sign-in, callback errors, OAuth or micro-app automatic
|
|
4
|
+
login, and `useDingtalkSignIn` for custom UI. The DingTalk JSAPI is loaded only
|
|
5
|
+
inside a DingTalk browser during internal automatic login.
|
|
6
|
+
|
|
7
|
+
Installing the Registry also adds a DingTalk component Demo under Authentication.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useLocation } from "react-router";
|
|
3
|
+
|
|
4
|
+
import { LoadingState } from "@/components/app-shell/loading-state";
|
|
5
|
+
import { clearAcl } from "@/lib/nocobase/acl";
|
|
6
|
+
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
7
|
+
import { resolvePortalUrl } from "@/providers/runtime-config";
|
|
8
|
+
|
|
9
|
+
import { isDingtalkBrowser } from "./use-dingtalk-sign-in";
|
|
10
|
+
|
|
11
|
+
type CheckLoginResponse = {
|
|
12
|
+
success?: boolean;
|
|
13
|
+
autoLoginType?: "oauth" | "internal";
|
|
14
|
+
authUrl?: string;
|
|
15
|
+
authenticator?: string;
|
|
16
|
+
corpId?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type SignInResponse = {
|
|
20
|
+
token?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type DingtalkAuthCodeResult = {
|
|
24
|
+
code?: string;
|
|
25
|
+
authCode?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type DingtalkAuthCodeOptions = {
|
|
29
|
+
corpId: string;
|
|
30
|
+
success: (result: DingtalkAuthCodeResult) => void;
|
|
31
|
+
fail: (error: { errorMessage?: string; message?: string }) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type DingtalkApi = {
|
|
35
|
+
ready?: (callback: () => void) => void;
|
|
36
|
+
getAuthCode?: (options: DingtalkAuthCodeOptions) => void;
|
|
37
|
+
runtime?: {
|
|
38
|
+
permission?: {
|
|
39
|
+
requestAuthCode?: (
|
|
40
|
+
options: DingtalkAuthCodeOptions
|
|
41
|
+
) => Promise<DingtalkAuthCodeResult> | void;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const publicAuthPaths = new Set([
|
|
47
|
+
"/login",
|
|
48
|
+
"/signin",
|
|
49
|
+
"/register",
|
|
50
|
+
"/forgot-password",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
async function requestDingtalkAuthCode(corpId: string) {
|
|
54
|
+
const module = await import("dingtalk-jsapi");
|
|
55
|
+
const dd = ((module as { default?: unknown }).default ??
|
|
56
|
+
module) as DingtalkApi;
|
|
57
|
+
|
|
58
|
+
return new Promise<string>((resolve, reject) => {
|
|
59
|
+
const success = (result: DingtalkAuthCodeResult) => {
|
|
60
|
+
const code = result?.code ?? result?.authCode;
|
|
61
|
+
if (code) {
|
|
62
|
+
resolve(String(code));
|
|
63
|
+
} else {
|
|
64
|
+
reject(new Error("DingTalk did not return an authorization code."));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const fail = (error: { errorMessage?: string; message?: string }) =>
|
|
68
|
+
reject(
|
|
69
|
+
new Error(
|
|
70
|
+
error?.errorMessage ||
|
|
71
|
+
error?.message ||
|
|
72
|
+
"Unable to request a DingTalk authorization code."
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
const run = () => {
|
|
76
|
+
const requestAuthCode = dd?.runtime?.permission?.requestAuthCode;
|
|
77
|
+
if (typeof requestAuthCode === "function") {
|
|
78
|
+
const result = requestAuthCode({ corpId, success, fail });
|
|
79
|
+
if (result?.then) result.then(success).catch(fail);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (typeof dd?.getAuthCode === "function") {
|
|
83
|
+
dd.getAuthCode({ corpId, success, fail });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
fail({ message: "DingTalk auth-code API is unavailable." });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
if (typeof dd.ready === "function") {
|
|
91
|
+
dd.ready(run);
|
|
92
|
+
} else {
|
|
93
|
+
run();
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
fail(error as Error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default function DingtalkAutoLoginProvider({
|
|
102
|
+
children,
|
|
103
|
+
}: React.PropsWithChildren) {
|
|
104
|
+
const { pathname, search } = useLocation();
|
|
105
|
+
const attemptedRef = useRef(false);
|
|
106
|
+
const insideDingtalk = isDingtalkBrowser();
|
|
107
|
+
const redirect = resolvePortalUrl(`${pathname}${search}`);
|
|
108
|
+
const shouldCheck =
|
|
109
|
+
insideDingtalk &&
|
|
110
|
+
!nocobaseClient.getToken() &&
|
|
111
|
+
!publicAuthPaths.has(pathname);
|
|
112
|
+
const [isChecking, setIsChecking] = useState(shouldCheck);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!shouldCheck || attemptedRef.current) {
|
|
116
|
+
setIsChecking(false);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
attemptedRef.current = true;
|
|
120
|
+
setIsChecking(true);
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
|
|
123
|
+
void (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const result = await nocobaseClient.action<CheckLoginResponse>(
|
|
126
|
+
"dingtalk",
|
|
127
|
+
"checkLogin",
|
|
128
|
+
{
|
|
129
|
+
method: "GET",
|
|
130
|
+
query: { isDingTalkBrowser: true, redirect },
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
authenticator: null,
|
|
133
|
+
includeRole: false,
|
|
134
|
+
withAclMeta: false,
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (result?.success !== false) return;
|
|
139
|
+
if (result.autoLoginType === "oauth" && result.authUrl) {
|
|
140
|
+
window.location.replace(nocobaseClient.resolveUrl(result.authUrl));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
result.autoLoginType !== "internal" ||
|
|
145
|
+
!result.authenticator ||
|
|
146
|
+
!result.corpId
|
|
147
|
+
) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const authCode = await requestDingtalkAuthCode(result.corpId);
|
|
152
|
+
const signedIn = await nocobaseClient.action<SignInResponse>(
|
|
153
|
+
"auth",
|
|
154
|
+
"signIn",
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
authenticator: result.authenticator,
|
|
158
|
+
body: { authCode, loginType: "internal" },
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
if (!signedIn.token) {
|
|
162
|
+
throw new Error("NocoBase did not return an access token.");
|
|
163
|
+
}
|
|
164
|
+
nocobaseClient.setAuthenticator(result.authenticator);
|
|
165
|
+
nocobaseClient.setToken(signedIn.token);
|
|
166
|
+
nocobaseClient.setRole(null);
|
|
167
|
+
clearAcl();
|
|
168
|
+
window.location.replace(redirect);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
|
171
|
+
console.warn("Unable to complete DingTalk automatic login", error);
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
setIsChecking(false);
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
|
|
178
|
+
return () => controller.abort();
|
|
179
|
+
}, [redirect, shouldCheck]);
|
|
180
|
+
|
|
181
|
+
if (isChecking) {
|
|
182
|
+
return <LoadingState className="min-h-svh" />;
|
|
183
|
+
}
|
|
184
|
+
return children;
|
|
185
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AuthMethodDemo } from "@/components/auth/demo";
|
|
2
|
+
import DingtalkSignInButton from "./dingtalk-sign-in-button";
|
|
3
|
+
|
|
4
|
+
const authenticator = {
|
|
5
|
+
name: "company-dingtalk",
|
|
6
|
+
authType: "dingtalk",
|
|
7
|
+
title: "Sign in via DingTalk",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function DingtalkAuthDemoPage() {
|
|
11
|
+
return (
|
|
12
|
+
<AuthMethodDemo
|
|
13
|
+
authType="dingtalk"
|
|
14
|
+
methodName="DingTalk"
|
|
15
|
+
description="Provide DingTalk OAuth login and support internal micro-app automatic login through DingTalk JSAPI."
|
|
16
|
+
>
|
|
17
|
+
<DingtalkSignInButton
|
|
18
|
+
authenticator={authenticator}
|
|
19
|
+
onSignIn={() => {}}
|
|
20
|
+
/>
|
|
21
|
+
</AuthMethodDemo>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MessageSquare } 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 { useDingtalkSignIn } from "./use-dingtalk-sign-in";
|
|
13
|
+
|
|
14
|
+
export default function DingtalkSignInButton({
|
|
15
|
+
authenticator,
|
|
16
|
+
onSignIn,
|
|
17
|
+
}: AuthenticatorComponentProps & { onSignIn?: () => void }) {
|
|
18
|
+
const { signIn, isPending, error } = useDingtalkSignIn(authenticator);
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-3">
|
|
21
|
+
{error && (
|
|
22
|
+
<Alert variant="destructive">
|
|
23
|
+
<AlertTitle>DingTalk 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
|
+
<MessageSquare />
|
|
35
|
+
{!onSignIn && isPending
|
|
36
|
+
? "Redirecting…"
|
|
37
|
+
: resolveTranslatableText(
|
|
38
|
+
authenticator.title ||
|
|
39
|
+
authenticator.authTypeTitle ||
|
|
40
|
+
"Sign in via DingTalk"
|
|
41
|
+
)}
|
|
42
|
+
</Button>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
import { MessageSquare } 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 DingtalkSignInButton = lazy(() => import("./dingtalk-sign-in-button"));
|
|
9
|
+
const DingtalkAutoLoginProvider = lazy(() => import("./auto-login-provider"));
|
|
10
|
+
const DingtalkAuthDemoPage = lazy(() =>
|
|
11
|
+
import("./demo").then((module) => ({
|
|
12
|
+
default: module.DingtalkAuthDemoPage,
|
|
13
|
+
}))
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const dingtalkAuthExtension: AppExtension = {
|
|
17
|
+
id: "nocobase-auth-dingtalk",
|
|
18
|
+
AuthRuntimeProvider: DingtalkAutoLoginProvider,
|
|
19
|
+
authRuntimePriority: 10,
|
|
20
|
+
dev: {
|
|
21
|
+
resources: [
|
|
22
|
+
{
|
|
23
|
+
name: "auth-dingtalk-demo",
|
|
24
|
+
list: "auth/dingtalk",
|
|
25
|
+
meta: {
|
|
26
|
+
parent: "auth-components",
|
|
27
|
+
label: "DingTalk",
|
|
28
|
+
icon: <MessageSquare />,
|
|
29
|
+
acl: { type: "authenticated" },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
routes: (
|
|
34
|
+
<Route
|
|
35
|
+
path="auth/dingtalk"
|
|
36
|
+
element={
|
|
37
|
+
<AuthDemoRoute>
|
|
38
|
+
<DingtalkAuthDemoPage />
|
|
39
|
+
</AuthDemoRoute>
|
|
40
|
+
}
|
|
41
|
+
/>
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
authAdapters: [
|
|
45
|
+
{
|
|
46
|
+
authType: "dingtalk",
|
|
47
|
+
placement: "button",
|
|
48
|
+
Component: DingtalkSignInButton,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default dingtalkAuthExtension;
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
type DingtalkAuthUrlResponse = {
|
|
9
|
+
url?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function isDingtalkBrowser() {
|
|
13
|
+
return (
|
|
14
|
+
typeof navigator !== "undefined" &&
|
|
15
|
+
navigator.userAgent.toLowerCase().includes("dingtalk")
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useDingtalkSignIn(authenticator: Authenticator) {
|
|
20
|
+
const [searchParams] = useSearchParams();
|
|
21
|
+
const [isPending, setIsPending] = useState(false);
|
|
22
|
+
const [error, setError] = useState<Error>();
|
|
23
|
+
const callbackError = useMemo(() => {
|
|
24
|
+
if (searchParams.get("authenticator") !== authenticator.name) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const message = searchParams.get("error");
|
|
28
|
+
return message ? new Error(message) : undefined;
|
|
29
|
+
}, [authenticator.name, searchParams]);
|
|
30
|
+
|
|
31
|
+
const signIn = useCallback(async () => {
|
|
32
|
+
setError(undefined);
|
|
33
|
+
setIsPending(true);
|
|
34
|
+
try {
|
|
35
|
+
const redirect = resolvePortalUrl(
|
|
36
|
+
searchParams.get("to") || searchParams.get("redirect") || "/"
|
|
37
|
+
);
|
|
38
|
+
const result = await nocobaseClient.action<DingtalkAuthUrlResponse>(
|
|
39
|
+
"dingtalk",
|
|
40
|
+
"getAuthUrl",
|
|
41
|
+
{
|
|
42
|
+
method: "POST",
|
|
43
|
+
authenticator: authenticator.name,
|
|
44
|
+
includeRole: false,
|
|
45
|
+
withAclMeta: false,
|
|
46
|
+
body: { redirect },
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
if (!result?.url) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"NocoBase did not return a DingTalk authorization URL."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
window.location.replace(nocobaseClient.resolveUrl(result.url));
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
setError(
|
|
57
|
+
cause instanceof Error
|
|
58
|
+
? cause
|
|
59
|
+
: new Error("Unable to start DingTalk sign-in.")
|
|
60
|
+
);
|
|
61
|
+
setIsPending(false);
|
|
62
|
+
}
|
|
63
|
+
}, [authenticator.name, searchParams]);
|
|
64
|
+
|
|
65
|
+
return { signIn, isPending, error: error ?? callbackError };
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AuthMethodDemo } from "@/components/auth/demo";
|
|
2
|
+
import LdapSignInForm from "./ldap-sign-in-form";
|
|
3
|
+
|
|
4
|
+
const authenticator = {
|
|
5
|
+
name: "company-ldap",
|
|
6
|
+
authType: "LDAP",
|
|
7
|
+
title: "Company directory",
|
|
8
|
+
options: { autoSignup: true },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function LdapAuthDemoPage() {
|
|
12
|
+
return (
|
|
13
|
+
<AuthMethodDemo
|
|
14
|
+
authType="LDAP"
|
|
15
|
+
methodName="LDAP directory"
|
|
16
|
+
description="Use the familiar account and password form while sending credentials through the selected LDAP authenticator."
|
|
17
|
+
>
|
|
18
|
+
<LdapSignInForm authenticator={authenticator} onSignIn={() => {}} />
|
|
19
|
+
</AuthMethodDemo>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { lazy } from "react";
|
|
2
|
+
import { ContactRound } 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 LdapSignInForm = lazy(() => import("./ldap-sign-in-form"));
|
|
9
|
+
const LdapAuthDemoPage = lazy(() =>
|
|
10
|
+
import("./demo").then((module) => ({ default: module.LdapAuthDemoPage }))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const ldapAuthExtension: AppExtension = {
|
|
14
|
+
id: "nocobase-auth-ldap",
|
|
15
|
+
dev: {
|
|
16
|
+
resources: [
|
|
17
|
+
{
|
|
18
|
+
name: "auth-ldap-demo",
|
|
19
|
+
list: "auth/ldap",
|
|
20
|
+
meta: {
|
|
21
|
+
parent: "auth-components",
|
|
22
|
+
label: "LDAP",
|
|
23
|
+
icon: <ContactRound />,
|
|
24
|
+
acl: { type: "authenticated" },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
routes: (
|
|
29
|
+
<Route
|
|
30
|
+
path="auth/ldap"
|
|
31
|
+
element={
|
|
32
|
+
<AuthDemoRoute>
|
|
33
|
+
<LdapAuthDemoPage />
|
|
34
|
+
</AuthDemoRoute>
|
|
35
|
+
}
|
|
36
|
+
/>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
authAdapters: [
|
|
40
|
+
{
|
|
41
|
+
authType: "LDAP",
|
|
42
|
+
placement: "form",
|
|
43
|
+
Component: LdapSignInForm,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default ldapAuthExtension;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useLdapSignIn } from "./use-ldap-sign-in";
|