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:
YANG QIA 2023-12-21 20:19:25 +08:00 committed by GitHub
parent e68053b006
commit 06f11a2d08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 475 additions and 937 deletions

View File

@ -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 = {

View File

@ -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> {}

View File

@ -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) : <></>;
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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 });
};

View File

@ -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);
}
}

View File

@ -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';

View File

@ -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' });

View File

@ -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';

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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 }}
/>
);
};

View File

@ -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';

View File

@ -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>
);
};

View File

@ -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);
};

View File

@ -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")}}',

View File

@ -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 }} />;
}; };

View File

@ -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 }} />;
}; };

View File

@ -0,0 +1,3 @@
export * from './SignInForm';
export * from './SignUpForm';
export * from './Options';

View File

@ -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';

View File

@ -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>
<Outlet /> <AuthenticatorsContext.Provider value={authenticators as any}>
<Outlet />
</AuthenticatorsContext.Provider>
<div <div
className={css` className={css`
position: absolute; position: absolute;

View File

@ -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>
);
};

View File

@ -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 });
};

View File

@ -0,0 +1,3 @@
export * from './AuthLayout';
export * from './SignInPage';
export * from './SignUpPage';

View File

@ -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;
}); });

View File

@ -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"
} }

View File

@ -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": "登录"
} }

View File

@ -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);
});
}); });
}); });

View File

@ -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) => {
name: authenticator.name, const authType = authManager.getAuthConfig(authenticator.authType);
authType: authenticator.authType, return {
title: authenticator.title, name: authenticator.name,
options: authenticator.options?.public || {}, authType: authenticator.authType,
})); authTypeTitle: authType?.title || '',
title: authenticator.title,
options: authenticator.options?.public || {},
};
});
await next(); await next();
}, },
destroy: async (ctx: Context, next: Next) => { destroy: async (ctx: Context, next: Next) => {

View File

@ -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;
} }

View File

@ -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() {}
}

View File

@ -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,
}, },
{ {

View File

@ -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",

View File

@ -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();

View File

@ -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,
},
});
} }
} }

View File

@ -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"
} }
} }

View File

@ -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;

View File

@ -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>
);
};

View File

@ -76,7 +76,7 @@ const schema = {
'x-component-props': { 'x-component-props': {
style: { style: {
width: '15%', width: '15%',
'min-width': '100px', minWidth: '100px',
}, },
}, },
}, },

View File

@ -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,
},
});
} }
} }

View File

@ -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"
} }
} }

View File

@ -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();

View File

@ -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>
);
};

View File

@ -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,
},
});
} }
} }

View File

@ -1,2 +0,0 @@
/node_modules
/src

View File

@ -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.

View File

@ -1,2 +0,0 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -1 +0,0 @@
module.exports = require('./dist/client/index.js');

View File

@ -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"
}

View File

@ -1,2 +0,0 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -1 +0,0 @@
module.exports = require('./dist/server/index.js');

View File

@ -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;

View File

@ -1,2 +0,0 @@
export * from './server';
export { default } from './server';

View File

@ -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;

View File

@ -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"

View File

@ -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',

View File

@ -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>
);
};

View File

@ -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,
},
});
} }
} }