diff --git a/README.md b/README.md index 6c2826743..4b44ddfa1 100644 --- a/README.md +++ b/README.md @@ -108,12 +108,12 @@ _Server version:_ If successful, this automatically starts a Chrome/Chromium bro **Authentication & Authorization** -Wether you start the API with `yarn dev-ms-api` or `yarn dev-ms-api-auth0` you can log in with two default users just by typing their name in the 'Sign in with Development Users' section: +When you start the API with `yarn dev-ms-api`, you can log in with two default users just by typing their name in the 'Sign in with Development Users' section: - Admin: With the username `admin`. - John Doe: With the username `johndoe`. -If you start the API with `yarn dev-ms-api-auth0`, two users are created on the development Auth0 environment: +Additionaly, if you have set up the environments folder in `src/management-system/src/backend/server/environment-configurations/` and `useAuth0` is set to `true` these two default users are created in the development Auth0 environment. - Admin: With the username `admin` and the password `ProceedAdm1n!`. - John Doe: With the username `johndoe` and the password `JohnDoe1!`. diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx index 45615d420..519444dcc 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx @@ -60,7 +60,7 @@ const RolesPage: FC = () => { width: 100, render: (id: string, role: Role) => selectedRowKeys.length === 0 ? ( - + any; + createProcess?: (values: { name: string; description?: string }) => any; wrapperElement?: ReactNode; }; @@ -26,7 +26,7 @@ const ProcessCreationButton: React.FC = ({ const [isProcessModalOpen, setIsProcessModalOpen] = useState(false); const { mutateAsync: postProcess } = usePostAsset('/process'); - const createNewProcess = async (values: { name: string; description: string }) => { + const createNewProcess = async (values: { name: string; description?: string }) => { const { metaInfo, bpmn } = await createProcess(values); try { await postProcess({ diff --git a/src/management-system-v2/components/process-edit-button.tsx b/src/management-system-v2/components/process-edit-button.tsx new file mode 100644 index 000000000..e8de1f0c3 --- /dev/null +++ b/src/management-system-v2/components/process-edit-button.tsx @@ -0,0 +1,80 @@ +'use client'; + +import React, { ReactNode, useState } from 'react'; + +import { Button } from 'antd'; +import type { ButtonProps } from 'antd'; +import ProcessModal from './process-modal'; +import { useGetAsset, usePutAsset } from '@/lib/fetch-data'; + +type ProcessEditButtonProps = ButtonProps & { + definitionId: string; + wrapperElement?: ReactNode; + onEdited?: () => any; +}; + +/** + * Button to edit Processes including a Modal for updating values. Alternatively, a custom wrapper element can be used instead of a button. + */ +const ProcessEditButton: React.FC = ({ + definitionId, + wrapperElement, + onEdited, + ...props +}) => { + const [isProcessModalOpen, setIsProcessModalOpen] = useState(false); + const { data: processData } = useGetAsset('/process/{definitionId}', { + params: { path: { definitionId } }, + }); + + const { mutateAsync: updateProcess } = usePutAsset('/process/{definitionId}'); + + const editProcess = async (values: { name: string; description?: string }) => { + try { + await updateProcess({ + params: { path: { definitionId } }, + body: { ...values }, + }); + onEdited && onEdited(); + } catch (err) { + console.log(err); + } + }; + + return ( + <> + {wrapperElement ? ( +
{ + setIsProcessModalOpen(true); + }} + > + {wrapperElement} +
+ ) : ( + + )} + { + setIsProcessModalOpen(false); + + if (values) { + editProcess(values); + } + }} + show={isProcessModalOpen} + > + + ); +}; + +export default ProcessEditButton; diff --git a/src/management-system-v2/components/process-list.tsx b/src/management-system-v2/components/process-list.tsx index 13cb3f768..e2684fe02 100644 --- a/src/management-system-v2/components/process-list.tsx +++ b/src/management-system-v2/components/process-list.tsx @@ -23,6 +23,7 @@ import React, { import { CopyOutlined, ExportOutlined, + EditOutlined, DeleteOutlined, StarOutlined, EyeOutlined, @@ -36,6 +37,9 @@ import Preview from './previewProcess'; import useLastClickedStore from '@/lib/use-last-clicked-process-store'; import classNames from 'classnames'; import { generateDateString } from '@/lib/utils'; +import ProcessEditButton from './process-edit-button'; +import { AuthCan } from '@/lib/iamComponents'; +import { toCaslResource } from '@/lib/ability/caslAbility'; import { ApiData, useDeleteAsset, useInvalidateAsset, usePostAsset } from '@/lib/fetch-data'; import { useUserPreferences } from '@/lib/user-preferences'; import ProcessDeleteSingleModal from './process-delete-single'; @@ -79,6 +83,7 @@ const ProcessList: FC = ({ }) => { const router = useRouter(); + const invalidateProcesses = useInvalidateAsset('/process'); const refreshData = useInvalidateAsset('/process'); const [previewerOpen, setPreviewerOpen] = useState(false); @@ -188,6 +193,19 @@ const ProcessList: FC = ({ }} />
+ + + + + } + onEdited={() => { + invalidateProcesses(); + }} + /> + {ability.can('delete', 'Process') && ( { - const [submittable, setSubmittable] = useState(false); - - // Watch all values - const values = Form.useWatch([], form); - - React.useEffect(() => { - form.validateFields({ validateOnly: true }).then( - () => { - setSubmittable(true); - }, - () => { - setSubmittable(false); - }, - ); - }, [form, values]); - +const ModalSubmitButton = ({ + submittable, + onSubmit, + submitText, +}: { + submittable: boolean; + onSubmit: Function; + submitText: string; +}) => { return ( ); }; type ProcessModalProps = { show: boolean; - close: (values?: { name: string; description: string }) => void; + close: (values?: { name: string; description?: string }) => void; + initialProcessData?: { name?: string; description?: string }; }; -const ProcessModal: React.FC = ({ show, close }) => { +const ProcessModal: React.FC = ({ show, close, initialProcessData }) => { + const [submittable, setSubmittable] = useState(false); const [form] = Form.useForm(); + // Watch all values + const values = Form.useWatch([], form); + + React.useEffect(() => { + form.setFieldsValue(initialProcessData); + }, [form, initialProcessData]); + + React.useEffect(() => { + // Only allow to submit in modal if any of the values was changed + const initialFieldValuesModified = + form.isFieldsTouched() && + (form.getFieldValue('name') !== initialProcessData?.name || + form.getFieldValue('description') !== initialProcessData?.description); + + if (initialFieldValuesModified) { + form.validateFields({ validateOnly: true }).then( + () => { + setSubmittable(true); + }, + () => { + setSubmittable(false); + }, + ); + } else { + setSubmittable(false); + } + }, [form, initialProcessData, values]); + return ( { close(); + form.resetFields(); }} footer={[ , - , + { + close(values); + form.resetFields(); + }} + submitText={initialProcessData ? 'Edit Process' : 'Create Process'} + >, ]} >
- + = ({ setThumbHeight(newThumbHeight); setScrollPosition(newScrollPosition); } - }, [reachedEndCallBack, scrolledToTH, threshhold]); + }, [reachedEndCallBack, scrolledToTH, threshold]); const handleScrollbarClick = useCallback((e: MouseEvent) => { if (containerRef.current && thumbRef.current) { diff --git a/src/management-system-v2/components/version-toolbar.tsx b/src/management-system-v2/components/version-toolbar.tsx index f0486accc..8ce8332fe 100644 --- a/src/management-system-v2/components/version-toolbar.tsx +++ b/src/management-system-v2/components/version-toolbar.tsx @@ -72,7 +72,7 @@ const VersionToolbar: React.FC = () => { setIsConfirmationModalOpen(true); }; - const createNewProcess = async (values: { name: string; description: string }) => { + const createNewProcess = async (values: { name: string; description?: string }) => { const saveXMLResult = await modeler?.saveXML({ format: true }); if (saveXMLResult?.xml) { try { diff --git a/src/management-system-v2/next.config.js b/src/management-system-v2/next.config.js index b4e20b71a..ce4936caf 100644 --- a/src/management-system-v2/next.config.js +++ b/src/management-system-v2/next.config.js @@ -12,6 +12,7 @@ try { oauthProvidersConfig = { NEXTAUTH_SECRET: environmentsContent.nextAuthSecret, + USE_AUTH0: environmentsContent.useAuth0, AUTH0_CLIENT_ID: environmentsContent.clientID, AUTH0_CLIENT_SECRET: environmentsContent.clientSecret, AUTH0_clientCredentialScope: environmentsContent.clientCredentialScope, @@ -33,8 +34,8 @@ const nextConfig = { transpilePackages: ['antd'], env: { API_URL: - process.env.NODE_ENV === 'development' ? 'https://localhost:33080/api' : process.env.API_URL, - BACKEND_URL: process.env.NODE_ENV === 'development' ? 'https://localhost:33080' : 'FIXME', + process.env.NODE_ENV === 'development' ? 'http://localhost:33080/api' : process.env.API_URL, + BACKEND_URL: process.env.NODE_ENV === 'development' ? 'http://localhost:33080' : 'FIXME', NEXT_PUBLIC_USE_AUTH: process.env.USE_AUTHORIZATION === 'true', NEXTAUTH_SECRET: process.env.NODE_ENV === 'development' diff --git a/src/management-system/dev-server.ts b/src/management-system/dev-server.ts index 54051c766..a6bbde12a 100644 --- a/src/management-system/dev-server.ts +++ b/src/management-system/dev-server.ts @@ -47,28 +47,6 @@ const startManagementSystem = () => { // check if start with or without IAM if (process.env.MODE === 'iam') { - if (process.env.USE_AUTH0) { - const environmentsFolder = './src/backend/server/environment-configurations/'; - const path = environmentsFolder + 'development/config_iam.json'; - - // if submodule doesn't exist, add environment submodule - if (!fs.existsSync(path)) { - console.log('Cloning dev environment: https://github.com/PROCEED-Labs/environments'); - console.log( - "If you're in a terminal, you're going to need a personal-access-token (https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) because passwords are no longer allowed (https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls).", - ); - console.log( - 'You can also run the server in VS Code and let VS Code handle the authentication flow.', - ); - - execSync( - 'cd ./src/backend/server/ && git clone https://github.com/PROCEED-Labs/environments environment-configurations', - ); - - fs.rmSync(environmentsFolder + '.git', { recursive: true, force: true }); - } - } - // start docker container in separate subprocess const opaProcess = spawn('docker compose', ['-f', 'docker-compose-dev-iam.yml', 'up'], { cwd: __dirname, diff --git a/src/management-system/src/backend/server/iam/authentication/client.js b/src/management-system/src/backend/server/iam/authentication/client.js index a14b6a829..0194db363 100644 --- a/src/management-system/src/backend/server/iam/authentication/client.js +++ b/src/management-system/src/backend/server/iam/authentication/client.js @@ -81,8 +81,6 @@ const getClient = async (config) => { await createAdminUser(); } - if (process.env.NODE_ENV === 'development') await createDevelopmentUsers(); - // set global roles for authorization const globalRoles = { everybodyRole: '', guestRole: '' }; for (const role of getRoles()) { diff --git a/src/management-system/src/backend/server/iam/utils/config.js b/src/management-system/src/backend/server/iam/utils/config.js index cd10a68ff..3a3dc4a75 100644 --- a/src/management-system/src/backend/server/iam/utils/config.js +++ b/src/management-system/src/backend/server/iam/utils/config.js @@ -152,8 +152,8 @@ const schema = yup.object({ .default([defaultFrontendAddress, puppeteerAddress]), nextAuthSecret: yup.string().when('useAuthorization', { is: true, - then: (schema) => (process.env.API_ONLY ? schema.required() : schema.optional), - otherwise: (schema) => (process.env.API_ONLY ? schema.required() : schema.optional), + then: (schema) => (process.env.API_ONLY ? schema.required() : schema.optional()), + otherwise: (schema) => (process.env.API_ONLY ? schema.required() : schema.optional()), }), }); @@ -204,7 +204,6 @@ const createConfig = async (params = {}) => { trustedOrigins: process.env.TRUSTED_ORIGINS ? process.env.TRUSTED_ORIGINS.split(',') : undefined, - useAuth0: process.env.USE_AUTH0, nextAuthSecret: process.env.NEXTAUTH_SECRET || (process.env.API_ONLY && diff --git a/src/management-system/src/backend/server/iam/utils/developmentUsers.ts b/src/management-system/src/backend/server/iam/utils/developmentUsers.ts index 8bfbc898e..184adde26 100644 --- a/src/management-system/src/backend/server/iam/utils/developmentUsers.ts +++ b/src/management-system/src/backend/server/iam/utils/developmentUsers.ts @@ -88,7 +88,7 @@ async function createUser(user: User, role?: any) { if (role) addRoleMappingForUser(newUser, role); } -export async function createDevelopmentUsers() { +export async function createDevelopmentUsers(config) { const roles = await getRoles(); const processAdminRole = roles.find((role) => role.name === '@process_admin'); @@ -116,7 +116,9 @@ export async function createDevelopmentUsers() { }, adminRole, ); - } else { + } + + if (config.useAuth0) { await createUser( { given_name: 'John', diff --git a/src/management-system/src/backend/server/index.js b/src/management-system/src/backend/server/index.js index e0e5becfc..48903cbe8 100644 --- a/src/management-system/src/backend/server/index.js +++ b/src/management-system/src/backend/server/index.js @@ -23,6 +23,7 @@ import getClient from './iam/authentication/client.js'; import { getStorePath } from '../shared-electron-server/data/store.js'; import { abilityMiddleware, initialiazeRulesCache } from './iam/middleware/authorization'; import { getSessionFromCookie } from './iam/middleware/nextAuthMiddleware.js'; +import { createDevelopmentUsers } from './iam/utils/developmentUsers'; const configPath = process.env.NODE_ENV === 'development' @@ -94,7 +95,7 @@ async function init() { backendServer.use(express.json()); if (config.useAuthorization) { - if (process.env.USE_AUTH0) { + if (config.useAuth0) { try { client = await getClient(config); } catch (e) { @@ -107,6 +108,8 @@ async function init() { } } + if (process.env.NODE_ENV === 'development') await createDevelopmentUsers(config); + if (process.env.API_ONLY) { backendServer.use(getSessionFromCookie(config)); } else { @@ -142,49 +145,59 @@ async function init() { initialiazeRulesCache(config); backendServer.use(abilityMiddleware); - // allow requests for Let's Encrypt - const letsencryptPath = path.join(__dirname, 'lets-encrypt'); - if (process.env.NODE_ENV === 'production') { - await fse.ensureDir(letsencryptPath); - - // create a server that will be used to serve the Let's Encrypt Challenge and then reused to forward to https for http requests - const httpApp = express(); + const apiRouter = createApiRouter(config, client); + backendServer.use(['/api', '/resources'], apiRouter); - httpApp.use(`/.well-known`, express.static(letsencryptPath, { dotfiles: 'allow' })); - // reuse for redirect on other requests - httpApp.get('*', (req, res) => { - res.redirect('https://' + req.headers.host + req.url); + let frontendServer, websocketServer; + if (process.env.API_ONLY) { + // Frontend + REST API + backendServer.listen(ports.frontend, () => { + logger.info( + `MS HTTPS server started on port ${ports.frontend}. Open: http://:${ports.frontend}/`, + ); }); + } else { + // allow requests for Let's Encrypt + const letsencryptPath = path.join(__dirname, 'lets-encrypt'); + if (process.env.NODE_ENV === 'production') { + await fse.ensureDir(letsencryptPath); + + // create a server that will be used to serve the Let's Encrypt Challenge and then reused to forward to https for http requests + const httpApp = express(); + + httpApp.use(`/.well-known`, express.static(letsencryptPath, { dotfiles: 'allow' })); + // reuse for redirect on other requests + httpApp.get('*', (req, res) => { + res.redirect('https://' + req.headers.host + req.url); + }); - await httpApp.listen(80); - } - - // get the best certificate available (user provided > automatic Lets' Encrypt Cert > Self Signed Dev Cert) - const options = await getCertificate(letsencryptPath); + await httpApp.listen(80); + } - const apiRouter = createApiRouter(config, client); - backendServer.use(['/api', '/resources'], apiRouter); + // get the best certificate available (user provided > automatic Lets' Encrypt Cert > Self Signed Dev Cert) + const options = await getCertificate(letsencryptPath); - // Frontend + REST API - const frontendServer = https.createServer(options, backendServer).listen(ports.frontend, () => { - logger.info( - `MS HTTPS server started on port ${ports.frontend}. Open: https://:${ports.frontend}/`, - ); - }); + // Frontend + REST API + frontendServer = https.createServer(options, backendServer).listen(ports.frontend, () => { + logger.info( + `MS HTTPS server started on port ${ports.frontend}. Open: https://:${ports.frontend}/`, + ); + }); - // Puppeteer Endpoint - https.createServer(options, backendPuppeteerApp).listen(ports.puppeteer, 'localhost', () => { - logger.debug( - `HTTPS Server for Puppeteer started on port ${ports.puppeteer}. Open: https://localhost:${ports.frontend}/bpmn-modeller.html`, - ); - }); + // Puppeteer Endpoint + https.createServer(options, backendPuppeteerApp).listen(ports.puppeteer, 'localhost', () => { + logger.debug( + `HTTPS Server for Puppeteer started on port ${ports.puppeteer}. Open: https://localhost:${ports.frontend}/bpmn-modeller.html`, + ); + }); - // WebSocket Endpoint for Collaborative Editing - // Only here for API_ONLY because we need the const in the call below. - const websocketServer = https.createServer(options); + // WebSocket Endpoint for Collaborative Editing + // Only here for API_ONLY because we need the const in the call below. + websocketServer = https.createServer(options); - if (process.env.NODE_ENV === 'production') { - handleLetsEncrypt(letsencryptPath, [frontendServer, websocketServer]); + if (process.env.NODE_ENV === 'production') { + handleLetsEncrypt(letsencryptPath, [frontendServer, websocketServer]); + } } if (process.env.API_ONLY) { @@ -192,7 +205,6 @@ async function init() { } startWebsocketServer(websocketServer, loginSession, config); - // Load BPMN Modeller for Server after Websocket Endpoint is started (await import('./puppeteerStartWebviewWithBpmnModeller.js')).default(); } diff --git a/src/management-system/src/backend/server/rest-api/index.js b/src/management-system/src/backend/server/rest-api/index.js index 27c4bd189..d143b78d9 100644 --- a/src/management-system/src/backend/server/rest-api/index.js +++ b/src/management-system/src/backend/server/rest-api/index.js @@ -21,7 +21,7 @@ const createApiRouter = (config, client) => { apiRouter.use('/machines', machinesRouter); apiRouter.use('/speech', speechAssistantRouter); if (config.useAuthorization) { - if (process.env.USE_AUTH0) + if (config.useAuth0) url.parse(client.issuer.issuer).hostname.match('\\.auth0\\.com$') ? apiRouter.use('/users', auth0UserRouter) : apiRouter.use('/users', keycloakUserRouter);