mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 12:26:22 +00:00
feat(basic-auth): allows to configure sign up form (#5639)
* feat(auth): allows to configure signup form * feat(basic-auth): allows to configure sign up form * test: remove only
This commit is contained in:
parent
31351f2d35
commit
6198c99706
@ -9,7 +9,8 @@
|
||||
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useAuthTranslation } from '../locale';
|
||||
import { lang, useAuthTranslation } from '../locale';
|
||||
import { FormTab, ArrayTable } from '@formily/antd-v5';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
export const Options = () => {
|
||||
@ -17,30 +18,137 @@ export const Options = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
scope={{ t }}
|
||||
components={{ Alert }}
|
||||
components={{ Alert, FormTab, ArrayTable }}
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
public: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowSignUp: {
|
||||
'x-decorator': 'FormItem',
|
||||
type: 'boolean',
|
||||
title: '{{t("Allow to sign up")}}',
|
||||
'x-component': 'Checkbox',
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
notice: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Alert',
|
||||
'x-component-props': {
|
||||
showIcon: true,
|
||||
message: '{{t("The authentication allows users to sign in via username or email.")}}',
|
||||
},
|
||||
},
|
||||
public: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
collapse: {
|
||||
type: 'void',
|
||||
'x-component': 'FormTab',
|
||||
properties: {
|
||||
basic: {
|
||||
type: 'void',
|
||||
'x-component': 'FormTab.TabPane',
|
||||
'x-component-props': {
|
||||
tab: lang('Sign up settings'),
|
||||
},
|
||||
properties: {
|
||||
allowSignUp: {
|
||||
'x-decorator': 'FormItem',
|
||||
type: 'boolean',
|
||||
title: '{{t("Allow to sign up")}}',
|
||||
'x-component': 'Checkbox',
|
||||
default: true,
|
||||
},
|
||||
signupForm: {
|
||||
title: '{{t("Sign up form")}}',
|
||||
type: 'array',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayTable',
|
||||
'x-component-props': {
|
||||
bordered: false,
|
||||
},
|
||||
'x-validator': `{{ (value) => {
|
||||
const field = value?.filter((item) => item.show && item.required);
|
||||
if (!field?.length) {
|
||||
return t('At least one field is required');
|
||||
}
|
||||
} }}`,
|
||||
default: [
|
||||
{
|
||||
field: 'username',
|
||||
show: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
show: false,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-decorator': 'ArrayItems.Item',
|
||||
properties: {
|
||||
column0: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayTable.Column',
|
||||
'x-component-props': { width: 20, align: 'center' },
|
||||
properties: {
|
||||
sort: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayTable.SortHandle',
|
||||
},
|
||||
},
|
||||
},
|
||||
column1: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayTable.Column',
|
||||
'x-component-props': { width: 100, title: lang('Field') },
|
||||
properties: {
|
||||
field: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: [
|
||||
{
|
||||
label: lang('Username'),
|
||||
value: 'username',
|
||||
},
|
||||
{
|
||||
label: lang('Email'),
|
||||
value: 'email',
|
||||
},
|
||||
],
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column2: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayTable.Column',
|
||||
'x-component-props': { width: 80, title: lang('Show') },
|
||||
properties: {
|
||||
show: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
},
|
||||
},
|
||||
column3: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayTable.Column',
|
||||
'x-component-props': { width: 80, title: lang('Required') },
|
||||
properties: {
|
||||
required: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import { ISchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useAuthTranslation } from '../locale';
|
||||
import { useAPIClient } from '@nocobase/client';
|
||||
@ -26,6 +26,23 @@ export interface UseSignupProps {
|
||||
};
|
||||
}
|
||||
|
||||
const schemas = {
|
||||
username: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
'x-validator': { username: true },
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Username")}}', style: {} },
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
'x-validator': 'email',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Email")}}', style: {} },
|
||||
},
|
||||
};
|
||||
|
||||
export const useSignUp = (props?: UseSignupProps) => {
|
||||
const navigate = useNavigate();
|
||||
const form = useForm();
|
||||
@ -43,19 +60,12 @@ export const useSignUp = (props?: UseSignupProps) => {
|
||||
};
|
||||
};
|
||||
|
||||
const signupPageSchema: ISchema = {
|
||||
const getSignupPageSchema = (fieldSchemas: any): ISchema => ({
|
||||
type: 'object',
|
||||
name: uid(),
|
||||
'x-component': 'FormV2',
|
||||
properties: {
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-validator': { username: true },
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Username")}}', style: {} },
|
||||
},
|
||||
...fieldSchemas,
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
@ -121,7 +131,7 @@ const signupPageSchema: ISchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const SignUpForm = ({ authenticatorName: name }: { authenticatorName: string }) => {
|
||||
const { t } = useAuthTranslation();
|
||||
@ -130,8 +140,27 @@ export const SignUpForm = ({ authenticatorName: name }: { authenticatorName: str
|
||||
};
|
||||
const authenticator = useAuthenticator(name);
|
||||
const { options } = authenticator;
|
||||
let { signupForm } = options;
|
||||
if (!(signupForm?.length && signupForm.some((item: any) => item.show && item.required))) {
|
||||
signupForm = [{ field: 'username', show: true, required: true }];
|
||||
}
|
||||
const fieldSchemas = useMemo(() => {
|
||||
return signupForm
|
||||
.filter((field: { show: boolean }) => field.show)
|
||||
.reduce((prev: any, item: { field: string; required: boolean }) => {
|
||||
const field = item.field;
|
||||
if (schemas[field]) {
|
||||
prev[field] = schemas[field];
|
||||
if (item.required) {
|
||||
prev[field].required = true;
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
}, {});
|
||||
}, [signupForm]);
|
||||
if (!options?.allowSignUp) {
|
||||
return <Navigate to="/not-found" replace={true} />;
|
||||
}
|
||||
return <SchemaComponent schema={signupPageSchema} scope={{ useBasicSignUp, t }} />;
|
||||
const schema = getSignupPageSchema(fieldSchemas);
|
||||
return <SchemaComponent schema={schema} scope={{ useBasicSignUp, t }} />;
|
||||
};
|
||||
|
@ -8,9 +8,14 @@
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18n } from '@nocobase/client';
|
||||
|
||||
export const NAMESPACE = 'auth';
|
||||
|
||||
export function useAuthTranslation() {
|
||||
return useTranslation([NAMESPACE, 'client'], { nsMode: 'fallback' });
|
||||
}
|
||||
|
||||
export function lang(key: string) {
|
||||
return i18n.t(key, { ns: [NAMESPACE, 'client'], nsMode: 'fallback' });
|
||||
}
|
||||
|
@ -24,5 +24,9 @@
|
||||
"The password is inconsistent, please re-enter": "The password is inconsistent, please re-enter",
|
||||
"Sign-in": "Sign-in",
|
||||
"Password": "Password",
|
||||
"The username/email or password is incorrect, please re-enter": "The username/email or password is incorrect, please re-enter"
|
||||
"The username/email or password is incorrect, please re-enter": "The username/email or password is incorrect, please re-enter",
|
||||
"Show": "Show",
|
||||
"Sign up settings": "Sign up settings",
|
||||
"Sign up form": "Sign up form",
|
||||
"At least one field is required": "At least one field is required"
|
||||
}
|
||||
|
@ -24,5 +24,9 @@
|
||||
"The password is inconsistent, please re-enter": "密码不一致,请重新输入",
|
||||
"Sign-in": "登录",
|
||||
"Password": "密码",
|
||||
"The username/email or password is incorrect, please re-enter": "用户名/邮箱或密码有误,请重新输入"
|
||||
"The username/email or password is incorrect, please re-enter": "用户名/邮箱或密码有误,请重新输入",
|
||||
"Show": "显示",
|
||||
"Sign up settings": "注册设置",
|
||||
"Sign up form": "注册表单",
|
||||
"At least one field is required": "至少需要设置一个必填字段"
|
||||
}
|
||||
|
@ -260,6 +260,11 @@ describe('actions', () => {
|
||||
});
|
||||
|
||||
it('should check username when signing up', async () => {
|
||||
const res1 = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
username: '',
|
||||
});
|
||||
expect(res1.statusCode).toEqual(400);
|
||||
expect(res1.error.text).toBe('Please enter a valid username');
|
||||
const res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
username: '@@',
|
||||
});
|
||||
@ -267,6 +272,39 @@ describe('actions', () => {
|
||||
expect(res.error.text).toBe('Please enter a valid username');
|
||||
});
|
||||
|
||||
it('should check email when signing up', async () => {
|
||||
const repo = db.getRepository('authenticators');
|
||||
await repo.update({
|
||||
filter: {
|
||||
name: 'basic',
|
||||
},
|
||||
values: {
|
||||
options: {
|
||||
public: {
|
||||
allowSignUp: true,
|
||||
signupForm: [{ field: 'email', show: true, required: true }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const res1 = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
email: '',
|
||||
});
|
||||
expect(res1.statusCode).toEqual(400);
|
||||
expect(res1.error.text).toBe('Please enter a valid email address');
|
||||
const res2 = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
email: 'abc',
|
||||
});
|
||||
expect(res2.statusCode).toEqual(400);
|
||||
expect(res2.error.text).toBe('Please enter a valid email address');
|
||||
const res3 = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
email: 'test1@nocobase.com',
|
||||
password: '123',
|
||||
confirm_password: '123',
|
||||
});
|
||||
expect(res3.statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
it('should check password when signing up', async () => {
|
||||
const res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({
|
||||
username: 'new',
|
||||
|
@ -50,6 +50,30 @@ export class BasicAuth extends BaseAuth {
|
||||
return user;
|
||||
}
|
||||
|
||||
private verfiySignupParams(values: any) {
|
||||
const options = this.authenticator.options?.public || {};
|
||||
let { signupForm } = options;
|
||||
if (!(signupForm?.length && signupForm.some((item: any) => item.show && item.required))) {
|
||||
signupForm = [{ field: 'username', show: true, required: true }];
|
||||
}
|
||||
const { username, email } = values;
|
||||
const usernameSetting = signupForm.find((item: any) => item.field === 'username');
|
||||
if (usernameSetting && usernameSetting.show) {
|
||||
if ((username && !this.validateUsername(username)) || (usernameSetting.required && !username)) {
|
||||
throw new Error('Please enter a valid username');
|
||||
}
|
||||
}
|
||||
const emailSetting = signupForm.find((item: any) => item.field === 'email');
|
||||
if (emailSetting && emailSetting.show) {
|
||||
if (email && !/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(email)) {
|
||||
throw new Error('Please enter a valid email address');
|
||||
}
|
||||
if (emailSetting.required && !email) {
|
||||
throw new Error('Please enter a valid email address');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async signUp() {
|
||||
const ctx = this.ctx;
|
||||
const options = this.authenticator.options?.public || {};
|
||||
@ -58,9 +82,11 @@ export class BasicAuth extends BaseAuth {
|
||||
}
|
||||
const User = ctx.db.getRepository('users');
|
||||
const { values } = ctx.action.params;
|
||||
const { username, password, confirm_password } = values;
|
||||
if (!this.validateUsername(username)) {
|
||||
ctx.throw(400, ctx.t('Please enter a valid username', { ns: namespace }));
|
||||
const { username, email, password, confirm_password } = values;
|
||||
try {
|
||||
this.verfiySignupParams(values);
|
||||
} catch (error) {
|
||||
ctx.throw(400, this.ctx.t(error.message, { ns: namespace }));
|
||||
}
|
||||
if (!password) {
|
||||
ctx.throw(400, ctx.t('Please enter a password', { ns: namespace }));
|
||||
@ -68,7 +94,7 @@ export class BasicAuth extends BaseAuth {
|
||||
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 } });
|
||||
const user = await User.create({ values: { username, email, password } });
|
||||
return user;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user