feat(users): add reset password for users

This commit is contained in:
chenos 2021-03-22 13:41:00 +08:00
parent b95e2da129
commit bd1449e87a
15 changed files with 403 additions and 64 deletions

View File

@ -43,6 +43,22 @@ const data = [
template: 'register', template: 'register',
order: 130, 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 () => { (async () => {
@ -92,7 +108,7 @@ const data = [
await Storage.create({ await Storage.create({
name: `ali-oss`, name: `ali-oss`,
type: 'ali-oss', type: 'ali-oss',
baseUrl: process.env.STORAGE_BASE_URL, baseUrl: process.env.ALIYUN_STORAGE_BASE_URL,
options: { options: {
region: process.env.ALIYUN_OSS_REGION,// 'oss-cn-beijing', region: process.env.ALIYUN_OSS_REGION,// 'oss-cn-beijing',
accessKeyId: process.env.ALIYUN_OSS_ACCESS_KEY_ID,// 'LTAI4GEGDJsdGantisvSaz47', accessKeyId: process.env.ALIYUN_OSS_ACCESS_KEY_ID,// 'LTAI4GEGDJsdGantisvSaz47',

View File

@ -32,6 +32,13 @@ export const request: RequestConfig = {
], ],
}; };
const pathnames = [
'/login',
'/register',
'/lostpassword',
'/resetpassword',
];
export async function getInitialState() { export async function getInitialState() {
const { pathname, search } = location; const { pathname, search } = location;
console.log(location); console.log(location);
@ -43,7 +50,7 @@ export async function getInitialState() {
redirect = `?redirect=${pathname}${search}`; redirect = `?redirect=${pathname}${search}`;
// } // }
if (pathname !== '/login' && pathname !== '/register') { if (!pathnames.includes(pathname)) {
try { try {
const { data = {} } = await umiRequest('/users:check', { const { data = {} } = await umiRequest('/users:check', {
method: 'post', method: 'post',

View File

@ -72,7 +72,10 @@ export function Login(props: any) {
width: '100%', width: '100%',
}, },
placeholder: '密码', placeholder: '密码',
} },
"x-props": {
help: <Link to={'/lostpassword'}></Link>
},
}, },
} }
}}> }}>

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

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

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

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

View File

@ -12,5 +12,7 @@ export default {
'page4': require('@/pages/page4').default, 'page4': require('@/pages/page4').default,
login: require('@/pages/login').default, login: require('@/pages/login').default,
register: require('@/pages/register').default, register: require('@/pages/register').default,
lostpassword: require('@/pages/lostpassword').default,
resetpassword: require('@/pages/resetpassword').default,
AdminLoader: require('@/components/pages/AdminLoader').default, AdminLoader: require('@/components/pages/AdminLoader').default,
}; };

View File

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

View File

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

View File

@ -1,6 +0,0 @@
import { actions } from '@nocobase/actions';
export default async (ctx: actions.Context, next: actions.Next) => {
ctx.body = {};
await next();
}

View File

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

View 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();
}

View File

@ -85,6 +85,16 @@ export default {
filterable: false, filterable: false,
developerMode: true, developerMode: true,
}, },
{
interface: 'string',
type: 'string',
name: 'reset_token',
title: 'Reset Token',
unique: true,
hidden: true,
filterable: false,
developerMode: true,
},
], ],
actions: [ actions: [
{ {

View File

@ -5,10 +5,7 @@ import Resourcer from '@nocobase/resourcer';
import * as fields from './fields'; import * as fields from './fields';
// import hooks from './hooks'; // import hooks from './hooks';
import login from './actions/login'; import * as usersActions from './actions/users';
import register from './actions/register';
import logout from './actions/logout';
import check from './actions/check';
import { makeOptions } from './hooks/collection-after-create'; import { makeOptions } from './hooks/collection-after-create';
import * as middlewares from './middlewares'; import * as middlewares from './middlewares';
@ -39,12 +36,9 @@ export default async function (options = {}) {
directory: path.resolve(__dirname, 'collections'), directory: path.resolve(__dirname, 'collections'),
}); });
resourcer.registerActionHandlers({ for (const [key, action] of Object.entries(usersActions)) {
'users:login': login, resourcer.registerActionHandler(`users:${key}`, action);
'users:register': register, }
'users:logout': logout,
'users:check': check,
});
resourcer.use(middlewares.parseToken(options)); resourcer.use(middlewares.parseToken(options));
} }