Merge tag 'feat-RBAC-0.9' into feature/price_manager

# Conflicts:
#	src/main.jsx
#	src/stores/Auth.js
feature/price_manager
Jimmy Liow 1 year ago
commit 2adeb31e73

@ -74,6 +74,16 @@ VALUES ('审核价格', '/products/offer/audit', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('录入价格', '/products/offer/put', 'products')
-- 默认页面
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('最新计划', 'route=/reservation/newest', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('机票订票', 'route=/airticket', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(客服)', 'route=/products', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(供应商)', 'route=/products?from', 'page')
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (1, 1)
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])

Binary file not shown.

@ -12,7 +12,10 @@
"createdOn": "创建时间",
"action": "操作",
"action.edit": "编辑",
"action.enable": "启用",
"action.disable": "禁用",
"action.enable.title": "确定启用该账号吗?",
"action.disable.title": "确定禁用该账号吗?",
"action.resetPassword": "重置密码",
"accountList": "管理账号",

@ -32,8 +32,7 @@ import AirticketPlan from "@/views/airticket/Plan";
import { ThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage'
import { isNotEmpty } from '@/utils/commons'
import { appendRequestParams } from '@/utils/request'
import { fireAuth } from "./utils/lifecycle"
import { notifyAuth } from "./utils/lifecycle"
import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail';
@ -42,19 +41,13 @@ import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET,
import './i18n';
const { loginToken, userId } = usingStorage()
const initAppliction = async () => {
if (isNotEmpty(loginToken)) {
appendRequestParams('token', loginToken)
appendRequestParams('lmi_sn', userId)
}
const { loginToken, userId } = usingStorage()
if (isNotEmpty(userId)) {
appendRequestParams('lmi_sn', userId)
await fireAuth()
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
await notifyAuth()
}
}

@ -70,16 +70,15 @@ const useAccountStore = create((set, get) => ({
accountList: [],
disableAccount: async (userId) => {
toggleAccountStatus: async (userId, status) => {
const statusValue = status ? 'enable' : 'disable'
const formData = new FormData()
formData.append('lmi_sn', userId)
// enable | disable
formData.append('account_status', 'disable')
const result = await postAccountStatus(formData)
formData.append('account_status', statusValue)
console.info(result)
return postAccountStatus(formData)
},
resetAccountPassword: async (userId, password) => {
@ -91,7 +90,7 @@ const useAccountStore = create((set, get) => ({
return postAccountPassword(formData)
},
newRole: () => {
newEmptyRole: () => {
return {
role_id: null,
role_name: '',
@ -99,6 +98,19 @@ const useAccountStore = create((set, get) => ({
}
},
newEmptyAccount: () => {
return {
accountId: null,
userId: null,
lmi2_sn: null,
username: '',
realname: '',
email: '',
travelAgencyId: null,
roleId: ''
}
},
saveOrUpdateRole: async (formValues) => {
const formData = new FormData()
formData.append('role_id', formValues.role_id)
@ -117,7 +129,6 @@ const useAccountStore = create((set, get) => ({
formData.append('user_name', formValues.username)
formData.append('real_name', formValues.realname)
formData.append('email', formValues.email)
formData.append('travel_agency_id', formValues.travelAgencyId)
formData.append('roles', formValues.roleId)
@ -147,6 +158,7 @@ const useAccountStore = create((set, get) => ({
lastLogin: r.wu_lastlogindate,
travelAgencyName: r.travel_agency_name,
travelAgencyId: r.travel_agency_id,
disabled: r.wu_limitsign,
// 数据库支持逗号分隔多角色(5,6,7),目前界面只需单个。
roleId: parseInt(r.roles),
role: r.roles_name,

@ -3,14 +3,12 @@ import { appendRequestParams, fetchJSON, postForm } from '@/utils/request'
import { HT_HOST } from "@/config"
import { loadPageSpy } from '@/pageSpy'
import { usingStorage } from '@/hooks/usingStorage'
import { devtools } from 'zustand/middleware'
import { obervseLifecycle } from '@/utils/lifecycle'
import { lifecycleware } from '@/utils/lifecycle'
const KEY_LOGIN_TOKEN = 'G-STR:LOGIN_TOKEN'
const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID'
const KEY_USER_ID = 'G-INT:USER_ID'
const KEY_USER_DETAIL = 'G-JSON:USER_DETAIL'
const WILDCARD_TOKEN = '*'
@ -45,67 +43,52 @@ async function fetchLastRequet() {
return errcode !== 0 ? {} : result
}
const useAuthStore = create(obervseLifecycle((set, get) => ({
const useAuthStore = create(lifecycleware((set, get) => ({
onAuth: () => {
onAuth: async () => {
const { startTokenInterval, loadUserPermission } = get()
const { userId } = usingStorage()
loadUserPermission(userId)
startTokenInterval()
},
tokenInterval: null,
const { userId, loginToken } = usingStorage()
tokenTimeout: false,
loginStatus: 0,
permissionList: [],
isPermitted: (perm) => {
const { permissionList } = get()
// 测试权限使用:
// if (perm === '/account/management') return false
// if (perm === '/account/role/new') return false
// return true
// 以上是 Hardcode 判断
// 以下是权限列表从数据库读取后使用的方法
return permissionList.some((value) => {
if (value.indexOf(WILDCARD_TOKEN) == 0) {
return true
}
if (value === perm) {
return true
}
return false
})
appendRequestParams('token', loginToken)
appendRequestParams('lmi_sn', userId)
await loadUserPermission(userId)
startTokenInterval()
},
validateUserPassword: async (usr, pwd) => {
const { startTokenInterval, loadUserPermission } = get()
authenticate: async (usr, pwd) => {
const { onAuth } = get()
const { setStorage } = usingStorage()
const { token: loginToken } = await fetchLoginToken(usr, pwd)
const userDetail = await fetchUserDetail(loginToken)
await loadUserPermission(userDetail.LMI_SN)
setStorage(KEY_LOGIN_TOKEN, loginToken)
setStorage(KEY_USER_ID, userDetail.LMI_SN)
setStorage(KEY_TRAVEL_AGENCY_ID, userDetail.LMI_VEI_SN)
await onAuth()
set(() => ({
tokenTimeout: false,
loginStatus: 302
}))
setStorage(KEY_LOGIN_TOKEN, loginToken)
setStorage(KEY_USER_ID, userDetail.LMI_SN)
setStorage(KEY_TRAVEL_AGENCY_ID, userDetail.LMI_VEI_SN)
appendRequestParams('token', loginToken)
appendRequestParams('lmi_sn', userDetail.LMI_SN)
// loadPageSpy(`${json.Result.VName}-${json.Result.LoginName}`)
startTokenInterval()
},
loadUserPermission: async(userId) => {
let deaultPage = '/'
const permissionResult = await fetchPermissionListByUserId(userId)
const pageList = permissionResult.filter(p => {
return p.res_category === 'page'
})
if (pageList.length > 0) {
const resPattern = pageList[0].res_pattern
const splitResult = resPattern.split('=')
if (splitResult.length > 1)
deaultPage = splitResult[1]
}
set(() => ({
defaultRoute: deaultPage,
permissionList: permissionResult.map(p => p.res_pattern)
}))
},
@ -116,6 +99,7 @@ const useAuthStore = create(obervseLifecycle((set, get) => ({
clearStorage()
clearInterval(tokenInterval)
set(() => ({
defaultRoute: '/',
loginStatus: 0,
tokenInterval: null,
tokenTimeout: true
@ -152,6 +136,7 @@ const useAuthStore = create(obervseLifecycle((set, get) => ({
}))
},
// 迁移到 Account.js
changeUserPassword: (password, newPassword) => {
const { userId } = usingStorage()
const formData = new FormData();
@ -170,6 +155,35 @@ const useAuthStore = create(obervseLifecycle((set, get) => ({
});
},
isPermitted: (perm) => {
const { permissionList } = get()
// 测试权限使用:
// if (perm === '/account/management') return false
// if (perm === '/account/role/new') return false
// return true
// 以上是 Hardcode 判断
// 以下是权限列表从数据库读取后使用的方法
return permissionList.some((value) => {
if (value.indexOf(WILDCARD_TOKEN) == 0) {
return true
}
if (value === perm) {
return true
}
return false
})
},
tokenInterval: null,
tokenTimeout: false,
loginStatus: 0,
defaltRoute: '',
permissionList: [],
})))
export default useAuthStore

@ -1,28 +1,28 @@
const initListener = []
const authListener = []
export const onInit = (fn) => {
export const addInitLinstener = (fn) => {
initListener.push(fn)
}
export const onAuth = (fn) => {
export const addAuthLinstener = (fn) => {
authListener.push(fn)
}
export const fireInit = async () => {
export const notifyInit = async () => {
initListener.forEach(async (fn) => {
await fn()
})
}
export const fireAuth = (obj) => {
authListener.forEach(fn => fn(obj))
export const notifyAuth = async (obj) => {
authListener.forEach(async (fn) => await fn(obj))
}
// Zustand 中间件,用于订阅前端应用的生命周期,实验阶段
export const obervseLifecycle = (fn) => (set, get, store) => {
export const lifecycleware = (fn) => (set, get, store) => {
onInit(() => {
addInitLinstener(() => {
if (store.getState().hasOwnProperty('onInit')) {
store.getState().onInit()
} else {
@ -30,7 +30,7 @@ export const obervseLifecycle = (fn) => (set, get, store) => {
}
})
onAuth(() => {
addAuthLinstener(() => {
if (store.getState().hasOwnProperty('onAuth')) {
store.getState().onAuth()
} else {

@ -29,8 +29,8 @@ function App() {
const [password, setPassword] = useState('')
const [userDetail, setUserDetail] = useState({})
const [validateUserPassword, tokenTimeout, isPermitted] = useAuthStore(
(state) => [state.validateUserPassword, state.tokenTimeout, state.isPermitted])
const [authenticate, tokenTimeout, isPermitted] = useAuthStore(
(state) => [state.authenticate, state.tokenTimeout, state.isPermitted])
const { loginToken } = usingStorage()
@ -61,7 +61,7 @@ function App() {
}, [href])
const onSubmit = () => {
validateUserPassword(userDetail?.username, password)
authenticate(userDetail?.username, password)
.catch(ex => {
console.error(ex)
alert(t('Validation.LoginFailed'))

@ -6,9 +6,8 @@ import useAuthStore from '@/stores/Auth'
import useNoticeStore from '@/stores/Notice'
function Login() {
const [validateUserPassword, loginStatus] =
useAuthStore((state) => [state.validateUserPassword, state.loginStatus])
const getBulletinUnReadCount = useNoticeStore((state) => state.getBulletinUnReadCount)
const [authenticate, loginStatus, defaultRoute] =
useAuthStore((state) => [state.authenticate, state.loginStatus, state.defaultRoute])
const { t, i18n } = useTranslation()
const { notification } = App.useApp()
@ -17,12 +16,12 @@ function Login() {
useEffect (() => {
if (loginStatus === 302) {
navigate('/')
navigate(defaultRoute)
}
}, [loginStatus])
const onFinish = (values) => {
validateUserPassword(values.username, values.password)
authenticate(values.username, values.password)
.catch(ex => {
console.error(ex)
notification.error({

@ -3,7 +3,7 @@ import useAccountStore, { fetchRoleList, fetchTravelAgencyByName } from '@/store
import useFormStore from '@/stores/Form'
import { isEmpty } from '@/utils/commons'
import { ExclamationCircleFilled } from '@ant-design/icons'
import { App, Button, Col, Form, Input, Modal, Row, Select, Space, Table, Typography } from 'antd'
import { App, Button, Col, Form, Input, Modal, Row, Select, Space, Table, Typography, Switch } from 'antd'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -41,7 +41,7 @@ function Management() {
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD HH:mm:ss'))
},
{
title: t('account:action.edit'),
title: t('account:action'),
dataIndex: 'account:action',
render: actionRender
},
@ -53,10 +53,12 @@ function Management() {
)
}
function actionRender(text, account) {
function actionRender(_, account) {
return (
<Space key='actionRenderSpace' size='middle'>
<Button type='link' key='disable' onClick={() => showDisableConfirm(account)}>{t('account:action.disable')}</Button>
<Switch checkedChildren={t('account:action.enable')} unCheckedChildren={t('account:action.disable')} checked={account.disabled==0} onChange={(checked) => {
showDisableConfirm(account, checked)
}} />
<Button type='link' key='resetPassword' onClick={() => showResetPasswordConfirm(account)}>{t('account:action.resetPassword')}</Button>
</Space>
)
@ -69,9 +71,9 @@ function Management() {
const [currentTravelAgency, setCurrentTravelAgency] = useState(null)
const [accountForm] = Form.useForm()
const [searchAccountByCriteria, accountList, disableAccount, saveOrUpdateAccount, resetAccountPassword] =
const [searchAccountByCriteria, accountList, toggleAccountStatus, saveOrUpdateAccount, resetAccountPassword, newEmptyAccount] =
useAccountStore((state) =>
[state.searchAccountByCriteria, state.accountList, state.disableAccount, state.saveOrUpdateAccount, state.resetAccountPassword])
[state.searchAccountByCriteria, state.accountList, state.toggleAccountStatus, state.saveOrUpdateAccount, state.resetAccountPassword, state.newEmptyAccount])
const formValues = useFormStore(state => state.formValues)
const { notification, modal } = App.useApp()
@ -115,8 +117,13 @@ function Management() {
setAccountModalOpen(true)
}
const onNewAccount = () => {
const emptyAccount = newEmptyAccount()
accountForm.setFieldsValue(emptyAccount)
setAccountModalOpen(true)
}
const onAccountFinish = (values) => {
console.log(values)
saveOrUpdateAccount(values)
.then(() => {
handelAccountSearch()
@ -156,13 +163,27 @@ function Management() {
setCurrentTravelAgency(newValue)
}
const showDisableConfirm = (account) => {
const showDisableConfirm = (account, status) => {
const confirmTitle = status ? t('account:action.enable.title') : t('account:action.disable.title')
modal.confirm({
title: 'Do you want to disable this account?',
title: confirmTitle,
icon: <ExclamationCircleFilled />,
content: `Username: ${account.username}, Realname: ${account.realname}`,
content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname,
onOk() {
disableAccount(account.userId)
toggleAccountStatus(account.userId, status)
.then(() => {
handelAccountSearch()
})
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
},
onCancel() {
},
@ -170,7 +191,7 @@ function Management() {
}
const showResetPasswordConfirm = (account) => {
const randomPassword = account.username + (Math.floor(Math.random() * 900) + 100)
const randomPassword = account.username + '@' + (Math.floor(Math.random() * 900) + 100)
modal.confirm({
title: 'Do you want to reset password?',
icon: <ExclamationCircleFilled />,
@ -179,8 +200,8 @@ function Management() {
resetAccountPassword(account.userId, randomPassword)
.then(() => {
notification.info({
message: '新密码:' + randomPassword,
description: `请复制密码给 [${account.realname}]`,
message: `请复制新密码给 [${account.realname}]`,
description: '新密码:' + randomPassword,
placement: 'top',
duration: 60,
})
@ -317,7 +338,7 @@ function Management() {
<Row>
<Col span={24}>
<Space>
<Button onClick={() => setAccountModalOpen(true)}>{t('account:newAccount')}</Button>
<Button onClick={() => onNewAccount()}>{t('account:newAccount')}</Button>
</Space>
</Col>
</Row>

@ -69,6 +69,7 @@ function RoleList() {
['domestic', '国内供应商'],
['air-ticket', '机票供应商'],
['products', '产品价格'],
['page', '默认页面'],
]);
const permissionTree = []
@ -107,11 +108,11 @@ function RoleList() {
const [roleAllList, setRoleAllList] = useState([])
const [roleForm] = Form.useForm()
const [saveOrUpdateRole, newRole] =
const [saveOrUpdateRole, newEmptyRole] =
useAccountStore((state) =>
[state.saveOrUpdateRole, state.newRole])
[state.saveOrUpdateRole, state.newEmptyRole])
const { notification, modal } = App.useApp()
const { notification } = App.useApp()
const onRoleSeleted = (role) => {
fetchPermissionListByRoleId({ role_id: role.role_id })
@ -123,7 +124,7 @@ function RoleList() {
}
const onNewRole = () => {
const role = newRole()
const role = newEmptyRole()
roleForm.setFieldsValue(role)
setRoleModalOpen(true)
}

Loading…
Cancel
Save