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 { Context, Next } from '@nocobase/actions';
|
||||||
import { Model } from '@nocobase/database';
|
|
||||||
import { Registry } from '@nocobase/utils';
|
import { Registry } from '@nocobase/utils';
|
||||||
import { Auth, AuthExtend } from './auth';
|
import { Auth, AuthExtend } from './auth';
|
||||||
import { JwtOptions, JwtService } from './base/jwt-service';
|
import { JwtOptions, JwtService } from './base/jwt-service';
|
||||||
import { ITokenBlacklistService } from './base/token-blacklist-service';
|
import { ITokenBlacklistService } from './base/token-blacklist-service';
|
||||||
|
|
||||||
|
export interface Authenticator {
|
||||||
|
authType: string;
|
||||||
|
options: Record<string, any>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Storer {
|
export interface Storer {
|
||||||
get: (name: string) => Promise<Model>;
|
get: (name: string) => Promise<Authenticator>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthManagerOptions = {
|
export type AuthManagerOptions = {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Context } from '@nocobase/actions';
|
import { Context } from '@nocobase/actions';
|
||||||
import { Model } from '@nocobase/database';
|
import { Model } from '@nocobase/database';
|
||||||
|
import { Authenticator } from './auth-manager';
|
||||||
|
|
||||||
export type AuthConfig = {
|
export type AuthConfig = {
|
||||||
authenticator: Model;
|
authenticator: Authenticator;
|
||||||
options: {
|
options: {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
@ -22,7 +23,7 @@ interface IAuth {
|
|||||||
|
|
||||||
export abstract class Auth implements IAuth {
|
export abstract class Auth implements IAuth {
|
||||||
abstract user: Model;
|
abstract user: Model;
|
||||||
protected authenticator: Model;
|
protected authenticator: Authenticator;
|
||||||
protected options: {
|
protected options: {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
@ -36,7 +37,7 @@ export abstract class Auth implements IAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The abstract methods are required to be implemented by all authentications.
|
// 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.
|
// The following methods are mainly designed for user authentications.
|
||||||
async signIn(): Promise<any> {}
|
async signIn(): Promise<any> {}
|
||||||
async signUp(): 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 './appInfo';
|
||||||
export * from './application';
|
export * from './application';
|
||||||
export * from './async-data-provider';
|
export * from './async-data-provider';
|
||||||
export * from './auth';
|
|
||||||
export * from './block-provider';
|
export * from './block-provider';
|
||||||
export * from './china-region';
|
export * from './china-region';
|
||||||
export * from './collection-manager';
|
export * from './collection-manager';
|
||||||
|
@ -9,12 +9,11 @@ import { ACLPlugin } from '../acl';
|
|||||||
import { useAPIClient } from '../api-client';
|
import { useAPIClient } from '../api-client';
|
||||||
import { Application } from '../application';
|
import { Application } from '../application';
|
||||||
import { Plugin } from '../application/Plugin';
|
import { Plugin } from '../application/Plugin';
|
||||||
import { SigninPage, SigninPageExtensionPlugin, SignupPage } from '../auth';
|
|
||||||
import { BlockSchemaComponentPlugin } from '../block-provider';
|
import { BlockSchemaComponentPlugin } from '../block-provider';
|
||||||
import { RemoteDocumentTitlePlugin } from '../document-title';
|
import { RemoteDocumentTitlePlugin } from '../document-title';
|
||||||
import { PinnedListPlugin } from '../plugin-manager';
|
import { PinnedListPlugin } from '../plugin-manager';
|
||||||
import { PMPlugin } from '../pm';
|
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 { AntdSchemaComponentPlugin, SchemaComponentPlugin, menuItemInitializer } from '../schema-component';
|
||||||
import { ErrorFallback } from '../schema-component/antd/error-fallback';
|
import { ErrorFallback } from '../schema-component/antd/error-fallback';
|
||||||
import { AssociationFilterPlugin, SchemaInitializerPlugin } from '../schema-initializer';
|
import { AssociationFilterPlugin, SchemaInitializerPlugin } from '../schema-initializer';
|
||||||
@ -285,25 +284,10 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
|||||||
path: '/admin/:name',
|
path: '/admin/:name',
|
||||||
Component: 'RouteSchemaComponent',
|
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() {
|
addComponents() {
|
||||||
this.app.addComponents({
|
this.app.addComponents({
|
||||||
AuthLayout,
|
|
||||||
SigninPage,
|
|
||||||
SignupPage,
|
|
||||||
ErrorFallback,
|
ErrorFallback,
|
||||||
RouteSchemaComponent,
|
RouteSchemaComponent,
|
||||||
BlockTemplatePage,
|
BlockTemplatePage,
|
||||||
@ -329,7 +313,6 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
|||||||
await this.app.pm.add(SchemaInitializerPlugin, { name: 'schema-initializer' });
|
await this.app.pm.add(SchemaInitializerPlugin, { name: 'schema-initializer' });
|
||||||
await this.app.pm.add(BlockSchemaComponentPlugin, { name: 'block-schema-component' });
|
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(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(ACLPlugin, { name: 'builtin-acl' });
|
||||||
await this.app.pm.add(RemoteDocumentTitlePlugin, { name: 'remote-document-title' });
|
await this.app.pm.add(RemoteDocumentTitlePlugin, { name: 'remote-document-title' });
|
||||||
await this.app.pm.add(PMPlugin, { name: 'builtin-pm' });
|
await this.app.pm.add(PMPlugin, { name: 'builtin-pm' });
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from './admin-layout';
|
export * from './admin-layout';
|
||||||
export * from './auth-layout';
|
|
||||||
export * from './route-schema-component';
|
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 './CurrentUser';
|
||||||
export * from './CurrentUserProvider';
|
export * from './CurrentUserProvider';
|
||||||
export * from './CurrentUserSettingsMenuProvider';
|
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: {
|
public: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
allowSignup: {
|
allowSignUp: {
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: '{{t("Allow to sign up")}}',
|
title: '{{t("Allow to sign up")}}',
|
||||||
|
@ -1,7 +1,34 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { Authenticator, SchemaComponent, SignupPageContext, useSignIn } from '@nocobase/client';
|
import { SchemaComponent, useAPIClient, useCurrentUserContext } from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useAuthTranslation } from '../locale';
|
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 = {
|
const passwordForm: ISchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -51,27 +78,27 @@ const passwordForm: ISchema = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
signup: {
|
signUp: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'Link',
|
'x-component': 'Link',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
to: '{{ signupLink }}',
|
to: '{{ signUpLink }}',
|
||||||
},
|
},
|
||||||
'x-content': '{{t("Create an account")}}',
|
'x-content': '{{t("Create an account")}}',
|
||||||
'x-visible': '{{ allowSignUp }}',
|
'x-visible': '{{ allowSignUp }}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export default (props: { authenticator: Authenticator }) => {
|
export const SignInForm = (props: { authenticator: Authenticator }) => {
|
||||||
const { t } = useAuthTranslation();
|
const { t } = useAuthTranslation();
|
||||||
const authenticator = props.authenticator;
|
const authenticator = props.authenticator;
|
||||||
const { authType, name, options } = authenticator;
|
const { authType, name, options } = authenticator;
|
||||||
const signupPages = useContext(SignupPageContext);
|
const signUpPages = useSignUpForms();
|
||||||
const allowSignUp = !!signupPages[authType] && options?.allowSignup;
|
const allowSignUp = !!signUpPages[authType] && options?.allowSignUp;
|
||||||
const signupLink = `/signup?authType=${authType}&name=${name}`;
|
const signUpLink = `/signup?name=${name}`;
|
||||||
|
|
||||||
const useBasicSignIn = () => {
|
const useBasicSignIn = () => {
|
||||||
return useSignIn(name);
|
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 { ISchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { useAuthTranslation } from '../locale';
|
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 = {
|
const signupPageSchema: ISchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -63,7 +93,7 @@ const signupPageSchema: ISchema = {
|
|||||||
block: true,
|
block: true,
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
htmlType: 'submit',
|
htmlType: 'submit',
|
||||||
useAction: '{{ useBasicSignup }}',
|
useAction: '{{ useBasicSignUp }}',
|
||||||
style: { width: '100%' },
|
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 { t } = useAuthTranslation();
|
||||||
const useBasicSignup = () => {
|
const useBasicSignUp = () => {
|
||||||
return useSignup({ authenticator: props.name });
|
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 { Plugin } from '@nocobase/client';
|
||||||
import { AuthPluginProvider } from './AuthPluginProvider';
|
|
||||||
import { AuthProvider } from './AuthProvider';
|
import { AuthProvider } from './AuthProvider';
|
||||||
import { NAMESPACE } from './locale';
|
import { NAMESPACE } from './locale';
|
||||||
import { Authenticator } from './settings/Authenticator';
|
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 {
|
export class AuthPlugin extends Plugin {
|
||||||
|
authTypes = new Registry<AuthOptions>();
|
||||||
|
|
||||||
|
registerType(authType: string, options: AuthOptions) {
|
||||||
|
this.authTypes.register(authType, options);
|
||||||
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.app.pluginSettingsManager.add(NAMESPACE, {
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
icon: 'LoginOutlined',
|
icon: 'LoginOutlined',
|
||||||
@ -12,9 +32,38 @@ export class AuthPlugin extends Plugin {
|
|||||||
Component: Authenticator,
|
Component: Authenticator,
|
||||||
aclSnippet: 'pm.auth.authenticators',
|
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.providers.unshift([AuthProvider, {}]);
|
||||||
this.app.use(AuthPluginProvider);
|
|
||||||
|
this.registerType(presetAuthType, {
|
||||||
|
components: {
|
||||||
|
SignInForm: SignInForm,
|
||||||
|
SignUpForm: SignUpForm,
|
||||||
|
AdminSettingsForm: Options,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AuthPlugin;
|
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 { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { PoweredBy } from '../../../powered-by';
|
import { useSystemSettings, PoweredBy, useRequest, useAPIClient } from '@nocobase/client';
|
||||||
import { useSystemSettings } from '../../../system-settings';
|
import { AuthenticatorsContext } from '../authenticator';
|
||||||
|
|
||||||
export function AuthLayout(props: any) {
|
export function AuthLayout(props: any) {
|
||||||
const { data } = useSystemSettings();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -15,7 +29,9 @@ export function AuthLayout(props: any) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h1>{data?.data?.title}</h1>
|
<h1>{data?.data?.title}</h1>
|
||||||
|
<AuthenticatorsContext.Provider value={authenticators as any}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</AuthenticatorsContext.Provider>
|
||||||
<div
|
<div
|
||||||
className={css`
|
className={css`
|
||||||
position: absolute;
|
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 { 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 { useEffect } from 'react';
|
||||||
|
import AuthPlugin from '..';
|
||||||
|
|
||||||
export const useValuesFromOptions = (options) => {
|
export const useValuesFromOptions = (options) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
@ -26,9 +28,15 @@ export const useValuesFromOptions = (options) => {
|
|||||||
return result;
|
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(() => {
|
export const Options = observer(() => {
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
const component = useOptionsComponent(form.values.authType || record.authType);
|
const Component = useAdminSettingsForm(form.values.authType || record.authType);
|
||||||
return component;
|
return Component ? <Component /> : null;
|
||||||
});
|
});
|
||||||
|
@ -19,5 +19,8 @@
|
|||||||
"SMS": "SMS",
|
"SMS": "SMS",
|
||||||
"Username/Email": "Username/Email",
|
"Username/Email": "Username/Email",
|
||||||
"Auth UID": "Auth UID",
|
"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": "短信",
|
"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.": "该认证方式支持用户通过用户名或邮箱登录。",
|
||||||
|
"No authentication methods available.": "没有可用的认证方式。",
|
||||||
|
"The password is inconsistent, please re-enter": "密码不一致,请重新输入",
|
||||||
|
"Sign-in": "登录"
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ describe('actions', () => {
|
|||||||
let db: Database;
|
let db: Database;
|
||||||
let agent;
|
let agent;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeEach(async () => {
|
||||||
app = mockServer();
|
app = mockServer();
|
||||||
process.env.INIT_ROOT_EMAIL = 'test@nocobase.com';
|
process.env.INIT_ROOT_EMAIL = 'test@nocobase.com';
|
||||||
process.env.INT_ROOT_USERNAME = 'test';
|
process.env.INT_ROOT_USERNAME = 'test';
|
||||||
@ -97,8 +97,8 @@ describe('actions', () => {
|
|||||||
agent = app.agent();
|
agent = app.agent();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterEach(async () => {
|
||||||
await db.close();
|
await app.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sign in with password', async () => {
|
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({
|
let res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||||
username: 'new',
|
username: 'new',
|
||||||
password: 'new',
|
password: 'new',
|
||||||
|
confirm_password: 'new',
|
||||||
});
|
});
|
||||||
expect(res.statusCode).toEqual(200);
|
expect(res.statusCode).toEqual(200);
|
||||||
|
|
||||||
@ -199,5 +200,14 @@ describe('actions', () => {
|
|||||||
});
|
});
|
||||||
expect(res2.statusCode).toEqual(200);
|
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 { Context, Next } from '@nocobase/actions';
|
||||||
import { Model, Repository } from '@nocobase/database';
|
import { Model, Repository } from '@nocobase/database';
|
||||||
import { namespace } from '../../preset';
|
import { namespace } from '../../preset';
|
||||||
|
import { AuthManager } from '@nocobase/auth';
|
||||||
|
|
||||||
async function checkCount(repository: Repository, id: number[]) {
|
async function checkCount(repository: Repository, id: number[]) {
|
||||||
// TODO(yangqia): This is a temporary solution, may cause concurrency problem.
|
// TODO(yangqia): This is a temporary solution, may cause concurrency problem.
|
||||||
@ -24,6 +25,7 @@ export default {
|
|||||||
},
|
},
|
||||||
publicList: async (ctx: Context, next: Next) => {
|
publicList: async (ctx: Context, next: Next) => {
|
||||||
const repo = ctx.db.getRepository('authenticators');
|
const repo = ctx.db.getRepository('authenticators');
|
||||||
|
const authManager = ctx.app.authManager as AuthManager;
|
||||||
const authenticators = await repo.find({
|
const authenticators = await repo.find({
|
||||||
fields: ['name', 'authType', 'title', 'options', 'sort'],
|
fields: ['name', 'authType', 'title', 'options', 'sort'],
|
||||||
filter: {
|
filter: {
|
||||||
@ -31,12 +33,16 @@ export default {
|
|||||||
},
|
},
|
||||||
sort: 'sort',
|
sort: 'sort',
|
||||||
});
|
});
|
||||||
ctx.body = authenticators.map((authenticator: Model) => ({
|
ctx.body = authenticators.map((authenticator: Model) => {
|
||||||
|
const authType = authManager.getAuthConfig(authenticator.authType);
|
||||||
|
return {
|
||||||
name: authenticator.name,
|
name: authenticator.name,
|
||||||
authType: authenticator.authType,
|
authType: authenticator.authType,
|
||||||
|
authTypeTitle: authType?.title || '',
|
||||||
title: authenticator.title,
|
title: authenticator.title,
|
||||||
options: authenticator.options?.public || {},
|
options: authenticator.options?.public || {},
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
await next();
|
await next();
|
||||||
},
|
},
|
||||||
destroy: async (ctx: Context, next: Next) => {
|
destroy: async (ctx: Context, next: Next) => {
|
||||||
|
@ -51,11 +51,14 @@ export class BasicAuth extends BaseAuth {
|
|||||||
}
|
}
|
||||||
const User = ctx.db.getRepository('users');
|
const User = ctx.db.getRepository('users');
|
||||||
const { values } = ctx.action.params;
|
const { values } = ctx.action.params;
|
||||||
const { username } = values;
|
const { username, password, confirm_password } = values;
|
||||||
if (!/^[^@.<>"'/]{2,16}$/.test(username)) {
|
if (!/^[^@.<>"'/]{2,16}$/.test(username)) {
|
||||||
ctx.throw(400, ctx.t('Please enter a valid username', { ns: namespace }));
|
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;
|
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';
|
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) {
|
async findUser(uuid: string) {
|
||||||
let user: Model;
|
let user: Model;
|
||||||
const users = await this.getUsers({
|
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;
|
let user: Model;
|
||||||
const db: Database = (this.constructor as any).database;
|
const db: Database = (this.constructor as any).database;
|
||||||
await this.sequelize.transaction(async (transaction) => {
|
await this.sequelize.transaction(async (transaction) => {
|
||||||
// Create a new user if not exists
|
// Create a new user if not exists
|
||||||
user = await this.createUser(
|
user = await this.createUser(
|
||||||
values || {
|
userValues || {
|
||||||
nickname: uuid,
|
nickname: uuid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
"@nocobase/database": "0.x",
|
"@nocobase/database": "0.x",
|
||||||
"@nocobase/sdk": "0.x",
|
"@nocobase/sdk": "0.x",
|
||||||
"@nocobase/server": "0.x",
|
"@nocobase/server": "0.x",
|
||||||
"@nocobase/test": "0.x"
|
"@nocobase/test": "0.x",
|
||||||
|
"@nocobase/plugin-auth": ">=0.17.0-alpha.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "5.x",
|
"@ant-design/icons": "5.x",
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Authenticator } from '@nocobase/client';
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { LoginOutlined } from '@ant-design/icons';
|
import { LoginOutlined } from '@ant-design/icons';
|
||||||
import { Button, Space, message } from 'antd';
|
import { Button, Space, message } from 'antd';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { getSubAppName } from '@nocobase/sdk';
|
import { getSubAppName } from '@nocobase/sdk';
|
||||||
|
import { Authenticator } from '@nocobase/plugin-auth/client';
|
||||||
|
|
||||||
export const SigninPage = (props: { authenticator: Authenticator }) => {
|
export const SigninPage = (props: { authenticator: Authenticator }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -1,24 +1,18 @@
|
|||||||
import { OptionsComponentProvider, SigninPageExtensionProvider, SignupPageProvider } from '@nocobase/client';
|
|
||||||
import React from 'react';
|
|
||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
|
|
||||||
import { SigninPage } from './SigninPage';
|
import { SigninPage } from './SigninPage';
|
||||||
import { Options } from './Options';
|
import { Options } from './Options';
|
||||||
import { authType } from '../constants';
|
import { authType } from '../constants';
|
||||||
|
import AuthPlugin from '@nocobase/plugin-auth/client';
|
||||||
export function CASProvider(props) {
|
|
||||||
return (
|
|
||||||
<OptionsComponentProvider authType={authType} component={Options}>
|
|
||||||
<SigninPageExtensionProvider authType={authType} component={SigninPage}>
|
|
||||||
{props.children}
|
|
||||||
</SigninPageExtensionProvider>
|
|
||||||
</OptionsComponentProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SamlPlugin extends Plugin {
|
export class SamlPlugin extends Plugin {
|
||||||
async load() {
|
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/client": "0.x",
|
||||||
"@nocobase/database": "0.x",
|
"@nocobase/database": "0.x",
|
||||||
"@nocobase/server": "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 { 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 { Button, Space, message } from 'antd';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useOidcTranslation } from './locale';
|
import { useOidcTranslation } from './locale';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { Authenticator } from '@nocobase/plugin-auth/client';
|
||||||
|
|
||||||
export interface OIDCProvider {
|
export interface OIDCProvider {
|
||||||
clientId: string;
|
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': {
|
'x-component-props': {
|
||||||
style: {
|
style: {
|
||||||
width: '15%',
|
width: '15%',
|
||||||
'min-width': '100px',
|
minWidth: '100px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
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 {
|
export class OidcPlugin extends Plugin {
|
||||||
async load() {
|
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/database": "0.x",
|
||||||
"@nocobase/sdk": "0.x",
|
"@nocobase/sdk": "0.x",
|
||||||
"@nocobase/server": "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 { 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 { Button, Space, message } from 'antd';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useSamlTranslation } from './locale';
|
import { useSamlTranslation } from './locale';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { Authenticator } from '@nocobase/plugin-auth/client';
|
||||||
|
|
||||||
export const SAMLButton = ({ authenticator }: { authenticator: Authenticator }) => {
|
export const SAMLButton = ({ authenticator }: { authenticator: Authenticator }) => {
|
||||||
const { t } = useSamlTranslation();
|
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 { 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 {
|
export class SamlPlugin extends Plugin {
|
||||||
async load() {
|
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/auth": "0.x",
|
||||||
"@nocobase/client": "0.x",
|
"@nocobase/client": "0.x",
|
||||||
"@nocobase/database": "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/plugin-verification": "0.x",
|
||||||
"@nocobase/server": "0.x",
|
"@nocobase/server": "0.x",
|
||||||
"@nocobase/test": "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 { ISchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import VerificationCode from './VerificationCode';
|
import VerificationCode from './VerificationCode';
|
||||||
|
import { Authenticator, useSignIn } from '@nocobase/plugin-auth/client';
|
||||||
|
|
||||||
const phoneForm: ISchema = {
|
const phoneForm: ISchema = {
|
||||||
type: 'object',
|
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 { 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 {
|
export class SmsAuthPlugin extends Plugin {
|
||||||
async load() {
|
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