@cryptotaxi247 / CoPilot / commits / e38b1779

Edit 3rd party (#339)

* feat: Add additional information for Office365 integration update * feat: Add YouTube video links for Office365 and Crowdstrike integration updates * feat: Add success message and additional info for BitDefender integration update * feat: Refactor integration response handling to centralize success messages and additional info for Office365, Crowdstrike, and BitDefender * delete stream, index, and grafana folder * feat: Enhance folder and index deletion functionality with improved error handling and response parsing * precommit fixes * feat: Add decommission response handling with additional manual steps for integrations * precommit fixes * feat: Add alert type to execution arguments in customer notification workflow * feat: Allow customizable alert type in customer notification handling * chore: update dependencies in frontend * feat: improved auth check * refactor: login page layout * feat: add update integration api * refactor: agent components * feat: add integration form * feat: updated integration form * feat: enhance update integration logic to handle multiple auth keys * precommit-fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Nov 22, 2024 at 09:01 UTC e38b1779b16e140499686fcf5fb7b75666d39936
20 files changed +724 -216
backend/app/connectors/grafana/schema/folders.py new
+13
@@ -0,0 +1,13 @@
1 +from typing import List
2 +
3 +from pydantic import BaseModel
4 +
5 +
6 +class Folder(BaseModel):
7 + id: int
8 + uid: str
9 + title: str
10 +
11 +
12 +class FoldersResponse(BaseModel):
13 + folders: List[Folder]
backend/app/connectors/grafana/services/folders.py new
+48
@@ -0,0 +1,48 @@
1 +from fastapi import HTTPException
2 +from loguru import logger
3 +
4 +from app.connectors.grafana.schema.folders import Folder
5 +from app.connectors.grafana.schema.folders import FoldersResponse
6 +from app.connectors.grafana.utils.universal import create_grafana_client
7 +
8 +
9 +async def delete_folder(
10 + organization_id: int,
11 + folder_id: int,
12 +) -> dict:
13 + """
14 + Delete a folder for a given organization.
15 +
16 + Args:
17 + organization_id (int): The ID of the organization.
18 + folder_id (int): The ID of the folder to be deleted.
19 +
20 + Returns:
21 + dict: The response from the Grafana client.
22 + """
23 + logger.info(
24 + f"Updating dashboards for organization {organization_id} and folder {folder_id}",
25 + )
26 + try:
27 + grafana_client = await create_grafana_client("Grafana")
28 + # Switch to the newly created organization
29 + grafana_client.user.switch_actual_user_organisation(organization_id)
30 + logger.info(
31 + f"Deleting folder {folder_id} for organization {organization_id}",
32 + )
33 + list_all_folders = grafana_client.folder.get_all_folders()
34 +
35 + # Parse the response using the Pydantic model
36 + folders_response = FoldersResponse(folders=[Folder(**folder) for folder in list_all_folders])
37 + logger.info(f"Search for folder with ID {folder_id}")
38 +
39 + # Ensure the folder_id is being compared correctly
40 + folder_uid = next((folder.uid for folder in folders_response.folders if folder.id == folder_id), None)
41 +
42 + if not folder_uid:
43 + raise HTTPException(status_code=404, detail=f"Folder with ID {folder_id} not found")
44 +
45 + return grafana_client.folder.delete_folder(folder_uid)
46 + except Exception as e:
47 + logger.error(f"Error deleting folder: {e}")
48 + raise HTTPException(status_code=500, detail=f"Error deleting dashboard folder: {e}")
backend/app/connectors/graylog/services/management.py
+17
@@ -55,6 +55,23 @@ async def delete_index(index_name: DeletedIndexBody) -> DeletedIndexResponse:
55 )
56
57
58 +async def delete_index_by_id(index_id: str) -> DeletedIndexResponse:
59 + """Delete an index from Graylog.
60 +
61 + Args:
62 + index_id (str): The ID of the index to be deleted.
63 +
64 + Returns:
65 + DeletedIndexResponse: The response indicating the success or failure of the index deletion.
66 + """
67 + logger.info(f"Deleting index {index_id} from Graylog")
68 + await send_delete_request(endpoint=f"/api/system/indices/index_sets/{index_id}")
69 + return DeletedIndexResponse(
70 + success=True,
71 + message=f"Successfully deleted index {index_id}",
72 + )
73 +
74 +
75 async def stop_input(input_id: StopInputBody) -> StopInputResponse:
76 """Stop an input in Graylog.
77
backend/app/connectors/graylog/services/streams.py
+24
@@ -5,6 +5,7 @@ from loguru import logger
5
6 from app.connectors.graylog.schema.streams import GraylogStreamsResponse
7 from app.connectors.graylog.schema.streams import Stream
8 +from app.connectors.graylog.utils.universal import send_delete_request
9 from app.connectors.graylog.utils.universal import send_get_request
10 from app.connectors.graylog.utils.universal import send_put_request
11
@@ -96,3 +97,26 @@ async def assign_stream_to_index(stream_id: str, index_id: str) -> bool:
97 status_code=500,
98 detail=f"Failed to assign stream {stream_id} to index {index_id}",
99 )
100 +
101 +
102 +async def delete_stream(stream_id: str) -> bool:
103 + """Delete a stream.
104 +
105 + Args:
106 + stream_id (str): The ID of the stream to delete.
107 +
108 + Returns:
109 + bool: True if the stream is successfully deleted, False if it is not.
110 +
111 + Raises:
112 + HTTPException: If there is an error deleting the stream.
113 + """
114 + logger.info(f"Deleting stream {stream_id}")
115 + response = await send_delete_request(endpoint=f"/api/streams/{stream_id}")
116 + if response["success"]:
117 + return True
118 + else:
119 + raise HTTPException(
120 + status_code=500,
121 + detail=f"Failed to delete stream {stream_id}",
122 + )
backend/app/incidents/services/incident_alert.py
+8 -2
@@ -463,7 +463,12 @@ async def build_alert_payload(
463 )
464
465
466 -async def handle_customer_notifications(customer_code: str, alert_payload: CreatedAlertPayload, session: AsyncSession):
466 +async def handle_customer_notifications(
467 + customer_code: str,
468 + alert_payload: CreatedAlertPayload,
469 + session: AsyncSession,
470 + type: str = "alert",
471 +) -> None:
472 customer_notifications = await get_customer_notification(customer_code, session)
473 if customer_notifications and customer_notifications[0].enabled:
474 logger.info(f"Executing workflow for customer code {customer_code}")
@@ -471,6 +476,7 @@ async def handle_customer_notifications(customer_code: str, alert_payload: Creat
476 ExecuteWorkflowRequest(
477 workflow_id=customer_notifications[0].shuffle_workflow_id,
478 execution_arguments={
479 + "type": type,
480 "customer_code": customer_code,
481 "alert_context_payload": alert_payload.alert_context_payload,
482 "alert_title": alert_payload.alert_title_payload,
@@ -525,7 +531,7 @@ async def create_alert_full(alert_payload: CreatedAlertPayload, customer_code: s
531 f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset_id} and ioc ID {ioc_id}",
532 )
533 logger.info(f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset_id}")
528 - await handle_customer_notifications(customer_code, alert_payload, session)
534 + await handle_customer_notifications(customer_code=customer_code, alert_payload=alert_payload, session=session)
535
536 await add_alert_to_document(CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id), alert_id)
537
backend/app/integrations/routes.py
+144 -58
@@ -14,6 +14,9 @@ from sqlalchemy.future import select
14 from sqlalchemy.orm import joinedload
15
16 from app.auth.utils import AuthHandler
17 +from app.connectors.grafana.services.folders import delete_folder
18 +from app.connectors.graylog.services.management import delete_index_by_id
19 +from app.connectors.graylog.services.streams import delete_stream
20 from app.db.db_session import get_db
21 from app.db.universal_models import Customers
22 from app.db.universal_models import CustomersMeta
@@ -517,6 +520,53 @@ def process_customer_integrations(customer_integrations_data):
520 return processed_customer_integrations
521
522
523 +def generate_integration_response(customer_code: str, integration_name: str) -> CustomerIntegrationCreateResponse:
524 + additional_info_map = {
525 + "Office365": (
526 + "Make sure to update the Office365 integration block in the Wazuh Manager ossec.conf file and restart the Wazuh Manager service. "
527 + "Also make sure to update the Office365 Graylog stream rule for this customer with the new organization ID if this has changed. "
528 + "YouTube video: https://youtu.be/ihj2F2rA6BQ?si=p4c8Xnk6PX8r29IB"
529 + ),
530 + "Crowdstrike": (
531 + "Make sure to update the Crowdstrike docker application with the new connection details and restart the docker container. "
532 + "YouTube video: https://youtu.be/YOVUOpZDEzM?si=jzpHw8vcnqnfVPzt"
533 + ),
534 + "BitDefender": (
535 + "Make sure to update the BitDefender docker application with the new connection details and restart the docker container."
536 + ),
537 + }
538 +
539 + additional_info = additional_info_map.get(integration_name, "")
540 + if additional_info == "":
541 + additional_info = None
542 +
543 + return CustomerIntegrationCreateResponse(
544 + message=f"Customer integration {customer_code} {integration_name} successfully updated.",
545 + success=True,
546 + additional_info=additional_info,
547 + )
548 +
549 +
550 +def generate_decommission_response(customer_code: str, integration_name: str) -> CustomerIntegrationDeleteResponse:
551 + additional_info_map = {
552 + "Office365": (
553 + "Make sure to remove the Office365 integration block from the Wazuh Manager ossec.conf file and restart the Wazuh Manager service. "
554 + ),
555 + "Crowdstrike": ("Make sure to remove the Crowdstrike docker application."),
556 + "BitDefender": ("Make sure to remove the BitDefender docker application."),
557 + }
558 +
559 + additional_info = additional_info_map.get(integration_name, "")
560 + if additional_info == "":
561 + additional_info = None
562 +
563 + return CustomerIntegrationDeleteResponse(
564 + message=f"Customer integration {customer_code} {integration_name} successfully deleted.",
565 + success=True,
566 + additional_info=additional_info,
567 + )
568 +
569 +
570 @integration_settings_router.get(
571 "/available_integrations",
572 response_model=AvailableIntegrationsResponse,
@@ -798,32 +848,30 @@ async def update_integration(
848 session,
849 )
850
801 - subscription_id = get_subscription_id(
802 - customer_integration,
803 - customer_integration_update.integration_name,
804 - customer_integration_update.integration_auth_keys[0].auth_key_name,
805 - )
806 -
807 - if not subscription_id:
808 - raise HTTPException(
809 - status_code=404,
810 - detail=f"Integration auth key {customer_integration_update.integration_auth_keys[0].auth_key_name} not found.",
851 + for auth_key in customer_integration_update.integration_auth_keys:
852 + subscription_id = get_subscription_id(
853 + customer_integration,
854 + customer_integration_update.integration_name,
855 + auth_key.auth_key_name,
856 )
857
813 - await session.execute(
814 - update(IntegrationAuthKeys)
815 - .where(IntegrationAuthKeys.subscription_id == subscription_id)
816 - .values(
817 - auth_value=customer_integration_update.integration_auth_keys[0].auth_value,
818 - ),
819 - )
858 + if not subscription_id:
859 + raise HTTPException(
860 + status_code=404,
861 + detail=f"Integration auth key {auth_key.auth_key_name} not found.",
862 + )
863 +
864 + await session.execute(
865 + update(IntegrationAuthKeys)
866 + .where(IntegrationAuthKeys.subscription_id == subscription_id)
867 + .values(
868 + auth_value=auth_key.auth_value,
869 + ),
870 + )
871
872 await session.commit()
873
823 - return CustomerIntegrationCreateResponse(
824 - message=f"Customer integration {customer_code} {customer_integration_update.integration_name} successfully updated.",
825 - success=True,
826 - )
874 + return generate_integration_response(customer_code, customer_integration_update.integration_name)
875
876
877 @integration_settings_router.put(
@@ -864,6 +912,30 @@ async def update_available_integrations(
912 )
913
914
915 +async def fetch_customer_integration_meta(session: AsyncSession, customer_code: str, integration_name: str):
916 + """
917 + Fetches customer integrations metadata from the database.
918 + """
919 + stmt = select(CustomerIntegrationsMeta).where(
920 + CustomerIntegrationsMeta.customer_code == customer_code,
921 + CustomerIntegrationsMeta.integration_name == integration_name,
922 + )
923 + result = await session.execute(stmt)
924 + return result.scalars().first()
925 +
926 +
927 +async def delete_customer_integration_meta(session: AsyncSession, customer_code: str, integration_name: str):
928 + """
929 + Deletes customer integrations metadata from the database.
930 + """
931 + await session.execute(
932 + delete(CustomerIntegrationsMeta).where(
933 + CustomerIntegrationsMeta.customer_code == customer_code,
934 + CustomerIntegrationsMeta.integration_name == integration_name,
935 + ),
936 + )
937 +
938 +
939 @integration_settings_router.delete(
940 "/delete_integration",
941 response_model=CustomerIntegrationDeleteResponse,
@@ -901,48 +973,62 @@ async def delete_integration(
973 detail="No subscriptions found for customer integration",
974 )
975
976 + stream_id = (await fetch_customer_integration_meta(session, customer_code, integration_name)).graylog_stream_id
977 + logger.info(f"stream_id: {stream_id}")
978 + await delete_stream(stream_id=stream_id)
979 +
980 + index_id = (await fetch_customer_integration_meta(session, customer_code, integration_name)).graylog_index_id
981 + logger.info(f"index_id: {index_id}")
982 + await delete_index_by_id(index_id=index_id)
983 +
984 + # Delete the folder in Grafana
985 + grafana_org_id = (await fetch_customer_integration_meta(session, customer_code, integration_name)).grafana_org_id
986 + grafana_dashboard_folder_id = (
987 + await fetch_customer_integration_meta(session, customer_code, integration_name)
988 + ).grafana_dashboard_folder_id
989 +
990 + await delete_folder(grafana_org_id, int(grafana_dashboard_folder_id))
991 +
992 await delete_metadata(session, subscription_ids)
993 await delete_subscriptions(session, subscription_ids)
994 await delete_configs(session, integration_service_id)
995 await delete_integration_service(session, integration_service_id)
996 await delete_customer_integration_record(session, customer_id)
997 + await delete_customer_integration_meta(session, customer_code, integration_name)
998
999 await session.commit()
1000
912 - return CustomerIntegrationDeleteResponse(
913 - message=f"Customer integration {customer_code} {integration_name} successfully deleted.",
914 - success=True,
915 - )
916 -
917 -
918 -@integration_settings_router.delete(
919 - "/delete_integration_meta",
920 - response_model=CustomerIntegrationsMetaResponse,
921 - description="Delete a customer integration metadata.",
922 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
923 -)
924 -async def delete_integration_meta(
925 - customer_integration_meta: CustomerIntegrationsMetaSchema,
926 - session: AsyncSession = Depends(get_db),
927 -):
928 - """
929 - Endpoint to delete a customer integration metadata.
930 - """
931 - try:
932 - stmt = delete(CustomerIntegrationsMeta).where(
933 - CustomerIntegrationsMeta.customer_code == customer_integration_meta.customer_code,
934 - CustomerIntegrationsMeta.integration_name == customer_integration_meta.integration_name,
935 - )
936 - await session.execute(stmt)
937 - await session.commit()
938 - return CustomerIntegrationsMetaResponse(
939 - message="Customer integration metadata successfully deleted.",
940 - success=True,
941 - )
942 - except Exception as e:
943 - logger.error(f"Error while deleting customer integration metadata: {e}")
944 - return CustomerIntegrationsMetaResponse(
945 - customer_integrations_meta=None,
946 - message="Error while deleting customer integration metadata.",
947 - success=False,
948 - )
1001 + return generate_decommission_response(customer_code, integration_name)
1002 +
1003 +
1004 +# @integration_settings_router.delete(
1005 +# "/delete_integration_meta",
1006 +# response_model=CustomerIntegrationsMetaResponse,
1007 +# description="Delete a customer integration metadata.",
1008 +# dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
1009 +# )
1010 +# async def delete_integration_meta(
1011 +# customer_integration_meta: CustomerIntegrationsMetaSchema,
1012 +# session: AsyncSession = Depends(get_db),
1013 +# ):
1014 +# """
1015 +# Endpoint to delete a customer integration metadata.
1016 +# """
1017 +# try:
1018 +# stmt = delete(CustomerIntegrationsMeta).where(
1019 +# CustomerIntegrationsMeta.customer_code == customer_integration_meta.customer_code,
1020 +# CustomerIntegrationsMeta.integration_name == customer_integration_meta.integration_name,
1021 +# )
1022 +# await session.execute(stmt)
1023 +# await session.commit()
1024 +# return CustomerIntegrationsMetaResponse(
1025 +# message="Customer integration metadata successfully deleted.",
1026 +# success=True,
1027 +# )
1028 +# except Exception as e:
1029 +# logger.error(f"Error while deleting customer integration metadata: {e}")
1030 +# return CustomerIntegrationsMetaResponse(
1031 +# customer_integrations_meta=None,
1032 +# message="Error while deleting customer integration metadata.",
1033 +# success=False,
1034 +# )
backend/app/integrations/schema.py
+8
@@ -93,6 +93,10 @@ class CustomerIntegrationCreateResponse(BaseModel):
93 ...,
94 description="The success status.",
95 )
96 + additional_info: Optional[str] = Field(
97 + None,
98 + description="The additional information of manaul steps that need to be performed for the integration.",
99 + )
100
101
102 class CustomerIntegrationDeleteResponse(BaseModel):
@@ -104,6 +108,10 @@ class CustomerIntegrationDeleteResponse(BaseModel):
108 ...,
109 description="The success status.",
110 )
111 + additional_info: Optional[str] = Field(
112 + None,
113 + description="The additional information of manaul steps that need to be performed for the integration.",
114 + )
115
116
117 # class IntegrationConfig(BaseModel):
frontend/package-lock.json
+26 -20
@@ -15,7 +15,7 @@
15 "@fontsource/public-sans": "^5.1.1",
16 "@shikijs/markdown-it": "^1.23.1",
17 "@tailwindcss/container-queries": "^0.1.1",
18 - "@vueuse/core": "^11.2.0",
18 + "@vueuse/core": "^11.3.0",
19 "axios": "^1.7.7",
20 "bytes": "^3.1.2",
21 "colord": "^2.9.3",
@@ -56,14 +56,14 @@
56 "@types/fs-extra": "^11.0.4",
57 "@types/jsdom": "^21.1.7",
58 "@types/lodash": "^4.17.13",
59 - "@types/node": "^22.9.0",
59 + "@types/node": "^22.9.1",
60 "@types/validator": "^13.12.2",
61 "@vitejs/plugin-vue": "^5.2.0",
62 "@vitejs/plugin-vue-jsx": "^4.1.0",
63 "@vue/test-utils": "^2.4.6",
64 "@vue/tsconfig": "^0.6.0",
65 "autoprefixer": "^10.4.20",
66 - "cypress": "^13.15.2",
66 + "cypress": "^13.16.0",
67 "depcheck": "^1.4.7",
68 "eslint": "^9.15.0",
69 "flourite": "^1.3.0",
@@ -2691,10 +2691,11 @@
2691 "dev": true
2692 },
2693 "node_modules/@types/node": {
2694 - "version": "22.9.0",
2695 - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
2696 - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
2694 + "version": "22.9.1",
2695 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz",
2696 + "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==",
2697 "dev": true,
2698 + "license": "MIT",
2699 "dependencies": {
2700 "undici-types": "~6.19.8"
2701 }
@@ -3797,13 +3798,14 @@
3798 }
3799 },
3800 "node_modules/@vueuse/core": {
3800 - "version": "11.2.0",
3801 - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.2.0.tgz",
3802 - "integrity": "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==",
3801 + "version": "11.3.0",
3802 + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz",
3803 + "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==",
3804 + "license": "MIT",
3805 "dependencies": {
3806 "@types/web-bluetooth": "^0.0.20",
3805 - "@vueuse/metadata": "11.2.0",
3806 - "@vueuse/shared": "11.2.0",
3807 + "@vueuse/metadata": "11.3.0",
3808 + "@vueuse/shared": "11.3.0",
3809 "vue-demi": ">=0.14.10"
3810 },
3811 "funding": {
@@ -3836,17 +3838,19 @@
3838 }
3839 },
3840 "node_modules/@vueuse/metadata": {
3839 - "version": "11.2.0",
3840 - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.2.0.tgz",
3841 - "integrity": "sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==",
3841 + "version": "11.3.0",
3842 + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz",
3843 + "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==",
3844 + "license": "MIT",
3845 "funding": {
3846 "url": "https://github.com/sponsors/antfu"
3847 }
3848 },
3849 "node_modules/@vueuse/shared": {
3847 - "version": "11.2.0",
3848 - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.2.0.tgz",
3849 - "integrity": "sha512-VxFjie0EanOudYSgMErxXfq6fo8vhr5ICI+BuE3I9FnX7ePllEsVrRQ7O6Q1TLgApeLuPKcHQxAXpP+KnlrJsg==",
3850 + "version": "11.3.0",
3851 + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz",
3852 + "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==",
3853 + "license": "MIT",
3854 "dependencies": {
3855 "vue-demi": ">=0.14.10"
3856 },
@@ -3859,6 +3863,7 @@
3863 "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
3864 "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
3865 "hasInstallScript": true,
3866 + "license": "MIT",
3867 "bin": {
3868 "vue-demi-fix": "bin/vue-demi-fix.js",
3869 "vue-demi-switch": "bin/vue-demi-switch.js"
@@ -5237,11 +5242,12 @@
5242 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
5243 },
5244 "node_modules/cypress": {
5240 - "version": "13.15.2",
5241 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.2.tgz",
5242 - "integrity": "sha512-ARbnUorjcCM3XiPwgHKuqsyr5W9Qn+pIIBPaoilnoBkLdSC2oLQjV1BUpnmc7KR+b7Avah3Ly2RMFnfxr96E/A==",
5245 + "version": "13.16.0",
5246 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.0.tgz",
5247 + "integrity": "sha512-g6XcwqnvzXrqiBQR/5gN+QsyRmKRhls1y5E42fyOvsmU7JuY+wM6uHJWj4ZPttjabzbnRvxcik2WemR8+xT6FA==",
5248 "dev": true,
5249 "hasInstallScript": true,
5250 + "license": "MIT",
5251 "dependencies": {
5252 "@cypress/request": "^3.0.6",
5253 "@cypress/xvfb": "^1.2.4",
frontend/package.json
+3 -3
@@ -43,7 +43,7 @@
43 "@fontsource/public-sans": "^5.1.1",
44 "@shikijs/markdown-it": "^1.23.1",
45 "@tailwindcss/container-queries": "^0.1.1",
46 - "@vueuse/core": "^11.2.0",
46 + "@vueuse/core": "^11.3.0",
47 "axios": "^1.7.7",
48 "bytes": "^3.1.2",
49 "colord": "^2.9.3",
@@ -87,14 +87,14 @@
87 "@types/fs-extra": "^11.0.4",
88 "@types/jsdom": "^21.1.7",
89 "@types/lodash": "^4.17.13",
90 - "@types/node": "^22.9.0",
90 + "@types/node": "^22.9.1",
91 "@types/validator": "^13.12.2",
92 "@vitejs/plugin-vue": "^5.2.0",
93 "@vitejs/plugin-vue-jsx": "^4.1.0",
94 "@vue/test-utils": "^2.4.6",
95 "@vue/tsconfig": "^0.6.0",
96 "autoprefixer": "^10.4.20",
97 - "cypress": "^13.15.2",
97 + "cypress": "^13.16.0",
98 "depcheck": "^1.4.7",
99 "eslint": "^9.15.0",
100 "flourite": "^1.3.0",
frontend/src/api/endpoints/integrations.ts
+14 -4
@@ -2,14 +2,16 @@ import type { FlaskBaseResponse } from "@/types/flask.d"
2 import type { AvailableIntegration, CustomerIntegration } from "@/types/integrations.d"
3 import { HttpClient } from "../httpClient"
4
5 +export interface IntegrationAuthKeyPairs {
6 + auth_key_name: string
7 + auth_value: string
8 +}
9 +
10 export interface NewIntegration {
11 customer_code: string
12 customer_name: string
13 integration_name: string
9 - integration_auth_keys: {
10 - auth_key_name: string
11 - auth_value: string
12 - }[]
14 + integration_auth_keys: IntegrationAuthKeyPairs[]
15 }
16
17 export interface NewIntegrationPayload extends NewIntegration {
@@ -20,6 +22,8 @@ export interface NewIntegrationPayload extends NewIntegration {
22 }
23 }
24
25 +export type UpdateIntegrationPayload = Omit<NewIntegration, "customer_name">
26 +
27 export default {
28 // #region Integrations
29 getAvailableIntegrations() {
@@ -43,6 +47,12 @@ export default {
47 }
48 return HttpClient.post<FlaskBaseResponse>(`/integrations/create_integration`, payload)
49 },
50 + updateIntegration(payload: UpdateIntegrationPayload) {
51 + return HttpClient.put<FlaskBaseResponse & { additional_info: string | null }>(
52 + `/integrations/update_integration/${payload.customer_code}`,
53 + payload
54 + )
55 + },
56 deleteIntegration(customerCode: string, integrationName: string) {
57 return HttpClient.delete<FlaskBaseResponse>(`/integrations/delete_integration`, {
58 data: { customer_code: customerCode, integration_name: integrationName }
frontend/src/components/activeResponse/ActiveResponseInvokeForm.vue
+1 -1
@@ -2,7 +2,7 @@
2 <div class="active-response-invoke-form flex grow flex-col justify-between">
3 <div class="form-box">
4 <n-spin v-model:show="loading">
5 - <n-form ref="formRef" :label-width="80" :model="form" :rules="rules">
5 + <n-form ref="formRef" :label-width="80" :model="form" :rules>
6 <div class="grid-auto-fit-200 grid gap-6">
7 <n-form-item label="Action" path="action">
8 <n-select v-model:value="form.action" :options="invokeActionOptions" />
frontend/src/components/agents/utils.ts
+1 -1
@@ -116,7 +116,7 @@ export function deleteAgent({ agent, cbBefore, cbSuccess, cbAfter, cbError, mess
116 .deleteAgent(agent.agent_id)
117 .then(res => {
118 if (res.data.success) {
119 - message.success("Agent was successfully deleted.")
119 + message.success(res.data?.message || "Agent was successfully deleted.")
120
121 if (cbSuccess && typeof cbSuccess === "function") {
122 cbSuccess()
frontend/src/components/auth/AuthForm.vue
+15 -35
@@ -1,28 +1,24 @@
1 <template>
2 <div class="form-wrap">
3 - <Logo mini :dark="isDark" class="mb-4" />
4 - <div class="title mb-4">
5 - {{ title }}
3 + <div>
4 + <Logo mini :dark="isDark" class="mb-4" />
5 + <div class="title mb-4">
6 + {{ title }}
7 + </div>
8 + <div class="text">Access the world of OpenSource security: Simplified, Streamlined, Accessible.</div>
9 </div>
7 - <div class="text mb-12">Access the world of OpenSource security: Simplified, Streamlined, Accessible.</div>
10
9 - <div class="form">
10 - <transition name="form-fade" mode="out-in" appear>
11 - <SignIn v-if="typeRef === 'signin'" key="signin" @goto-forgot-password="gotoForgotPassword()" />
12 - <ForgotPassword v-else-if="typeRef === 'forgotpassword'" key="forgotpassword" />
13 - <SignUp v-else-if="typeRef === 'signup'" key="signup" @goto-signin="gotoSignIn()" />
14 - </transition>
15 - </div>
11 + <transition name="form-fade" mode="out-in" appear class="min-h-114 my-10">
12 + <SignIn v-if="typeRef === 'signin'" key="signin" />
13 + <SignUp v-else-if="typeRef === 'signup'" key="signup" />
14 + </transition>
15
17 - <div class="sign-text mt-10 text-center">
18 - <div v-if="typeRef === 'signin'" class="sign-text">
16 + <div class="text-center">
17 + <div v-if="typeRef === 'signin'">
18 Don't you have an account?
19 <n-button text type="primary" size="large" @click="gotoSignUp()">Sign up</n-button>
20 </div>
22 - <div v-if="typeRef === 'forgotpassword'" class="sign-text">
23 - <n-button text type="primary" size="large" @click="gotoSignIn()">Back to Sign in</n-button>
24 - </div>
25 - <div v-if="typeRef === 'signup'" class="sign-text">
21 + <div v-if="typeRef === 'signup'">
22 Do you have an account?
23 <n-button text type="primary" size="large" @click="gotoSignIn()">Sign in</n-button>
24 </div>
@@ -36,8 +32,6 @@ import Logo from "@/app-layouts/common/Logo.vue"
32 import { useThemeStore } from "@/stores/theme"
33 import { NButton } from "naive-ui"
34 import { computed, onBeforeMount, ref } from "vue"
39 -import { useRouter } from "vue-router"
40 -import ForgotPassword from "./ForgotPassword.vue"
35 import SignIn from "./SignIn.vue"
36 import SignUp from "./SignUp.vue"
37
@@ -47,7 +41,6 @@ const props = defineProps<{
41 }>()
42
43 const typeRef = ref<FormType>("signin")
50 -const router = useRouter()
44 const themeStore = useThemeStore()
45 const isDark = computed<boolean>(() => themeStore.isThemeDark)
46 const title = computed<string>(() =>
@@ -59,24 +52,11 @@ const title = computed<string>(() =>
52 )
53
54 function gotoSignIn() {
62 - if (!props.useOnlyRouter) {
63 - typeRef.value = "signin"
64 - }
65 - router.replace({ name: "Login" })
55 + typeRef.value = "signin"
56 }
57
58 function gotoSignUp() {
69 - if (!props.useOnlyRouter) {
70 - typeRef.value = "signup"
71 - }
72 - router.replace({ name: "Register" })
73 -}
74 -
75 -function gotoForgotPassword() {
76 - if (!props.useOnlyRouter) {
77 - typeRef.value = "forgotpassword"
78 - }
79 - router.replace({ name: "ForgotPassword" })
59 + typeRef.value = "signup"
60 }
61
62 onBeforeMount(() => {
frontend/src/components/auth/SignIn.vue
+25 -19
@@ -21,13 +21,15 @@
21 />
22 </n-form-item>
23 <div class="flex flex-col items-end gap-6">
24 - <!--
25 - <div class="flex justify-end w-full">
26 - <n-button text type="primary" @click="emit('goto-forgot-password')">Forgot Password?</n-button>
27 - </div>
28 - -->
24 <div class="w-full">
30 - <n-button type="primary" class="!w-full" size="large" :loading="loading" @click="signIn">
25 + <n-button
26 + type="primary"
27 + class="!w-full"
28 + size="large"
29 + :loading="loading"
30 + :disabled="!isValid"
31 + @click="signIn"
32 + >
33 Sign in
34 </n-button>
35 </div>
@@ -48,27 +50,21 @@ import {
50 NInput,
51 useMessage
52 } from "naive-ui"
51 -import { ref } from "vue"
53 +import { computed, ref, watch } from "vue"
54 import { useRouter } from "vue-router"
55
56 interface ModelType {
55 - username: string
56 - password: string
57 + username: string | null
58 + password: string | null
59 }
60
59 -/*
60 -const emit = defineEmits<{
61 - (e: "goto-forgot-password"): void
62 -}>()
63 -*/
64 -
61 const loading = ref(false)
62 const router = useRouter()
63 const formRef = ref<FormInst | null>(null)
64 const message = useMessage()
65 const model = ref<ModelType>({
70 - username: "",
71 - password: ""
66 + username: null,
67 + password: null
68 })
69 const authStore = useAuthStore()
70
@@ -89,6 +85,10 @@ const rules: FormRules = {
85 ]
86 }
87
88 +const isValid = computed(() => {
89 + return model.value.username && model.value.password
90 +})
91 +
92 function signIn(e: Event) {
93 e.preventDefault()
94 formRef.value?.validate((errors: Array<FormValidationError> | undefined) => {
@@ -96,8 +96,8 @@ function signIn(e: Event) {
96 loading.value = true
97
98 const payload: LoginPayload = {
99 - username: model.value.username,
100 - password: model.value.password
99 + username: model.value.username || "",
100 + password: model.value.password || ""
101 }
102
103 authStore
@@ -116,4 +116,10 @@ function signIn(e: Event) {
116 }
117 })
118 }
119 +
120 +watch(isValid, val => {
121 + if (val) {
122 + formRef.value?.validate()
123 + }
124 +})
125 </script>
frontend/src/components/customers/integrations/CustomerIntegrationActions.vue
+14 -35
@@ -23,7 +23,8 @@ import type { Size } from "naive-ui/es/button/src/interface"
23 import Api from "@/api"
24 import Icon from "@/components/common/Icon.vue"
25 import { NButton, useDialog, useMessage } from "naive-ui"
26 -import { computed, h, ref, watch } from "vue"
26 +import { computed, ref, watch } from "vue"
27 +import { handleDeleteIntegration } from "./utils"
28
29 const { integration, hideDeleteButton, size } = defineProps<{
30 integration: CustomerIntegration
@@ -120,41 +121,19 @@ function provision() {
121 }
122
123 function handleDelete() {
123 - dialog.warning({
124 - title: "Confirm",
125 - content: () =>
126 - h("div", {
127 - innerHTML: `Are you sure you want to delete the integration: <strong>${serviceName.value}</strong> ?`
128 - }),
129 - positiveText: "Yes I'm sure",
130 - negativeText: "Cancel",
131 - onPositiveClick: () => {
132 - deleteIntegration()
124 + handleDeleteIntegration({
125 + integration,
126 + cbBefore: () => {
127 + loadingDelete.value = true
128 },
134 - onNegativeClick: () => {
135 - message.info("Delete canceled")
136 - }
137 - })
138 -}
139 -
140 -function deleteIntegration() {
141 - loadingDelete.value = true
142 -
143 - Api.integrations
144 - .deleteIntegration(customerCode.value, serviceName.value)
145 - .then(res => {
146 - if (res.data.success) {
147 - emit("deleted")
148 - message.success(res.data?.message || "Customer integration successfully deleted.")
149 - } else {
150 - message.warning(res.data?.message || "An error occurred. Please try again later.")
151 - }
152 - })
153 - .catch(err => {
154 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
155 - })
156 - .finally(() => {
129 + cbSuccess: () => {
130 + emit("deleted")
131 + },
132 + cbAfter: () => {
133 loadingDelete.value = false
158 - })
134 + },
135 + message,
136 + dialog
137 + })
138 }
139 </script>
frontend/src/components/customers/integrations/CustomerIntegrationDetails.vue new
+258
@@ -0,0 +1,258 @@
1 +<template>
2 + <div class="flex flex-col">
3 + <n-collapse-transition :show="mode === 'view'">
4 + <div class="flex min-h-80 grow flex-col justify-between gap-5">
5 + <div class="grid-auto-fit-200 grid gap-2">
6 + <CardKV v-for="ak of authKeys" :key="ak.key">
7 + <template #key>
8 + {{ ak.key }}
9 + </template>
10 + <template #value>
11 + {{ ak.value || "-" }}
12 + </template>
13 + </CardKV>
14 + </div>
15 +
16 + <div class="flex items-center justify-end gap-3">
17 + <n-button :loading="updating" :disabled="deleting" secondary @click.stop="switchMode('edit')">
18 + <template #icon>
19 + <Icon :name="EditIcon"></Icon>
20 + </template>
21 + Edit
22 + </n-button>
23 +
24 + <n-button type="error" :loading="deleting" secondary @click.stop="handleDelete">
25 + <template #icon>
26 + <Icon :name="DeleteIcon"></Icon>
27 + </template>
28 + Delete
29 + </n-button>
30 + </div>
31 + </div>
32 + </n-collapse-transition>
33 + <n-collapse-transition :show="mode === 'edit'">
34 + <n-spin v-model:show="updating" class="flex min-h-80" content-class="flex flex-col grow">
35 + <div class="flex grow flex-col justify-between gap-5">
36 + <n-form ref="form" :rules :label-width="80" :model>
37 + <div class="flex flex-wrap gap-2">
38 + <div v-for="(_, key) of model" :key class="min-w-72 grow">
39 + <n-form-item :label="key" :path="key">
40 + <n-input v-model:value="model[key]" :placeholder="`${key}...`" clearable />
41 + </n-form-item>
42 + </div>
43 + </div>
44 + </n-form>
45 +
46 + <div class="flex items-center justify-between gap-3">
47 + <n-button secondary @click="switchMode('view')">
48 + <template #icon>
49 + <Icon :name="BackIcon"></Icon>
50 + </template>
51 + Back
52 + </n-button>
53 +
54 + <div class="flex items-center justify-end gap-3">
55 + <n-button :disabled="updating" @click="reset()">Reset</n-button>
56 +
57 + <n-button :loading="updating" type="success" :disabled="!isValid" @click="validate()">
58 + <template #icon>
59 + <Icon :name="UpdateIcon"></Icon>
60 + </template>
61 + Submit
62 + </n-button>
63 + </div>
64 + </div>
65 + </div>
66 + </n-spin>
67 + </n-collapse-transition>
68 + </div>
69 +</template>
70 +
71 +<script setup lang="ts">
72 +import type { IntegrationAuthKeyPairs, UpdateIntegrationPayload } from "@/api/endpoints/integrations"
73 +import type { CustomerIntegration, IntegrationAuthKey } from "@/types/integrations.d"
74 +import Api from "@/api"
75 +import CardKV from "@/components/common/cards/CardKV.vue"
76 +import Icon from "@/components/common/Icon.vue"
77 +import _get from "lodash/get"
78 +import _trim from "lodash/trim"
79 +import _uniqBy from "lodash/uniqBy"
80 +import {
81 + type FormInst,
82 + type FormRules,
83 + type FormValidationError,
84 + NButton,
85 + NCollapseTransition,
86 + NForm,
87 + NFormItem,
88 + NInput,
89 + NSpin,
90 + useDialog,
91 + useMessage
92 +} from "naive-ui"
93 +import { computed, ref } from "vue"
94 +import { handleDeleteIntegration } from "./utils"
95 +
96 +const props = defineProps<{
97 + integration: CustomerIntegration
98 +}>()
99 +
100 +const emit = defineEmits<{
101 + (e: "deleted"): void
102 + (e: "updated", value: CustomerIntegration): void
103 +}>()
104 +
105 +const EditIcon = "uil:edit-alt"
106 +const BackIcon = "carbon:arrow-left"
107 +const DeleteIcon = "ph:trash"
108 +const UpdateIcon = "carbon:save"
109 +const integration = ref(props.integration)
110 +const dialog = useDialog()
111 +const message = useMessage()
112 +const form = ref<FormInst | null>(null)
113 +const model = ref<Record<string, string | null>>({})
114 +const mode = ref<"view" | "edit">("view")
115 +const deleting = ref<boolean>(false)
116 +const updating = ref<boolean>(false)
117 +const authKeys = ref(getAuthKeys(integration.value))
118 +
119 +const rules = computed(() =>
120 + authKeys.value.reduce((acc, cur) => {
121 + acc[cur.key] = {
122 + required: true,
123 + message: `Please insert the ${cur.key}`,
124 + trigger: ["input", "blur"]
125 + }
126 + return acc
127 + }, {} as FormRules)
128 +)
129 +
130 +const isValid = computed(() => {
131 + let valid = true
132 +
133 + for (const field of Object.entries(model.value)) {
134 + if (!field[1]) {
135 + valid = false
136 + }
137 + }
138 +
139 + return valid
140 +})
141 +
142 +function validate() {
143 + if (!form.value) return
144 +
145 + form.value.validate((errors?: Array<FormValidationError>) => {
146 + if (!errors) {
147 + updateIntegration()
148 + } else {
149 + message.warning("You must fill in the required fields correctly.")
150 + return false
151 + }
152 + })
153 +}
154 +
155 +function getAuthKeys(integration: CustomerIntegration) {
156 + const keys: { key: string; value: string }[] = []
157 +
158 + for (const subscriptions of integration.integration_subscriptions) {
159 + for (const ak of subscriptions.integration_auth_keys) {
160 + keys.push({
161 + key: ak.auth_key_name,
162 + value: ak.auth_value
163 + })
164 + }
165 + }
166 +
167 + return _uniqBy(keys, "key")
168 +}
169 +
170 +function updateAuthKeys(integrationAuthKeys: IntegrationAuthKeyPairs[]) {
171 + for (const subscriptions of integration.value.integration_subscriptions) {
172 + for (const ak of subscriptions.integration_auth_keys) {
173 + const ia = integrationAuthKeys.find(i => i.auth_key_name === ak.auth_key_name)
174 + ak.auth_value = ia?.auth_value || ak.auth_value
175 + }
176 + }
177 +
178 + authKeys.value = getAuthKeys(integration.value)
179 +
180 + return integration.value
181 +}
182 +
183 +function switchMode(newMode: "view" | "edit") {
184 + mode.value = newMode
185 +
186 + if (newMode === "edit") {
187 + model.value = authKeys.value.reduce(
188 + (acc, cur) => {
189 + acc[cur.key] = cur.value
190 + return acc
191 + },
192 + {} as Record<string, string>
193 + )
194 + }
195 +}
196 +
197 +function reset() {
198 + model.value = authKeys.value.reduce(
199 + (acc, cur) => {
200 + acc[cur.key] = null
201 + return acc
202 + },
203 + {} as Record<string, string | null>
204 + )
205 +}
206 +
207 +function handleDelete() {
208 + handleDeleteIntegration({
209 + integration: integration.value,
210 + cbBefore: () => {
211 + deleting.value = true
212 + },
213 + cbSuccess: () => {
214 + emit("deleted")
215 + },
216 + cbAfter: () => {
217 + deleting.value = false
218 + },
219 + message,
220 + dialog
221 + })
222 +}
223 +
224 +function updateIntegration() {
225 + updating.value = true
226 +
227 + const payload: UpdateIntegrationPayload = {
228 + customer_code: integration.value.customer_code,
229 + integration_name: integration.value.integration_service_name,
230 + integration_auth_keys: Object.entries(model.value).map(([key, val]) => ({
231 + auth_key_name: key,
232 + auth_value: val || ""
233 + }))
234 + }
235 +
236 + Api.integrations
237 + .updateIntegration(payload)
238 + .then(res => {
239 + if (res.data?.success) {
240 + message.success(res.data?.message || "Customer integration successfully updated")
241 +
242 + if (res.data?.additional_info) {
243 + message.info(res.data.additional_info, { duration: 0, closable: true })
244 + }
245 +
246 + emit("updated", updateAuthKeys(payload.integration_auth_keys))
247 + } else {
248 + message.warning(res.data?.message || "An error occurred. Please try again later.")
249 + }
250 + })
251 + .catch(err => {
252 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
253 + })
254 + .finally(() => {
255 + updating.value = false
256 + })
257 +}
258 +</script>
frontend/src/components/customers/integrations/CustomerIntegrationItem.vue
+10 -34
@@ -17,7 +17,7 @@
17 <div class="flex flex-wrap gap-3">
18 <n-button size="small" @click.stop="showDetails = true">
19 <template #icon>
20 - <Icon :name="InfoIcon"></Icon>
20 + <Icon :name="DetailsIcon"></Icon>
21 </template>
22 Details
23 </n-button>
@@ -25,7 +25,6 @@
25 <CustomerIntegrationActions
26 class="flex flex-wrap gap-3"
27 :integration
28 - hide-delete-button
28 size="small"
29 @deployed="emit('deployed')"
30 @deleted="emit('deleted')"
@@ -37,21 +36,13 @@
36 <n-modal
37 v-model:show="showDetails"
38 preset="card"
40 - :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(400px, 90vh)', overflow: 'hidden' }"
39 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(404px, 90vh)', overflow: 'hidden' }"
40 :title="serviceName"
41 :bordered="false"
42 segmented
43 + display-directive="show"
44 >
45 - <div class="grid-auto-fit-200 grid gap-2">
46 - <CardKV v-for="ak of authKeys" :key="ak.key">
47 - <template #key>
48 - {{ ak.key }}
49 - </template>
50 - <template #value>
51 - {{ ak.value || "-" }}
52 - </template>
53 - </CardKV>
54 - </div>
45 + <CustomerIntegrationDetails :integration @deleted="emit('deleted')" @updated="integration = $event" />
46 </n-modal>
47 </div>
48 </template>
@@ -60,41 +51,26 @@
51 import type { CustomerIntegration } from "@/types/integrations.d"
52 import Badge from "@/components/common/Badge.vue"
53 import CardEntity from "@/components/common/cards/CardEntity.vue"
63 -import CardKV from "@/components/common/cards/CardKV.vue"
54 import Icon from "@/components/common/Icon.vue"
65 -import _uniqBy from "lodash/uniqBy"
55 import { NButton, NModal } from "naive-ui"
67 -import { computed, ref, toRefs } from "vue"
56 +import { computed, defineAsyncComponent, ref } from "vue"
57 import CustomerIntegrationActions from "./CustomerIntegrationActions.vue"
58
70 -const props = defineProps<{
59 +const { integration: customerIntegration, embedded } = defineProps<{
60 integration: CustomerIntegration
61 embedded?: boolean
62 }>()
63 +
64 const emit = defineEmits<{
65 (e: "deployed"): void
66 (e: "deleted"): void
67 }>()
68
79 -const { integration, embedded } = toRefs(props)
69 +const CustomerIntegrationDetails = defineAsyncComponent(() => import("./CustomerIntegrationDetails.vue"))
70
71 const DeployIcon = "carbon:deploy"
82 -const InfoIcon = "carbon:information"
83 -
72 +const DetailsIcon = "carbon:settings-adjust"
73 +const integration = ref(customerIntegration)
74 const showDetails = ref(false)
75 const serviceName = computed(() => integration.value.integration_service_name)
86 -const authKeys = computed(() => {
87 - const keys: { key: string; value: string }[] = []
88 -
89 - for (const subscriptions of integration.value.integration_subscriptions) {
90 - for (const ak of subscriptions.integration_auth_keys) {
91 - keys.push({
92 - key: ak.auth_key_name,
93 - value: ak.auth_value
94 - })
95 - }
96 - }
97 -
98 - return _uniqBy(keys, "key")
99 -})
76 </script>
frontend/src/components/customers/integrations/utils.ts new
+88
@@ -0,0 +1,88 @@
1 +import type { CustomerIntegration } from "@/types/integrations.d"
2 +import type { DialogApiInjection } from "naive-ui/es/dialog/src/DialogProvider"
3 +import type { MessageApiInjection } from "naive-ui/es/message/src/MessageProvider"
4 +import Api from "@/api"
5 +import { h } from "vue"
6 +
7 +export interface DeleteIntegrationParams {
8 + integration: CustomerIntegration
9 + cbBefore?: () => void
10 + cbSuccess?: () => void
11 + cbAfter?: () => void
12 + cbError?: () => void
13 + message: MessageApiInjection
14 + dialog: DialogApiInjection
15 +}
16 +
17 +export function handleDeleteIntegration({
18 + integration,
19 + cbBefore,
20 + cbSuccess,
21 + cbAfter,
22 + cbError,
23 + dialog,
24 + message
25 +}: DeleteIntegrationParams) {
26 + dialog.warning({
27 + title: "Confirm",
28 + content: () =>
29 + h("div", {
30 + innerHTML: `Are you sure you want to delete the integration: <strong>${integration.integration_service_name}</strong> ?`
31 + }),
32 + positiveText: "Yes I'm sure",
33 + negativeText: "Cancel",
34 + onPositiveClick: () => {
35 + deleteIntegration({ integration, cbBefore, cbSuccess, cbAfter, cbError, dialog, message })
36 + },
37 + onNegativeClick: () => {
38 + message.info("Delete canceled")
39 + }
40 + })
41 +}
42 +
43 +export function deleteIntegration({
44 + integration,
45 + cbBefore,
46 + cbSuccess,
47 + cbAfter,
48 + cbError,
49 + message
50 +}: DeleteIntegrationParams) {
51 + if (cbBefore && typeof cbBefore === "function") {
52 + cbBefore()
53 + }
54 +
55 + Api.integrations
56 + .deleteIntegration(integration.customer_code, integration.integration_service_name)
57 + .then(res => {
58 + if (res.data.success) {
59 + message.success(res.data?.message || "Customer integration successfully deleted.")
60 +
61 + if (cbSuccess && typeof cbSuccess === "function") {
62 + cbSuccess()
63 + }
64 + } else {
65 + message.error(res.data?.message || "An error occurred. Please try again later.")
66 +
67 + if (cbError && typeof cbError === "function") {
68 + cbError()
69 + }
70 + }
71 + })
72 + .catch(err => {
73 + if (err.response?.status === 401) {
74 + message.error(err.response?.data?.message || "Agent Delete returned Unauthorized.")
75 + } else {
76 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
77 + }
78 +
79 + if (cbError && typeof cbError === "function") {
80 + cbError()
81 + }
82 + })
83 + .finally(() => {
84 + if (cbAfter && typeof cbAfter === "function") {
85 + cbAfter()
86 + }
87 + })
88 +}
frontend/src/utils/auth.ts
+6 -4
@@ -29,14 +29,16 @@ export function authCheck(route: RouteLocationNormalized) {
29
30 if (route?.redirectedFrom?.name === "Logout") authStore.setLogout()
31
32 + const loginPath = `/login${window.location.search}`
33 +
34 if (auth && !authStore.isLogged) {
33 - window.location.href = `/login${window.location.search}`
34 - return false
35 + window.location.replace(loginPath)
36 + return loginPath
37 }
38
39 if (auth && roles && !authStore.isRoleGranted(roles)) {
38 - window.location.href = `/login${window.location.search}`
39 - return false
40 + window.location.replace(loginPath)
41 + return loginPath
42 }
43
44 if (checkAuth && authStore.isLogged) {
frontend/tailwind.config.js
+1
@@ -61,6 +61,7 @@ export default {
61 "60vh": "60vh"
62 },
63 minHeight: {
64 + 114: "28.5rem",
65 120: "30rem"
66 },
67 width: {