refactor(auth): move auth client from core to the plugin & refactor auth client api (#3215)
* refactor(auth): auth client api * fix: build * fix: dependencies * fix: fix T-2777 * fix: fix T-2776 * chore: update type * fix: build * fix: allowSignUp * fix: file name * fix: file name * refactor: client api * fix: build * chore: update name * fix: tsx must be loaded with --import instead of --loader * fix: type * fix: type * fix: type * fix: type * fix: bug * chore: improve wording * fix: test --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
e68053b006
commit
06f11a2d08
@ -1,12 +1,17 @@
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import { Model } from '@nocobase/database';
|
||||
import { Registry } from '@nocobase/utils';
|
||||
import { Auth, AuthExtend } from './auth';
|
||||
import { JwtOptions, JwtService } from './base/jwt-service';
|
||||
import { ITokenBlacklistService } from './base/token-blacklist-service';
|
||||
|
||||
export interface Authenticator {
|
||||
authType: string;
|
||||
options: Record<string, any>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Storer {
|
||||
get: (name: string) => Promise<Model>;
|
||||
get: (name: string) => Promise<Authenticator>;
|
||||
}
|
||||
|
||||
export type AuthManagerOptions = {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Context } from '@nocobase/actions';
|
||||
import { Model } from '@nocobase/database';
|
||||
import { Authenticator } from './auth-manager';
|
||||
|
||||
export type AuthConfig = {
|
||||
authenticator: Model;
|
||||
authenticator: Authenticator;
|
||||
options: {
|
||||
[key: string]: any;
|
||||
};
|
||||
@ -22,7 +23,7 @@ interface IAuth {
|
||||
|
||||
export abstract class Auth implements IAuth {
|
||||
abstract user: Model;
|
||||
protected authenticator: Model;
|
||||
protected authenticator: Authenticator;
|
||||
protected options: {
|
||||
[key: string]: any;
|
||||
};
|
||||
@ -36,7 +37,7 @@ export abstract class Auth implements IAuth {
|
||||
}
|
||||
|
||||
// The abstract methods are required to be implemented by all authentications.
|
||||
abstract check();
|
||||
abstract check(): Promise<Model>;
|
||||
// The following methods are mainly designed for user authentications.
|
||||
async signIn(): Promise<any> {}
|
||||
async signUp(): Promise<any> {}
|
||||
|
@ -1,23 +0,0 @@
|
||||
import React, { FunctionComponent, createContext, useContext, createElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const OptionsComponentContext = createContext<{
|
||||
[authType: string]: FunctionComponent;
|
||||
}>({});
|
||||
|
||||
export const OptionsComponentProvider: React.FC<{ authType: string; component: FunctionComponent }> = (props) => {
|
||||
const components = useContext(OptionsComponentContext);
|
||||
components[props.authType] = props.component;
|
||||
return <OptionsComponentContext.Provider value={components}>{props.children}</OptionsComponentContext.Provider>;
|
||||
};
|
||||
|
||||
export const useHasOptionsComponent = (authType: string) => {
|
||||
const components = useContext(OptionsComponentContext);
|
||||
return components[authType];
|
||||
};
|
||||
|
||||
export const useOptionsComponent = (authType: string) => {
|
||||
const { t } = useTranslation();
|
||||
const component = useHasOptionsComponent(authType);
|
||||
return component ? createElement(component) : <></>;
|
||||
};
|
@ -1,158 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { Space, Tabs } from 'antd';
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
FunctionComponentElement,
|
||||
createContext,
|
||||
createElement,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAPIClient, useApp, useCurrentDocumentTitle, useCurrentUserContext, useRequest, useViewport } from '..';
|
||||
import { useSigninPageExtension } from './SigninPageExtension';
|
||||
|
||||
const SigninPageContext = createContext<{
|
||||
[authType: string]: {
|
||||
component: FunctionComponent;
|
||||
tabTitle?: string;
|
||||
};
|
||||
}>({});
|
||||
|
||||
export const SigninPageProvider: React.FC<{
|
||||
authType: string;
|
||||
component: FunctionComponent<{ authenticator: Authenticator }>;
|
||||
tabTitle?: string;
|
||||
}> = (props) => {
|
||||
const components = useContext(SigninPageContext);
|
||||
components[props.authType] = {
|
||||
component: props.component,
|
||||
tabTitle: props.tabTitle,
|
||||
};
|
||||
return <SigninPageContext.Provider value={components}>{props.children}</SigninPageContext.Provider>;
|
||||
};
|
||||
|
||||
export type Authenticator = {
|
||||
name: string;
|
||||
authType: string;
|
||||
title?: string;
|
||||
options?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
sort?: number;
|
||||
};
|
||||
|
||||
export const AuthenticatorsContext = createContext<Authenticator[]>([]);
|
||||
|
||||
export function useRedirect(next = '/admin') {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
return useCallback(() => {
|
||||
navigate(searchParams.get('redirect') || '/admin', { replace: true });
|
||||
}, [navigate, searchParams]);
|
||||
}
|
||||
|
||||
export const useSignIn = (authenticator) => {
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const redirect = useRedirect();
|
||||
const { refreshAsync } = useCurrentUserContext();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await api.auth.signIn(form.values, authenticator);
|
||||
await refreshAsync();
|
||||
redirect();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const SigninPage = () => {
|
||||
const { t } = useTranslation();
|
||||
useCurrentDocumentTitle('Signin');
|
||||
useViewport();
|
||||
const signInPages = useContext(SigninPageContext);
|
||||
const api = useAPIClient();
|
||||
const [authenticators, setAuthenticators] = useState<Authenticator[]>([]);
|
||||
const [tabs, setTabs] = useState<
|
||||
(Authenticator & {
|
||||
component: FunctionComponentElement<{ authenticator: Authenticator }>;
|
||||
tabTitle: string;
|
||||
})[]
|
||||
>([]);
|
||||
const signinExtension = useSigninPageExtension(authenticators);
|
||||
|
||||
const handleAuthenticators = (authenticators: Authenticator[]) => {
|
||||
const tabs = authenticators
|
||||
.map((authenticator) => {
|
||||
const page = signInPages[authenticator.authType];
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
component: createElement<{
|
||||
authenticator: Authenticator;
|
||||
}>(page.component, { authenticator }),
|
||||
tabTitle: authenticator.title || page.tabTitle || authenticator.name,
|
||||
...authenticator,
|
||||
};
|
||||
})
|
||||
.filter((i) => i);
|
||||
|
||||
setAuthenticators(authenticators);
|
||||
setTabs(tabs);
|
||||
};
|
||||
|
||||
const { error } = useRequest(
|
||||
() =>
|
||||
api
|
||||
.resource('authenticators')
|
||||
.publicList()
|
||||
.then((res) => {
|
||||
return res?.data?.data || [];
|
||||
}),
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
handleAuthenticators(data);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!authenticators.length) {
|
||||
return <div style={{ color: '#ccc' }}>{t('Oops! No authentication methods available.')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthenticatorsContext.Provider value={authenticators}>
|
||||
<Space
|
||||
direction="vertical"
|
||||
className={css`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
{tabs.length > 1 ? (
|
||||
<Tabs items={tabs.map((tab) => ({ label: t(tab.tabTitle), key: tab.name, children: tab.component }))} />
|
||||
) : tabs.length ? (
|
||||
<div>{tabs[0].component}</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Space
|
||||
direction="vertical"
|
||||
className={css`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
{signinExtension}
|
||||
</Space>
|
||||
</Space>
|
||||
</AuthenticatorsContext.Provider>
|
||||
);
|
||||
};
|
@ -1,40 +0,0 @@
|
||||
import React, { createContext, FunctionComponent, useContext } from 'react';
|
||||
import { Authenticator, AuthenticatorsContext } from './SigninPage';
|
||||
|
||||
export interface SigninPageExtensionContextValue {
|
||||
components: {
|
||||
[authType: string]: FunctionComponent<{
|
||||
authenticator: Authenticator;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const SigninPageExtensionContext = createContext<SigninPageExtensionContextValue>({ components: {} });
|
||||
|
||||
export const useSigninPageExtension = (authenticators = []) => {
|
||||
const { components } = useContext(SigninPageExtensionContext);
|
||||
const types = Object.keys(components);
|
||||
return authenticators
|
||||
.filter((authenticator) => types.includes(authenticator.authType))
|
||||
.map((authenticator, index) =>
|
||||
React.createElement(components[authenticator.authType], { key: index, authenticator }),
|
||||
);
|
||||
};
|
||||
|
||||
export const SigninPageExtensionProvider: React.FC<{
|
||||
authType: string;
|
||||
component: FunctionComponent<{
|
||||
authenticator: Authenticator;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
}> = (props) => {
|
||||
const { components } = useContext(SigninPageExtensionContext);
|
||||
if (!components[props.authType]) {
|
||||
components[props.authType] = props.component;
|
||||
}
|
||||
|
||||
return (
|
||||
<SigninPageExtensionContext.Provider value={{ components }}>{props.children}</SigninPageExtensionContext.Provider>
|
||||
);
|
||||
};
|
@ -1,70 +0,0 @@
|
||||
import { message } from 'antd';
|
||||
import React, { useContext, createContext, FunctionComponent, createElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAPIClient, useCurrentDocumentTitle, useViewport } from '..';
|
||||
import { useForm } from '@formily/react';
|
||||
|
||||
export const SignupPageContext = createContext<{
|
||||
[authType: string]: {
|
||||
component: FunctionComponent<{
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
}>({});
|
||||
|
||||
export const SignupPageProvider: React.FC<{
|
||||
authType: string;
|
||||
component: FunctionComponent<{
|
||||
name: string;
|
||||
}>;
|
||||
}> = (props) => {
|
||||
const components = useContext(SignupPageContext);
|
||||
components[props.authType] = {
|
||||
component: props.component,
|
||||
};
|
||||
return <SignupPageContext.Provider value={components}>{props.children}</SignupPageContext.Provider>;
|
||||
};
|
||||
|
||||
export interface UseSignupProps {
|
||||
authenticator?: string;
|
||||
message?: {
|
||||
success?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const useSignup = (props?: UseSignupProps) => {
|
||||
const navigate = useNavigate();
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await api.auth.signUp(form.values, props?.authenticator);
|
||||
message.success(props?.message?.success || t('Sign up successfully, and automatically jump to the sign in page'));
|
||||
setTimeout(() => {
|
||||
navigate('/signin');
|
||||
}, 2000);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const SignupPage = () => {
|
||||
const { t } = useTranslation();
|
||||
useViewport();
|
||||
useCurrentDocumentTitle('Signup');
|
||||
const [searchParams] = useSearchParams();
|
||||
const authType = searchParams.get('authType');
|
||||
const name = searchParams.get('name');
|
||||
const signUpPages = useContext(SignupPageContext);
|
||||
if (!signUpPages[authType]) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ color: '#ccc' }}>{t('Oops! The authentication type does not allow sign-up.')}</div>
|
||||
<Link to="/signin">{t('Return')}</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return createElement(signUpPages[authType].component, { name });
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import { Plugin } from '../application/Plugin';
|
||||
import { SigninPageExtensionProvider } from './SigninPageExtension';
|
||||
|
||||
export * from './OptionsComponent';
|
||||
export * from './SigninPage';
|
||||
export * from './SigninPageExtension';
|
||||
export * from './SignupPage';
|
||||
|
||||
export class SigninPageExtensionPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(SigninPageExtensionProvider, this.options);
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@ export * from './api-client';
|
||||
export * from './appInfo';
|
||||
export * from './application';
|
||||
export * from './async-data-provider';
|
||||
export * from './auth';
|
||||
export * from './block-provider';
|
||||
export * from './china-region';
|
||||
export * from './collection-manager';
|
||||
|
@ -9,12 +9,11 @@ import { ACLPlugin } from '../acl';
|
||||
import { useAPIClient } from '../api-client';
|
||||
import { Application } from '../application';
|
||||
import { Plugin } from '../application/Plugin';
|
||||
import { SigninPage, SigninPageExtensionPlugin, SignupPage } from '../auth';
|
||||
import { BlockSchemaComponentPlugin } from '../block-provider';
|
||||
import { RemoteDocumentTitlePlugin } from '../document-title';
|
||||
import { PinnedListPlugin } from '../plugin-manager';
|
||||
import { PMPlugin } from '../pm';
|
||||
import { AdminLayoutPlugin, AuthLayout, RouteSchemaComponent } from '../route-switch';
|
||||
import { AdminLayoutPlugin, RouteSchemaComponent } from '../route-switch';
|
||||
import { AntdSchemaComponentPlugin, SchemaComponentPlugin, menuItemInitializer } from '../schema-component';
|
||||
import { ErrorFallback } from '../schema-component/antd/error-fallback';
|
||||
import { AssociationFilterPlugin, SchemaInitializerPlugin } from '../schema-initializer';
|
||||
@ -285,25 +284,10 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
||||
path: '/admin/:name',
|
||||
Component: 'RouteSchemaComponent',
|
||||
});
|
||||
|
||||
this.router.add('auth', {
|
||||
Component: 'AuthLayout',
|
||||
});
|
||||
this.router.add('auth.signin', {
|
||||
path: '/signin',
|
||||
Component: 'SigninPage',
|
||||
});
|
||||
this.router.add('auth.signup', {
|
||||
path: '/signup',
|
||||
Component: 'SignupPage',
|
||||
});
|
||||
}
|
||||
|
||||
addComponents() {
|
||||
this.app.addComponents({
|
||||
AuthLayout,
|
||||
SigninPage,
|
||||
SignupPage,
|
||||
ErrorFallback,
|
||||
RouteSchemaComponent,
|
||||
BlockTemplatePage,
|
||||
@ -329,7 +313,6 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
||||
await this.app.pm.add(SchemaInitializerPlugin, { name: 'schema-initializer' });
|
||||
await this.app.pm.add(BlockSchemaComponentPlugin, { name: 'block-schema-component' });
|
||||
await this.app.pm.add(AntdSchemaComponentPlugin, { name: 'antd-schema-component' });
|
||||
await this.app.pm.add(SigninPageExtensionPlugin, { name: 'signin-page-extension' });
|
||||
await this.app.pm.add(ACLPlugin, { name: 'builtin-acl' });
|
||||
await this.app.pm.add(RemoteDocumentTitlePlugin, { name: 'remote-document-title' });
|
||||
await this.app.pm.add(PMPlugin, { name: 'builtin-pm' });
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from './admin-layout';
|
||||
export * from './auth-layout';
|
||||
export * from './route-schema-component';
|
||||
|
@ -1,184 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { Space, Tabs } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..';
|
||||
import { useSigninPageExtension } from './SigninPageExtension';
|
||||
import VerificationCode from './VerificationCode';
|
||||
|
||||
const passwordForm: ISchema = {
|
||||
type: 'object',
|
||||
name: 'passwordForm',
|
||||
'x-component': 'FormV2',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-validator': 'email',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Email")}}', style: {} },
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
'x-component': 'Password',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Password")}}', style: {} },
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
submit: {
|
||||
title: '{{t("Sign in")}}',
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
htmlType: 'submit',
|
||||
block: true,
|
||||
type: 'primary',
|
||||
useAction: '{{ usePasswordSignIn }}',
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function useRedirect(next = '/admin') {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
return useCallback(() => {
|
||||
navigate(searchParams.get('redirect') || '/admin', { replace: true });
|
||||
}, [navigate, searchParams]);
|
||||
}
|
||||
|
||||
export const usePasswordSignIn = () => {
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const redirect = useRedirect();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await api.auth.signIn(form.values);
|
||||
redirect();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const phoneForm: ISchema = {
|
||||
type: 'object',
|
||||
name: 'phoneForm',
|
||||
'x-component': 'Form',
|
||||
properties: {
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-validator': 'phone',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Phone")}}', style: {} },
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'VerificationCode',
|
||||
'x-component-props': {
|
||||
actionType: 'users:signin',
|
||||
targetFieldName: 'phone',
|
||||
},
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
actions: {
|
||||
title: '{{t("Sign in")}}',
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
htmlType: 'submit',
|
||||
block: true,
|
||||
type: 'primary',
|
||||
useAction: '{{ usePhoneSignIn }}',
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function usePhoneSignIn() {
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const redirect = useRedirect();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await api.auth.signIn(form.values, 'sms');
|
||||
redirect();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface SigninPageProps {
|
||||
schema?: ISchema;
|
||||
components?: any;
|
||||
scope?: any;
|
||||
}
|
||||
|
||||
export const SigninPage = (props: SigninPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
useCurrentDocumentTitle('Signin');
|
||||
const ctx = useSystemSettings();
|
||||
const signinExtension = useSigninPageExtension();
|
||||
const { allowSignUp, smsAuthEnabled } = ctx?.data?.data || {};
|
||||
const { schema, components, scope } = props;
|
||||
return (
|
||||
<Space
|
||||
direction="vertical"
|
||||
className={css`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
{smsAuthEnabled ? (
|
||||
<Tabs
|
||||
defaultActiveKey="password"
|
||||
items={[
|
||||
{
|
||||
label: t('Sign in via account'),
|
||||
key: 'password',
|
||||
children: <SchemaComponent scope={{ usePasswordSignIn }} schema={schema || passwordForm} />,
|
||||
},
|
||||
{
|
||||
label: t('Sign in via phone'),
|
||||
key: 'phone',
|
||||
children: (
|
||||
<SchemaComponent
|
||||
schema={phoneForm}
|
||||
scope={{ usePhoneSignIn, ...scope }}
|
||||
components={{
|
||||
VerificationCode,
|
||||
...components,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<SchemaComponent
|
||||
components={{ ...components }}
|
||||
scope={{ usePasswordSignIn, ...scope }}
|
||||
schema={schema || passwordForm}
|
||||
/>
|
||||
)}
|
||||
<div>{signinExtension}</div>
|
||||
{allowSignUp && (
|
||||
<div>
|
||||
<Link to="/signup">{t('Create an account')}</Link>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import React, { createContext, FunctionComponent, useContext } from 'react';
|
||||
|
||||
export interface SigninPageExtensionContextValue {
|
||||
components: FunctionComponent[];
|
||||
}
|
||||
|
||||
export const SigninPageExtensionContext = createContext<SigninPageExtensionContextValue>({ components: [] });
|
||||
|
||||
export const useSigninPageExtension = () => {
|
||||
const { components } = useContext(SigninPageExtensionContext);
|
||||
return components.map((component, index) => React.createElement(component, { key: index }));
|
||||
};
|
||||
|
||||
export const SigninPageExtensionProvider = (props: { component: FunctionComponent; children: JSX.Element }) => {
|
||||
const { components } = useContext(SigninPageExtensionContext);
|
||||
|
||||
const list = props.component ? [...components, props.component] : components;
|
||||
|
||||
return (
|
||||
<SigninPageExtensionContext.Provider value={{ components: list }}>
|
||||
{props.children}
|
||||
</SigninPageExtensionContext.Provider>
|
||||
);
|
||||
};
|
@ -1,159 +0,0 @@
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { message } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..';
|
||||
import VerificationCode from './VerificationCode';
|
||||
|
||||
const signupPageSchema: ISchema = {
|
||||
type: 'object',
|
||||
name: uid(),
|
||||
'x-component': 'FormV2',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-validator': 'email',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Email")}}', style: {} },
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-validator': 'phone',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Phone")}}', style: {} },
|
||||
'x-visible': '{{smsAuthEnabled}}',
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'VerificationCode',
|
||||
'x-component-props': {
|
||||
actionType: 'users:signup',
|
||||
targetFieldName: 'phone',
|
||||
},
|
||||
'x-decorator': 'FormItem',
|
||||
'x-visible': '{{smsAuthEnabled}}',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'Password',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Password")}}', checkStrength: true, style: {} },
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.confirm_password'],
|
||||
fulfill: {
|
||||
state: {
|
||||
selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? t("Password mismatch") : ""}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
confirm_password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'Password',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Confirm password")}}', style: {} },
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.password'],
|
||||
fulfill: {
|
||||
state: {
|
||||
selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? t("Password mismatch") : ""}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
submit: {
|
||||
title: '{{t("Sign up")}}',
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
block: true,
|
||||
type: 'primary',
|
||||
htmlType: 'submit',
|
||||
useAction: '{{ useSignup }}',
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
link: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
properties: {
|
||||
link: {
|
||||
type: 'void',
|
||||
'x-component': 'Link',
|
||||
'x-component-props': { to: '/signin' },
|
||||
'x-content': '{{t("Log in with an existing account")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface UseSignupProps {
|
||||
message?: {
|
||||
success?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const useSignup = (props?: UseSignupProps) => {
|
||||
const navigate = useNavigate();
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await api.resource('users').signup({
|
||||
values: form.values,
|
||||
});
|
||||
message.success(props?.message?.success || t('Sign up successfully, and automatically jump to the sign in page'));
|
||||
setTimeout(() => {
|
||||
navigate('/signin');
|
||||
}, 2000);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface SignupPageProps {
|
||||
schema?: ISchema;
|
||||
components?: any;
|
||||
scope?: any;
|
||||
}
|
||||
|
||||
export const SignupPage = (props: SignupPageProps) => {
|
||||
useCurrentDocumentTitle('Signup');
|
||||
const ctx = useSystemSettings();
|
||||
const { allowSignUp, smsAuthEnabled } = ctx?.data?.data || {};
|
||||
if (!allowSignUp) {
|
||||
return <Navigate replace to={'/signin'} />;
|
||||
}
|
||||
const { schema, components, scope } = props;
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={schema || signupPageSchema}
|
||||
components={{
|
||||
VerificationCode,
|
||||
...components,
|
||||
}}
|
||||
scope={{ useSignup, smsAuthEnabled, ...scope }}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,6 +1,3 @@
|
||||
export * from './CurrentUser';
|
||||
export * from './CurrentUserProvider';
|
||||
export * from './CurrentUserSettingsMenuProvider';
|
||||
// export * from './SigninPage';
|
||||
// export * from './SignupPage';
|
||||
// export * from './SigninPageExtension';
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { OptionsComponentProvider, SigninPageProvider, SignupPageProvider } from '@nocobase/client';
|
||||
import React, { FC } from 'react';
|
||||
import { presetAuthType } from '../preset';
|
||||
import { Options } from './basic/Options';
|
||||
import SigninPage from './basic/SigninPage';
|
||||
import SignupPage from './basic/SignupPage';
|
||||
import { useAuthTranslation } from './locale';
|
||||
|
||||
export const AuthPluginProvider: FC = (props) => {
|
||||
const { t } = useAuthTranslation();
|
||||
return (
|
||||
<OptionsComponentProvider authType={presetAuthType} component={Options}>
|
||||
<SigninPageProvider authType={presetAuthType} tabTitle={t('Sign in via password')} component={SigninPage}>
|
||||
<SignupPageProvider authType={presetAuthType} component={SignupPage}>
|
||||
{props.children}
|
||||
</SignupPageProvider>
|
||||
</SigninPageProvider>
|
||||
</OptionsComponentProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type Authenticator = {
|
||||
name: string;
|
||||
authType: string;
|
||||
authTypeTitle: string;
|
||||
title?: string;
|
||||
options?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
sort?: number;
|
||||
};
|
||||
|
||||
export const AuthenticatorsContext = createContext<Authenticator[]>([]);
|
||||
|
||||
export const useAuthenticator = (name: string) => {
|
||||
const authenticators = useContext(AuthenticatorsContext);
|
||||
return authenticators.find((a) => a.name === name);
|
||||
};
|
@ -15,7 +15,7 @@ export const Options = () => {
|
||||
public: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowSignup: {
|
||||
allowSignUp: {
|
||||
'x-decorator': 'FormItem',
|
||||
type: 'boolean',
|
||||
title: '{{t("Allow to sign up")}}',
|
||||
|
@ -1,7 +1,34 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { Authenticator, SchemaComponent, SignupPageContext, useSignIn } from '@nocobase/client';
|
||||
import React, { useContext } from 'react';
|
||||
import { SchemaComponent, useAPIClient, useCurrentUserContext } from '@nocobase/client';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useAuthTranslation } from '../locale';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useForm } from '@formily/react';
|
||||
import { useSignUpForms } from '../pages';
|
||||
import { Authenticator } from '../authenticator';
|
||||
|
||||
export function useRedirect(next = '/admin') {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
return useCallback(() => {
|
||||
navigate(searchParams.get('redirect') || '/admin', { replace: true });
|
||||
}, [navigate, searchParams]);
|
||||
}
|
||||
|
||||
export const useSignIn = (authenticator: string) => {
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const redirect = useRedirect();
|
||||
const { refreshAsync } = useCurrentUserContext();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await api.auth.signIn(form.values, authenticator);
|
||||
await refreshAsync();
|
||||
redirect();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const passwordForm: ISchema = {
|
||||
type: 'object',
|
||||
@ -51,27 +78,27 @@ const passwordForm: ISchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
signup: {
|
||||
signUp: {
|
||||
type: 'void',
|
||||
'x-component': 'Link',
|
||||
'x-component-props': {
|
||||
to: '{{ signupLink }}',
|
||||
to: '{{ signUpLink }}',
|
||||
},
|
||||
'x-content': '{{t("Create an account")}}',
|
||||
'x-visible': '{{ allowSignUp }}',
|
||||
},
|
||||
},
|
||||
};
|
||||
export default (props: { authenticator: Authenticator }) => {
|
||||
export const SignInForm = (props: { authenticator: Authenticator }) => {
|
||||
const { t } = useAuthTranslation();
|
||||
const authenticator = props.authenticator;
|
||||
const { authType, name, options } = authenticator;
|
||||
const signupPages = useContext(SignupPageContext);
|
||||
const allowSignUp = !!signupPages[authType] && options?.allowSignup;
|
||||
const signupLink = `/signup?authType=${authType}&name=${name}`;
|
||||
const signUpPages = useSignUpForms();
|
||||
const allowSignUp = !!signUpPages[authType] && options?.allowSignUp;
|
||||
const signUpLink = `/signup?name=${name}`;
|
||||
|
||||
const useBasicSignIn = () => {
|
||||
return useSignIn(name);
|
||||
};
|
||||
return <SchemaComponent schema={passwordForm} scope={{ useBasicSignIn, allowSignUp, signupLink, t }} />;
|
||||
return <SchemaComponent schema={passwordForm} scope={{ useBasicSignIn, allowSignUp, signUpLink, t }} />;
|
||||
};
|
@ -1,8 +1,38 @@
|
||||
import { SchemaComponent, useSignup } from '@nocobase/client';
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { ISchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useAuthTranslation } from '../locale';
|
||||
import { useAPIClient } from '@nocobase/client';
|
||||
import { useForm } from '@formily/react';
|
||||
import { useNavigate, Navigate } from 'react-router-dom';
|
||||
import { message } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthenticator } from '../authenticator';
|
||||
|
||||
export interface UseSignupProps {
|
||||
authenticator?: string;
|
||||
message?: {
|
||||
success?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const useSignUp = (props?: UseSignupProps) => {
|
||||
const navigate = useNavigate();
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await api.auth.signUp(form.values, props?.authenticator);
|
||||
message.success(props?.message?.success || t('Sign up successfully, and automatically jump to the sign in page'));
|
||||
setTimeout(() => {
|
||||
navigate('/signin');
|
||||
}, 2000);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const signupPageSchema: ISchema = {
|
||||
type: 'object',
|
||||
@ -63,7 +93,7 @@ const signupPageSchema: ISchema = {
|
||||
block: true,
|
||||
type: 'primary',
|
||||
htmlType: 'submit',
|
||||
useAction: '{{ useBasicSignup }}',
|
||||
useAction: '{{ useBasicSignUp }}',
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
@ -84,10 +114,15 @@ const signupPageSchema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
export default (props: { name: string }) => {
|
||||
export const SignUpForm = ({ authenticatorName: name }: { authenticatorName: string }) => {
|
||||
const { t } = useAuthTranslation();
|
||||
const useBasicSignup = () => {
|
||||
return useSignup({ authenticator: props.name });
|
||||
const useBasicSignUp = () => {
|
||||
return useSignUp({ authenticator: name });
|
||||
};
|
||||
return <SchemaComponent schema={signupPageSchema} scope={{ useBasicSignup, t }} />;
|
||||
const authenticator = useAuthenticator(name);
|
||||
const { options } = authenticator;
|
||||
if (!options?.allowSignUp) {
|
||||
return <Navigate to="/not-found" replace={true} />;
|
||||
}
|
||||
return <SchemaComponent schema={signupPageSchema} scope={{ useBasicSignUp, t }} />;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export * from './SignInForm';
|
||||
export * from './SignUpForm';
|
||||
export * from './Options';
|
@ -1,10 +1,30 @@
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { AuthPluginProvider } from './AuthPluginProvider';
|
||||
import { AuthProvider } from './AuthProvider';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { Authenticator } from './settings/Authenticator';
|
||||
import { AuthLayout, SignInPage, SignUpPage } from './pages';
|
||||
import { ComponentType } from 'react';
|
||||
import { Registry } from '@nocobase/utils/client';
|
||||
import { presetAuthType } from '../preset';
|
||||
import { SignInForm, SignUpForm, Options } from './basic';
|
||||
import { Authenticator as AuthenticatorType } from './authenticator';
|
||||
|
||||
export type AuthOptions = {
|
||||
components: Partial<{
|
||||
SignInForm: ComponentType<{ authenticator: AuthenticatorType }>;
|
||||
SignInButton: ComponentType<{ authenticator: AuthenticatorType }>;
|
||||
SignUpForm: ComponentType<{ authenticatorName: string }>;
|
||||
AdminSettingsForm: ComponentType;
|
||||
}>;
|
||||
};
|
||||
|
||||
export class AuthPlugin extends Plugin {
|
||||
authTypes = new Registry<AuthOptions>();
|
||||
|
||||
registerType(authType: string, options: AuthOptions) {
|
||||
this.authTypes.register(authType, options);
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||
icon: 'LoginOutlined',
|
||||
@ -12,9 +32,38 @@ export class AuthPlugin extends Plugin {
|
||||
Component: Authenticator,
|
||||
aclSnippet: 'pm.auth.authenticators',
|
||||
});
|
||||
|
||||
this.router.add('auth', {
|
||||
Component: 'AuthLayout',
|
||||
});
|
||||
this.router.add('auth.signin', {
|
||||
path: '/signin',
|
||||
Component: 'SignInPage',
|
||||
});
|
||||
this.router.add('auth.signup', {
|
||||
path: '/signup',
|
||||
Component: 'SignUpPage',
|
||||
});
|
||||
|
||||
this.app.addComponents({
|
||||
AuthLayout,
|
||||
SignInPage,
|
||||
SignUpPage,
|
||||
});
|
||||
|
||||
this.app.providers.unshift([AuthProvider, {}]);
|
||||
this.app.use(AuthPluginProvider);
|
||||
|
||||
this.registerType(presetAuthType, {
|
||||
components: {
|
||||
SignInForm: SignInForm,
|
||||
SignUpForm: SignUpForm,
|
||||
AdminSettingsForm: Options,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthPlugin;
|
||||
export { useSignIn } from './basic';
|
||||
export { useAuthenticator, AuthenticatorsContext } from './authenticator';
|
||||
export type { Authenticator } from './authenticator';
|
||||
|
@ -1,11 +1,25 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { PoweredBy } from '../../../powered-by';
|
||||
import { useSystemSettings } from '../../../system-settings';
|
||||
import { useSystemSettings, PoweredBy, useRequest, useAPIClient } from '@nocobase/client';
|
||||
import { AuthenticatorsContext } from '../authenticator';
|
||||
|
||||
export function AuthLayout(props: any) {
|
||||
const { data } = useSystemSettings();
|
||||
const api = useAPIClient();
|
||||
const { data: authenticators = [], error } = useRequest(() =>
|
||||
api
|
||||
.resource('authenticators')
|
||||
.publicList()
|
||||
.then((res) => {
|
||||
return res?.data?.data || [];
|
||||
}),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@ -15,7 +29,9 @@ export function AuthLayout(props: any) {
|
||||
}}
|
||||
>
|
||||
<h1>{data?.data?.title}</h1>
|
||||
<AuthenticatorsContext.Provider value={authenticators as any}>
|
||||
<Outlet />
|
||||
</AuthenticatorsContext.Provider>
|
||||
<div
|
||||
className={css`
|
||||
position: absolute;
|
@ -0,0 +1,92 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Space, Tabs } from 'antd';
|
||||
import React, { createElement, useContext } from 'react';
|
||||
import { useCurrentDocumentTitle, usePlugin, useViewport } from '@nocobase/client';
|
||||
import AuthPlugin, { AuthOptions } from '..';
|
||||
import { Authenticator, AuthenticatorsContext } from '../authenticator';
|
||||
import { useAuthTranslation } from '../locale';
|
||||
|
||||
export const useSignInForms = (): {
|
||||
[authType: string]: AuthOptions['components']['SignInForm'];
|
||||
} => {
|
||||
const plugin = usePlugin(AuthPlugin);
|
||||
const authTypes = plugin.authTypes.getEntities();
|
||||
const signInForms = {};
|
||||
for (const [authType, options] of authTypes) {
|
||||
if (options.components?.SignInForm) {
|
||||
signInForms[authType] = options.components.SignInForm;
|
||||
}
|
||||
}
|
||||
return signInForms;
|
||||
};
|
||||
|
||||
export const useSignInButtons = (authenticators = []) => {
|
||||
const plugin = usePlugin(AuthPlugin);
|
||||
const authTypes = plugin.authTypes.getEntities();
|
||||
const customs = {};
|
||||
for (const [authType, options] of authTypes) {
|
||||
if (options.components?.SignInButton) {
|
||||
customs[authType] = options.components.SignInButton;
|
||||
}
|
||||
}
|
||||
|
||||
const types = Object.keys(customs);
|
||||
return authenticators
|
||||
.filter((authenticator) => types.includes(authenticator.authType))
|
||||
.map((authenticator, index) => React.createElement(customs[authenticator.authType], { key: index, authenticator }));
|
||||
};
|
||||
|
||||
export const SignInPage = () => {
|
||||
const { t } = useAuthTranslation();
|
||||
useCurrentDocumentTitle('Signin');
|
||||
useViewport();
|
||||
const signInForms = useSignInForms();
|
||||
const authenticators = useContext(AuthenticatorsContext);
|
||||
const signInButtons = useSignInButtons(authenticators);
|
||||
|
||||
if (!authenticators.length) {
|
||||
return <div style={{ color: '#ccc' }}>{t('No authentication methods available.')}</div>;
|
||||
}
|
||||
|
||||
const tabs = authenticators
|
||||
.map((authenticator) => {
|
||||
const C = signInForms[authenticator.authType];
|
||||
if (!C) {
|
||||
return;
|
||||
}
|
||||
const defaultTabTitle = `${t('Sign-in')} (${t(authenticator.authTypeTitle || authenticator.authType)})`;
|
||||
return {
|
||||
component: createElement<{
|
||||
authenticator: Authenticator;
|
||||
}>(C, { authenticator }),
|
||||
tabTitle: authenticator.title || defaultTabTitle,
|
||||
...authenticator,
|
||||
};
|
||||
})
|
||||
.filter((i) => i);
|
||||
|
||||
return (
|
||||
<Space
|
||||
direction="vertical"
|
||||
className={css`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
{tabs.length > 1 ? (
|
||||
<Tabs items={tabs.map((tab) => ({ label: tab.tabTitle, key: tab.name, children: tab.component }))} />
|
||||
) : tabs.length ? (
|
||||
<div>{tabs[0].component}</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Space
|
||||
direction="vertical"
|
||||
className={css`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
{signInButtons}
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import { useCurrentDocumentTitle, usePlugin, useViewport } from '@nocobase/client';
|
||||
import React, { useContext, createContext, FunctionComponent, createElement } from 'react';
|
||||
import { Navigate, useSearchParams } from 'react-router-dom';
|
||||
import AuthPlugin, { AuthOptions } from '..';
|
||||
import { useAuthenticator } from '../authenticator';
|
||||
|
||||
export const SignupPageContext = createContext<{
|
||||
[authType: string]: {
|
||||
component: FunctionComponent<{
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
}>({});
|
||||
|
||||
export const SignupPageProvider: React.FC<{
|
||||
authType: string;
|
||||
component: FunctionComponent<{
|
||||
name: string;
|
||||
}>;
|
||||
}> = (props) => {
|
||||
const components = useContext(SignupPageContext);
|
||||
components[props.authType] = {
|
||||
component: props.component,
|
||||
};
|
||||
return <SignupPageContext.Provider value={components}>{props.children}</SignupPageContext.Provider>;
|
||||
};
|
||||
|
||||
export const useSignUpForms = (): {
|
||||
[authType: string]: AuthOptions['components']['SignUpForm'];
|
||||
} => {
|
||||
const plugin = usePlugin(AuthPlugin);
|
||||
const authTypes = plugin.authTypes.getEntities();
|
||||
const signUpForms = {};
|
||||
for (const [authType, options] of authTypes) {
|
||||
if (options.components?.SignUpForm) {
|
||||
signUpForms[authType] = options.components.SignUpForm;
|
||||
}
|
||||
}
|
||||
return signUpForms;
|
||||
};
|
||||
|
||||
export const SignUpPage = () => {
|
||||
useViewport();
|
||||
useCurrentDocumentTitle('Signup');
|
||||
const signUpForms = useSignUpForms();
|
||||
const [searchParams] = useSearchParams();
|
||||
const name = searchParams.get('name');
|
||||
const authenticator = useAuthenticator(name);
|
||||
const { authType } = authenticator || {};
|
||||
if (!signUpForms[authType]) {
|
||||
return <Navigate to="/not-found" replace={true} />;
|
||||
}
|
||||
return createElement(signUpForms[authType], { authenticatorName: name });
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export * from './AuthLayout';
|
||||
export * from './SignInPage';
|
||||
export * from './SignUpPage';
|
@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { observer, useForm } from '@formily/react';
|
||||
import { useActionContext, useOptionsComponent, useRecord, useRequest } from '@nocobase/client';
|
||||
import { useActionContext, usePlugin, useRecord, useRequest } from '@nocobase/client';
|
||||
import { useEffect } from 'react';
|
||||
import AuthPlugin from '..';
|
||||
|
||||
export const useValuesFromOptions = (options) => {
|
||||
const record = useRecord();
|
||||
@ -26,9 +28,15 @@ export const useValuesFromOptions = (options) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useAdminSettingsForm = (authType: string) => {
|
||||
const plugin = usePlugin(AuthPlugin);
|
||||
const auth = plugin.authTypes.get(authType);
|
||||
return auth?.components?.AdminSettingsForm;
|
||||
};
|
||||
|
||||
export const Options = observer(() => {
|
||||
const form = useForm();
|
||||
const record = useRecord();
|
||||
const component = useOptionsComponent(form.values.authType || record.authType);
|
||||
return component;
|
||||
const Component = useAdminSettingsForm(form.values.authType || record.authType);
|
||||
return Component ? <Component /> : null;
|
||||
});
|
||||
|
@ -19,5 +19,8 @@
|
||||
"SMS": "SMS",
|
||||
"Username/Email": "Username/Email",
|
||||
"Auth UID": "Auth UID",
|
||||
"The authentication allows users to sign in via username or email.": "The authentication allows users to sign in via username or email."
|
||||
"The authentication allows users to sign in via username or email.": "The authentication allows users to sign in via username or email.",
|
||||
"No authentication methods available.": "No authentication methods available.",
|
||||
"The password is inconsistent, please re-enter": "The password is inconsistent, please re-enter",
|
||||
"Sign-in": "Sign-in"
|
||||
}
|
||||
|
@ -19,5 +19,8 @@
|
||||
"SMS": "短信",
|
||||
"Username/Email": "用户名/邮箱",
|
||||
"Auth UID": "认证标识",
|
||||
"The authentication allows users to sign in via username or email.": "该认证方式支持用户通过用户名或邮箱登录。"
|
||||
"The authentication allows users to sign in via username or email.": "该认证方式支持用户通过用户名或邮箱登录。",
|
||||
"No authentication methods available.": "没有可用的认证方式。",
|
||||
"The password is inconsistent, please re-enter": "密码不一致,请重新输入",
|
||||
"Sign-in": "登录"
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ describe('actions', () => {
|
||||
let db: Database;
|
||||
let agent;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
process.env.INIT_ROOT_EMAIL = 'test@nocobase.com';
|
||||
process.env.INT_ROOT_USERNAME = 'test';
|
||||
@ -97,8 +97,8 @@ describe('actions', () => {
|
||||
agent = app.agent();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.close();
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should sign in with password', async () => {
|
||||
@ -125,6 +125,7 @@ describe('actions', () => {
|
||||
let res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
username: 'new',
|
||||
password: 'new',
|
||||
confirm_password: 'new',
|
||||
});
|
||||
expect(res.statusCode).toEqual(200);
|
||||
|
||||
@ -199,5 +200,14 @@ describe('actions', () => {
|
||||
});
|
||||
expect(res2.statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
it('should check confirm password', async () => {
|
||||
const res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
username: 'new',
|
||||
password: 'new',
|
||||
confirm_password: 'new1',
|
||||
});
|
||||
expect(res.statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import { Model, Repository } from '@nocobase/database';
|
||||
import { namespace } from '../../preset';
|
||||
import { AuthManager } from '@nocobase/auth';
|
||||
|
||||
async function checkCount(repository: Repository, id: number[]) {
|
||||
// TODO(yangqia): This is a temporary solution, may cause concurrency problem.
|
||||
@ -24,6 +25,7 @@ export default {
|
||||
},
|
||||
publicList: async (ctx: Context, next: Next) => {
|
||||
const repo = ctx.db.getRepository('authenticators');
|
||||
const authManager = ctx.app.authManager as AuthManager;
|
||||
const authenticators = await repo.find({
|
||||
fields: ['name', 'authType', 'title', 'options', 'sort'],
|
||||
filter: {
|
||||
@ -31,12 +33,16 @@ export default {
|
||||
},
|
||||
sort: 'sort',
|
||||
});
|
||||
ctx.body = authenticators.map((authenticator: Model) => ({
|
||||
ctx.body = authenticators.map((authenticator: Model) => {
|
||||
const authType = authManager.getAuthConfig(authenticator.authType);
|
||||
return {
|
||||
name: authenticator.name,
|
||||
authType: authenticator.authType,
|
||||
authTypeTitle: authType?.title || '',
|
||||
title: authenticator.title,
|
||||
options: authenticator.options?.public || {},
|
||||
}));
|
||||
};
|
||||
});
|
||||
await next();
|
||||
},
|
||||
destroy: async (ctx: Context, next: Next) => {
|
||||
|
@ -51,11 +51,14 @@ export class BasicAuth extends BaseAuth {
|
||||
}
|
||||
const User = ctx.db.getRepository('users');
|
||||
const { values } = ctx.action.params;
|
||||
const { username } = values;
|
||||
const { username, password, confirm_password } = values;
|
||||
if (!/^[^@.<>"'/]{2,16}$/.test(username)) {
|
||||
ctx.throw(400, ctx.t('Please enter a valid username', { ns: namespace }));
|
||||
}
|
||||
const user = await User.create({ values });
|
||||
if (password !== confirm_password) {
|
||||
ctx.throw(400, ctx.t('The password is inconsistent, please re-enter', { ns: namespace }));
|
||||
}
|
||||
const user = await User.create({ values: { username, password } });
|
||||
return user;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { Migration } from '@nocobase/server';
|
||||
import { presetAuthType } from '../../preset';
|
||||
|
||||
export default class FixAllowSignUpMigration extends Migration {
|
||||
async up() {
|
||||
const repo = this.context.db.getRepository('authenticators');
|
||||
const authenticators = await repo.find({
|
||||
filter: {
|
||||
authType: presetAuthType,
|
||||
},
|
||||
});
|
||||
for (const authenticator of authenticators) {
|
||||
const options = authenticator.get('options');
|
||||
const oldAllowSignUp = options?.public?.allowSignup;
|
||||
if (oldAllowSignUp === undefined || oldAllowSignUp === null) {
|
||||
continue;
|
||||
}
|
||||
options.public.allowSignUp = oldAllowSignUp;
|
||||
delete options.public.allowSignup;
|
||||
await repo.update({
|
||||
values: {
|
||||
options,
|
||||
},
|
||||
filter: {
|
||||
name: authenticator.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async down() {}
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
import { Authenticator } from '@nocobase/auth';
|
||||
import { Database, Model } from '@nocobase/database';
|
||||
|
||||
export class AuthModel extends Model {
|
||||
export class AuthModel extends Model implements Authenticator {
|
||||
declare authType: string;
|
||||
declare options: any;
|
||||
|
||||
async findUser(uuid: string) {
|
||||
let user: Model;
|
||||
const users = await this.getUsers({
|
||||
@ -14,13 +18,13 @@ export class AuthModel extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
async newUser(uuid: string, values?: any) {
|
||||
async newUser(uuid: string, userValues?: any) {
|
||||
let user: Model;
|
||||
const db: Database = (this.constructor as any).database;
|
||||
await this.sequelize.transaction(async (transaction) => {
|
||||
// Create a new user if not exists
|
||||
user = await this.createUser(
|
||||
values || {
|
||||
userValues || {
|
||||
nickname: uuid,
|
||||
},
|
||||
{
|
||||
|
@ -13,7 +13,8 @@
|
||||
"@nocobase/database": "0.x",
|
||||
"@nocobase/sdk": "0.x",
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
"@nocobase/test": "0.x",
|
||||
"@nocobase/plugin-auth": ">=0.17.0-alpha.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "5.x",
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Authenticator } from '@nocobase/client';
|
||||
import React, { useEffect } from 'react';
|
||||
import { LoginOutlined } from '@ant-design/icons';
|
||||
import { Button, Space, message } from 'antd';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getSubAppName } from '@nocobase/sdk';
|
||||
import { Authenticator } from '@nocobase/plugin-auth/client';
|
||||
|
||||
export const SigninPage = (props: { authenticator: Authenticator }) => {
|
||||
const location = useLocation();
|
||||
|
@ -1,24 +1,18 @@
|
||||
import { OptionsComponentProvider, SigninPageExtensionProvider, SignupPageProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Plugin } from '@nocobase/client';
|
||||
|
||||
import { SigninPage } from './SigninPage';
|
||||
import { Options } from './Options';
|
||||
import { authType } from '../constants';
|
||||
|
||||
export function CASProvider(props) {
|
||||
return (
|
||||
<OptionsComponentProvider authType={authType} component={Options}>
|
||||
<SigninPageExtensionProvider authType={authType} component={SigninPage}>
|
||||
{props.children}
|
||||
</SigninPageExtensionProvider>
|
||||
</OptionsComponentProvider>
|
||||
);
|
||||
}
|
||||
import AuthPlugin from '@nocobase/plugin-auth/client';
|
||||
|
||||
export class SamlPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(CASProvider);
|
||||
const auth = this.app.pm.get(AuthPlugin);
|
||||
auth.registerType(authType, {
|
||||
components: {
|
||||
SignInForm: SigninPage,
|
||||
AdminSettingsForm: Options,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@
|
||||
"@nocobase/client": "0.x",
|
||||
"@nocobase/database": "0.x",
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
"@nocobase/test": "0.x",
|
||||
"@nocobase/plugin-auth": ">=0.17.0-alpha.7"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { LoginOutlined } from '@ant-design/icons';
|
||||
import { Authenticator, css, useAPIClient } from '@nocobase/client';
|
||||
import { css, useAPIClient } from '@nocobase/client';
|
||||
import { Button, Space, message } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useOidcTranslation } from './locale';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Authenticator } from '@nocobase/plugin-auth/client';
|
||||
|
||||
export interface OIDCProvider {
|
||||
clientId: string;
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { OptionsComponentProvider, SigninPageExtensionProvider } from '@nocobase/client';
|
||||
import React, { FC } from 'react';
|
||||
import { OIDCButton } from './OIDCButton';
|
||||
import { authType } from '../constants';
|
||||
import { Options } from './Options';
|
||||
|
||||
export const OidcProvider: FC = (props) => {
|
||||
return (
|
||||
<SigninPageExtensionProvider component={OIDCButton} authType={authType}>
|
||||
<OptionsComponentProvider authType={authType} component={Options}>
|
||||
{props.children}
|
||||
</OptionsComponentProvider>
|
||||
</SigninPageExtensionProvider>
|
||||
);
|
||||
};
|
@ -76,7 +76,7 @@ const schema = {
|
||||
'x-component-props': {
|
||||
style: {
|
||||
width: '15%',
|
||||
'min-width': '100px',
|
||||
minWidth: '100px',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,9 +1,18 @@
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { OidcProvider } from './OidcProvider';
|
||||
import AuthPlugin from '@nocobase/plugin-auth/client';
|
||||
import { authType } from '../constants';
|
||||
import { OIDCButton } from './OIDCButton';
|
||||
import { Options } from './Options';
|
||||
|
||||
export class OidcPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(OidcProvider);
|
||||
const auth = this.app.pm.get(AuthPlugin);
|
||||
auth.registerType(authType, {
|
||||
components: {
|
||||
SignInButton: OIDCButton,
|
||||
AdminSettingsForm: Options,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
"@nocobase/database": "0.x",
|
||||
"@nocobase/sdk": "0.x",
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
"@nocobase/test": "0.x",
|
||||
"@nocobase/plugin-auth": ">=0.17.0-alpha.7"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { LoginOutlined } from '@ant-design/icons';
|
||||
import { Authenticator, css, useAPIClient } from '@nocobase/client';
|
||||
import { css, useAPIClient } from '@nocobase/client';
|
||||
import { Button, Space, message } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSamlTranslation } from './locale';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Authenticator } from '@nocobase/plugin-auth/client';
|
||||
|
||||
export const SAMLButton = ({ authenticator }: { authenticator: Authenticator }) => {
|
||||
const { t } = useSamlTranslation();
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { OptionsComponentProvider, SigninPageExtensionProvider } from '@nocobase/client';
|
||||
import React, { FC } from 'react';
|
||||
import { SAMLButton } from './SAMLButton';
|
||||
import { Options } from './Options';
|
||||
import { authType } from '../constants';
|
||||
|
||||
export const SamlProvider: FC = (props) => {
|
||||
return (
|
||||
<SigninPageExtensionProvider component={SAMLButton} authType={authType}>
|
||||
<OptionsComponentProvider authType={authType} component={Options}>
|
||||
{props.children}
|
||||
</OptionsComponentProvider>
|
||||
</SigninPageExtensionProvider>
|
||||
);
|
||||
};
|
@ -1,9 +1,18 @@
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { SamlProvider } from './SamlProvider';
|
||||
import AuthPlugin from '@nocobase/plugin-auth/client';
|
||||
import { authType } from '../constants';
|
||||
import { SAMLButton } from './SAMLButton';
|
||||
import { Options } from './Options';
|
||||
|
||||
export class SamlPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(SamlProvider);
|
||||
const auth = this.app.pm.get(AuthPlugin);
|
||||
auth.registerType(authType, {
|
||||
components: {
|
||||
SignInButton: SAMLButton,
|
||||
AdminSettingsForm: Options,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,2 +0,0 @@
|
||||
/node_modules
|
||||
/src
|
@ -1,28 +0,0 @@
|
||||
# Custom sign up page
|
||||
|
||||
## Register
|
||||
|
||||
```ts
|
||||
yarn pm add sample-custom-signup-page
|
||||
```
|
||||
|
||||
## Activate
|
||||
|
||||
```bash
|
||||
yarn pm enable sample-custom-signup-page
|
||||
```
|
||||
|
||||
## Launch the app
|
||||
|
||||
```bash
|
||||
# for development
|
||||
yarn dev
|
||||
|
||||
# for production
|
||||
yarn build
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Visit the custom sign up page
|
||||
|
||||
Open [http://localhost:13000/signup](http://localhost:13000/signup) in a web browser.
|
@ -1,2 +0,0 @@
|
||||
export * from './dist/client';
|
||||
export { default } from './dist/client';
|
@ -1 +0,0 @@
|
||||
module.exports = require('./dist/client/index.js');
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-sample-custom-signup-page",
|
||||
"version": "0.17.0-alpha.7",
|
||||
"main": "./dist/server/index.js",
|
||||
"devDependencies": {
|
||||
"@formily/react": "2.x",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "0.x",
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
},
|
||||
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './dist/server';
|
||||
export { default } from './dist/server';
|
@ -1 +0,0 @@
|
||||
module.exports = require('./dist/server/index.js');
|
@ -1,34 +0,0 @@
|
||||
import { useForm } from '@formily/react';
|
||||
import { Plugin, SignupPage, useSignup } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
const useCustomSignup = () => {
|
||||
const { run } = useSignup();
|
||||
const form = useForm();
|
||||
return {
|
||||
async run() {
|
||||
console.log('useCustomSignup');
|
||||
form.setValuesIn('url', 'bb');
|
||||
await run();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const CustomSignupPage = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<div>Custom sign up page</div>
|
||||
<SignupPage {...props} scope={{ useSignup: useCustomSignup }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
class CustomSignupPagePlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.addComponents({
|
||||
SignupPage: CustomSignupPage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomSignupPagePlugin;
|
@ -1,2 +0,0 @@
|
||||
export * from './server';
|
||||
export { default } from './server';
|
@ -1,15 +0,0 @@
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
|
||||
export class CustomSchemaScopePlugin extends Plugin {
|
||||
beforeLoad() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
async load() {}
|
||||
|
||||
async install(options: InstallOptions) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomSchemaScopePlugin;
|
@ -17,7 +17,7 @@
|
||||
"@nocobase/auth": "0.x",
|
||||
"@nocobase/client": "0.x",
|
||||
"@nocobase/database": "0.x",
|
||||
"@nocobase/plugin-auth": "0.x",
|
||||
"@nocobase/plugin-auth": ">=0.17.0-alpha.7",
|
||||
"@nocobase/plugin-verification": "0.x",
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Authenticator, SchemaComponent, useSignIn } from '@nocobase/client';
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { ISchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import VerificationCode from './VerificationCode';
|
||||
import { Authenticator, useSignIn } from '@nocobase/plugin-auth/client';
|
||||
|
||||
const phoneForm: ISchema = {
|
||||
type: 'object',
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { OptionsComponentProvider, SigninPageProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { SigninPage } from './SigninPage';
|
||||
import { useAuthTranslation } from './locale';
|
||||
import { authType } from '../constants';
|
||||
import { Options } from './Options';
|
||||
|
||||
export const SmsAuthProvider = (props) => {
|
||||
const { t } = useAuthTranslation();
|
||||
return (
|
||||
<OptionsComponentProvider authType={authType} component={Options}>
|
||||
<SigninPageProvider authType={authType} tabTitle={t('Sign in via SMS')} component={SigninPage}>
|
||||
{props.children}
|
||||
</SigninPageProvider>
|
||||
</OptionsComponentProvider>
|
||||
);
|
||||
};
|
@ -1,9 +1,18 @@
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { SmsAuthProvider } from './SmsAuthProvider';
|
||||
import AuthPlugin from '@nocobase/plugin-auth/client';
|
||||
import { SigninPage } from './SigninPage';
|
||||
import { Options } from './Options';
|
||||
import { authType } from '../constants';
|
||||
|
||||
export class SmsAuthPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(SmsAuthProvider);
|
||||
const auth = this.app.pm.get(AuthPlugin);
|
||||
auth.registerType(authType, {
|
||||
components: {
|
||||
SignInForm: SigninPage,
|
||||
AdminSettingsForm: Options,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user