Implemented multiple app passwords.
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-03-02 01:58:34 +00:00
parent 7ea5eaf1f5
commit 1e2adb2b28
15 changed files with 717 additions and 20 deletions

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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>
</>
)

View File

@ -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,

View File

@ -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
}

View File

@ -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
}

View 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

View File

@ -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,

View 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
}
]
}

View File

@ -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'],

View File

@ -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 />} />,