Implemented multiple app passwords.
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
This commit is contained in:
parent
7ea5eaf1f5
commit
1e2adb2b28
8
assets/icons/apppasswordicon.svg
Normal file
8
assets/icons/apppasswordicon.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.913777,0,0,0.913777,-11.957695,-8.458487)">
|
||||
<path d="M56.866,48.187C56.775,48.817 56.734,49.463 56.734,50.118C56.734,52.243 57.143,54.279 57.906,56.141C54.515,53.972 49.815,52.352 44,52.352C31.125,52.352 23.734,60.274 23.734,64.477C23.734,64.93 23.953,65.165 24.547,65.165L63.031,65.165L63.031,71.774L24.766,71.774C19.047,71.774 16.219,69.852 16.219,65.774C16.219,56.587 27.547,45.727 44,45.727C48.778,45.727 53.123,46.643 56.866,48.187ZM57.984,27.368C57.984,35.743 51.766,42.477 44,42.477C36.25,42.477 30.031,35.758 30.031,27.399C30.031,19.227 36.328,12.54 44,12.54C51.719,12.54 57.984,19.165 57.984,27.368ZM36.812,27.384C36.812,32.352 40.109,35.868 44,35.868C47.938,35.868 51.203,32.321 51.203,27.368C51.203,22.587 47.922,19.149 44,19.149C40.125,19.149 36.812,22.634 36.812,27.384Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M72.469,39.508C66.531,39.508 61.812,44.29 61.812,50.118C61.812,54.462 64.266,58.212 68.109,59.899L68.109,74.743C68.109,75.29 68.359,75.821 68.766,76.243L71.609,78.915C72.141,79.399 72.969,79.462 73.562,78.868L78.5,73.946C79.125,73.305 79.062,72.383 78.484,71.774L76.031,69.258L79.641,65.649C80.234,65.055 80.25,64.118 79.609,63.43L76.234,60.087C80.641,57.962 83.125,54.399 83.125,50.118C83.125,44.29 78.375,39.508 72.469,39.508ZM72.453,43.696C74.094,43.696 75.422,45.055 75.422,46.696C75.422,48.321 74.094,49.696 72.453,49.696C70.844,49.696 69.469,48.321 69.469,46.696C69.469,45.055 70.797,43.696 72.453,43.696Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
99
src/components/Dashboard/Management/AppPasswords.jsx
Normal file
99
src/components/Dashboard/Management/AppPasswords.jsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
|
||||
import NewAppPassword from './AppPasswords/NewAppPassword'
|
||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||
import ColumnViewButton from '../common/ColumnViewButton'
|
||||
import ObjectTable from '../common/ObjectTable'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
import ListIcon from '../../Icons/ListIcon'
|
||||
import GridIcon from '../../Icons/GridIcon'
|
||||
import useViewMode from '../hooks/useViewMode'
|
||||
import ExportListButton from '../common/ExportListButton'
|
||||
|
||||
const AppPasswords = () => {
|
||||
const [newAppPasswordOpen, setNewAppPasswordOpen] = useState(false)
|
||||
const tableRef = useRef()
|
||||
|
||||
const [viewMode, setViewMode] = useViewMode('appPassword')
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useColumnVisibility('appPassword')
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
label: 'New App Password',
|
||||
key: 'newAppPassword',
|
||||
icon: <PlusIcon />
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Reload List',
|
||||
key: 'reloadList',
|
||||
icon: <ReloadIcon />
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reloadList') {
|
||||
tableRef.current?.reload()
|
||||
} else if (key === 'newAppPassword') {
|
||||
setNewAppPasswordOpen(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex vertical={'true'} gap='large'>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
<ColumnViewButton
|
||||
type='appPassword'
|
||||
loading={false}
|
||||
visibleState={columnVisibility}
|
||||
updateVisibleState={setColumnVisibility}
|
||||
/>
|
||||
<ExportListButton objectType='appPassword' />
|
||||
</Space>
|
||||
<Space>
|
||||
<Button
|
||||
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
|
||||
onClick={() =>
|
||||
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
<ObjectTable
|
||||
ref={tableRef}
|
||||
type='appPassword'
|
||||
cards={viewMode === 'cards'}
|
||||
visibleColumns={columnVisibility}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={newAppPasswordOpen}
|
||||
footer={null}
|
||||
width={700}
|
||||
onCancel={() => {
|
||||
setNewAppPasswordOpen(false)
|
||||
}}
|
||||
>
|
||||
<NewAppPassword
|
||||
onOk={() => {
|
||||
setNewAppPasswordOpen(false)
|
||||
tableRef.current?.reload()
|
||||
}}
|
||||
reset={newAppPasswordOpen}
|
||||
/>
|
||||
</Modal>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppPasswords
|
||||
@ -0,0 +1,212 @@
|
||||
import { useRef, useState, useContext } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Space, Flex, Modal } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import ViewButton from '../../common/ViewButton'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import ObjectForm from '../../common/ObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import RegenerateAppPasswordSecret from './RegenerateAppPasswordSecret.jsx'
|
||||
import { getModelByName } from '../../../../database/ObjectModels.js'
|
||||
import { AuthContext } from '../../context/AuthContext.jsx'
|
||||
|
||||
const AppPasswordInfo = () => {
|
||||
const location = useLocation()
|
||||
const objectFormRef = useRef(null)
|
||||
const actionHandlerRef = useRef(null)
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
const appPasswordId = new URLSearchParams(location.search).get(
|
||||
'appPasswordId'
|
||||
)
|
||||
const [regenerateSecretOpen, setRegenerateSecretOpen] = useState(false)
|
||||
const [collapseState, updateCollapseState] = useCollapseState(
|
||||
'AppPasswordInfo',
|
||||
{
|
||||
info: true,
|
||||
auditLogs: true
|
||||
}
|
||||
)
|
||||
const [objectFormState, setEditFormState] = useState({
|
||||
isEditing: false,
|
||||
editLoading: false,
|
||||
formValid: false,
|
||||
lock: null,
|
||||
loading: false,
|
||||
objectData: {}
|
||||
})
|
||||
|
||||
const actions = {
|
||||
reload: () => {
|
||||
objectFormRef?.current?.handleFetchObject?.()
|
||||
return true
|
||||
},
|
||||
regenerateSecret: () => {
|
||||
setRegenerateSecretOpen(true)
|
||||
return false
|
||||
},
|
||||
edit: () => {
|
||||
objectFormRef?.current?.startEditing?.()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
objectFormRef?.current?.cancelEditing?.()
|
||||
return true
|
||||
},
|
||||
finishEdit: () => {
|
||||
objectFormRef?.current?.handleUpdate?.()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const editDisabled = getModelByName('appPassword')
|
||||
.actions.find((action) => action.name === 'edit')
|
||||
.disabled({ ...objectFormState.objectData, _user: userProfile })
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ maxHeight: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='appPassword'
|
||||
id={appPasswordId}
|
||||
disabled={objectFormState.loading}
|
||||
objectData={objectFormState.objectData}
|
||||
/>
|
||||
<ViewButton
|
||||
disabled={objectFormState.loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'App Password Information' },
|
||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||
]}
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='appPassword'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='appPassword'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
</Space>
|
||||
<LockIndicator lock={objectFormState.lock} />
|
||||
</Space>
|
||||
<Space>
|
||||
<EditButtons
|
||||
isEditing={objectFormState.isEditing}
|
||||
handleUpdate={() => {
|
||||
actionHandlerRef.current.callAction('finishEdit')
|
||||
}}
|
||||
cancelEditing={() => {
|
||||
actionHandlerRef.current.callAction('cancelEdit')
|
||||
}}
|
||||
startEditing={() => {
|
||||
actionHandlerRef.current.callAction('edit')
|
||||
}}
|
||||
editLoading={objectFormState.editLoading}
|
||||
formValid={objectFormState.formValid}
|
||||
disabled={
|
||||
objectFormState.lock?.locked ||
|
||||
objectFormState.loading ||
|
||||
editDisabled
|
||||
}
|
||||
loading={objectFormState.editLoading}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
<ScrollBox>
|
||||
<Flex vertical gap={'large'}>
|
||||
<ActionHandler
|
||||
actions={actions}
|
||||
loading={objectFormState.loading}
|
||||
ref={actionHandlerRef}
|
||||
>
|
||||
<InfoCollapse
|
||||
title='App Password Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
||||
collapseKey='info'
|
||||
>
|
||||
<ObjectForm
|
||||
id={appPasswordId}
|
||||
type='appPassword'
|
||||
style={{ height: '100%' }}
|
||||
ref={objectFormRef}
|
||||
onStateChange={(state) => {
|
||||
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||
}}
|
||||
>
|
||||
{({ loading, isEditing, objectData }) => (
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
indicator={<LoadingOutlined />}
|
||||
isEditing={isEditing}
|
||||
type='appPassword'
|
||||
objectData={objectData}
|
||||
/>
|
||||
)}
|
||||
</ObjectForm>
|
||||
</InfoCollapse>
|
||||
</ActionHandler>
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
collapseKey='auditLogs'
|
||||
>
|
||||
{objectFormState.loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': appPasswordId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</ScrollBox>
|
||||
</Flex>
|
||||
|
||||
<Modal
|
||||
open={regenerateSecretOpen}
|
||||
destroyOnClose
|
||||
width={650}
|
||||
onCancel={() => {
|
||||
actionHandlerRef.current?.clearAction?.()
|
||||
setRegenerateSecretOpen(false)
|
||||
}}
|
||||
footer={null}
|
||||
>
|
||||
<RegenerateAppPasswordSecret id={appPasswordId} />
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppPasswordInfo
|
||||
@ -0,0 +1,91 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import NewObjectForm from '../../common/NewObjectForm'
|
||||
import WizardView from '../../common/WizardView'
|
||||
|
||||
const NewAppPassword = ({ onOk, reset, defaultValues = {} }) => {
|
||||
return (
|
||||
<NewObjectForm
|
||||
type='appPassword'
|
||||
reset={reset}
|
||||
defaultValues={{
|
||||
active: true,
|
||||
...defaultValues
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||
const steps = [
|
||||
{
|
||||
title: 'Required',
|
||||
key: 'required',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='appPassword'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={true}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Optional',
|
||||
key: 'optional',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='appPassword'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Summary',
|
||||
key: 'summary',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='appPassword'
|
||||
column={1}
|
||||
bordered={false}
|
||||
visibleProperties={{
|
||||
_id: false,
|
||||
createdAt: false,
|
||||
updatedAt: false,
|
||||
secret: false
|
||||
}}
|
||||
isEditing={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
return (
|
||||
<WizardView
|
||||
steps={steps}
|
||||
loading={submitLoading}
|
||||
formValid={formValid}
|
||||
title='New App Password'
|
||||
onSubmit={async () => {
|
||||
const result = await handleSubmit()
|
||||
if (result) {
|
||||
onOk()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</NewObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
NewAppPassword.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
reset: PropTypes.bool,
|
||||
defaultValues: PropTypes.object
|
||||
}
|
||||
|
||||
export default NewAppPassword
|
||||
@ -0,0 +1,77 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useContext, useState } from 'react'
|
||||
import { Result, Typography, Flex, Button } from 'antd'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import CopyButton from '../../common/CopyButton'
|
||||
import AppPasswordIcon from '../../../Icons/AppPasswordIcon.jsx'
|
||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const RegenerateAppPasswordSecret = ({ id }) => {
|
||||
const { sendObjectFunction } = useContext(ApiServerContext)
|
||||
const [appPassword, setAppPassword] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [passwordGenerated, setPasswordGenerated] = useState(false)
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
setLoading(true)
|
||||
setAppPassword(null)
|
||||
try {
|
||||
const result = await sendObjectFunction(
|
||||
id,
|
||||
'appPassword',
|
||||
'regenerateSecret',
|
||||
{}
|
||||
)
|
||||
if (result?.appPassword) {
|
||||
setAppPassword(result.appPassword)
|
||||
setPasswordGenerated(true)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical align='center'>
|
||||
<Result
|
||||
title={
|
||||
passwordGenerated ? 'Secret Regenerated' : 'Regenerate Secret'
|
||||
}
|
||||
disabled={passwordGenerated}
|
||||
subTitle={
|
||||
appPassword ? (
|
||||
<Text>Copy this secret now. It will not be shown again.</Text>
|
||||
) : (
|
||||
<Text>Generate a new secret for this app password.</Text>
|
||||
)
|
||||
}
|
||||
icon={<AppPasswordIcon />}
|
||||
>
|
||||
<Flex justify='center' style={{ minWidth: '395px' }}>
|
||||
<Flex justify='center'>
|
||||
<Flex gap='small' align='center' justify='center'>
|
||||
<CopyButton size='default' text={appPassword} />
|
||||
<Text code style={{ fontSize: '18px' }}>
|
||||
{appPassword || '••••••••••••••••••••••••••••••••'}
|
||||
</Text>
|
||||
<Button
|
||||
type='text'
|
||||
loading={loading}
|
||||
onClick={handleRegenerate}
|
||||
icon={<ReloadIcon />}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Result>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
RegenerateAppPasswordSecret.propTypes = {
|
||||
id: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default RegenerateAppPasswordSecret
|
||||
@ -21,6 +21,7 @@ import CourierIcon from '../../Icons/CourierIcon'
|
||||
import CourierServiceIcon from '../../Icons/CourierServiceIcon'
|
||||
import TaxRateIcon from '../../Icons/TaxRateIcon'
|
||||
import TaxRecordIcon from '../../Icons/TaxRecordIcon'
|
||||
import AppPasswordIcon from '../../Icons/AppPasswordIcon'
|
||||
|
||||
const items = [
|
||||
{
|
||||
@ -131,6 +132,12 @@ const items = [
|
||||
label: 'Users',
|
||||
path: '/dashboard/management/users'
|
||||
},
|
||||
{
|
||||
key: 'appPasswords',
|
||||
icon: <AppPasswordIcon />,
|
||||
label: 'App Passwords',
|
||||
path: '/dashboard/management/apppasswords'
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
icon: <SettingsIcon />,
|
||||
@ -167,6 +174,7 @@ const routeKeyMap = {
|
||||
'/dashboard/management/filaments': 'filaments',
|
||||
'/dashboard/management/parts': 'parts',
|
||||
'/dashboard/management/users': 'users',
|
||||
'/dashboard/management/apppasswords': 'appPasswords',
|
||||
'/dashboard/management/products': 'products',
|
||||
'/dashboard/management/vendors': 'vendors',
|
||||
'/dashboard/management/couriers': 'couriers',
|
||||
|
||||
@ -10,6 +10,7 @@ import ViewButton from '../../common/ViewButton'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import AppPasswordIcon from '../../../Icons/AppPasswordIcon.jsx'
|
||||
import ObjectForm from '../../common/ObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
@ -20,16 +21,18 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import SetAppPassword from './SetAppPassword.jsx'
|
||||
import NewAppPassword from '../AppPasswords/NewAppPassword.jsx'
|
||||
|
||||
const UserInfo = () => {
|
||||
const location = useLocation()
|
||||
const objectFormRef = useRef(null)
|
||||
const appPasswordsTableRef = useRef(null)
|
||||
const actionHandlerRef = useRef(null)
|
||||
const userId = new URLSearchParams(location.search).get('userId')
|
||||
const [setAppPasswordOpen, setSetAppPasswordOpen] = useState(false)
|
||||
const [newAppPasswordOpen, setNewAppPasswordOpen] = useState(false)
|
||||
const [collapseState, updateCollapseState] = useCollapseState('UserInfo', {
|
||||
info: true,
|
||||
appPasswords: true,
|
||||
notes: true,
|
||||
auditLogs: true
|
||||
})
|
||||
@ -44,11 +47,11 @@ const UserInfo = () => {
|
||||
|
||||
const actions = {
|
||||
reload: () => {
|
||||
objectFormRef?.current?.fetchObject?.()
|
||||
objectFormRef?.current?.handleFetchObject?.()
|
||||
return true
|
||||
},
|
||||
setAppPassword: () => {
|
||||
setSetAppPasswordOpen(true)
|
||||
newAppPassword: () => {
|
||||
setNewAppPasswordOpen(true)
|
||||
return false
|
||||
},
|
||||
edit: () => {
|
||||
@ -85,6 +88,7 @@ const UserInfo = () => {
|
||||
disabled={objectFormState.loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'User Information' },
|
||||
{ key: 'appPasswords', label: 'App Passwords' },
|
||||
{ key: 'notes', label: 'Notes' },
|
||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||
]}
|
||||
@ -158,6 +162,26 @@ const UserInfo = () => {
|
||||
</ObjectForm>
|
||||
</InfoCollapse>
|
||||
</ActionHandler>
|
||||
<InfoCollapse
|
||||
title='App Passwords'
|
||||
icon={<AppPasswordIcon />}
|
||||
active={collapseState.appPasswords}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('appPasswords', expanded)
|
||||
}
|
||||
collapseKey='appPasswords'
|
||||
>
|
||||
{!userId ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='appPassword'
|
||||
masterFilter={{ user: userId }}
|
||||
visibleColumns={{ user: false }}
|
||||
ref={appPasswordsTableRef}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
@ -193,16 +217,23 @@ const UserInfo = () => {
|
||||
</Flex>
|
||||
|
||||
<Modal
|
||||
open={setAppPasswordOpen}
|
||||
open={newAppPasswordOpen}
|
||||
destroyOnClose
|
||||
width={650}
|
||||
width={700}
|
||||
onCancel={() => {
|
||||
actionHandlerRef.current?.clearAction?.()
|
||||
setSetAppPasswordOpen(false)
|
||||
setNewAppPasswordOpen(false)
|
||||
}}
|
||||
footer={null}
|
||||
>
|
||||
<SetAppPassword id={userId} />
|
||||
<NewAppPassword
|
||||
onOk={() => {
|
||||
setNewAppPasswordOpen(false)
|
||||
appPasswordsTableRef.current?.reload?.()
|
||||
}}
|
||||
reset={newAppPasswordOpen}
|
||||
defaultValues={{ user: { ...objectFormState.objectData } }}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { createElement } from 'react'
|
||||
import { createElement, useContext } from 'react'
|
||||
import { Dropdown, Button } from 'antd'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useActionsModal } from '../context/ActionsModalContext'
|
||||
import KeyboardShortcut from './KeyboardShortcut'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
|
||||
// Recursively filter actions based on visibleActions
|
||||
function filterActionsByVisibility(actions, visibleActions) {
|
||||
@ -47,9 +48,11 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
|
||||
var disabled = actionUrl && actionUrl === currentUrlWithActions
|
||||
var visible = true
|
||||
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
|
||||
if (action.disabled) {
|
||||
if (typeof action.disabled === 'function') {
|
||||
disabled = action.disabled(objectData)
|
||||
disabled = action.disabled({ ...objectData, _user: userProfile })
|
||||
} else {
|
||||
disabled = action.disabled
|
||||
}
|
||||
@ -105,7 +108,6 @@ const ObjectActions = ({
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { showActionsModal } = useActionsModal()
|
||||
|
||||
// Get current url without 'action' param
|
||||
const currentUrlWithoutActions = stripActionParam(
|
||||
location.pathname,
|
||||
|
||||
@ -105,7 +105,7 @@ const ObjectTable = forwardRef(
|
||||
const { token } = useContext(AuthContext)
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
const onStateChangeRef = useRef(onStateChange)
|
||||
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
useEffect(() => {
|
||||
onStateChangeRef.current = onStateChange
|
||||
}, [onStateChange])
|
||||
@ -191,7 +191,10 @@ const ObjectTable = forwardRef(
|
||||
var disabled = false
|
||||
if (action.disabled) {
|
||||
if (typeof action.disabled === 'function') {
|
||||
disabled = action.disabled(objectData)
|
||||
disabled = action.disabled({
|
||||
...objectData,
|
||||
_user: userProfile
|
||||
})
|
||||
} else {
|
||||
disabled = action.disabled
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import PropTypes from 'prop-types'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
import { AuthContext } from './AuthContext'
|
||||
|
||||
const ActionsModalContext = createContext()
|
||||
|
||||
@ -63,6 +64,7 @@ const ActionsModalProvider = ({ children }) => {
|
||||
const { Text } = Typography
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
@ -129,7 +131,7 @@ const ActionsModalProvider = ({ children }) => {
|
||||
|
||||
if (typeof action.disabled !== 'undefined') {
|
||||
if (typeof action.disabled === 'function') {
|
||||
disabled = action.disabled(objectData)
|
||||
disabled = action.disabled({ ...objectData, _user: userProfile })
|
||||
} else {
|
||||
disabled = action.disabled
|
||||
}
|
||||
|
||||
6
src/components/Icons/AppPasswordIcon.jsx
Normal file
6
src/components/Icons/AppPasswordIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
||||
import Icon from '@ant-design/icons'
|
||||
import CustomIconSvg from '../../../assets/icons/apppasswordicon.svg?react'
|
||||
|
||||
const AppPasswordIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||
|
||||
export default AppPasswordIcon
|
||||
@ -22,6 +22,7 @@ import { OrderItem } from './models/OrderItem'
|
||||
import { Shipment } from './models/Shipment'
|
||||
import { AuditLog } from './models/AuditLog'
|
||||
import { User } from './models/User'
|
||||
import { AppPassword } from './models/AppPassword.js'
|
||||
import { NoteType } from './models/NoteType'
|
||||
import { Note } from './models/Note'
|
||||
import { DocumentSize } from './models/DocumentSize.js'
|
||||
@ -61,6 +62,7 @@ export const objectModels = [
|
||||
Shipment,
|
||||
AuditLog,
|
||||
User,
|
||||
AppPassword,
|
||||
NoteType,
|
||||
Note,
|
||||
DocumentSize,
|
||||
@ -101,6 +103,7 @@ export {
|
||||
Shipment,
|
||||
AuditLog,
|
||||
User,
|
||||
AppPassword,
|
||||
NoteType,
|
||||
Note,
|
||||
DocumentSize,
|
||||
|
||||
139
src/database/models/AppPassword.js
Normal file
139
src/database/models/AppPassword.js
Normal file
@ -0,0 +1,139 @@
|
||||
import AppPasswordIcon from '../../components/Icons/AppPasswordIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||
import LockIcon from '../../components/Icons/LockIcon'
|
||||
|
||||
export const AppPassword = {
|
||||
name: 'appPassword',
|
||||
label: 'App Password',
|
||||
prefix: 'APP',
|
||||
icon: AppPasswordIcon,
|
||||
actions: [
|
||||
{
|
||||
name: 'info',
|
||||
label: 'Info',
|
||||
default: true,
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/management/apppasswords/info?appPasswordId=${_id}`
|
||||
},
|
||||
{
|
||||
name: 'reload',
|
||||
label: 'Reload',
|
||||
icon: ReloadIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/management/apppasswords/info?appPasswordId=${_id}&action=reload`
|
||||
},
|
||||
|
||||
{
|
||||
name: 'edit',
|
||||
label: 'Edit',
|
||||
row: true,
|
||||
icon: EditIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/management/apppasswords/info?appPasswordId=${_id}&action=edit`,
|
||||
visible: (objectData) => {
|
||||
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||
},
|
||||
disabled: (objectData) => {
|
||||
return objectData?._user?._id != objectData?.user?._id
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'finishEdit',
|
||||
label: 'Save Edits',
|
||||
icon: CheckIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/management/apppasswords/info?appPasswordId=${_id}&action=finishEdit`,
|
||||
visible: (objectData) => {
|
||||
return objectData?._isEditing && objectData?._isEditing == true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cancelEdit',
|
||||
label: 'Cancel Edits',
|
||||
icon: XMarkIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/management/apppasswords/info?appPasswordId=${_id}&action=cancelEdit`,
|
||||
visible: (objectData) => {
|
||||
return objectData?._isEditing && objectData?._isEditing == true
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'regenerateSecret',
|
||||
label: 'Regenerate Secret',
|
||||
type: 'button',
|
||||
row: true,
|
||||
icon: LockIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/management/apppasswords/info?appPasswordId=${_id}&action=regenerateSecret`,
|
||||
disabled: (objectData) => {
|
||||
return objectData?._user?._id != objectData?.user?._id
|
||||
}
|
||||
}
|
||||
],
|
||||
columns: ['name', '_reference', 'user', 'active', 'createdAt', 'updatedAt'],
|
||||
filters: ['_id', 'name', 'user', 'active', 'user._id'],
|
||||
sorters: ['name', 'user', 'active', 'createdAt', 'updatedAt'],
|
||||
properties: [
|
||||
{
|
||||
name: '_id',
|
||||
label: 'ID',
|
||||
columnFixed: 'left',
|
||||
type: 'id',
|
||||
objectType: 'appPassword',
|
||||
showCopy: true
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
label: 'Created At',
|
||||
type: 'dateTime',
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
columnFixed: 'left',
|
||||
required: true,
|
||||
type: 'text'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'updatedAt',
|
||||
label: 'Updated At',
|
||||
type: 'dateTime',
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
label: 'User',
|
||||
required: true,
|
||||
readOnly: (objectData) => {
|
||||
return objectData?.createdAt != null
|
||||
},
|
||||
type: 'object',
|
||||
objectType: 'user',
|
||||
showHyperlink: true
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
label: 'Active',
|
||||
required: true,
|
||||
type: 'bool'
|
||||
},
|
||||
{
|
||||
name: 'secret',
|
||||
label: 'Secret',
|
||||
type: 'password',
|
||||
required: false,
|
||||
readOnly: true,
|
||||
value: (objectData) =>
|
||||
objectData?._id ? '••••••••••••••••••••••••••••••••' : undefined
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import PersonIcon from '../../components/Icons/PersonIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import LockIcon from '../../components/Icons/LockIcon'
|
||||
import AppPasswordIcon from '../../components/Icons/AppPasswordIcon'
|
||||
|
||||
export const User = {
|
||||
name: 'user',
|
||||
@ -25,11 +25,15 @@ export const User = {
|
||||
`/dashboard/management/users/info?userId=${_id}&action=reload`
|
||||
},
|
||||
{
|
||||
name: 'setAppPassword',
|
||||
label: 'Set App Password',
|
||||
icon: LockIcon,
|
||||
name: 'newAppPassword',
|
||||
label: 'New App Password',
|
||||
type: 'button',
|
||||
icon: AppPasswordIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/management/users/info?userId=${_id}&action=setAppPassword`
|
||||
`/dashboard/management/users/info?userId=${_id}&action=newAppPassword`,
|
||||
disabled: (objectData) => {
|
||||
return objectData?._user?._id != objectData?._id
|
||||
}
|
||||
}
|
||||
],
|
||||
columns: ['name', '_reference', 'username', 'email', 'role', 'createdAt'],
|
||||
|
||||
@ -21,6 +21,8 @@ const NoteTypeInfo = lazy(() => import('../components/Dashboard/Management/NoteT
|
||||
const NoteInfo = lazy(() => import('../components/Dashboard/Management/Notes/NoteInfo.jsx'))
|
||||
const Users = lazy(() => import('../components/Dashboard/Management/Users.jsx'))
|
||||
const UserInfo = lazy(() => import('../components/Dashboard/Management/Users/UserInfo.jsx'))
|
||||
const AppPasswords = lazy(() => import('../components/Dashboard/Management/AppPasswords.jsx'))
|
||||
const AppPasswordInfo = lazy(() => import('../components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx'))
|
||||
const Hosts = lazy(() => import('../components/Dashboard/Management/Hosts.jsx'))
|
||||
const HostInfo = lazy(() => import('../components/Dashboard/Management/Hosts/HostInfo.jsx'))
|
||||
const DocumentSizes = lazy(() => import('../components/Dashboard/Management/DocumentSizes.jsx'))
|
||||
@ -151,6 +153,16 @@ const ManagementRoutes = [
|
||||
element={<DocumentTemplateDesign />}
|
||||
/>,
|
||||
<Route key='users' path='management/users' element={<Users />} />,
|
||||
<Route
|
||||
key='apppasswords'
|
||||
path='management/apppasswords'
|
||||
element={<AppPasswords />}
|
||||
/>,
|
||||
<Route
|
||||
key='apppasswords-info'
|
||||
path='management/apppasswords/info'
|
||||
element={<AppPasswordInfo />}
|
||||
/>,
|
||||
<Route key='settings' path='management/settings' element={<Settings />} />,
|
||||
<Route key='auditlogs' path='management/auditlogs' element={<AuditLogs />} />,
|
||||
<Route key='taxrates' path='management/taxrates' element={<TaxRates />} />,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user