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 <chenlinxh@gmail.com>
This commit is contained in:
parent
972b2be10e
commit
be6b949f55
@ -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
|
||||
|
@ -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
|
||||
|
@ -17,6 +17,7 @@ type AuthManagerOptions = {
|
||||
|
||||
type AuthConfig = {
|
||||
auth: AuthExtend<Auth>; // 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) {
|
||||
|
@ -33,3 +33,4 @@ export * from './time';
|
||||
export * from './updatedAt';
|
||||
export * from './updatedBy';
|
||||
export * from './url';
|
||||
export * from './username';
|
||||
|
@ -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,
|
||||
};
|
@ -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": "添加角色",
|
||||
|
@ -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}
|
||||
</span>
|
||||
</Dropdown>
|
||||
</DropdownVisibleContext.Provider>
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -30,7 +30,7 @@ export const AuthProvider: FC = (props) => {
|
||||
}}
|
||||
>
|
||||
<OptionsComponentProvider authType={presetAuthType} component={Options}>
|
||||
<SigninPageProvider authType={presetAuthType} tabTitle={t('Sign in via email')} component={SigninPage}>
|
||||
<SigninPageProvider authType={presetAuthType} tabTitle={t('Sign in via password')} component={SigninPage}>
|
||||
<SignupPageProvider authType={presetAuthType} component={SignupPage}>
|
||||
{props.children}
|
||||
</SignupPageProvider>
|
||||
|
@ -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 (
|
||||
<SchemaComponent
|
||||
scope={{ t }}
|
||||
components={{ Alert }}
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -24,6 +26,14 @@ export const Options = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
notice: {
|
||||
type: 'void',
|
||||
'x-component': 'Alert',
|
||||
'x-component-props': {
|
||||
showIcon: true,
|
||||
message: '{{t("The authentication allows users to sign in via username or email.")}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -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 <SchemaComponent schema={passwordForm} scope={{ useBasicSignIn, allowSignUp, signupLink }} />;
|
||||
return <SchemaComponent schema={passwordForm} scope={{ useBasicSignIn, allowSignUp, signupLink, t }} />;
|
||||
};
|
||||
|
@ -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 <SchemaComponent schema={signupPageSchema} scope={{ useBasicSignup }} />;
|
||||
return <SchemaComponent schema={signupPageSchema} scope={{ useBasicSignup, t }} />;
|
||||
};
|
||||
|
@ -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 = () => {
|
||||
<SchemaComponent
|
||||
schema={authenticatorsSchema}
|
||||
components={{ AddNew, Options }}
|
||||
scope={{ types, useValuesFromOptions, useCanNotDelete }}
|
||||
scope={{ types, useValuesFromOptions, useCanNotDelete, t }}
|
||||
/>
|
||||
</AuthTypesContext.Provider>
|
||||
</Card>
|
||||
|
@ -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 }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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,12 +101,15 @@ 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,
|
||||
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);
|
||||
@ -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);
|
||||
|
@ -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<PasswordField>('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;
|
||||
}
|
||||
|
@ -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': '请输入有效的用户名',
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
@ -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: {
|
||||
|
@ -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, {
|
||||
|
@ -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',
|
||||
|
@ -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() {}
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -31,6 +31,7 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
|
||||
this.app.i18n.addResources('en-US', namespace, enUS);
|
||||
const cmd = this.app.findCommand('install');
|
||||
if (cmd) {
|
||||
cmd.requiredOption('-u, --root-username <rootUsername>', '', process.env.INIT_ROOT_USERNAME);
|
||||
cmd.requiredOption('-e, --root-email <rootEmail>', '', process.env.INIT_ROOT_EMAIL);
|
||||
cmd.requiredOption('-p, --root-password <rootPassword>', '', process.env.INIT_ROOT_PASSWORD);
|
||||
cmd.option('-n, --root-nickname <rootNickname>');
|
||||
@ -137,21 +138,23 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
|
||||
}
|
||||
|
||||
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<UserPluginConfig> {
|
||||
email: rootEmail,
|
||||
password: rootPassword,
|
||||
nickname: rootNickname,
|
||||
username: rootUsername,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
|
||||
|
13
yarn.lock
13
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"
|
||||
|
Loading…
Reference in New Issue
Block a user