From be6b949f559872698b500a18967c9e1f6a3df7b0 Mon Sep 17 00:00:00 2001 From: YANG QIA <2013xile@gmail.com> Date: Sat, 19 Aug 2023 22:02:26 +0800 Subject: [PATCH] feat(auth): support signing in with username (#2376) * feat(auth): support signing in with username * chore: compatibility && add INIT_ROOT_USERNAME * chore: add default username of root user * chore: add notice * fix: typo * chore: change rule of username * fix: sqlite add unique constraint --------- Co-authored-by: chenos --- .env.example | 1 + .env.test.example | 1 + packages/core/auth/src/auth-manager.ts | 6 ++- .../collection-manager/interfaces/index.ts | 1 + .../collection-manager/interfaces/username.ts | 40 +++++++++++++++++++ packages/core/client/src/locale/zh_CN.ts | 2 + packages/core/client/src/user/CurrentUser.tsx | 2 +- packages/core/client/src/user/EditProfile.tsx | 9 ++++- .../templates/app/.env.tpl | 1 + .../plugins/auth/src/client/AuthProvider.tsx | 2 +- .../plugins/auth/src/client/basic/Options.tsx | 10 +++++ .../auth/src/client/basic/SigninPage.tsx | 22 +++++++--- .../auth/src/client/basic/SignupPage.tsx | 10 +++-- .../src/client/settings/Authenticator.tsx | 12 +++--- .../client/settings/schemas/authenticators.ts | 9 +++-- packages/plugins/auth/src/locale/zh-CN.ts | 12 +++++- .../auth/src/server/__tests__/actions.test.ts | 25 ++++++++---- .../plugins/auth/src/server/basic-auth.ts | 24 +++++++---- .../plugins/auth/src/server/locale/zh-CN.ts | 5 ++- .../20230506152253-basic-authenticator.ts | 2 +- packages/plugins/auth/src/server/plugin.ts | 3 +- .../plugins/sms-auth/src/server/sms-auth.ts | 1 - .../users/src/server/collections/users.ts | 13 ++++++ .../migrations/20230802170800-add-username.ts | 39 ++++++++++++++++++ .../20230802183100-update-username.ts | 29 ++++++++++++++ packages/plugins/users/src/server/server.ts | 8 +++- packages/presets/nocobase/src/server/index.ts | 34 ++++++++++++++++ yarn.lock | 13 +++++- 28 files changed, 289 insertions(+), 47 deletions(-) create mode 100644 packages/core/client/src/collection-manager/interfaces/username.ts create mode 100644 packages/plugins/users/src/server/migrations/20230802170800-add-username.ts create mode 100644 packages/plugins/users/src/server/migrations/20230802183100-update-username.ts diff --git a/.env.example b/.env.example index def91805e..b8878ebe0 100644 --- a/.env.example +++ b/.env.example @@ -45,6 +45,7 @@ DB_TABLE_PREFIX= INIT_ROOT_EMAIL=admin@nocobase.com INIT_ROOT_PASSWORD=admin123 INIT_ROOT_NICKNAME=Super Admin +INIT_ROOT_USERNAME=nocobase # local or ali-oss DEFAULT_STORAGE_TYPE=local diff --git a/.env.test.example b/.env.test.example index d6c983ea6..f34afedcd 100644 --- a/.env.test.example +++ b/.env.test.example @@ -29,6 +29,7 @@ DB_STORAGE=storage/db/nocobase-test.sqlite INIT_ROOT_EMAIL=admin@nocobase.com INIT_ROOT_PASSWORD=admin123 INIT_ROOT_NICKNAME=Super Admin +INIT_ROOT_USERNAME=nocobase # local or ali-oss DEFAULT_STORAGE_TYPE=local diff --git a/packages/core/auth/src/auth-manager.ts b/packages/core/auth/src/auth-manager.ts index e028fba9e..59a394fc4 100644 --- a/packages/core/auth/src/auth-manager.ts +++ b/packages/core/auth/src/auth-manager.ts @@ -17,6 +17,7 @@ type AuthManagerOptions = { type AuthConfig = { auth: AuthExtend; // The authentication class. + title?: string; // The display name of the authentication type. }; export class AuthManager { @@ -52,7 +53,10 @@ export class AuthManager { } listTypes() { - return Array.from(this.authTypes.getKeys()); + return Array.from(this.authTypes.getEntities()).map(([authType, authConfig]) => ({ + name: authType, + title: authConfig.title, + })); } getAuthConfig(authType: string) { diff --git a/packages/core/client/src/collection-manager/interfaces/index.ts b/packages/core/client/src/collection-manager/interfaces/index.ts index 98a790fb6..a8cf312df 100644 --- a/packages/core/client/src/collection-manager/interfaces/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/index.ts @@ -33,3 +33,4 @@ export * from './time'; export * from './updatedAt'; export * from './updatedBy'; export * from './url'; +export * from './username'; diff --git a/packages/core/client/src/collection-manager/interfaces/username.ts b/packages/core/client/src/collection-manager/interfaces/username.ts new file mode 100644 index 000000000..6039cbcfa --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/username.ts @@ -0,0 +1,40 @@ +import { i18n } from '../../i18n'; +import { defaultProps, operators, unique } from './properties'; +import { IField } from './types'; +import { registerValidateRules } from '@formily/core'; + +registerValidateRules({ + username(value) { + return /^[^@.<>"'/]{2,16}$/.test(value) || i18n.t('2 to 16 characters (excluding @.<>"\'/)'); + }, +}); + +export const username: IField = { + name: 'username', + type: 'object', + group: 'basic', + order: 1, + title: '{{t("Username")}}', + sortable: true, + default: { + type: 'string', + // name, + uiSchema: { + type: 'string', + // title, + 'x-component': 'Input', + 'x-validator': { username: true }, + required: true, + }, + }, + availableTypes: ['string'], + hasDefaultValue: false, + properties: { + ...defaultProps, + unique, + }, + filterable: { + operators: operators.string, + }, + titleUsable: true, +}; diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 8628eb58e..7a5c7bd47 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -247,6 +247,7 @@ export default { "Long text": "多行文本", "Phone": "手机号码", "Email": "电子邮箱", + "Username": "用户名", 'Null': '空值', "Boolean": "逻辑值", "Number": "数字", @@ -472,6 +473,7 @@ export default { "Verification code": "验证码", "Send code": "发送验证码", "Retry after {{count}} seconds": "{{count}} 秒后重试", + '2 to 16 characters (excluding @.<>"\'/)': '2到16个字符(不包含@.<>"\'/)', "Roles": "角色", "Add role": "添加角色", diff --git a/packages/core/client/src/user/CurrentUser.tsx b/packages/core/client/src/user/CurrentUser.tsx index 4689cff83..b3b85c299 100644 --- a/packages/core/client/src/user/CurrentUser.tsx +++ b/packages/core/client/src/user/CurrentUser.tsx @@ -184,7 +184,7 @@ export const CurrentUser = () => { `} style={{ cursor: 'pointer', border: 0, padding: '16px', color: 'rgba(255, 255, 255, 0.65)' }} > - {data?.data?.nickname || data?.data?.email} + {data?.data?.nickname || data?.data?.username || data?.data?.email} diff --git a/packages/core/client/src/user/EditProfile.tsx b/packages/core/client/src/user/EditProfile.tsx index 9ad6f1701..3dba66b72 100644 --- a/packages/core/client/src/user/EditProfile.tsx +++ b/packages/core/client/src/user/EditProfile.tsx @@ -70,6 +70,13 @@ const schema: ISchema = { title: "{{t('Nickname')}}", 'x-decorator': 'FormItem', 'x-component': 'Input', + }, + username: { + type: 'string', + title: '{{t("Username")}}', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-validator': { username: true }, required: true, }, email: { @@ -78,7 +85,6 @@ const schema: ISchema = { 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'email', - required: true, }, phone: { type: 'string', @@ -86,7 +92,6 @@ const schema: ISchema = { 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'phone', - required: true, }, footer: { 'x-component': 'Action.Drawer.Footer', diff --git a/packages/core/create-nocobase-app/templates/app/.env.tpl b/packages/core/create-nocobase-app/templates/app/.env.tpl index 2271f8d44..0dc14dc6a 100644 --- a/packages/core/create-nocobase-app/templates/app/.env.tpl +++ b/packages/core/create-nocobase-app/templates/app/.env.tpl @@ -14,3 +14,4 @@ DB_DIALECT={{{env.DB_DIALECT}}} INIT_ROOT_EMAIL=admin@nocobase.com INIT_ROOT_PASSWORD=admin123 INIT_ROOT_NICKNAME=Super Admin +INIT_ROOT_USERNAME=nocobase diff --git a/packages/plugins/auth/src/client/AuthProvider.tsx b/packages/plugins/auth/src/client/AuthProvider.tsx index d2e3115d6..0e9410c05 100644 --- a/packages/plugins/auth/src/client/AuthProvider.tsx +++ b/packages/plugins/auth/src/client/AuthProvider.tsx @@ -30,7 +30,7 @@ export const AuthProvider: FC = (props) => { }} > - + {props.children} diff --git a/packages/plugins/auth/src/client/basic/Options.tsx b/packages/plugins/auth/src/client/basic/Options.tsx index f8d4aa210..6392104b2 100644 --- a/packages/plugins/auth/src/client/basic/Options.tsx +++ b/packages/plugins/auth/src/client/basic/Options.tsx @@ -1,12 +1,14 @@ import { SchemaComponent } from '@nocobase/client'; import React from 'react'; import { useAuthTranslation } from '../locale'; +import { Alert } from 'antd'; export const Options = () => { const { t } = useAuthTranslation(); return ( { }, }, }, + notice: { + type: 'void', + 'x-component': 'Alert', + 'x-component-props': { + showIcon: true, + message: '{{t("The authentication allows users to sign in via username or email.")}}', + }, + }, }, }} /> diff --git a/packages/plugins/auth/src/client/basic/SigninPage.tsx b/packages/plugins/auth/src/client/basic/SigninPage.tsx index e10ef75ba..5cdef680a 100644 --- a/packages/plugins/auth/src/client/basic/SigninPage.tsx +++ b/packages/plugins/auth/src/client/basic/SigninPage.tsx @@ -1,19 +1,30 @@ import { ISchema } from '@formily/react'; import { Authenticator, SchemaComponent, SignupPageContext, useSignIn } from '@nocobase/client'; import React, { useContext } from 'react'; +import { useAuthTranslation } from '../locale'; const passwordForm: ISchema = { type: 'object', name: 'passwordForm', 'x-component': 'FormV2', properties: { - email: { + account: { type: 'string', - required: true, 'x-component': 'Input', - 'x-validator': 'email', + 'x-validator': `{{(value) => { + if (!value) { + return t("Please enter your username or email"); + } + if (value.includes('@')) { + if (!/^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$/.test(value)) { + return t("Please enter a valid email"); + } + } else { + return /^[^@.<>"'/]{2,16}$/.test(value) || t("Please enter a valid username"); + } + }}}`, 'x-decorator': 'FormItem', - 'x-component-props': { placeholder: '{{t("Email")}}', style: {} }, + 'x-component-props': { placeholder: '{{t("Username/Email")}}', style: {} }, }, password: { type: 'string', @@ -52,6 +63,7 @@ const passwordForm: ISchema = { }, }; export default (props: { authenticator: Authenticator }) => { + const { t } = useAuthTranslation(); const authenticator = props.authenticator; const { authType, name, options } = authenticator; const signupPages = useContext(SignupPageContext); @@ -61,5 +73,5 @@ export default (props: { authenticator: Authenticator }) => { const useBasicSignIn = () => { return useSignIn(name); }; - return ; + return ; }; diff --git a/packages/plugins/auth/src/client/basic/SignupPage.tsx b/packages/plugins/auth/src/client/basic/SignupPage.tsx index b8f9c432f..aa4fe54df 100644 --- a/packages/plugins/auth/src/client/basic/SignupPage.tsx +++ b/packages/plugins/auth/src/client/basic/SignupPage.tsx @@ -2,19 +2,20 @@ import { SchemaComponent, useSignup } from '@nocobase/client'; import { ISchema } from '@formily/react'; import React from 'react'; import { uid } from '@formily/shared'; +import { useAuthTranslation } from '../locale'; const signupPageSchema: ISchema = { type: 'object', name: uid(), 'x-component': 'FormV2', properties: { - email: { + username: { type: 'string', required: true, 'x-component': 'Input', - 'x-validator': 'email', + 'x-validator': { username: true }, 'x-decorator': 'FormItem', - 'x-component-props': { placeholder: '{{t("Email")}}', style: {} }, + 'x-component-props': { placeholder: '{{t("Username")}}', style: {} }, }, password: { type: 'string', @@ -84,8 +85,9 @@ const signupPageSchema: ISchema = { }; export default (props: { name: string }) => { + const { t } = useAuthTranslation(); const useBasicSignup = () => { return useSignup({ authenticator: props.name }); }; - return ; + return ; }; diff --git a/packages/plugins/auth/src/client/settings/Authenticator.tsx b/packages/plugins/auth/src/client/settings/Authenticator.tsx index 6d076f6fa..4a613d05c 100644 --- a/packages/plugins/auth/src/client/settings/Authenticator.tsx +++ b/packages/plugins/auth/src/client/settings/Authenticator.tsx @@ -14,6 +14,7 @@ import { PlusOutlined, DownOutlined } from '@ant-design/icons'; import { AuthTypeContext, AuthTypesContext, useAuthTypes } from './authType'; import { useValuesFromOptions, Options } from './Options'; import { useTranslation } from 'react-i18next'; +import { useAuthTranslation } from '../locale'; const useCloseAction = () => { const { setVisible } = useActionContext(); @@ -59,6 +60,7 @@ const useCanNotDelete = () => { }; export const Authenticator = () => { + const { t } = useAuthTranslation(); const [types, setTypes] = useState([]); const api = useAPIClient(); useRequest( @@ -68,10 +70,10 @@ export const Authenticator = () => { .listTypes() .then((res) => { const types = res?.data?.data || []; - return types.map((type: string) => ({ - key: type, - label: type, - value: type, + return types.map((type: { name: string; title?: string }) => ({ + key: type.name, + label: t(type.title || type.name), + value: type.name, })); }), { @@ -87,7 +89,7 @@ export const Authenticator = () => { diff --git a/packages/plugins/auth/src/client/settings/schemas/authenticators.ts b/packages/plugins/auth/src/client/settings/schemas/authenticators.ts index de90c3d46..0b618671e 100644 --- a/packages/plugins/auth/src/client/settings/schemas/authenticators.ts +++ b/packages/plugins/auth/src/client/settings/schemas/authenticators.ts @@ -21,7 +21,7 @@ const collection = { name: 'name', uiSchema: { type: 'string', - title: '{{t("Name")}}', + title: '{{t("Auth UID")}}', 'x-component': 'Input', required: true, }, @@ -32,8 +32,9 @@ const collection = { name: 'authType', uiSchema: { type: 'string', - title: '{{t("Auth Type", {ns: "auth"})}}', + title: '{{t("Auth Type")}}', 'x-component': 'Select', + dataSource: '{{ types }}', required: true, }, }, @@ -251,14 +252,16 @@ export const authenticatorsSchema: ISchema = { }, }, authType: { + title: '{{t("Auth Type")}}', type: 'void', 'x-decorator': 'Table.Column.Decorator', 'x-component': 'Table.Column', properties: { authType: { type: 'string', - 'x-component': 'CollectionField', + 'x-component': 'Select', 'x-read-pretty': true, + enum: '{{ types }}', }, }, }, diff --git a/packages/plugins/auth/src/locale/zh-CN.ts b/packages/plugins/auth/src/locale/zh-CN.ts index aa7800971..73ffbd3a6 100644 --- a/packages/plugins/auth/src/locale/zh-CN.ts +++ b/packages/plugins/auth/src/locale/zh-CN.ts @@ -3,15 +3,23 @@ const locale = { Authenticators: '认证器', Authentication: '用户认证', 'Sign in via email': '邮箱登录', + 'Sign in via password': '密码登录', 'Not allowed to sign up': '禁止注册', 'Allow to sign up': '允许注册', - 'The email is incorrect, please re-enter': '邮箱有误,请重新输入', - 'Please fill in your email address': '请填写邮箱', + 'The username or email is incorrect, please re-enter': '用户名或邮箱有误,请重新输入', 'The password is incorrect, please re-enter': '密码有误,请重新输入', 'Not a valid cellphone number, please re-enter': '不是有效的手机号,请重新输入', 'The phone number has been registered, please login directly': '手机号已注册,请直接登录', 'The phone number is not registered, please register first': '手机号未注册,请先注册', 'Please keep and enable at least one authenticator': '请至少保留并启用一个认证器', + 'Allow to sign in with': '允许使用以下方式登录', + 'Please enter a valid username': '请输入有效的用户名', + 'Please enter a valid email': '请输入有效的邮箱', + 'Please enter your username or email': '请输入用户名或邮箱', + SMS: '短信', + 'Username/Email': '用户名/邮箱', + 'Auth UID': '认证标识', + 'The authentication allows users to sign in via username or email.': '该认证方式支持用户通过用户名或邮箱登录。', }; export default locale; diff --git a/packages/plugins/auth/src/server/__tests__/actions.test.ts b/packages/plugins/auth/src/server/__tests__/actions.test.ts index 690faa82a..43b9ee0bb 100644 --- a/packages/plugins/auth/src/server/__tests__/actions.test.ts +++ b/packages/plugins/auth/src/server/__tests__/actions.test.ts @@ -32,7 +32,12 @@ describe('actions', () => { it('should list authenticator types', async () => { const res = await agent.resource('authenticators').listTypes(); - expect(res.body.data).toEqual(['Email/Password']); + expect(res.body.data).toEqual([ + { + name: 'Email/Password', + title: 'Password', + }, + ]); }); it('should return enabled authenticators with public options', async () => { @@ -82,6 +87,7 @@ describe('actions', () => { beforeAll(async () => { app = mockServer(); process.env.INIT_ROOT_EMAIL = 'test@nocobase.com'; + process.env.INT_ROOT_USERNAME = 'test'; process.env.INIT_ROOT_PASSWORD = '123456'; process.env.INIT_ROOT_NICKNAME = 'Test'; app.plugin(AuthPlugin); @@ -95,14 +101,17 @@ describe('actions', () => { await db.close(); }); - it('should sign in with email and password', async () => { + it('should sign in with password', async () => { let res = await agent.resource('auth').check(); expect(res.body.data.id).toBeUndefined(); - res = await agent.post('/auth:signIn').set({ 'X-Authenticator': 'basic' }).send({ - email: process.env.INIT_ROOT_EMAIL, - password: process.env.INIT_ROOT_PASSWORD, - }); + res = await agent + .post('/auth:signIn') + .set({ 'X-Authenticator': 'basic' }) + .send({ + account: process.env.INIT_ROOT_USERNAME || process.env.INIT_ROOT_EMAIL, + password: process.env.INIT_ROOT_PASSWORD, + }); expect(res.statusCode).toEqual(200); const data = res.body.data; const token = data.token; @@ -114,7 +123,7 @@ describe('actions', () => { it('should disable sign up', async () => { let res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({ - email: 'new', + username: 'new', password: 'new', }); expect(res.statusCode).toEqual(200); @@ -133,7 +142,7 @@ describe('actions', () => { }, }); res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({ - email: process.env.INIT_ROOT_EMAIL, + username: process.env.INIT_ROOT_USERNAME, password: process.env.INIT_ROOT_PASSWORD, }); expect(res.statusCode).toEqual(403); diff --git a/packages/plugins/auth/src/server/basic-auth.ts b/packages/plugins/auth/src/server/basic-auth.ts index 5b98b943e..68502ee89 100644 --- a/packages/plugins/auth/src/server/basic-auth.ts +++ b/packages/plugins/auth/src/server/basic-auth.ts @@ -11,23 +11,29 @@ export class BasicAuth extends BaseAuth { async validate() { const ctx = this.ctx; - const { uniqueField = 'email', values } = ctx.action.params; + const { + values: { + account, // Username or email + email, // Old parameter, compatible with old api + password, + }, + } = ctx.action.params; - if (!values[uniqueField]) { - ctx.throw(400, ctx.t('Please fill in your email address', { ns: namespace })); + if (!account && !email) { + ctx.throw(400, ctx.t('Please enter your username or email', { ns: namespace })); } const user = await this.userRepository.findOne({ - where: { - [uniqueField]: values[uniqueField], + filter: { + $or: [{ username: account || null }, { email: account || email }], }, }); if (!user) { - ctx.throw(401, ctx.t('The email is incorrect, please re-enter', { ns: namespace })); + ctx.throw(401, ctx.t('The username or email is incorrect, please re-enter', { ns: namespace })); } const field = this.userCollection.getField('password'); - const valid = await field.verify(values.password, user.password); + const valid = await field.verify(password, user.password); if (!valid) { ctx.throw(401, ctx.t('The password is incorrect, please re-enter', { ns: namespace })); } @@ -42,6 +48,10 @@ export class BasicAuth extends BaseAuth { } const User = ctx.db.getRepository('users'); const { values } = ctx.action.params; + const { username } = values; + if (!/^[^@.<>"'/]{2,16}$/.test(username)) { + ctx.throw(400, ctx.t('Please enter a valid username', { ns: namespace })); + } const user = await User.create({ values }); return user; } diff --git a/packages/plugins/auth/src/server/locale/zh-CN.ts b/packages/plugins/auth/src/server/locale/zh-CN.ts index 8265a9444..3c308c042 100644 --- a/packages/plugins/auth/src/server/locale/zh-CN.ts +++ b/packages/plugins/auth/src/server/locale/zh-CN.ts @@ -1,9 +1,10 @@ export default { - 'The email is incorrect, please re-enter': '邮箱有误,请重新输入', - 'Please fill in your email address': '请填写邮箱', + 'The username or email is incorrect, please re-enter': '用户名或邮箱有误,请重新输入', 'The password is incorrect, please re-enter': '密码有误,请重新输入', 'Not a valid cellphone number, please re-enter': '不是有效的手机号,请重新输入', 'The phone number has been registered, please login directly': '手机号已注册,请直接登录', 'The phone number is not registered, please register first': '手机号未注册,请先注册', 'Please keep and enable at least one authenticator': '请至少保留并启用一个认证器', + 'Please enter your username or email': '请输入用户名或邮箱', + 'Please enter a valid username': '请输入有效的用户名', }; diff --git a/packages/plugins/auth/src/server/migrations/20230506152253-basic-authenticator.ts b/packages/plugins/auth/src/server/migrations/20230506152253-basic-authenticator.ts index 621d9fb75..504ba8933 100644 --- a/packages/plugins/auth/src/server/migrations/20230506152253-basic-authenticator.ts +++ b/packages/plugins/auth/src/server/migrations/20230506152253-basic-authenticator.ts @@ -12,7 +12,7 @@ export default class AddBasicAuthMigration extends Migration { values: { name: presetAuthenticator, authType: presetAuthType, - description: 'Sign in with email and password.', + description: 'Sign in with username/email.', enabled: true, }, }); diff --git a/packages/plugins/auth/src/server/plugin.ts b/packages/plugins/auth/src/server/plugin.ts index 677ed6bdc..276a6c78f 100644 --- a/packages/plugins/auth/src/server/plugin.ts +++ b/packages/plugins/auth/src/server/plugin.ts @@ -47,6 +47,7 @@ export class AuthPlugin extends Plugin { this.app.authManager.registerTypes(presetAuthType, { auth: BasicAuth, + title: 'Password', }); // Register actions Object.entries(authActions).forEach(([action, handler]) => @@ -76,7 +77,7 @@ export class AuthPlugin extends Plugin { values: { name: presetAuthenticator, authType: presetAuthType, - description: 'Sign in with email and password.', + description: 'Sign in with username/email.', enabled: true, options: { public: { diff --git a/packages/plugins/sms-auth/src/server/sms-auth.ts b/packages/plugins/sms-auth/src/server/sms-auth.ts index 69ae1d145..911548a78 100644 --- a/packages/plugins/sms-auth/src/server/sms-auth.ts +++ b/packages/plugins/sms-auth/src/server/sms-auth.ts @@ -39,7 +39,6 @@ export class SMSAuth extends BaseAuth { } // New data const { autoSignup } = this.authenticator.options?.public || {}; - console.log(autoSignup); const authenticator = this.authenticator as AuthModel; if (autoSignup) { user = await authenticator.findOrCreateUser(phone, { diff --git a/packages/plugins/users/src/server/collections/users.ts b/packages/plugins/users/src/server/collections/users.ts index 74af63552..ea0fe0517 100644 --- a/packages/plugins/users/src/server/collections/users.ts +++ b/packages/plugins/users/src/server/collections/users.ts @@ -33,6 +33,19 @@ export default { 'x-component': 'Input', }, }, + { + interface: 'username', + type: 'string', + name: 'username', + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Username")}}', + 'x-component': 'Input', + 'x-validator': { username: true }, + required: true, + }, + }, { interface: 'email', type: 'string', diff --git a/packages/plugins/users/src/server/migrations/20230802170800-add-username.ts b/packages/plugins/users/src/server/migrations/20230802170800-add-username.ts new file mode 100644 index 000000000..30ed54e9f --- /dev/null +++ b/packages/plugins/users/src/server/migrations/20230802170800-add-username.ts @@ -0,0 +1,39 @@ +import { Migration } from '@nocobase/server'; + +export default class AddUserNameMigration extends Migration { + async up() { + const match = await this.app.version.satisfies('<=0.12.0-alpha.4'); + if (!match) { + return; + } + const Field = this.context.db.getRepository('fields'); + const existed = await Field.count({ + filter: { + name: 'username', + collectionName: 'users', + }, + }); + if (!existed) { + await Field.create({ + values: { + name: 'username', + collectionName: 'users', + type: 'string', + unique: true, + interface: 'input', + uiSchema: { + type: 'string', + title: '{{t("Username")}}', + 'x-component': 'Input', + 'x-validator': { username: true }, + required: true, + }, + }, + // NOTE: to trigger hook + context: {}, + }); + } + } + + async down() {} +} diff --git a/packages/plugins/users/src/server/migrations/20230802183100-update-username.ts b/packages/plugins/users/src/server/migrations/20230802183100-update-username.ts new file mode 100644 index 000000000..8e93999ac --- /dev/null +++ b/packages/plugins/users/src/server/migrations/20230802183100-update-username.ts @@ -0,0 +1,29 @@ +import { Migration } from '@nocobase/server'; + +export default class UpdateUserNameMigration extends Migration { + async up() { + const match = await this.app.version.satisfies('<=0.12.0-alpha.4'); + if (!match) { + return; + } + const repo = this.context.db.getRepository('users'); + const user = await repo.findOne({ + filter: { + email: 'admin@nocobase.com', + username: null, + }, + }); + if (user) { + await repo.update({ + values: { + username: process.env.INIT_ROOT_USERNAME || 'nocobase', + }, + filter: { + id: user.id, + }, + }); + } + } + + async down() {} +} diff --git a/packages/plugins/users/src/server/server.ts b/packages/plugins/users/src/server/server.ts index 716b89ea5..b1f5f811f 100644 --- a/packages/plugins/users/src/server/server.ts +++ b/packages/plugins/users/src/server/server.ts @@ -31,6 +31,7 @@ export default class UsersPlugin extends Plugin { this.app.i18n.addResources('en-US', namespace, enUS); const cmd = this.app.findCommand('install'); if (cmd) { + cmd.requiredOption('-u, --root-username ', '', process.env.INIT_ROOT_USERNAME); cmd.requiredOption('-e, --root-email ', '', process.env.INIT_ROOT_EMAIL); cmd.requiredOption('-p, --root-password ', '', process.env.INIT_ROOT_PASSWORD); cmd.option('-n, --root-nickname '); @@ -137,21 +138,23 @@ export default class UsersPlugin extends Plugin { } getInstallingData(options: any = {}) { - const { INIT_ROOT_NICKNAME, INIT_ROOT_PASSWORD, INIT_ROOT_EMAIL } = process.env; + const { INIT_ROOT_NICKNAME, INIT_ROOT_PASSWORD, INIT_ROOT_EMAIL, INIT_ROOT_USERNAME } = process.env; const { rootEmail = INIT_ROOT_EMAIL, rootPassword = INIT_ROOT_PASSWORD, rootNickname = INIT_ROOT_NICKNAME || 'Super Admin', + rootUsername = INIT_ROOT_USERNAME || 'nocobase', } = options.users || options?.cliArgs?.[0] || {}; return { rootEmail, rootPassword, rootNickname, + rootUsername, }; } async install(options) { - const { rootNickname, rootPassword, rootEmail } = this.getInstallingData(options); + const { rootNickname, rootPassword, rootEmail, rootUsername } = this.getInstallingData(options); const User = this.db.getCollection('users'); if (await User.repository.findOne({ filter: { email: rootEmail } })) { return; @@ -162,6 +165,7 @@ export default class UsersPlugin extends Plugin { email: rootEmail, password: rootPassword, nickname: rootNickname, + username: rootUsername, }, }); diff --git a/packages/presets/nocobase/src/server/index.ts b/packages/presets/nocobase/src/server/index.ts index 67252e8ae..94fe2ecc0 100644 --- a/packages/presets/nocobase/src/server/index.ts +++ b/packages/presets/nocobase/src/server/index.ts @@ -83,6 +83,40 @@ export class PresetNocoBase extends Plugin { console.log(`The version number before upgrade is ${version}`); }); + this.app.on('beforeUpgrade', async () => { + if (!this.db.inDialect('sqlite')) { + return; + } + await this.app.load({ method: 'upgrade' }); + const Field = this.db.getRepository('fields'); + const existed = await Field.count({ + filter: { + name: 'username', + collectionName: 'users', + }, + }); + if (!existed) { + await this.db.getRepository('fields').create({ + values: { + name: 'username', + collectionName: 'users', + type: 'string', + unique: true, + interface: 'input', + uiSchema: { + type: 'string', + title: '{{t("Username")}}', + 'x-component': 'Input', + 'x-validator': { username: true }, + required: true, + }, + }, + // NOTE: to trigger hook + context: {}, + }); + } + }); + this.app.on('beforeUpgrade', async () => { const result = await this.app.version.satisfies('<0.8.0-alpha.1'); diff --git a/yarn.lock b/yarn.lock index c4eb5dd32..5c709d1fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9639,7 +9639,7 @@ commander@^10.0.0: version "10.0.1" resolved "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" -commander@^2.19.0, commander@^2.20.0, commander@^2.8.1, commander@^2.9.0: +commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1, commander@^2.9.0: version "2.20.3" resolved "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -10351,6 +10351,10 @@ cssesc@^3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" +cssfilter@0.0.10: + version "0.0.10" + resolved "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" + cssnano-preset-default@^4.0.8: version "4.0.8" resolved "https://registry.npmmirror.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff" @@ -25996,6 +26000,13 @@ xregexp@2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" +xss@^1.0.14: + version "1.0.14" + resolved "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz#4f3efbde75ad0d82e9921cc3c95e6590dd336694" + dependencies: + commander "^2.20.3" + cssfilter "0.0.10" + xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"