feat(users): add reset password for users
This commit is contained in:
parent
b95e2da129
commit
bd1449e87a
@ -43,6 +43,22 @@ const data = [
|
||||
template: 'register',
|
||||
order: 130,
|
||||
},
|
||||
{
|
||||
title: '忘记密码',
|
||||
path: '/lostpassword',
|
||||
type: 'page',
|
||||
inherit: false,
|
||||
template: 'lostpassword',
|
||||
order: 140,
|
||||
},
|
||||
{
|
||||
title: '重置密码',
|
||||
path: '/resetpassword',
|
||||
type: 'page',
|
||||
inherit: false,
|
||||
template: 'resetpassword',
|
||||
order: 150,
|
||||
},
|
||||
];
|
||||
|
||||
(async () => {
|
||||
@ -92,7 +108,7 @@ const data = [
|
||||
await Storage.create({
|
||||
name: `ali-oss`,
|
||||
type: 'ali-oss',
|
||||
baseUrl: process.env.STORAGE_BASE_URL,
|
||||
baseUrl: process.env.ALIYUN_STORAGE_BASE_URL,
|
||||
options: {
|
||||
region: process.env.ALIYUN_OSS_REGION,// 'oss-cn-beijing',
|
||||
accessKeyId: process.env.ALIYUN_OSS_ACCESS_KEY_ID,// 'LTAI4GEGDJsdGantisvSaz47',
|
||||
|
@ -32,6 +32,13 @@ export const request: RequestConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
const pathnames = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/lostpassword',
|
||||
'/resetpassword',
|
||||
];
|
||||
|
||||
export async function getInitialState() {
|
||||
const { pathname, search } = location;
|
||||
console.log(location);
|
||||
@ -43,7 +50,7 @@ export async function getInitialState() {
|
||||
redirect = `?redirect=${pathname}${search}`;
|
||||
// }
|
||||
|
||||
if (pathname !== '/login' && pathname !== '/register') {
|
||||
if (!pathnames.includes(pathname)) {
|
||||
try {
|
||||
const { data = {} } = await umiRequest('/users:check', {
|
||||
method: 'post',
|
||||
|
@ -72,7 +72,10 @@ export function Login(props: any) {
|
||||
width: '100%',
|
||||
},
|
||||
placeholder: '密码',
|
||||
}
|
||||
},
|
||||
"x-props": {
|
||||
help: <Link to={'/lostpassword'}>忘记密码?</Link>
|
||||
},
|
||||
},
|
||||
}
|
||||
}}>
|
||||
|
67
packages/app/src/components/views/Form/LostPassword.tsx
Normal file
67
packages/app/src/components/views/Form/LostPassword.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Tooltip, Card, Button, message } from 'antd';
|
||||
import {
|
||||
SchemaForm,
|
||||
SchemaMarkupField as Field,
|
||||
createFormActions,
|
||||
createAsyncFormActions,
|
||||
Submit,
|
||||
Reset,
|
||||
FormButtonGroup,
|
||||
registerFormFields,
|
||||
FormValidator,
|
||||
setValidationLanguage,
|
||||
} from '@formily/antd';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Link, history, request, useModel, useLocation } from 'umi';
|
||||
|
||||
export function LostPassword(props: any) {
|
||||
const actions = createFormActions();
|
||||
const { initialState = {}, loading, error, refresh, setInitialState } = useModel('@@initialState');
|
||||
const { redirect } = props.location.query;
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { systemSettings = {} } = initialState as any;
|
||||
|
||||
console.log({systemSettings});
|
||||
|
||||
const { title } = systemSettings || {};
|
||||
|
||||
return (
|
||||
<div className={'users-form'}>
|
||||
<h1>{title||'NocoBase'}</h1>
|
||||
<h2>忘记密码</h2>
|
||||
<SchemaForm onSubmit={async (values) => {
|
||||
console.log(values);
|
||||
const { data = {} } = await request('/users:lostpassword', {
|
||||
method: 'post',
|
||||
data: values,
|
||||
});
|
||||
message.success(`重置链接已发送至邮箱 ${values.email},请注意查收!`)
|
||||
}} actions={actions} schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
title: '',
|
||||
required: true,
|
||||
'x-component-props': {
|
||||
size: 'large',
|
||||
placeholder: '邮箱',
|
||||
}
|
||||
},
|
||||
}
|
||||
}}>
|
||||
<FormButtonGroup>
|
||||
<Submit size={'large'}>获取新密码</Submit>
|
||||
<Button size={'large'} onClick={() => {
|
||||
history.push('/login');
|
||||
}}>使用已有账号登录</Button>
|
||||
</FormButtonGroup>
|
||||
</SchemaForm>
|
||||
</div>
|
||||
);
|
||||
}
|
151
packages/app/src/components/views/Form/ResetPassword.tsx
Normal file
151
packages/app/src/components/views/Form/ResetPassword.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import { Tooltip, Card, Button, message } from 'antd';
|
||||
import {
|
||||
SchemaForm,
|
||||
SchemaMarkupField as Field,
|
||||
createFormActions,
|
||||
createAsyncFormActions,
|
||||
Submit,
|
||||
Reset,
|
||||
FormButtonGroup,
|
||||
registerFormFields,
|
||||
FormEffectHooks,
|
||||
FormValidator,
|
||||
setValidationLanguage,
|
||||
} from '@formily/antd';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Redirect, history, request, useModel, useHistory } from 'umi';
|
||||
import api from '@/api-client';
|
||||
import { useRequest } from 'umi';
|
||||
|
||||
const { onFieldValueChange$ } = FormEffectHooks
|
||||
|
||||
const useLinkageValidateEffects = () => {
|
||||
const { setFieldState, getFieldState } = createFormActions()
|
||||
onFieldValueChange$('*(password,confirm)').subscribe(fieldState => {
|
||||
const selfName = fieldState.name
|
||||
const selfValue = fieldState.value
|
||||
const otherName = selfName == 'password' ? 'confirm' : 'password'
|
||||
const otherValue = getFieldState(otherName, state => state.value)
|
||||
setFieldState(otherName, state => {
|
||||
if (selfValue && otherValue && selfValue !== otherValue) {
|
||||
state.errors = ['两次密码输入不一致']
|
||||
} else {
|
||||
state.errors = []
|
||||
}
|
||||
})
|
||||
setFieldState(selfName, state => {
|
||||
if (selfValue && otherValue && selfValue !== otherValue) {
|
||||
state.errors = ['两次密码输入不一致']
|
||||
} else {
|
||||
state.errors = []
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function ResetPassword(props: any) {
|
||||
const actions = createFormActions();
|
||||
const { initialState = {}, loading: stateLoading, error, refresh, setInitialState } = useModel('@@initialState');
|
||||
|
||||
const history = useHistory<any>();
|
||||
// @ts-ignore
|
||||
const { token } = history.location.query;
|
||||
console.log({ token })
|
||||
const { data = {}, loading } = useRequest(() => {
|
||||
return api.resource('users').getUserByResetToken({
|
||||
token,
|
||||
});
|
||||
});
|
||||
|
||||
if (stateLoading || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data.id) {
|
||||
return <Redirect to={'/login'}/>
|
||||
}
|
||||
|
||||
const { systemSettings = {} } = initialState as any;
|
||||
|
||||
console.log({systemSettings});
|
||||
|
||||
const { title } = systemSettings || {};
|
||||
|
||||
return (
|
||||
<div className={'users-form'}>
|
||||
<h1>{title || 'NocoBase'}</h1>
|
||||
<h2>重置密码</h2>
|
||||
<SchemaForm
|
||||
effects={() => {
|
||||
useLinkageValidateEffects()
|
||||
}}
|
||||
initialValues={{
|
||||
email: data.email,
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
console.log(values);
|
||||
const { data = {} } = await request('/users:resetpassword', {
|
||||
method: 'post',
|
||||
data: {
|
||||
...values,
|
||||
reset_token: token,
|
||||
},
|
||||
});
|
||||
await actions.reset({
|
||||
validate: false,
|
||||
forceClear: true,
|
||||
});
|
||||
message.success('密码重置成功,将跳转至登录页');
|
||||
setTimeout(() => {
|
||||
history.push('/login');
|
||||
}, 1000);
|
||||
}} actions={actions} schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
title: '',
|
||||
required: true,
|
||||
'x-component-props': {
|
||||
size: 'large',
|
||||
placeholder: '邮箱',
|
||||
disabled: true,
|
||||
}
|
||||
},
|
||||
password: {
|
||||
type: 'password',
|
||||
title: '',
|
||||
required: true,
|
||||
'x-component-props': {
|
||||
size: 'large',
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
placeholder: '密码',
|
||||
}
|
||||
},
|
||||
confirm: {
|
||||
type: 'password',
|
||||
title: '',
|
||||
required: true,
|
||||
'x-component-props': {
|
||||
size: 'large',
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
placeholder: '确认密码',
|
||||
}
|
||||
},
|
||||
}
|
||||
}}>
|
||||
<FormButtonGroup align={'start'}>
|
||||
<Submit size={'large'}>重置密码</Submit>
|
||||
<Button size={'large'} onClick={() => {
|
||||
history.push('/login');
|
||||
}}>使用已有账号登录</Button>
|
||||
</FormButtonGroup>
|
||||
</SchemaForm>
|
||||
</div>
|
||||
);
|
||||
}
|
20
packages/app/src/pages/lostpassword.tsx
Normal file
20
packages/app/src/pages/lostpassword.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'umi';
|
||||
import { Page } from '@/components/pages';
|
||||
import ViewFactory from '@/components/views';
|
||||
import { Helmet, useModel } from 'umi';
|
||||
import get from 'lodash/get';
|
||||
import { LostPassword } from '@/components/views/Form/LostPassword';
|
||||
|
||||
export default (props: any) => {
|
||||
const { initialState = {}, refresh, setInitialState } = useModel('@@initialState');
|
||||
const siteTitle = get(initialState, 'systemSettings.title');
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{siteTitle ? `忘记密码 - ${siteTitle}` : '忘记密码'}</title>
|
||||
</Helmet>
|
||||
<LostPassword {...props}/>
|
||||
</div>
|
||||
);
|
||||
};
|
20
packages/app/src/pages/resetpassword.tsx
Normal file
20
packages/app/src/pages/resetpassword.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'umi';
|
||||
import { Page } from '@/components/pages';
|
||||
import ViewFactory from '@/components/views';
|
||||
import { Helmet, useModel } from 'umi';
|
||||
import get from 'lodash/get';
|
||||
import { ResetPassword } from '@/components/views/Form/ResetPassword';
|
||||
|
||||
export default (props: any) => {
|
||||
const { initialState = {}, refresh, setInitialState } = useModel('@@initialState');
|
||||
const siteTitle = get(initialState, 'systemSettings.title');
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{siteTitle ? `重置密码 - ${siteTitle}` : '重置密码'}</title>
|
||||
</Helmet>
|
||||
<ResetPassword {...props}/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -12,5 +12,7 @@ export default {
|
||||
'page4': require('@/pages/page4').default,
|
||||
login: require('@/pages/login').default,
|
||||
register: require('@/pages/register').default,
|
||||
lostpassword: require('@/pages/lostpassword').default,
|
||||
resetpassword: require('@/pages/resetpassword').default,
|
||||
AdminLoader: require('@/components/pages/AdminLoader').default,
|
||||
};
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { actions } from '@nocobase/actions';
|
||||
|
||||
export default async (ctx: actions.Context, next: actions.Next) => {
|
||||
ctx.body = ctx.state.currentUser;
|
||||
await next();
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { actions } from '@nocobase/actions';
|
||||
import { PASSWORD } from '@nocobase/database';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
|
||||
export default async (ctx: actions.Context, next: actions.Next) => {
|
||||
const { uniqueField = 'email', values } = ctx.action.params;
|
||||
// console.log(values);
|
||||
const User = ctx.db.getModel('users');
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
[uniqueField]: values[uniqueField],
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
ctx.throw(401, 'Unauthorized');
|
||||
}
|
||||
if (!PASSWORD.verify(values.password, user.password)) {
|
||||
ctx.throw(401, 'Unauthorized');
|
||||
}
|
||||
if (!user.token) {
|
||||
user.token = cryptoRandomString({length: 20});
|
||||
await user.save();
|
||||
}
|
||||
ctx.body = {
|
||||
data: user,
|
||||
};
|
||||
await next();
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { actions } from '@nocobase/actions';
|
||||
|
||||
export default async (ctx: actions.Context, next: actions.Next) => {
|
||||
ctx.body = {};
|
||||
await next();
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { actions } from '@nocobase/actions';
|
||||
|
||||
export default async (ctx: actions.Context, next: actions.Next) => {
|
||||
const User = ctx.db.getModel('users');
|
||||
const { values } = ctx.action.params;
|
||||
const user = await User.create(values);
|
||||
ctx.body = {
|
||||
data: user,
|
||||
};
|
||||
await next();
|
||||
}
|
100
packages/plugin-users/src/actions/users.ts
Normal file
100
packages/plugin-users/src/actions/users.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { actions } from '@nocobase/actions';
|
||||
import { PASSWORD } from '@nocobase/database';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
|
||||
export async function check(ctx: actions.Context, next: actions.Next) {
|
||||
ctx.body = ctx.state.currentUser;
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function login(ctx: actions.Context, next: actions.Next) {
|
||||
const { uniqueField = 'email', values } = ctx.action.params;
|
||||
// console.log(values);
|
||||
const User = ctx.db.getModel('users');
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
[uniqueField]: values[uniqueField],
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
ctx.throw(401, 'Unauthorized');
|
||||
}
|
||||
if (!PASSWORD.verify(values.password, user.password)) {
|
||||
ctx.throw(401, 'Unauthorized');
|
||||
}
|
||||
if (!user.token) {
|
||||
user.token = cryptoRandomString({length: 20});
|
||||
await user.save();
|
||||
}
|
||||
ctx.body = {
|
||||
data: user,
|
||||
};
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function logout(ctx: actions.Context, next: actions.Next) {
|
||||
ctx.body = {};
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function register(ctx: actions.Context, next: actions.Next) {
|
||||
const User = ctx.db.getModel('users');
|
||||
const { values } = ctx.action.params;
|
||||
const user = await User.create(values);
|
||||
ctx.body = {
|
||||
data: user,
|
||||
};
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function lostpassword(ctx: actions.Context, next: actions.Next) {
|
||||
const { values: { email } } = ctx.action.params;
|
||||
const User = ctx.db.getModel('users');
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
ctx.throw(401, 'Unauthorized');
|
||||
}
|
||||
user.reset_token = cryptoRandomString({length: 20});
|
||||
await user.save();
|
||||
ctx.body = user;
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function resetpassword(ctx: actions.Context, next: actions.Next) {
|
||||
const { values: { email, password, reset_token } } = ctx.action.params;
|
||||
const User = ctx.db.getModel('users');
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
email,
|
||||
reset_token,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
ctx.throw(401, 'Unauthorized');
|
||||
}
|
||||
user.token = null;
|
||||
user.reset_token = null;
|
||||
user.password = password;
|
||||
await user.save();
|
||||
ctx.body = user;
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function getUserByResetToken(ctx: actions.Context, next: actions.Next) {
|
||||
const { token } = ctx.action.params;
|
||||
const User = ctx.db.getModel('users');
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
reset_token: token,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
ctx.throw(401, 'Unauthorized');
|
||||
}
|
||||
ctx.body = user;
|
||||
await next();
|
||||
}
|
@ -85,6 +85,16 @@ export default {
|
||||
filterable: false,
|
||||
developerMode: true,
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'reset_token',
|
||||
title: 'Reset Token',
|
||||
unique: true,
|
||||
hidden: true,
|
||||
filterable: false,
|
||||
developerMode: true,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
@ -5,10 +5,7 @@ import Resourcer from '@nocobase/resourcer';
|
||||
|
||||
import * as fields from './fields';
|
||||
// import hooks from './hooks';
|
||||
import login from './actions/login';
|
||||
import register from './actions/register';
|
||||
import logout from './actions/logout';
|
||||
import check from './actions/check';
|
||||
import * as usersActions from './actions/users';
|
||||
import { makeOptions } from './hooks/collection-after-create';
|
||||
import * as middlewares from './middlewares';
|
||||
|
||||
@ -39,12 +36,9 @@ export default async function (options = {}) {
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
resourcer.registerActionHandlers({
|
||||
'users:login': login,
|
||||
'users:register': register,
|
||||
'users:logout': logout,
|
||||
'users:check': check,
|
||||
});
|
||||
for (const [key, action] of Object.entries(usersActions)) {
|
||||
resourcer.registerActionHandler(`users:${key}`, action);
|
||||
}
|
||||
|
||||
resourcer.use(middlewares.parseToken(options));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user