feat(users): add users module (#26)

* feat(users): add users module
* user check
This commit is contained in:
chenos 2020-11-29 16:26:53 +08:00 committed by GitHub
parent 3da40bd35b
commit d9e6d2e614
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 500 additions and 15 deletions

View File

@ -131,14 +131,14 @@ const data = {
icon: 'dashboard',
sort: 110,
},
{
title: '权限配置',
type: 'collection',
collection: 'roles',
path: '/settings/roles',
icon: 'dashboard',
sort: 120,
},
// {
// title: '权限配置',
// type: 'collection',
// collection: 'roles',
// path: '/settings/roles',
// icon: 'dashboard',
// sort: 120,
// },
]
},
],
@ -311,6 +311,31 @@ const data = {
// return await collection.modelInit();
// }));
await Page.create({
title: '登录页面',
path: '/login',
type: 'page',
inherit: false,
template: 'login',
order: 120,
});
await Page.create({
title: '注册页面',
path: '/register',
type: 'page',
inherit: false,
template: 'register',
order: 130,
});
await database.getModel('users').create({
nickname: "admin",
password: "admin",
username: "admin",
token: "38979f07e1fca68fb3d2",
});
api.listen(process.env.HTTP_PORT, () => {
console.log(`http://localhost:${process.env.HTTP_PORT}/`);
});

View File

@ -21,4 +21,37 @@ export const request: RequestConfig = {
await next();
}
],
};
};
export async function getInitialState() {
const { pathname, search } = location;
console.log(location);
let redirect = '';
// if (href.includes('?')) {
redirect = `?redirect=${pathname}${search}`;
// }
if (pathname !== '/login' && pathname !== '/register') {
try {
const { data = {} } = await umiRequest('/users:check', {
method: 'post',
});
if (!data.id) {
history.push('/login' + redirect);
return {
currentUser: {},
};
}
return {
currentUser: data,
};
} catch (error) {
console.log(error)
history.push('/login' + redirect);
}
}
return {};
}

View File

@ -29,9 +29,11 @@ export function Details(props: any) {
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/>
{loading ? <Spin/> : (
<Descriptions bordered column={1}>
{fields.map((field: any) => (
<Descriptions.Item label={field.title||field.name}>{data[field.name]}</Descriptions.Item>
))}
{fields.map((field: any) => {
return (
<Descriptions.Item label={field.title||field.name}>{JSON.stringify(data[field.name])}</Descriptions.Item>
)
})}
</Descriptions>
)}
</Card>

View File

@ -57,7 +57,8 @@ export const DrawerForm = forwardRef((props: any, ref) => {
title={title}
footer={[
<Button type={'primary'} onClick={async () => {
await actions.submit();
const values = await actions.submit();
console.log(values);
}}></Button>
]}
>

View File

@ -0,0 +1,74 @@
import React from 'react';
import { Tooltip, Card, Button } 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 Login(props: any) {
const actions = createAsyncFormActions();
const { initialState = {}, loading, error, refresh, setInitialState } = useModel('@@initialState');
const { redirect } = props.location.query;
return (
<div className={'users-form'}>
<h1>NocoBase</h1>
<h2></h2>
<SchemaForm onSubmit={async (values) => {
console.log(values);
const { data = {} } = await request('/users:login', {
method: 'post',
data: values,
});
if (data.data && data.data.token) {
localStorage.setItem('NOCOBASE_TOKEN', data.data.token);
setInitialState({currentUser :data.data});
await (window as any).routesReload();
history.push(redirect||'/');
}
}} actions={actions} schema={{
type: 'object',
properties: {
username: {
type: 'string',
title: '',
required: true,
'x-component-props': {
size: 'large',
placeholder: '用户名',
}
},
password: {
type: 'password',
title: '',
required: true,
'x-component-props': {
size: 'large',
style: {
width: '100%',
},
placeholder: '密码',
}
},
}
}}>
<FormButtonGroup>
<Submit size={'large'}></Submit>
<Button size={'large'} onClick={() => {
history.push('/register');
}}></Button>
</FormButtonGroup>
</SchemaForm>
</div>
);
}

View File

@ -0,0 +1,125 @@
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 { Link, history, request, useModel } 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 Register(props: any) {
const actions = createFormActions();
return (
<div className={'users-form'}>
<h1>NocoBase</h1>
<h2></h2>
<SchemaForm
effects={() => {
useLinkageValidateEffects()
}}
onSubmit={async (values) => {
console.log(values);
const { data = {} } = await request('/users:register', {
method: 'post',
data: values,
});
await actions.reset({
validate: false,
forceClear: true,
});
message.success('注册成功,将跳转登录页');
setTimeout(() => {
history.push('/login');
}, 1000);
}} actions={actions} schema={{
type: 'object',
properties: {
username: {
type: 'string',
title: '',
required: true,
'x-component-props': {
size: 'large',
placeholder: '用户名',
}
},
nickname: {
type: 'string',
title: '',
'x-component-props': {
size: 'large',
placeholder: '昵称',
}
},
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>
<Submit size={'large'}></Submit>
<Button size={'large'} onClick={() => {
history.push('/login');
}}>使</Button>
</FormButtonGroup>
</SchemaForm>
</div>
);
}

View File

@ -12,6 +12,7 @@ import {
FormValidator,
setValidationLanguage,
} from '@formily/antd';
import './style.less';
setup();
setValidationLanguage('zh-CN');

View File

@ -0,0 +1,5 @@
.users-form {
max-width: 368px;
margin: 0 auto;
padding-top: 100px;
}

View File

@ -86,7 +86,7 @@ export function SimpleTable(props: SimpleTableProps) {
{...tableProps}
/>
{paginated && (
<div>
<div className={'table-pagination'}>
<Pagination {...pagination} showQuickJumper showSizeChanger size={'default'}/>
</div>
)}

View File

@ -102,7 +102,7 @@ export function Table(props: TableProps) {
{...tableProps}
/>
{paginated && (
<div>
<div className={'table-pagination'}>
<Pagination {...pagination} showQuickJumper showSizeChanger size={'default'}/>
</div>
)}

View File

@ -7,6 +7,9 @@ import { SimpleTable } from './SimpleTable';
import { Table } from './Table';
import { Form, DrawerForm } from './Form/index';
import { Details } from './Details';
import './style.less';
import { Login } from './Form/Login';
import { Register } from './Form/Register';
const TEMPLATES = new Map<string, any>();
@ -24,6 +27,8 @@ registerView('Form', Form);
registerView('Table', Table);
registerView('SimpleTable', SimpleTable);
registerView('Details', Details);
registerView('Login', Login);
registerView('Register', Register);
export interface ViewProps {
resourceName: string;

View File

@ -0,0 +1,13 @@
.ant-table-cell:not(.ant-table-selection-column) {
min-width: 100px;
white-space: nowrap;
}
.ant-table-wrapper {
overflow: auto;
}
.table-pagination {
margin-top: 24px;
ul {
float: right;
}
}

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Link } from 'umi';
import { Page } from '@/components/pages';
import ViewFactory from '@/components/views';
export default (props: any) => {
return (
<div>
<ViewFactory
{...props}
viewName={'login'}
resourceName={'users'}
/>
</div>
);
};

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Link } from 'umi';
import { Page } from '@/components/pages';
import ViewFactory from '@/components/views';
export default (props: any) => {
return (
<div>
<ViewFactory
{...props}
viewName={'register'}
resourceName={'users'}
/>
</div>
);
};

View File

@ -10,4 +10,6 @@ export default {
'page2': require('@/pages/page2').default,
'page3': require('@/pages/page3').default,
'page4': require('@/pages/page4').default,
login: require('@/pages/login').default,
register: require('@/pages/register').default,
};

View File

@ -7,6 +7,8 @@
"umi": "^3.2.23"
},
"devDependencies": {
"@nocobase/actions": "^0.3.0-alpha.0",
"crypto-random-string": "^3.3.0",
"umi": "^3.2.23"
},
"dependencies": {

View File

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

View File

@ -0,0 +1,28 @@
import { actions } from '@nocobase/actions';
import cryptoRandomString from 'crypto-random-string';
import { Password } from "@nocobase/database";
export default async (ctx: actions.Context, next: actions.Next) => {
const { values } = ctx.action.params;
// console.log(values);
const User = ctx.db.getModel('users');
const user = await User.findOne({
where: {
username: values.username,
},
});
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

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

View File

@ -0,0 +1,11 @@
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

@ -5,9 +5,64 @@ export default {
title: '用户',
fields: [
{
interface: 'string',
type: 'string',
name: 'username',
title: '用户名',
unique: true,
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'string',
type: 'string',
name: 'nickname',
title: '昵称',
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'phone',
type: 'string',
name: 'phone',
unique: true,
title: '手机号',
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'password',
type: 'password',
name: 'password',
title: '密码',
hidden: true,
component: {
type: 'password',
showInForm: true,
},
},
{
interface: 'password',
type: 'string',
name: 'token',
title: 'Token',
unique: true,
hidden: true,
component: {
type: 'hidden',
},
},
],
actions: [
@ -41,6 +96,18 @@ export default {
title: '表单',
template: 'DrawerForm',
},
{
type: 'form',
name: 'login',
title: '登录',
template: 'Login',
},
{
type: 'form',
name: 'register',
title: '注册',
template: 'Register',
},
{
type: 'details',
name: 'details',

View File

@ -0,0 +1,32 @@
import { ResourceOptions } from "@nocobase/resourcer";
export default {
name: 'users',
middlewares: [
{
// only: ['check'],
handler: async (ctx, next) => {
const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
console.log(ctx.action.params.actionName);
const { actionName } = ctx.action.params;
if (actionName !== 'check') {
return next();
}
if (!token) {
ctx.throw(401, 'Unauthorized');
}
const User = ctx.db.getModel('users');
const user = await User.findOne({
where: {
token,
},
});
if (!user) {
ctx.throw(401, 'Unauthorized');
}
ctx.state.currentUser = user;
await next();
},
},
],
} as ResourceOptions;

View File

@ -1,6 +1,10 @@
import path from 'path';
import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import login from './actions/login';
import register from './actions/register';
import logout from './actions/logout';
import check from './actions/check';
export default async function (options = {}) {
const database: Database = this.database;
@ -9,4 +13,15 @@ export default async function (options = {}) {
database.import({
directory: path.resolve(__dirname, 'collections'),
});
resourcer.registerHandlers({
'users:login': login,
'users:register': register,
'users:logout': logout,
'users:check': check,
});
resourcer.import({
directory: path.resolve(__dirname, 'resources'),
});
}