feat(users): add users module (#26)
* feat(users): add users module * user check
This commit is contained in:
parent
3da40bd35b
commit
d9e6d2e614
@ -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}/`);
|
||||
});
|
||||
|
@ -22,3 +22,36 @@ export const request: RequestConfig = {
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
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 {};
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
]}
|
||||
>
|
||||
|
74
packages/app/src/components/views/Form/Login.tsx
Normal file
74
packages/app/src/components/views/Form/Login.tsx
Normal 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>
|
||||
);
|
||||
}
|
125
packages/app/src/components/views/Form/Register.tsx
Normal file
125
packages/app/src/components/views/Form/Register.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -12,6 +12,7 @@ import {
|
||||
FormValidator,
|
||||
setValidationLanguage,
|
||||
} from '@formily/antd';
|
||||
import './style.less';
|
||||
|
||||
setup();
|
||||
setValidationLanguage('zh-CN');
|
||||
|
5
packages/app/src/components/views/Form/style.less
Normal file
5
packages/app/src/components/views/Form/style.less
Normal file
@ -0,0 +1,5 @@
|
||||
.users-form {
|
||||
max-width: 368px;
|
||||
margin: 0 auto;
|
||||
padding-top: 100px;
|
||||
}
|
@ -86,7 +86,7 @@ export function SimpleTable(props: SimpleTableProps) {
|
||||
{...tableProps}
|
||||
/>
|
||||
{paginated && (
|
||||
<div>
|
||||
<div className={'table-pagination'}>
|
||||
<Pagination {...pagination} showQuickJumper showSizeChanger size={'default'}/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -102,7 +102,7 @@ export function Table(props: TableProps) {
|
||||
{...tableProps}
|
||||
/>
|
||||
{paginated && (
|
||||
<div>
|
||||
<div className={'table-pagination'}>
|
||||
<Pagination {...pagination} showQuickJumper showSizeChanger size={'default'}/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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;
|
||||
|
13
packages/app/src/components/views/style.less
Normal file
13
packages/app/src/components/views/style.less
Normal 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;
|
||||
}
|
||||
}
|
16
packages/app/src/pages/login.tsx
Normal file
16
packages/app/src/pages/login.tsx
Normal 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>
|
||||
);
|
||||
};
|
16
packages/app/src/pages/register.tsx
Normal file
16
packages/app/src/pages/register.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
@ -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": {
|
||||
|
6
packages/plugin-users/src/actions/check.ts
Normal file
6
packages/plugin-users/src/actions/check.ts
Normal 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();
|
||||
}
|
28
packages/plugin-users/src/actions/login.ts
Normal file
28
packages/plugin-users/src/actions/login.ts
Normal 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();
|
||||
}
|
6
packages/plugin-users/src/actions/logout.ts
Normal file
6
packages/plugin-users/src/actions/logout.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { actions } from '@nocobase/actions';
|
||||
|
||||
export default async (ctx: actions.Context, next: actions.Next) => {
|
||||
ctx.body = {};
|
||||
await next();
|
||||
}
|
11
packages/plugin-users/src/actions/register.ts
Normal file
11
packages/plugin-users/src/actions/register.ts
Normal 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();
|
||||
}
|
@ -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',
|
||||
|
32
packages/plugin-users/src/resources/users.ts
Normal file
32
packages/plugin-users/src/resources/users.ts
Normal 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;
|
@ -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'),
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user