diff --git a/.vscode/settings.json b/.vscode/settings.json index b8d00b662cbd..22c7f0c901d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,29 +17,11 @@ "files.trimFinalNewlines": true, "files.insertFinalNewline": true, "vetur.format.enable": false, - "python.formatting.provider": "black", - "python.formatting.blackArgs": [ - "--line-length", - "100" - ], - "python.linting.enabled": true, - "python.linting.flake8Enabled": true, - "python.linting.flake8Args": [ - "--ignore=E24,W504,E501", - "--verbose" - ], "python.testing.pytestEnabled": true, - "python.sortImports.args": [ - "--settings-path", - "${workspaceFolder}/setup.cfg" - ], - "python.linting.pylintArgs": [ - "--rcfile", - "${workspaceFolder}/setup.cfg" - ], "[python]": { "editor.codeActionsOnSave": { "source.organizeImports": "never" } }, + "codeQL.githubDatabase.update": "never", } diff --git a/data/dispatch-sample-data.dump b/data/dispatch-sample-data.dump index 475b49969892..af733b784449 100644 --- a/data/dispatch-sample-data.dump +++ b/data/dispatch-sample-data.dump @@ -2996,7 +2996,7 @@ CREATE TABLE dispatch_organization_default.entity ( name character varying, description character varying, value character varying, - source boolean, + source character varying, entity_type_id integer NOT NULL, search_vector tsvector, project_id integer, @@ -3037,7 +3037,7 @@ CREATE TABLE dispatch_organization_default.entity_type ( id integer NOT NULL, name character varying, description character varying, - jpath character varying NOT NULL, + jpath character varying, regular_expression character varying, enabled boolean, search_vector tsvector, @@ -3967,7 +3967,6 @@ ALTER SEQUENCE dispatch_organization_default.participant_role_id_seq OWNED BY di CREATE TABLE dispatch_organization_default.plugin_instance ( id integer NOT NULL, enabled boolean, - configuration json, plugin_id integer, project_id integer, _configuration character varying @@ -7666,7 +7665,7 @@ COPY dispatch_core.plugin_event (id, name, slug, description, plugin_id, search_ -- COPY dispatch_organization_default.alembic_version (version_num) FROM stdin; -3edb0476365a +928b725d64f6 \. @@ -7724,7 +7723,6 @@ COPY dispatch_organization_default.assoc_document_tags (document_id, tag_id) FRO COPY dispatch_organization_default.assoc_incident_tags (incident_id, tag_id) FROM stdin; 2 1 -7 1 \. @@ -8080,9 +8078,6 @@ COPY dispatch_organization_default.event (id, uuid, started_at, ended_at, source 56 6fe13da1-de96-41dd-ba9a-29a752060b46 2021-07-27 20:11:48.28756 2021-07-27 20:11:48.28756 Dispatch Core App Conversation added to incident null \N 6 'ad':5B 'app':3A 'convers':4B 'core':2A 'dispatch':1A 'incid':7B 2021-07-27 20:11:48.307412 2021-07-27 20:11:48.290557 \N \N \N \N \N 58 d66ec104-af72-46df-80a8-2b32a6fa8944 2021-07-27 20:11:59.021607 2021-07-27 20:11:59.021607 Dispatch Core App Incident notifications sent null \N 6 'app':3A 'core':2A 'dispatch':1A 'incid':4B 'notif':5B 'sent':6B 2021-07-27 20:11:59.04991 2021-07-27 20:11:59.022365 \N \N \N \N \N 59 7e011a9d-9bb9-4770-b5e3-1a21197e60c2 2021-07-28 17:13:49.192243 2021-07-28 17:13:49.192243 Dispatch Core App New incident task created by Kevin Glisson {"weblink": null} \N 4 'app':3A 'core':2A 'creat':7B 'dispatch':1A 'glisson':10B 'incid':5B 'kevin':9B 'new':4B 'task':6B 2021-07-28 17:13:49.218153 2021-07-28 17:13:49.199624 \N \N \N \N \N -60 2627886d-5466-47d8-a702-6596a17561a8 2024-02-04 02:51:26.185354 2024-02-04 02:51:26.185354 Dispatch Core App Incident created {"title": "Incident Test Created by Playwright", "description": "Test description created by Playwright", "type": "Denial of Service", "severity": "Undetermined", "priority": "Low", "status": "Active", "visibility": "Open"} \N 7 'app':3A 'core':2A 'creat':5B 'dispatch':1A 'incid':4B 2024-02-04 02:51:26.366892 2024-02-04 02:51:26.190379 \N \N Other \N t -61 5501e170-a4a6-4c29-a582-9d8e6367c35b 2024-02-04 02:51:26.752647 2024-02-04 02:51:26.752647 Dispatch Core App 3p1t6@example.com added to incident with Reporter role null \N 7 '3p1t6@example.com':4B 'ad':5B 'app':3A 'core':2A 'dispatch':1A 'incid':7B 'report':9B 'role':10B 2024-02-04 02:51:26.940241 2024-02-04 02:51:26.753164 \N \N Participant updated f -62 c1baff75-d46a-4cae-a793-9dab11d749e1 2024-02-04 02:51:27.112502 2024-02-04 02:51:27.112502 Dispatch Core App 3p1t6@example.com added to incident with Incident Commander role null \N 7 '3p1t6@example.com':4B 'ad':5B 'app':3A 'command':10B 'core':2A 'dispatch':1A 'incid':7B,9B 'role':11B 2024-02-04 02:51:27.222527 2024-02-04 02:51:27.113162 \N \N Participant updated f \. @@ -8135,7 +8130,6 @@ COPY dispatch_organization_default.incident (id, name, title, description, statu 4 dispatch-default-default-4 Heartbleed Sad PKI noises Stable Open 2021-07-27 19:52:57.757214 2021-07-27 19:54:03.96021 \N '4':9A 'default':7A,8A 'dispatch':6A 'dispatch-default-default':5A 'heartble':1B 'nois':4C 'pki':3C 'sad':2C 1 1 \N 1 2021-07-27 19:52:57.757221 2021-07-28 17:13:49.216785 Description of the actions taken to resolve the incident. Unknown America/Los_Angeles America/Los_Angeles America/Los_Angeles 2 2 2 \N \N \N 3 4 1 \N \N 5 dispatch-default-default-5 Solarwinds More like a solar tornado. Active Open 2021-07-27 20:06:15.252697 \N \N '5':11A 'default':9A,10A 'dispatch':8A 'dispatch-default-default':7A 'like':3C 'solar':5C 'solarwind':1B 'tornado':6C 2 1 \N 1 2021-07-27 20:06:15.252705 2021-07-27 20:06:41.627061 Description of the actions taken to resolve the incident. Unknown America/Los_Angeles America/Los_Angeles America/Los_Angeles 3 3 3 \N \N \N 5 6 1 \N \N 6 dispatch-default-default-6 Kaseya Those backups are good right? Active Open 2021-07-27 20:11:30.525883 \N \N '6':11A 'backup':3C 'default':9A,10A 'dispatch':8A 'dispatch-default-default':7A 'good':5C 'kaseya':1B 'right':6C 3 1 \N 1 2021-07-27 20:11:30.525893 2021-07-27 20:11:59.048666 Description of the actions taken to resolve the incident. Unknown America/Los_Angeles America/Los_Angeles America/Los_Angeles 4 4 4 \N \N \N 7 8 1 \N \N -7 \N Incident Test Created by Playwright Test description created by Playwright Active Open 2024-02-04 02:51:26.109901 \N \N 'creat':3B,8C 'descript':7C 'incid':1B 'playwright':5B,10C 'test':2B,6C 3 5 \N 1 2024-02-04 02:51:26.109912 2024-02-04 02:51:27.220352 Description of the actions taken to resolve the incident. example.com Unknown Unknown Unknown 5 5 \N \N \N \N \N \N 1 \N \N \. @@ -8272,7 +8266,6 @@ COPY dispatch_organization_default.participant (id, team, department, location, 2 Unknown Unknown America/Los_Angeles \N \N f \N 4 2 \N \N 3 Unknown Unknown America/Los_Angeles \N \N f \N 5 2 \N \N 4 Unknown Unknown America/Los_Angeles \N \N f \N 6 2 \N \N -5 example.com Unknown Unknown \N \N f \N 7 3 \N \N \. @@ -8301,8 +8294,6 @@ COPY dispatch_organization_default.participant_role (id, assumed_at, renounced_a 10 2021-07-27 20:11:32.314039 \N Reporter 4 1 11 2021-07-27 20:11:32.427753 \N Incident Commander 4 1 12 2021-07-27 20:11:32.491482 \N Liaison 4 1 -13 2024-02-04 02:51:26.570736 \N Reporter 5 0 -14 2024-02-04 02:51:27.008425 \N Incident Commander 5 0 \. @@ -8310,20 +8301,19 @@ COPY dispatch_organization_default.participant_role (id, assumed_at, renounced_a -- Data for Name: plugin_instance; Type: TABLE DATA; Schema: dispatch_organization_default; Owner: postgres -- -COPY dispatch_organization_default.plugin_instance (id, enabled, configuration, plugin_id, project_id, _configuration) FROM stdin; -1 t {} 2 1 \N -3 t {} 4 1 \N -9 \N {} 16 1 \N -11 f {} 19 1 \N -8 f {} 18 1 \N -7 f {} 13 1 \N -6 f {} 12 1 \N -14 f {} 10 1 \N -13 f {} 9 1 \N -12 f {} 8 1 \N -5 t {} 6 1 \N -2 t {} 3 1 \N -4 t {} 7 1 \N +COPY dispatch_organization_default.plugin_instance (id, enabled, plugin_id, project_id, _configuration) FROM stdin; +1 t 2 1 \N +9 \N 16 1 \N +11 f 19 1 \N +8 f 18 1 \N +7 f 13 1 \N +6 f 12 1 \N +14 f 10 1 \N +13 f 9 1 \N +12 f 8 1 \N +5 t 6 1 \N +2 t 3 1 \N +4 t 7 1 \N \. @@ -11676,6 +11666,13 @@ CREATE INDEX definition_search_vector_idx ON dispatch_organization_default.defin CREATE INDEX document_search_vector_idx ON dispatch_organization_default.document USING gin (search_vector); +-- +-- Name: entity_search_vector_idx; Type: INDEX; Schema: dispatch_organization_default; Owner: postgres +-- + +CREATE INDEX entity_search_vector_idx ON dispatch_organization_default.entity USING gin (search_vector); + + -- -- Name: entity_type_search_vector_idx; Type: INDEX; Schema: dispatch_organization_default; Owner: postgres -- @@ -11739,13 +11736,6 @@ CREATE INDEX incident_type_search_vector_idx ON dispatch_organization_default.in CREATE INDEX individual_contact_search_vector_idx ON dispatch_organization_default.individual_contact USING gin (search_vector); --- --- Name: ix_entity_search_vector; Type: INDEX; Schema: dispatch_organization_default; Owner: postgres --- - -CREATE INDEX ix_entity_search_vector ON dispatch_organization_default.entity USING gin (search_vector); - - -- -- Name: notification_search_vector_idx; Type: INDEX; Schema: dispatch_organization_default; Owner: postgres -- @@ -13664,14 +13654,6 @@ ALTER TABLE ONLY dispatch_organization_default.project ADD CONSTRAINT project_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES dispatch_core.organization(id); --- --- Name: project project_stable_priority_id_fkey; Type: FK CONSTRAINT; Schema: dispatch_organization_default; Owner: postgres --- - -ALTER TABLE ONLY dispatch_organization_default.project - ADD CONSTRAINT project_stable_priority_id_fkey FOREIGN KEY (stable_priority_id) REFERENCES dispatch_organization_default.incident_priority(id); - - -- -- Name: query query_project_id_fkey; Type: FK CONSTRAINT; Schema: dispatch_organization_default; Owner: postgres -- diff --git a/docker/Dockerfile b/docker/Dockerfile index 0ed550a1adf2..073ce00dae46 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.13.0-slim-bullseye as sdist +FROM python:3.13.1-slim-bullseye as sdist LABEL maintainer="oss@netflix.com" LABEL org.opencontainers.image.title="Dispatch PyPI Wheel" @@ -56,7 +56,7 @@ RUN YARN_CACHE_FOLDER="$(mktemp -d)" \ && mv /usr/src/dispatch/dist /dist # This is the image to be run -FROM python:3.13.0-slim-bullseye +FROM python:3.13.1-slim-bullseye LABEL maintainer="oss@dispatch.io" LABEL org.opencontainers.image.title="Dispatch" diff --git a/docs/docs/administration/settings/server.mdx b/docs/docs/administration/settings/server.mdx index e2d6759f2c38..17ebba7134cb 100644 --- a/docs/docs/administration/settings/server.mdx +++ b/docs/docs/administration/settings/server.mdx @@ -143,6 +143,28 @@ Make sure the reverse proxy strips this header from incoming requests (i.e. user > The HTTP request header to use as the user name, this value is case-insensitive. +#### Configuration for `dispatch-auth-provider-aws-alb` + +> Authenticate users based on [AWS Application Load Balancer authenticate](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html). + +#### `DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN` + +> ARN of your Load Balancer, used to validate the signer. +> The format is `arn:aws:elasticloadbalancing:region-code:account-id:loadbalancer/app/load-balancer-name/load-balancer-id`. +> This is required when using the `dispatch-auth-provider-aws-alb` auth provider. + +#### `DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM` \['default': email\] + +> Override where Dispatch should find the user email in the users claims. + +#### `DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS` \['default': 300\] + +> Override how long Dispatch should cache the public key, used to validate the payload. + +:::info +Add a ALB listener action without authenticate for `/api/v1/{organization}/events/*` if you want plugins to be public. Plugins determine their own authentication. +::: + ### Persistence #### `DATABASE_HOSTNAME` diff --git a/docs/package-lock.json b/docs/package-lock.json index 5cfa8ef2ec69..bef8d3c504c7 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -5229,9 +5229,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8598,15 +8598,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, diff --git a/playwright.config.ts b/playwright.config.ts index f86fa7ed8b62..47e5d0f5dd2f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,13 +18,13 @@ const config: PlaywrightTestConfig = { screenshot: "on", }, /* Maximum time one test can run for. */ - timeout: 100 * 1000, + timeout: 200 * 1000, expect: { /** * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 10000, + timeout: 20000, }, /* Run tests in files in parallel */ fullyParallel: true, diff --git a/requirements-base.txt b/requirements-base.txt index 880bb2f8861c..5d988e4c91e0 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -10,7 +10,7 @@ aiofiles==24.1.0 # via -r requirements-base.in aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.11.0 +aiohttp==3.11.10 # via -r requirements-base.in aiosignal==1.3.1 # via aiohttp @@ -31,7 +31,7 @@ attrs==22.1.0 # jsonschema backoff==2.2.1 # via schemathesis -bcrypt==4.2.0 +bcrypt==4.2.1 # via -r requirements-base.in blis==1.0.1 # via thinc @@ -118,7 +118,7 @@ email-validator==2.2.0 # via -r requirements-base.in emails==0.6 # via -r requirements-base.in -fastapi==0.115.5 +fastapi==0.115.6 # via -r requirements-base.in frozenlist==1.5.0 # via @@ -126,7 +126,7 @@ frozenlist==1.5.0 # aiosignal google-api-core==2.22.0 # via google-api-python-client -google-api-python-client==2.153.0 +google-api-python-client==2.155.0 # via -r requirements-base.in google-auth==2.36.0 # via @@ -154,7 +154,7 @@ httplib2==0.22.0 # google-api-python-client # google-auth-httplib2 # oauth2client -httpx==0.27.2 +httpx==0.28.1 # via # -r requirements-base.in # openai @@ -228,7 +228,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.5.0 # via cssutils -msal==1.31.0 +msal==1.31.1 # via -r requirements-base.in multidict==6.1.0 # via @@ -256,7 +256,7 @@ oauthlib[signedtoken]==3.2.2 # atlassian-python-api # jira # requests-oauthlib -openai==1.54.4 +openai==1.57.2 # via -r requirements-base.in packaging==24.2 # via @@ -300,7 +300,7 @@ protobuf==4.23.4 # proto-plus psycopg2-binary==2.9.10 # via -r requirements-base.in -pyarrow==18.0.0 +pyarrow==18.1.0 # via -r requirements-base.in pyasn1==0.6.1 # via @@ -353,7 +353,7 @@ python-dateutil==2.9.0.post0 # pandas python-jose==3.3.0 # via -r requirements-base.in -python-multipart==0.0.17 +python-multipart==0.0.19 # via -r requirements-base.in python-slugify==8.0.4 # via -r requirements-base.in @@ -423,9 +423,9 @@ six==1.16.0 # python-dateutil # sqlalchemy-filters # validators -slack-bolt==1.21.2 +slack-bolt==1.21.3 # via -r requirements-base.in -slack-sdk==3.33.3 +slack-sdk==3.33.5 # via # -r requirements-base.in # slack-bolt @@ -436,11 +436,10 @@ smart-open==7.0.5 sniffio==1.3.1 # via # anyio - # httpx # openai sortedcontainers==2.4.0 # via hypothesis -spacy==3.8.2 +spacy==3.8.3 # via -r requirements-base.in spacy-legacy==3.0.12 # via spacy @@ -510,7 +509,7 @@ urllib3==2.2.3 # pdpyras # requests # sentry-sdk -uvicorn==0.32.0 +uvicorn==0.32.1 # via -r requirements-base.in uvloop==0.21.0 # via -r requirements-base.in diff --git a/requirements-dev.txt b/requirements-dev.txt index d86c3804c929..bb6513b4f131 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ click==8.1.7 # via # -r requirements-dev.in # black -coverage==7.6.4 +coverage==7.6.9 # via -r requirements-dev.in decorator==5.1.1 # via ipython @@ -32,7 +32,7 @@ executing==2.1.0 # stack-data factory-boy==3.3.1 # via -r requirements-dev.in -faker==32.1.0 +faker==33.1.0 # via # -r requirements-dev.in # factory-boy @@ -42,7 +42,7 @@ identify==2.6.1 # via pre-commit iniconfig==2.0.0 # via pytest -ipython==8.29.0 +ipython==8.30.0 # via -r requirements-dev.in jedi==0.19.1 # via ipython @@ -86,7 +86,7 @@ python-dateutil==2.9.0.post0 # via faker pyyaml==6.0.2 # via pre-commit -ruff==0.7.3 +ruff==0.8.2 # via -r requirements-dev.in six==1.16.0 # via @@ -104,7 +104,7 @@ typing-extensions==4.12.2 # ipython virtualenv==20.27.1 # via pre-commit -vulture==2.13 +vulture==2.14 # via -r requirements-dev.in wcwidth==0.2.13 # via prompt-toolkit diff --git a/setup.py b/setup.py index f297a169ceac..4029e3428929 100644 --- a/setup.py +++ b/setup.py @@ -409,6 +409,7 @@ def run(self): "dispatch_atlassian_confluence = dispatch.plugins.dispatch_atlassian_confluence.plugin:ConfluencePagePlugin", "dispatch_atlassian_confluence_document = dispatch.plugins.dispatch_atlassian_confluence.docs.plugin:ConfluencePageDocPlugin", "dispatch_aws_sqs = dispatch.plugins.dispatch_aws.plugin:AWSSQSSignalConsumerPlugin", + "dispatch_aws_alb_auth = dispatch.plugins.dispatch_core.plugin:AwsAlbAuthProviderPlugin", "dispatch_auth_mfa = dispatch.plugins.dispatch_core.plugin:DispatchMfaPlugin", "dispatch_basic_auth = dispatch.plugins.dispatch_core.plugin:BasicAuthProviderPlugin", "dispatch_contact = dispatch.plugins.dispatch_core.plugin:DispatchContactPlugin", diff --git a/src/dispatch/case/flows.py b/src/dispatch/case/flows.py index 51dee63b5507..1ef8d1fcf6ca 100644 --- a/src/dispatch/case/flows.py +++ b/src/dispatch/case/flows.py @@ -10,6 +10,8 @@ from dispatch.conversation import flows as conversation_flows from dispatch.decorators import background_task from dispatch.document import flows as document_flows +from dispatch.email_templates import service as email_template_service +from dispatch.email_templates.enums import EmailTemplateTypes from dispatch.enums import DocumentResourceTypes, EventType, Visibility from dispatch.event import service as event_service from dispatch.group import flows as group_flows @@ -130,10 +132,19 @@ def case_add_or_reactivate_participant_flow( case, [participant.individual.email], db_session ) - # we send the welcome messages to the participant - send_case_welcome_participant_message( - participant_email=user_email, case=case, db_session=db_session - ) + # check to see if there is an override welcome message template + welcome_template = email_template_service.get_by_type( + db_session=db_session, + project_id=case.project_id, + email_template_type=EmailTemplateTypes.welcome, + ) + + send_case_welcome_participant_message( + participant_email=user_email, + case=case, + db_session=db_session, + welcome_template=welcome_template, + ) return participant @@ -1040,6 +1051,13 @@ def case_create_resources_flow( conversation_target=conversation_target, ) + # check to see if there is an override welcome message template + welcome_template = email_template_service.get_by_type( + db_session=db_session, + project_id=case.project_id, + email_template_type=EmailTemplateTypes.welcome, + ) + for user_email in set(individual_participants): send_participant_announcement_message( db_session=db_session, @@ -1047,6 +1065,13 @@ def case_create_resources_flow( subject=case, ) + send_case_welcome_participant_message( + participant_email=user_email, + case=case, + db_session=db_session, + welcome_template=welcome_template, + ) + event_service.log_case_event( db_session=db_session, source="Dispatch Core App", diff --git a/src/dispatch/case/messaging.py b/src/dispatch/case/messaging.py index d223a03d37b7..fa07b09fb118 100644 --- a/src/dispatch/case/messaging.py +++ b/src/dispatch/case/messaging.py @@ -7,9 +7,12 @@ import logging +from typing import Optional + from sqlalchemy.orm import Session from dispatch.database.core import resolve_attr +from dispatch.document import service as document_service from dispatch.case.models import Case, CaseRead from dispatch.messaging.strings import ( CASE_CLOSE_REMINDER, @@ -26,14 +29,13 @@ CASE_PRIORITY_CHANGE, CASE_CLOSED_RATING_FEEDBACK_NOTIFICATION, MessageType, + generate_welcome_message, ) from dispatch.config import DISPATCH_UI_URL +from dispatch.email_templates.models import EmailTemplates from dispatch.plugin import service as plugin_service from dispatch.event import service as event_service from dispatch.notification import service as notification_service -from dispatch.plugins.dispatch_slack.case.messages import ( - create_welcome_ephemeral_message_to_participant, -) from .enums import CaseStatus @@ -310,6 +312,7 @@ def send_case_welcome_participant_message( participant_email: str, case: Case, db_session: Session, + welcome_template: Optional[EmailTemplates] = None, ): if not case.dedicated_channel: return @@ -322,12 +325,52 @@ def send_case_welcome_participant_message( log.warning("Case participant welcome message not sent. No conversation plugin enabled.") return - welcome_message = create_welcome_ephemeral_message_to_participant(case=case) + # we send the ephemeral message + message_kwargs = { + "name": case.name, + "title": case.title, + "description": case.description, + "visibility": case.visibility, + "status": case.status, + "type": case.case_type.name, + "type_description": case.case_type.description, + "severity": case.case_severity.name, + "severity_description": case.case_severity.description, + "priority": case.case_priority.name, + "priority_description": case.case_priority.description, + "assignee_fullname": case.assignee.individual.name, + "assignee_team": case.assignee.team, + "assignee_weblink": case.assignee.individual.weblink, + "reporter_fullname": case.reporter.individual.name if case.reporter else None, + "reporter_team": case.reporter.team if case.reporter else None, + "reporter_weblink": case.reporter.individual.weblink if case.reporter else None, + "document_weblink": resolve_attr(case, "case_document.weblink"), + "storage_weblink": resolve_attr(case, "storage.weblink"), + "ticket_weblink": resolve_attr(case, "ticket.weblink"), + "conference_weblink": resolve_attr(case, "conference.weblink"), + "conference_challenge": resolve_attr(case, "conference.conference_challenge"), + } + faq_doc = document_service.get_incident_faq_document( + db_session=db_session, project_id=case.project_id + ) + if faq_doc: + message_kwargs.update({"faq_weblink": faq_doc.weblink}) + + conversation_reference = document_service.get_conversation_reference_document( + db_session=db_session, project_id=case.project_id + ) + if conversation_reference: + message_kwargs.update( + {"conversation_commands_reference_document_weblink": conversation_reference.weblink} + ) + plugin.instance.send_ephemeral( conversation_id=case.conversation.channel_id, user=participant_email, text=f"Welcome to {case.name}", - blocks=welcome_message, + message_template=generate_welcome_message(welcome_template, is_incident=False), + notification_type=MessageType.case_participant_welcome, + **message_kwargs, ) log.debug(f"Welcome ephemeral message sent to {participant_email}.") diff --git a/src/dispatch/case/models.py b/src/dispatch/case/models.py index 9065db786413..0b709c51e9ec 100644 --- a/src/dispatch/case/models.py +++ b/src/dispatch/case/models.py @@ -33,7 +33,6 @@ from dispatch.enums import Visibility from dispatch.event.models import EventRead from dispatch.group.models import Group, GroupRead -from dispatch.incident.models import IncidentReadMinimal from dispatch.messaging.strings import CASE_RESOLUTION_DEFAULT from dispatch.models import ( DispatchBase, @@ -230,6 +229,7 @@ class SignalInstanceRead(DispatchBase): class ProjectRead(DispatchBase): id: Optional[PrimaryKey] name: NameStr + display_name: Optional[str] color: Optional[str] allow_self_join: Optional[bool] = Field(True, nullable=True) @@ -267,6 +267,16 @@ class CaseCreate(CaseBase): tags: Optional[List[TagRead]] = [] +class CaseReadBasic(DispatchBase): + id: PrimaryKey + name: Optional[NameStr] + + +class IncidentReadBasic(DispatchBase): + id: PrimaryKey + name: Optional[NameStr] + + CaseReadMinimal = ForwardRef("CaseReadMinimal") @@ -277,8 +287,8 @@ class CaseReadMinimal(CaseBase): case_priority: CasePriorityRead case_severity: CaseSeverityRead case_type: CaseTypeRead - duplicates: Optional[List[CaseReadMinimal]] = [] - incidents: Optional[List[IncidentReadMinimal]] = [] + duplicates: Optional[List[CaseReadBasic]] = [] + incidents: Optional[List[IncidentReadBasic]] = [] related: Optional[List[CaseReadMinimal]] = [] closed_at: Optional[datetime] = None created_at: Optional[datetime] = None @@ -288,6 +298,8 @@ class CaseReadMinimal(CaseBase): project: ProjectRead reporter: Optional[ParticipantReadMinimal] reported_at: Optional[datetime] = None + tags: Optional[List[TagRead]] = [] + ticket: Optional[TicketRead] = None total_cost: float | None triage_at: Optional[datetime] = None @@ -306,12 +318,12 @@ class CaseRead(CaseBase): conversation: Optional[ConversationRead] = None created_at: Optional[datetime] = None documents: Optional[List[DocumentRead]] = [] - duplicates: Optional[List[CaseReadMinimal]] = [] + duplicates: Optional[List[CaseReadBasic]] = [] escalated_at: Optional[datetime] = None events: Optional[List[EventRead]] = [] genai_analysis: Optional[dict[str, Any]] = {} groups: Optional[List[GroupRead]] = [] - incidents: Optional[List[IncidentReadMinimal]] = [] + incidents: Optional[List[IncidentReadBasic]] = [] name: Optional[NameStr] participants: Optional[List[ParticipantRead]] = [] project: ProjectRead @@ -335,11 +347,11 @@ class CaseUpdate(CaseBase): case_severity: Optional[CaseSeverityBase] case_type: Optional[CaseTypeBase] closed_at: Optional[datetime] = None - duplicates: Optional[List[CaseRead]] = [] + duplicates: Optional[List[CaseReadBasic]] = [] related: Optional[List[CaseRead]] = [] reporter: Optional[ParticipantUpdate] escalated_at: Optional[datetime] = None - incidents: Optional[List[IncidentReadMinimal]] = [] + incidents: Optional[List[IncidentReadBasic]] = [] reported_at: Optional[datetime] = None tags: Optional[List[TagRead]] = [] triage_at: Optional[datetime] = None diff --git a/src/dispatch/case/views.py b/src/dispatch/case/views.py index 34de7640e621..bc70665bf1d0 100644 --- a/src/dispatch/case/views.py +++ b/src/dispatch/case/views.py @@ -117,6 +117,7 @@ def get_cases( expand: bool = Query(default=False), ): """Retrieves all cases.""" + common["include_keys"] = include pagination = search_filter_sort_paginate(model="Case", **common) if expand: diff --git a/src/dispatch/config.py b/src/dispatch/config.py index 4d5b5eb7674b..1c00cc354919 100644 --- a/src/dispatch/config.py +++ b/src/dispatch/config.py @@ -159,6 +159,16 @@ def __str__(self) -> str: "DISPATCH_AUTHENTICATION_PROVIDER_HEADER_NAME", default="remote-user" ) +DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN = config( + "DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN", default=None +) +DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM = config( + "DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM", default="email" +) +DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS = config( + "DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS", cast=int, default=300 +) + # sentry middleware SENTRY_ENABLED = config("SENTRY_ENABLED", default="") SENTRY_DSN = config("SENTRY_DSN", default="") diff --git a/src/dispatch/conversation/flows.py b/src/dispatch/conversation/flows.py index 08eefe7dcd24..50a4cce4d29d 100644 --- a/src/dispatch/conversation/flows.py +++ b/src/dispatch/conversation/flows.py @@ -49,9 +49,15 @@ def create_case_conversation( # Do not overwrite a case conversation with one of the same type (thread, channel) if case.conversation: if case.has_channel: - raise RuntimeError("Case already has a dedicated channel conversation.") + log.warning( + f"Trying to create case conversation but case {case.id} already has a dedicated channel conversation." + ) + return if case.has_thread and not case.dedicated_channel: - raise RuntimeError("Case already has a thread conversation.") + log.warning( + "Trying to create case conversation but case {case.id} already has a thread conversation." + ) + return # This case is a thread version, we send a new messaged (threaded) to the conversation target # for the configured case type @@ -466,11 +472,14 @@ def add_case_participants( return try: - plugin.instance.add_to_thread( - case.conversation.channel_id, - case.conversation.thread_id, - participant_emails, - ) + if case.has_thread: + plugin.instance.add_to_thread( + case.conversation.channel_id, + case.conversation.thread_id, + participant_emails, + ) + elif case.has_channel: + plugin.instance.add(case.conversation.channel_id, participant_emails) except Exception as e: event_service.log_case_event( db_session=db_session, diff --git a/src/dispatch/database/revisions/tenant/versions/2024-12-05_575ca7d954a8.py b/src/dispatch/database/revisions/tenant/versions/2024-12-05_575ca7d954a8.py new file mode 100644 index 000000000000..8365c4d919cc --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2024-12-05_575ca7d954a8.py @@ -0,0 +1,29 @@ +"""Adds incident summary to the incident table. + +Revision ID: 575ca7d954a8 +Revises: 928b725d64f6 +Create Date: 2024-12-05 15:05:46.932404 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "575ca7d954a8" +down_revision = "928b725d64f6" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("incident", sa.Column("summary", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("incident", "summary") + # ### end Alembic commands ### diff --git a/src/dispatch/database/revisions/tenant/versions/2024-12-12_2d9e4d392ea4.py b/src/dispatch/database/revisions/tenant/versions/2024-12-12_2d9e4d392ea4.py new file mode 100644 index 000000000000..01c33a2ed21e --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2024-12-12_2d9e4d392ea4.py @@ -0,0 +1,35 @@ +"""Adding display name to the projct model + +Revision ID: 2d9e4d392ea4 +Revises: 575ca7d954a8 +Create Date: 2024-12-12 16:34:58.098426 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "2d9e4d392ea4" +down_revision = "575ca7d954a8" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "project", sa.Column("display_name", sa.String(), server_default="", nullable=False) + ) + + # Copy data from 'name' column to 'display_name' column + op.execute("UPDATE project SET display_name = name") + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("project", "display_name") + # ### end Alembic commands ### diff --git a/src/dispatch/database/service.py b/src/dispatch/database/service.py index ba326ff4edea..d966be4c4cce 100644 --- a/src/dispatch/database/service.py +++ b/src/dispatch/database/service.py @@ -341,8 +341,7 @@ def apply_filters(query, filter_spec, model_cls=None, do_auto_join=True): return query -def apply_filter_specific_joins(model: Base, filter_spec: dict, query: orm.query): - """Applies any model specific implicitly joins.""" +def get_model_map(filters: dict) -> dict: # this is required because by default sqlalchemy-filter's auto-join # knows nothing about how to join many-many relationships. model_map = { @@ -371,19 +370,21 @@ def apply_filter_specific_joins(model: Base, filter_spec: dict, query: orm.query (SignalInstance, "EntityType"): (SignalInstance.entities, True), (Tag, "TagType"): (Tag.tag_type, False), } - filters = build_filters(filter_spec) - # Replace mapping if looking for commander - if "Commander" in str(filter_spec): + if "Commander" in filters: model_map.update({(Incident, "IndividualContact"): (Incident.commander, True)}) - if "Assignee" in str(filter_spec): + if "Assignee" in filters: model_map.update({(Case, "IndividualContact"): (Case.assignee, True)}) + return model_map - filter_models = get_named_models(filters) + +def apply_model_specific_joins(model: Base, models: List[str], query: orm.query): + model_map = get_model_map(models) joined_models = [] - for filter_model in filter_models: - if model_map.get((model, filter_model)): - joined_model, is_outer = model_map[(model, filter_model)] + + for include_model in models: + if model_map.get((model, include_model)): + joined_model, is_outer = model_map[(model, include_model)] try: if joined_model not in joined_models: query = query.join(joined_model, isouter=is_outer) @@ -394,6 +395,14 @@ def apply_filter_specific_joins(model: Base, filter_spec: dict, query: orm.query return query +def apply_filter_specific_joins(model: Base, filter_spec: dict, query: orm.query): + """Applies any model specific implicitly joins.""" + filters = build_filters(filter_spec) + filter_models = get_named_models(filters) + + return apply_model_specific_joins(model, filter_models, query) + + def composite_search(*, db_session, query_str: str, models: List[Base], current_user: DispatchUser): """Perform a multi-table search based on the supplied query.""" s = CompositeSearch(db_session, models) @@ -537,6 +546,7 @@ def search_filter_sort_paginate( model, query_str: str = None, filter_spec: str | dict | None = None, + include_keys: List[str] = None, page: int = 1, items_per_page: int = 5, sort_by: List[str] = None, @@ -574,6 +584,9 @@ def search_filter_sort_paginate( else: query = apply_filters(query, filter_spec, model_cls) + if include_keys: + query = apply_model_specific_joins(model_cls, include_keys, query) + if model == "Incident": query = query.intersect(query_restricted) for filter in tag_all_filters: diff --git a/src/dispatch/feedback/incident/models.py b/src/dispatch/feedback/incident/models.py index a6389093c2f7..e235a6cc6c8f 100644 --- a/src/dispatch/feedback/incident/models.py +++ b/src/dispatch/feedback/incident/models.py @@ -6,7 +6,7 @@ from sqlalchemy_utils import TSVectorType from dispatch.database.core import Base -from dispatch.incident.models import IncidentReadMinimal +from dispatch.incident.models import IncidentReadBasic from dispatch.models import ( DispatchBase, TimeStampMixin, @@ -45,7 +45,7 @@ class FeedbackBase(DispatchBase): created_at: Optional[datetime] rating: FeedbackRating = FeedbackRating.very_satisfied feedback: Optional[str] = Field(None, nullable=True) - incident: Optional[IncidentReadMinimal] + incident: Optional[IncidentReadBasic] case: Optional[CaseReadMinimal] participant: Optional[ParticipantRead] diff --git a/src/dispatch/incident/flows.py b/src/dispatch/incident/flows.py index f5bc36acf805..9288ec35acf5 100644 --- a/src/dispatch/incident/flows.py +++ b/src/dispatch/incident/flows.py @@ -4,7 +4,9 @@ from sqlalchemy.orm import Session +from dispatch.case import flows as case_flows from dispatch.case import service as case_service +from dispatch.case.enums import CaseResolutionReason, CaseStatus from dispatch.case.models import Case from dispatch.conference import flows as conference_flows from dispatch.conversation import flows as conversation_flows @@ -544,6 +546,19 @@ def incident_closed_status_flow(incident: Incident, db_session=None): document=document, db_session=db_session ) + for case in incident.cases: + try: + case.resolution = ( + f"Closed as part of incident {incident.name}. See incident for more details." + ) + case.resolution_reason = CaseResolutionReason.escalated + case.status = CaseStatus.closed + case_flows.case_closed_status_flow(case=case, db_session=db_session) + except Exception as e: + log.exception( + f"Failed to close case {case.name} while closing incident {incident.name}. Error: {str(e)}" + ) + # we send a direct message to the incident commander asking to review # the incident's information and to tag the incident if appropriate send_incident_closed_information_review_reminder(incident, db_session) @@ -552,6 +567,9 @@ def incident_closed_status_flow(incident: Incident, db_session=None): # to rate and provide feedback about the incident send_incident_rating_feedback_message(incident, db_session) + # if an AI plugin is enabled, we send the incident review doc for summary + incident_service.generate_incident_summary(incident=incident, db_session=db_session) + def conversation_topic_dispatcher( user_email: str, diff --git a/src/dispatch/incident/metrics.py b/src/dispatch/incident/metrics.py index b5a44b7ae39d..36bf4a9716ab 100644 --- a/src/dispatch/incident/metrics.py +++ b/src/dispatch/incident/metrics.py @@ -1,3 +1,4 @@ +import json import logging import math from calendar import monthrange @@ -31,12 +32,14 @@ def create_incident_metric_query( db_session, end_date: date, start_date: date = None, - filter_spec: List[dict] = None, + filter_spec: List[dict] | str | None = None, ): """Fetches eligible incidents.""" query = db_session.query(Incident) if filter_spec: + if isinstance(filter_spec, str): + filter_spec = json.loads(filter_spec) query = apply_filter_specific_joins(Incident, filter_spec, query) query = apply_filters(query, filter_spec) @@ -73,7 +76,7 @@ def make_forecast(incidents: List[Incident]): dataframe.drop("ds", inplace=True, axis=1) # fill periods without incidents with 0 - idx = pd.date_range(dataframe.index[0], dataframe.index[-1], freq="M") + idx = pd.date_range(dataframe.index[0], dataframe.index[-1], freq="ME") dataframe.index = pd.DatetimeIndex(dataframe.index) dataframe = dataframe.reindex(idx, fill_value=0) diff --git a/src/dispatch/incident/models.py b/src/dispatch/incident/models.py index 5d98f383768e..5ed67f2e5bf1 100644 --- a/src/dispatch/incident/models.py +++ b/src/dispatch/incident/models.py @@ -1,6 +1,6 @@ from collections import Counter, defaultdict from datetime import datetime -from typing import ForwardRef, List, Optional +from typing import List, Optional from pydantic import validator, Field, AnyHttpUrl @@ -220,6 +220,8 @@ def last_executive_report(self): notifications_group_id = Column(Integer, ForeignKey("group.id")) notifications_group = relationship("Group", foreign_keys=[notifications_group_id]) + summary = Column(String, nullable=True) + @hybrid_property def total_cost(self): total_cost = 0 @@ -240,6 +242,7 @@ class ProjectRead(DispatchBase): color: Optional[str] stable_priority: Optional[IncidentPriorityRead] = None allow_self_join: Optional[bool] = Field(True, nullable=True) + display_name: Optional[str] = Field(None, nullable=True) class CaseRead(DispatchBase): @@ -298,7 +301,9 @@ class IncidentCreate(IncidentBase): tags: Optional[List[TagRead]] = [] -IncidentReadMinimal = ForwardRef("IncidentReadMinimal") +class IncidentReadBasic(DispatchBase): + id: PrimaryKey + name: Optional[NameStr] class IncidentReadMinimal(IncidentBase): @@ -307,7 +312,7 @@ class IncidentReadMinimal(IncidentBase): commander: Optional[ParticipantReadMinimal] commanders_location: Optional[str] created_at: Optional[datetime] = None - duplicates: Optional[List[IncidentReadMinimal]] = [] + duplicates: Optional[List[IncidentReadBasic]] = [] incident_costs: Optional[List[IncidentCostRead]] = [] incident_document: Optional[DocumentRead] = None incident_priority: IncidentPriorityReadMinimal @@ -323,20 +328,18 @@ class IncidentReadMinimal(IncidentBase): reporters_location: Optional[str] stable_at: Optional[datetime] = None storage: Optional[StorageRead] = None + summary: Optional[str] = None tags: Optional[List[TagRead]] = [] tasks: Optional[List[TaskReadMinimal]] = [] total_cost: Optional[float] -IncidentReadMinimal.update_forward_refs() - - class IncidentUpdate(IncidentBase): cases: Optional[List[CaseRead]] = [] commander: Optional[ParticipantUpdate] delay_executive_report_reminder: Optional[datetime] = None delay_tactical_report_reminder: Optional[datetime] = None - duplicates: Optional[List[IncidentReadMinimal]] = [] + duplicates: Optional[List[IncidentReadBasic]] = [] incident_costs: Optional[List[IncidentCostUpdate]] = [] incident_priority: IncidentPriorityBase incident_severity: IncidentSeverityBase @@ -344,6 +347,7 @@ class IncidentUpdate(IncidentBase): reported_at: Optional[datetime] = None reporter: Optional[ParticipantUpdate] stable_at: Optional[datetime] = None + summary: Optional[str] = None tags: Optional[List[TagRead]] = [] terms: Optional[List[TermRead]] = [] @@ -375,7 +379,7 @@ class IncidentRead(IncidentBase): delay_executive_report_reminder: Optional[datetime] = None delay_tactical_report_reminder: Optional[datetime] = None documents: Optional[List[DocumentRead]] = [] - duplicates: Optional[List[IncidentReadMinimal]] = [] + duplicates: Optional[List[IncidentReadBasic]] = [] events: Optional[List[EventRead]] = [] incident_costs: Optional[List[IncidentCostRead]] = [] incident_priority: IncidentPriorityRead @@ -393,6 +397,7 @@ class IncidentRead(IncidentBase): reporters_location: Optional[str] stable_at: Optional[datetime] = None storage: Optional[StorageRead] = None + summary: Optional[str] = None tags: Optional[List[TagRead]] = [] tasks: Optional[List[TaskRead]] = [] terms: Optional[List[TermRead]] = [] diff --git a/src/dispatch/incident/priority/models.py b/src/dispatch/incident/priority/models.py index 4920201e7281..da088f6993d2 100644 --- a/src/dispatch/incident/priority/models.py +++ b/src/dispatch/incident/priority/models.py @@ -38,6 +38,7 @@ class IncidentPriority(Base, ProjectMixin): class ProjectRead(DispatchBase): id: Optional[PrimaryKey] name: NameStr + display_name: Optional[str] # Pydantic models... diff --git a/src/dispatch/incident/scheduled.py b/src/dispatch/incident/scheduled.py index 301bd1448189..bb13356b3057 100644 --- a/src/dispatch/incident/scheduled.py +++ b/src/dispatch/incident/scheduled.py @@ -286,25 +286,13 @@ def incident_report_weekly(db_session: Session, project: Project): if incident.visibility == Visibility.restricted: continue try: - pir_doc = storage_plugin.instance.get( - file_id=incident.incident_review_document.resource_id, - mime_type="text/plain", - ) - prompt = f""" - Given the text of the security post-incident review document below, - provide answers to the following questions in a paragraph format. - Do not include the questions in your response. - 1. What is the summary of what happened? - 2. What were the overall risk(s)? - 3. How were the risk(s) mitigated? - 4. How was the incident resolved? - 5. What are the follow-up tasks? - - {pir_doc} - """ - - response = ai_plugin.instance.chat_completion(prompt=prompt) - summary = response["choices"][0]["message"]["content"] + # if already summary generated, use that instead + if incident.summary: + summary = incident.summary + else: + summary = incident_service.generate_incident_summary( + db_session=db_session, incident=incident + ) item = { "commander_fullname": incident.commander.individual.name, diff --git a/src/dispatch/incident/service.py b/src/dispatch/incident/service.py index b4ce96f0d88f..6667c3e107ac 100644 --- a/src/dispatch/incident/service.py +++ b/src/dispatch/incident/service.py @@ -11,9 +11,11 @@ from typing import List, Optional from pydantic.error_wrappers import ErrorWrapper, ValidationError +from sqlalchemy.orm import Session + from dispatch.decorators import timer from dispatch.case import service as case_service -from dispatch.database.core import SessionLocal +from dispatch.enums import Visibility from dispatch.event import service as event_service from dispatch.exceptions import NotFoundError from dispatch.incident.priority import service as incident_priority_service @@ -27,6 +29,7 @@ from dispatch.project import service as project_service from dispatch.tag import service as tag_service from dispatch.term import service as term_service +from dispatch.ticket import flows as ticket_flows from .enums import IncidentStatus from .models import Incident, IncidentCreate, IncidentRead, IncidentUpdate @@ -35,9 +38,7 @@ log = logging.getLogger(__name__) -def resolve_and_associate_role( - db_session: SessionLocal, incident: Incident, role: ParticipantRoleType -): +def resolve_and_associate_role(db_session: Session, incident: Incident, role: ParticipantRoleType): """For a given role type resolve which individual email should be assigned that role.""" email_address = None service_id = None @@ -65,12 +66,12 @@ def resolve_and_associate_role( @timer -def get(*, db_session, incident_id: int) -> Optional[Incident]: +def get(*, db_session: Session, incident_id: int) -> Optional[Incident]: """Returns an incident based on the given id.""" return db_session.query(Incident).filter(Incident.id == incident_id).first() -def get_by_name(*, db_session, project_id: int, name: str) -> Optional[Incident]: +def get_by_name(*, db_session: Session, project_id: int, name: str) -> Optional[Incident]: """Returns an incident based on the given name.""" return ( db_session.query(Incident) @@ -80,7 +81,9 @@ def get_by_name(*, db_session, project_id: int, name: str) -> Optional[Incident] ) -def get_all_open_by_incident_type(*, db_session, incident_type_id: int) -> List[Optional[Incident]]: +def get_all_open_by_incident_type( + *, db_session: Session, incident_type_id: int +) -> List[Optional[Incident]]: """Returns all non-closed incidents based on the given incident type.""" return ( db_session.query(Incident) @@ -90,7 +93,9 @@ def get_all_open_by_incident_type(*, db_session, incident_type_id: int) -> List[ ) -def get_by_name_or_raise(*, db_session, project_id: int, incident_in: IncidentRead) -> Incident: +def get_by_name_or_raise( + *, db_session: Session, project_id: int, incident_in: IncidentRead +) -> Incident: """Returns an incident based on a given name or raises ValidationError""" incident = get_by_name(db_session=db_session, project_id=project_id, name=incident_in.name) @@ -110,12 +115,14 @@ def get_by_name_or_raise(*, db_session, project_id: int, incident_in: IncidentRe return incident -def get_all(*, db_session, project_id: int) -> List[Optional[Incident]]: +def get_all(*, db_session: Session, project_id: int) -> List[Optional[Incident]]: """Returns all incidents.""" return db_session.query(Incident).filter(Incident.project_id == project_id) -def get_all_by_status(*, db_session, status: str, project_id: int) -> List[Optional[Incident]]: +def get_all_by_status( + *, db_session: Session, status: str, project_id: int +) -> List[Optional[Incident]]: """Returns all incidents based on the given status.""" return ( db_session.query(Incident) @@ -125,7 +132,7 @@ def get_all_by_status(*, db_session, status: str, project_id: int) -> List[Optio ) -def get_all_last_x_hours(*, db_session, hours: int) -> List[Optional[Incident]]: +def get_all_last_x_hours(*, db_session: Session, hours: int) -> List[Optional[Incident]]: """Returns all incidents in the last x hours.""" now = datetime.utcnow() return ( @@ -134,7 +141,7 @@ def get_all_last_x_hours(*, db_session, hours: int) -> List[Optional[Incident]]: def get_all_last_x_hours_by_status( - *, db_session, status: str, hours: int, project_id: int + *, db_session: Session, status: str, hours: int, project_id: int ) -> List[Optional[Incident]]: """Returns all incidents of a given status in the last x hours.""" now = datetime.utcnow() @@ -167,7 +174,7 @@ def get_all_last_x_hours_by_status( ) -def create(*, db_session, incident_in: IncidentCreate) -> Incident: +def create(*, db_session: Session, incident_in: IncidentCreate) -> Incident: """Creates a new incident.""" project = project_service.get_by_name_or_default( db_session=db_session, project_in=incident_in.project @@ -326,7 +333,7 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident: return incident -def update(*, db_session, incident: Incident, incident_in: IncidentUpdate) -> Incident: +def update(*, db_session: Session, incident: Incident, incident_in: IncidentUpdate) -> Incident: """Updates an existing incident.""" incident_type = incident_type_service.get_by_name_or_default( db_session=db_session, @@ -378,6 +385,16 @@ def update(*, db_session, incident: Incident, incident_in: IncidentUpdate) -> In incident_cost_service.update_incident_response_cost( incident_id=incident.id, db_session=db_session ) + # if the new incident type has plugin metadata and the + # project key of the ticket is the same, also update the ticket with the new metadata + if incident_type.plugin_metadata: + ticket_flows.update_incident_ticket_metadata( + db_session=db_session, + ticket_id=incident.ticket.resource_id, + project_id=incident.project.id, + incident_id=incident.id, + incident_type=incident_type, + ) update_data = incident_in.dict( skip_defaults=True, @@ -417,7 +434,72 @@ def update(*, db_session, incident: Incident, incident_in: IncidentUpdate) -> In return incident -def delete(*, db_session, incident_id: int): +def delete(*, db_session: Session, incident_id: int): """Deletes an existing incident.""" db_session.query(Incident).filter(Incident.id == incident_id).delete() db_session.commit() + + +def generate_incident_summary(*, db_session: Session, incident: Incident) -> str: + """Generates a summary of the incident.""" + # Skip summary for restricted incidents + if incident.visibility == Visibility.restricted: + return "Incident summary not generated for restricted incident." + + # Skip if no incident review document + if not incident.incident_review_document or not incident.incident_review_document.resource_id: + log.info( + f"Incident summary not generated for incident {incident.id}. No review document found." + ) + return "Incident summary not generated. No review document found." + + # Don't generate if no enabled ai plugin or storage plugin + ai_plugin = plugin_service.get_active_instance( + db_session=db_session, plugin_type="artificial-intelligence", project_id=incident.project.id + ) + if not ai_plugin: + log.info( + f"Incident summary not generated for incident {incident.id}. No AI plugin enabled." + ) + return "Incident summary not generated. No AI plugin enabled." + + storage_plugin = plugin_service.get_active_instance( + db_session=db_session, plugin_type="storage", project_id=incident.project.id + ) + + if not storage_plugin: + log.info( + f"Incident summary not generated for incident {incident.id}. No storage plugin enabled." + ) + return "Incident summary not generated. No storage plugin enabled." + + try: + pir_doc = storage_plugin.instance.get( + file_id=incident.incident_review_document.resource_id, + mime_type="text/plain", + ) + prompt = f""" + Given the text of the security post-incident review document below, + provide answers to the following questions in a paragraph format. + Do not include the questions in your response. + 1. What is the summary of what happened? + 2. What were the overall risk(s)? + 3. How were the risk(s) mitigated? + 4. How was the incident resolved? + 5. What are the follow-up tasks? + + {pir_doc} + """ + + response = ai_plugin.instance.chat_completion(prompt=prompt) + summary = response["choices"][0]["message"]["content"] + + incident.summary = summary + db_session.add(incident) + db_session.commit() + + return summary + + except Exception as e: + log.exception(f"Error trying to generate summary for incident {incident.id}: {e}") + return "Incident summary not generated. An error occurred." diff --git a/src/dispatch/incident/views.py b/src/dispatch/incident/views.py index cdeaf24bbadc..4d201cb6d73e 100644 --- a/src/dispatch/incident/views.py +++ b/src/dispatch/incident/views.py @@ -47,7 +47,7 @@ IncidentRead, IncidentUpdate, ) -from .service import create, delete, get, update +from .service import create, delete, get, update, generate_incident_summary log = logging.getLogger(__name__) @@ -144,6 +144,7 @@ def create_incident( "/{incident_id}/resources", response_model=IncidentRead, summary="Creates resources for an existing incident.", + dependencies=[Depends(PermissionsDependency([IncidentViewPermission]))], ) def create_incident_resources( organization: OrganizationSlug, @@ -497,3 +498,18 @@ def get_incident_forecast( {"name": "Actual", "data": actual[1:]}, ], } + + +@router.get( + "/{incident_id}/regenerate", + summary="Regenerates incident sumamary", + dependencies=[Depends(PermissionsDependency([IncidentEventPermission]))], +) +def generate_summary( + db_session: DbSession, + current_incident: CurrentIncident, +): + return generate_incident_summary( + db_session=db_session, + incident=current_incident, + ) diff --git a/src/dispatch/messaging/email/utils.py b/src/dispatch/messaging/email/utils.py index 19aed7dfebaa..81f2a96dbdfe 100644 --- a/src/dispatch/messaging/email/utils.py +++ b/src/dispatch/messaging/email/utils.py @@ -31,6 +31,7 @@ def get_template(message_type: MessageType, project_id: int): MessageType.case_notification: ("notification.mjml", None), MessageType.incident_participant_welcome: ("notification.mjml", None), MessageType.incident_tactical_report: ("tactical_report.mjml", None), + MessageType.case_participant_welcome: ("notification.mjml", None), MessageType.incident_task_reminder: ( "notification_list.mjml", INCIDENT_TASK_REMINDER_DESCRIPTION, diff --git a/src/dispatch/messaging/strings.py b/src/dispatch/messaging/strings.py index b98cfc7e26a9..e8240612a6ef 100644 --- a/src/dispatch/messaging/strings.py +++ b/src/dispatch/messaging/strings.py @@ -44,6 +44,7 @@ class MessageType(DispatchEnum): task_add_to_incident = "task-add-to-incident" case_rating_feedback = "case-rating-feedback" case_feedback_daily_report = "case-feedback-daily-report" + case_participant_welcome = "case-participant-welcome" INCIDENT_STATUS_DESCRIPTIONS = { @@ -99,7 +100,7 @@ class MessageType(DispatchEnum): ).strip() INCIDENT_WEEKLY_REPORT_NO_INCIDENTS_DESCRIPTION = """ -No open incidents have been closed in the last week.""".replace( +No open visibility incidents have been closed in the last week.""".replace( "\n", " " ).strip() @@ -118,6 +119,7 @@ class MessageType(DispatchEnum): "\n", " " ).strip() + INCIDENT_REPORTER_DESCRIPTION = """ The person who reported the incident. Contact them if the report details need clarification.""".replace( "\n", " " @@ -157,12 +159,24 @@ class MessageType(DispatchEnum): "\n", " " ).strip() +CASE_CONVERSATION_REFERENCE_DOCUMENT_DESCRIPTION = """ +Document containing the list of slash commands available to the Assignee +and participants in the case conversation.""".replace( + "\n", " " +).strip() + INCIDENT_CONVERSATION_REFERENCE_DOCUMENT_DESCRIPTION = """ Document containing the list of slash commands available to the Incident Commander (IC) and participants in the incident conversation.""".replace( "\n", " " ).strip() +CASE_CONFERENCE_DESCRIPTION = """ +Video conference and phone bridge to be used throughout the case. Password: {{conference_challenge if conference_challenge else 'N/A'}} +""".replace( + "\n", "" +).strip() + INCIDENT_CONFERENCE_DESCRIPTION = """ Video conference and phone bridge to be used throughout the incident. Password: {{conference_challenge if conference_challenge else 'N/A'}} """.replace( @@ -197,6 +211,13 @@ class MessageType(DispatchEnum): "\n", " " ).strip() +CASE_FAQ_DOCUMENT_DESCRIPTION = """ +First time responding to a case? This +document answers common questions encountered when +helping us respond to a case.""".replace( + "\n", " " +).strip() + INCIDENT_FAQ_DOCUMENT_DESCRIPTION = """ First time responding to an incident? This document answers common questions encountered when @@ -246,6 +267,13 @@ class MessageType(DispatchEnum): "\n", " " ).strip() +CASE_PARTICIPANT_WELCOME_DESCRIPTION = """ +You\'ve been added to this case, because we think you may +be able to help resolve it. Please review the case details below and +reach out to the assignee if you have any questions.""".replace( + "\n", " " +).strip() + INCIDENT_PARTICIPANT_WELCOME_DESCRIPTION = """ You\'ve been added to this incident, because we think you may be able to help resolve it. Please review the incident details below and @@ -804,6 +832,24 @@ class MessageType(DispatchEnum): INCIDENT_STATUS, ] +CASE_DESCRIPTION = {"title": "Description", "text": "{{description}}"} + +CASE_VISIBILITY = { + "title": "Visibility - {{visibility}}", + "visibility_mapping": CASE_VISIBILITY_DESCRIPTIONS, +} + +CASE_TYPE = {"title": "Type - {{type}}", "text": "{{type_description}}"} + +CASE_SEVERITY = { + "title": "Severity - {{severity}}", + "text": "{{severity_description}}", +} + +CASE_PRIORITY = { + "title": "Priority - {{priority}}", + "text": "{{priority_description}}", +} CASE_CLOSE_REMINDER = [ { @@ -849,6 +895,43 @@ class MessageType(DispatchEnum): "text": CASE_ASSIGNEE_DESCRIPTION, } +CASE_CONFERENCE = { + "title": "Conference", + "title_link": "{{conference_weblink}}", + "text": CASE_CONFERENCE_DESCRIPTION, +} + +CASE_STORAGE = { + "title": "Storage", + "title_link": "{{storage_weblink}}", + "text": STORAGE_DESCRIPTION, +} + +CASE_CONVERSATION_COMMANDS_REFERENCE_DOCUMENT = { + "title": "Incident Conversation Commands Reference Document", + "title_link": "{{conversation_commands_reference_document_weblink}}", + "text": CASE_CONVERSATION_REFERENCE_DOCUMENT_DESCRIPTION, +} + +CASE_INVESTIGATION_DOCUMENT = { + "title": "Investigation Document", + "title_link": "{{document_weblink}}", + "text": CASE_INVESTIGATION_DOCUMENT_DESCRIPTION, +} + + +CASE_FAQ_DOCUMENT = { + "title": "FAQ Document", + "title_link": "{{faq_weblink}}", + "text": CASE_FAQ_DOCUMENT_DESCRIPTION, +} + +CASE_PARTICIPANT_WELCOME = { + "title": "Welcome to {{name}}", + "title_link": "{{ticket_weblink}}", + "text": CASE_PARTICIPANT_WELCOME_DESCRIPTION, +} + CASE_NOTIFICATION_COMMON = [CASE_TITLE] CASE_NOTIFICATION = CASE_NOTIFICATION_COMMON.copy() @@ -864,6 +947,24 @@ class MessageType(DispatchEnum): ] ) +CASE_PARTICIPANT_WELCOME_MESSAGE = [ + CASE_PARTICIPANT_WELCOME, + CASE_TITLE, + CASE_DESCRIPTION, + CASE_VISIBILITY, + CASE_STATUS, + CASE_TYPE, + CASE_SEVERITY, + CASE_PRIORITY, + CASE_REPORTER, + CASE_ASSIGNEE, + CASE_INVESTIGATION_DOCUMENT, + CASE_STORAGE, + CASE_CONFERENCE, + CASE_CONVERSATION_COMMANDS_REFERENCE_DOCUMENT, + CASE_FAQ_DOCUMENT, +] + INCIDENT_TASK_REMINDER = [ {"title": "Incident - {{ name }}", "text": "{{ title }}"}, @@ -1198,10 +1299,15 @@ def render_message_template(message_template: List[dict], **kwargs): return data -def generate_welcome_message(welcome_message: EmailTemplates) -> Optional[List[dict]]: +def generate_welcome_message( + welcome_message: EmailTemplates, is_incident: bool = True +) -> Optional[List[dict]]: """Generates the welcome message.""" if welcome_message is None: - return INCIDENT_PARTICIPANT_WELCOME_MESSAGE + if is_incident: + return INCIDENT_PARTICIPANT_WELCOME_MESSAGE + else: + return CASE_PARTICIPANT_WELCOME_MESSAGE participant_welcome = { "title": welcome_message.welcome_text, @@ -1210,20 +1316,26 @@ def generate_welcome_message(welcome_message: EmailTemplates) -> Optional[List[d } component_mapping = { - "Title": INCIDENT_TITLE, - "Description": INCIDENT_DESCRIPTION, - "Visibility": INCIDENT_VISIBILITY, - "Status": INCIDENT_STATUS, - "Type": INCIDENT_TYPE, - "Severity": INCIDENT_SEVERITY, - "Priority": INCIDENT_PRIORITY, - "Reporter": INCIDENT_REPORTER, - "Commander": INCIDENT_COMMANDER, - "Investigation Document": INCIDENT_INVESTIGATION_DOCUMENT, - "Storage": INCIDENT_STORAGE, - "Conference": INCIDENT_CONFERENCE, - "Slack Commands": INCIDENT_CONVERSATION_COMMANDS_REFERENCE_DOCUMENT, - "FAQ Document": INCIDENT_FAQ_DOCUMENT, + "Title": INCIDENT_TITLE if is_incident else CASE_TITLE, + "Description": INCIDENT_DESCRIPTION if is_incident else CASE_DESCRIPTION, + "Visibility": INCIDENT_VISIBILITY if is_incident else CASE_VISIBILITY, + "Status": INCIDENT_STATUS if is_incident else CASE_STATUS, + "Type": INCIDENT_TYPE if is_incident else CASE_TYPE, + "Severity": INCIDENT_SEVERITY if is_incident else CASE_SEVERITY, + "Priority": INCIDENT_PRIORITY if is_incident else CASE_PRIORITY, + "Reporter": INCIDENT_REPORTER if is_incident else CASE_REPORTER, + "Commander": INCIDENT_COMMANDER if is_incident else CASE_ASSIGNEE, + "Investigation Document": ( + INCIDENT_INVESTIGATION_DOCUMENT if is_incident else CASE_INVESTIGATION_DOCUMENT + ), + "Storage": INCIDENT_STORAGE if is_incident else CASE_STORAGE, + "Conference": INCIDENT_CONFERENCE if is_incident else CASE_CONFERENCE, + "Slack Commands": ( + INCIDENT_CONVERSATION_COMMANDS_REFERENCE_DOCUMENT + if is_incident + else CASE_CONVERSATION_COMMANDS_REFERENCE_DOCUMENT + ), + "FAQ Document": INCIDENT_FAQ_DOCUMENT if is_incident else CASE_FAQ_DOCUMENT, } message = [participant_welcome] diff --git a/src/dispatch/plugins/bases/signal_consumer.py b/src/dispatch/plugins/bases/signal_consumer.py index 4a2d516e107e..d3c1a7edfbde 100644 --- a/src/dispatch/plugins/bases/signal_consumer.py +++ b/src/dispatch/plugins/bases/signal_consumer.py @@ -14,6 +14,3 @@ class SignalConsumerPlugin(Plugin): def consume(self, **kwargs): raise NotImplementedError - - def delete(self, **kwargs): - raise NotImplementedError diff --git a/src/dispatch/plugins/dispatch_aws/plugin.py b/src/dispatch/plugins/dispatch_aws/plugin.py index 18c0d4fa8462..4f66bc358cb9 100644 --- a/src/dispatch/plugins/dispatch_aws/plugin.py +++ b/src/dispatch/plugins/dispatch_aws/plugin.py @@ -6,14 +6,16 @@ .. moduleauthor:: Kevin Glisson """ +import base64 import json import logging +import zlib from typing import TypedDict import boto3 -from pydantic import ValidationError from psycopg2.errors import UniqueViolation -from sqlalchemy.exc import IntegrityError +from pydantic import ValidationError +from sqlalchemy.exc import IntegrityError, ResourceClosedError from sqlalchemy.orm import Session from dispatch.metrics import provider as metrics_provider @@ -28,6 +30,13 @@ log = logging.getLogger(__name__) +def decompress_json(compressed_str: str) -> str: + """Decompress a base64 encoded zlibed JSON string.""" + decoded = base64.b64decode(compressed_str) + decompressed = zlib.decompress(decoded) + return decompressed.decode("utf-8") + + class SqsEntries(TypedDict): Id: str ReceiptHandle: str @@ -36,7 +45,7 @@ class SqsEntries(TypedDict): class AWSSQSSignalConsumerPlugin(SignalConsumerPlugin): title = "AWS SQS - Signal Consumer" slug = "aws-sqs-signal-consumer" - description = "Uses sqs to consume signals" + description = "Uses SQS to consume signals." version = __version__ author = "Netflix" @@ -60,20 +69,32 @@ def consume(self, db_session: Session, project: Project) -> None: WaitTimeSeconds=20, ) if not response.get("Messages") or len(response["Messages"]) == 0: - log.info("No messages received from SQS") + log.info("No messages received from SQS.") continue entries: list[SqsEntries] = [] for message in response["Messages"]: - body = json.loads(message["Body"]) - signal_data = json.loads(body["Message"]) + try: + message_body = json.loads(message["Body"]) + message_body_message = message_body.get("Message") + message_attributes = message_body.get("MessageAttributes", {}) + + if message_attributes.get("compressed", {}).get("Value") == "zlib": + # Message is compressed, decompress it + message_body_message = decompress_json(message_body_message) + + signal_data = json.loads(message_body_message) + except Exception as e: + log.exception(f"Unable to extract signal data from SQS message: {e}") + continue + try: signal_instance_in = SignalInstanceCreate( project=project, raw=signal_data, **signal_data ) except ValidationError as e: log.warning( - f"Received signal instance that does not conform to `SignalInstanceCreate` structure, skipping creation: {e}" + f"Received a signal instance that does not conform to the SignalInstanceCreate pydantic model. Skipping creation: {e}" ) continue @@ -83,7 +104,7 @@ def consume(self, db_session: Session, project: Project) -> None: db_session=db_session, signal_instance_id=signal_instance_in.raw["id"] ): log.info( - f"Received signal instance that already exists in the database, skipping creation: {signal_instance_in.raw['id']}" + f"Received a signal that already exists in the database. Skipping signal instance creation: {signal_instance_in.raw['id']}" ) continue @@ -96,13 +117,23 @@ def consume(self, db_session: Session, project: Project) -> None: except IntegrityError as e: if isinstance(e.orig, UniqueViolation): log.info( - f"Received signal instance that already exists in the database, skipping creation: {e}" + f"Received a signal that already exists in the database. Skipping signal instance creation: {e}" ) else: - log.exception(f"Integrity error when creating signal instance: {e}") + log.exception( + f"Encountered an integrity error when trying to create a signal instance: {e}" + ) + continue + except ResourceClosedError as e: + log.warning( + f"Encountered an error when trying to create a signal instance. The plugin will retry again as the message hasn't been deleted from the SQS queue. Signal name/variant: {signal_instance_in.raw['name'] if signal_instance_in.raw and signal_instance_in.raw['name'] else signal_instance_in.raw['variant']}. Error: {e}" + ) + db_session.rollback() continue except Exception as e: - log.exception(f"Unable to create signal instance: {e}") + log.exception( + f"Encountered an error when trying to create a signal instance. Signal name/variant: {signal_instance_in.raw['name'] if signal_instance_in.raw and signal_instance_in.raw['name'] else signal_instance_in.raw['variant']}. Error: {e}" + ) db_session.rollback() continue else: @@ -114,7 +145,7 @@ def consume(self, db_session: Session, project: Project) -> None: }, ) log.debug( - f"Received signal: name: {signal_instance.signal.name} id: {signal_instance.signal.id}" + f"Received a signal with name {signal_instance.signal.name} and id {signal_instance.signal.id}" ) entries.append( {"Id": message["MessageId"], "ReceiptHandle": message["ReceiptHandle"]} diff --git a/src/dispatch/plugins/dispatch_core/plugin.py b/src/dispatch/plugins/dispatch_core/plugin.py index ec3db480c076..050e2626596e 100644 --- a/src/dispatch/plugins/dispatch_core/plugin.py +++ b/src/dispatch/plugins/dispatch_core/plugin.py @@ -13,6 +13,7 @@ from uuid import UUID import requests +from cachetools import cached, TTLCache from fastapi import HTTPException from fastapi.security.utils import get_authorization_scheme_param from jose import JWTError, jwt @@ -24,6 +25,9 @@ from dispatch.auth.models import DispatchUser, MfaChallenge, MfaChallengeStatus, MfaPayload from dispatch.case import service as case_service from dispatch.config import ( + DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN, + DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM, + DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS, DISPATCH_AUTHENTICATION_PROVIDER_HEADER_NAME, DISPATCH_AUTHENTICATION_PROVIDER_PKCE_JWKS, DISPATCH_JWT_AUDIENCE, @@ -165,6 +169,65 @@ def get_current_user(self, request: Request, **kwargs): return value +class AwsAlbAuthProviderPlugin(AuthenticationProviderPlugin): + title = "Dispatch Plugin - AWS ALB Authentication Provider" + slug = "dispatch-auth-provider-aws-alb" + description = "AWS Application Load Balancer authentication provider." + version = dispatch_plugin.__version__ + + author = "ManyPets" + author_url = "https://manypets.com/" + + @cached(cache=TTLCache(maxsize=1024, ttl=DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_PUBLIC_KEY_CACHE_SECONDS)) + def get_public_key(self, kid: str, region: str): + log.debug("Cache miss. Requesting key from AWS endpoint.") + url = f"https://public-keys.auth.elb.{region}.amazonaws.com/{kid}" + req = requests.get(url) + return req.text + + def get_current_user(self, request: Request, **kwargs): + credentials_exception = HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail=[{"msg": "Could not validate credentials"}] + ) + + encoded_jwt: str = request.headers.get('x-amzn-oidc-data') + if not encoded_jwt: + log.error( + "Unable to authenticate. Header x-amzn-oidc-data not found." + ) + raise credentials_exception + + log.debug(f"Header x-amzn-oidc-data header received: {encoded_jwt}") + + # Validate the signer + jwt_headers = encoded_jwt.split('.')[0] + decoded_jwt_headers = base64.b64decode(jwt_headers) + decoded_json = json.loads(decoded_jwt_headers) + received_alb_arn = decoded_json['signer'] + + if received_alb_arn != DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN: + log.error( + f"Unable to authenticate. ALB ARN {received_alb_arn} does not match expected ARN {DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN}" + ) + raise credentials_exception + + # Get the key id from JWT headers (the kid field) + kid = decoded_json['kid'] + + # Get the region from the ARN + region = DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_ARN.split(':')[3] + + # Get the public key from regional endpoint + log.debug(f"Getting public key for kid {kid} in region {region}.") + pub_key = self.get_public_key(kid, region) + + # Get the payload + log.debug(f"Decoding {encoded_jwt} with public key {pub_key}.") + payload = jwt.decode(encoded_jwt, pub_key, algorithms=['ES256']) + + return payload[DISPATCH_AUTHENTICATION_PROVIDER_AWS_ALB_EMAIL_CLAIM] + + class DispatchTicketPlugin(TicketPlugin): title = "Dispatch Plugin - Ticket Management" slug = "dispatch-ticket" @@ -249,6 +312,14 @@ def create_case_ticket( "resource_type": "dispatch-internal-ticket", } + def update_metadata( + self, + ticket_id: str, + metadata: dict, + ): + """Updates the metadata of a Dispatch ticket.""" + return + def update_case_ticket( self, ticket_id: str, diff --git a/src/dispatch/plugins/dispatch_google/groups/plugin.py b/src/dispatch/plugins/dispatch_google/groups/plugin.py index 2d42c51d4ce3..74c502885e68 100644 --- a/src/dispatch/plugins/dispatch_google/groups/plugin.py +++ b/src/dispatch/plugins/dispatch_google/groups/plugin.py @@ -141,7 +141,8 @@ def create( ): """Creates a new Google Group.""" client = get_service(self.configuration, "admin", "directory_v1", self.scopes) - group_key = f"{name.lower()}@{self.configuration.google_domain}" + # note: group username is limited to 60 characters + group_key = f"{name.lower()[:60]}@{self.configuration.google_domain}" if not description: description = "Group automatically created by Dispatch." @@ -170,11 +171,16 @@ def remove(self, email: str, participants: List[str]): for p in participants: remove_member(client, email, p) - def list(self, email: str): + def list(self, email: str) -> list[str]: """Lists members from an existing Google Group.""" client = get_service(self.configuration, "admin", "directory_v1", self.scopes) - members = list_members(client, email) - return [m["email"] for m in members.get("members", [])] + try: + members = list_members(client, email) + return [m["email"] for m in members.get("members", [])] + except HttpError as e: + if e.resp.status == 404: + log.warning(f"Group does not exist. GroupKey={email} Trying to list members.") + return [] def delete(self, email: str): """Deletes an existing Google group.""" diff --git a/src/dispatch/plugins/dispatch_jira/plugin.py b/src/dispatch/plugins/dispatch_jira/plugin.py index d66d1b848f66..222dbda7d774 100644 --- a/src/dispatch/plugins/dispatch_jira/plugin.py +++ b/src/dispatch/plugins/dispatch_jira/plugin.py @@ -315,6 +315,7 @@ def create( reporter = get_user_field(client, self.configuration, reporter_email) project_id, issue_type_name = process_plugin_metadata(incident_type_plugin_metadata) + other_fields = create_dict_from_plugin_metadata(incident_type_plugin_metadata) if not project_id: project_id = self.configuration.default_project_id @@ -335,6 +336,7 @@ def create( "assignee": assignee, "reporter": reporter, "summary": title, + **other_fields, } ticket = create(self.configuration, client, issue_fields) @@ -401,6 +403,31 @@ def update( return update(self.configuration, client, issue, issue_fields, status) + def update_metadata( + self, + ticket_id: str, + metadata: dict, + ): + """Updates the metadata of a Jira issue.""" + client = create_client(self.configuration) + issue = client.issue(ticket_id) + + # check to make sure project id matches metadata + project_id, issue_type_name = process_plugin_metadata(metadata) + if project_id and issue.fields.project.key != project_id: + log.warning( + f"Project key mismatch between Jira issue {issue.fields.project.key} and metadata {project_id} for ticket {ticket_id}" + ) + return + other_fields = create_dict_from_plugin_metadata(metadata) + issue_fields = { + **other_fields, + } + if issue_type_name: + issue_fields["issuetype"] = {"name": issue_type_name} + + issue.update(fields=issue_fields) + def create_case_ticket( self, case_id: int, diff --git a/src/dispatch/plugins/dispatch_slack/case/enums.py b/src/dispatch/plugins/dispatch_slack/case/enums.py index 5f27b195078f..ee88ee22e310 100644 --- a/src/dispatch/plugins/dispatch_slack/case/enums.py +++ b/src/dispatch/plugins/dispatch_slack/case/enums.py @@ -12,6 +12,8 @@ class CaseNotificationActions(DispatchEnum): triage = "case-notification-triage" user_mfa = "case-notification-user-mfa" invite_user_case = ConversationButtonActions.invite_user_case + do_nothing = "case-do-not-add-user" + add_user = "case-add-user" class CasePaginateActions(DispatchEnum): diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py index 0e87c13a0fce..6708b06ca037 100644 --- a/src/dispatch/plugins/dispatch_slack/case/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py @@ -1,5 +1,6 @@ import json import logging +import re from datetime import datetime, timedelta, timezone from functools import partial from uuid import UUID @@ -12,6 +13,7 @@ Divider, Input, MarkdownText, + Message, Modal, Section, UsersSelect, @@ -34,6 +36,7 @@ from dispatch.event import service as event_service from dispatch.exceptions import ExistsError from dispatch.individual.models import IndividualContactRead +from dispatch.participant import flows as participant_flows from dispatch.participant import service as participant_service from dispatch.participant.models import ParticipantUpdate from dispatch.participant_role import service as participant_role_service @@ -72,7 +75,6 @@ entity_select, incident_priority_select, incident_type_select, - participant_select, project_select, relative_date_picker_input, resolution_input, @@ -80,6 +82,7 @@ ) from dispatch.plugins.dispatch_slack.middleware import ( action_context_middleware, + add_user_middleware, button_context_middleware, command_context_middleware, configuration_middleware, @@ -92,6 +95,7 @@ ) from dispatch.plugins.dispatch_slack.modals.common import send_success_modal from dispatch.plugins.dispatch_slack.models import ( + AddUserMetadata, CaseSubjects, FormData, FormMetadata, @@ -180,7 +184,7 @@ def handle_escalate_case_command( default_title = case.name default_description = case.description - default_project = {"text": case.project.name, "value": case.project.id} + default_project = {"text": case.project.display_name, "value": case.project.id} blocks = [ Context(elements=[MarkdownText(text="Accept the defaults or adjust as needed.")]), @@ -353,7 +357,6 @@ def handle_engage_user_command( """Handles engage user command.""" ack() - case = case_service.get(db_session=db_session, case_id=context["subject"].id) default_engagement = "We'd like to verify your identity. Can you please confirm this is you?" blocks = [ @@ -364,7 +367,7 @@ def handle_engage_user_command( ) ] ), - participant_select(label="Person to engage", participants=case.participants), + assignee_select(label="Person to engage", placeholder="Select user"), description_input(label="Engagement text", initial_value=default_engagement), ] @@ -402,14 +405,26 @@ def engage( """Handles the engage user action.""" ack() - if form_data.get(DefaultBlockIds.participant_select): - participant_id = form_data[DefaultBlockIds.participant_select]["value"] - participant = participant_service.get(db_session=db_session, participant_id=participant_id) - if participant: - user_email = participant.individual.email - else: - log.error(f"Participant not found for id {participant_id} when trying to engage user") - return + case = case_service.get(db_session=db_session, case_id=context["subject"].id) + if not case: + log.error("Case not found when trying to engage user") + return + + if form_data.get(DefaultBlockIds.case_assignee_select): + user_email = client.users_info( + user=form_data[DefaultBlockIds.case_assignee_select]["value"] + )["user"]["profile"]["email"] + conversation_flows.add_case_participants(case, [user_email], db_session) + participant = participant_service.get_by_case_id_and_email( + db_session=db_session, case_id=case.id, email=user_email + ) + if not participant: + participant_flows.add_participant( + user_email, + case, + db_session, + roles=[ParticipantRoleType.participant], + ) else: return @@ -419,8 +434,6 @@ def engage( log.warning("Engagement text not found") return - case = case_service.get(db_session=db_session, case_id=context["subject"].id) - user = client.users_lookupByEmail(email=user_email) result = client.chat_postMessage( @@ -1290,7 +1303,7 @@ def escalate_button_click( description_input(initial_value=case.description), project_select( db_session=db_session, - initial_option={"text": case.project.name, "value": case.project.id}, + initial_option={"text": case.project.display_name, "value": case.project.id}, action_id=CaseEscalateActions.project_select, dispatch_action=True, ), @@ -1345,7 +1358,7 @@ def handle_project_select_action( description_input(), project_select( db_session=db_session, - initial_option={"text": project.name, "value": project.id}, + initial_option={"text": project.display_name, "value": project.id}, action_id=CaseEscalateActions.project_select, dispatch_action=True, ), @@ -1643,6 +1656,141 @@ def handle_create_channel_event( ) +def extract_mentioned_users(text: str) -> list[str]: + """Extracts mentioned users from a message.""" + return re.findall(r"<@(\w+)>", text) + + +def format_emails(emails: list[str]) -> str: + """Format a list of names into a string with commas and 'and' before the last name.""" + usernames = [email.split("@")[0] for email in emails] + + if not usernames: + return "" + elif len(usernames) == 1: + return f"@{usernames[0]}" + elif len(usernames) == 2: + return f"@{usernames[0]} and @{usernames[1]}" + else: + return ", ".join(f"@{username}" for username in usernames[:-1]) + f", and @{usernames[-1]}" + + +@message_dispatcher.add( + subject=CaseSubjects.case, exclude={"subtype": ["channel_join", "group_join"]} +) # we ignore user channel and group join messages +def handle_user_mention( + ack: Ack, + context: BoltContext, + client: WebClient, + db_session: Session, + payload: dict, +) -> None: + """Handles user posted message events.""" + ack() + + case = case_service.get(db_session=db_session, case_id=context["subject"].id) + if not case or case.dedicated_channel: + # we do not need to handle mentions for cases with dedicated channels + return + + mentioned_users = extract_mentioned_users(payload["text"]) + users_not_in_case = [] + for user_id in mentioned_users: + user_email = dispatch_slack_service.get_user_email(client, user_id) + if not participant_service.get_by_case_id_and_email( + db_session=db_session, case_id=context["subject"].id, email=user_email + ): + users_not_in_case.append(user_email) + + if users_not_in_case: + # send a private message to the user who posted the message to see + # if they want to add the mentioned user(s) to the case + button_metadata = AddUserMetadata( + **dict(context["subject"]), + users=users_not_in_case, + ).json() + blocks = [ + Section( + text=f"You mentioned {format_emails(users_not_in_case)}, but they're not in this case." + ), + Actions( + block_id=DefaultBlockIds.add_user_actions, + elements=[ + Button( + text="Add Them", + style="primary", + action_id=CaseNotificationActions.add_user, + value=button_metadata, + ), + Button( + text="Do Nothing", + action_id=CaseNotificationActions.do_nothing, + ), + ], + ), + ] + blocks = Message(blocks=blocks).build()["blocks"] + client.chat_postEphemeral( + channel=payload["channel"], + thread_ts=payload.get("thread_ts"), + user=payload["user"], + blocks=blocks, + ) + + +@app.action( + CaseNotificationActions.add_user, + middleware=[add_user_middleware, button_context_middleware, db_middleware, user_middleware], +) +def add_users_to_case( + ack: Ack, + db_session: Session, + context: BoltContext, + respond: Respond, +): + ack() + + case_id = context["subject"].id + + case = case_service.get(db_session=db_session, case_id=case_id) + if not case: + log.error(f"Could not find case with id: {case_id}") + return + + users = context["users"] + if users: + for user_email in users: + conversation_flows.add_case_participants(case, [user_email], db_session) + participant = participant_service.get_by_case_id_and_email( + db_session=db_session, case_id=case.id, email=user_email + ) + if not participant: + participant_flows.add_participant( + user_email, + case, + db_session, + roles=[ParticipantRoleType.participant], + ) + + # Delete the ephemeral message + respond(delete_original=True) + + +@app.action(CaseNotificationActions.do_nothing) +def handle_do_nothing_button( + ack: Ack, + respond: Respond, +): + # Acknowledge the action + ack() + + try: + # Delete the ephemeral message + respond(delete_original=True) + except SlackApiError as e: + log.error(f"Error deleting ephemeral message: {e.response['error']}") + + @app.action( CaseNotificationActions.join_incident, middleware=[button_context_middleware, db_middleware, user_middleware], @@ -1906,12 +2054,12 @@ def handle_resolve_submission_event( case_id=updated_case.id, previous_case=previous_case, db_session=db_session, - reporter_email=updated_case.reporter.individual.email - if updated_case.reporter - else None, - assignee_email=updated_case.assignee.individual.email - if updated_case.assignee - else None, + reporter_email=( + updated_case.reporter.individual.email if updated_case.reporter else None + ), + assignee_email=( + updated_case.assignee.individual.email if updated_case.assignee else None + ), organization_slug=context["subject"].organization_slug, ) except Exception as e: @@ -1990,7 +2138,7 @@ def handle_report_project_select_action( description_input(), project_select( db_session=db_session, - initial_option={"text": project.name, "value": project.id}, + initial_option={"text": project.display_name, "value": project.id}, action_id=CaseReportActions.project_select, dispatch_action=True, ), @@ -2095,7 +2243,7 @@ def handle_report_case_type_select_action( description_input(), project_select( db_session=db_session, - initial_option={"text": project.name, "value": project.id}, + initial_option={"text": project.display_name, "value": project.id}, action_id=CaseReportActions.project_select, dispatch_action=True, ), @@ -2363,28 +2511,49 @@ def ack_mfa_required_submission_event( ) -> None: """Handles the add engagement submission event acknowledgement.""" + blocks = [] + if mfa_enabled: - mfa_text = ( - "🔐 To complete this action, you need to verify your identity through Multi-Factor Authentication (MFA).\n\n" - f"Please <{challenge_url}|*click here*> to open the MFA verification page." + blocks.extend( + [ + Section( + text="To complete this action, you need to verify your identity through Multi-Factor Authentication (MFA).\n\n" + "Please click the verify button to open the MFA verification page." + ), + Actions( + elements=[ + Button( + text="🔐 Verify", + action_id="button-link", + style="primary", + url=challenge_url, + ) + ] + ), + ] ) else: - mfa_text = "✅ No additional verification required. You can proceed with the confirmation." + blocks.append( + Section( + text="✅ No additional verification required. You can proceed with the confirmation." + ) + ) - blocks = [ - Section(text=mfa_text), - Divider(), - Context( - elements=[ - MarkdownText( - text="💡 This step protects against unauthorized confirmation if your account is compromised." - ) - ] - ), - ] + blocks.extend( + [ + Divider(), + Context( + elements=[ + MarkdownText( + text="💡 This step protects against unauthorized confirmation if your account is compromised." + ) + ] + ), + ] + ) modal = Modal( - title="Confirm Your Identity", + title="Verify Your Identity", close="Cancel", blocks=blocks, ).build() @@ -2423,6 +2592,9 @@ def handle_engagement_submission_event( mfa_plugin = plugin_service.get_active_instance( db_session=db_session, project_id=context["subject"].project_id, plugin_type="auth-mfa" ) + if not mfa_plugin: + log.error("Unable to engage user. No enabled MFA plugin found.") + return require_mfa = engagement.require_mfa if engagement else True mfa_enabled = True if mfa_plugin and require_mfa else False @@ -2532,14 +2704,23 @@ def send_engagement_response( if response == MfaChallengeStatus.APPROVED: # We only update engagement message (which removes Confirm/Deny button) for success # this allows the user to retry the confirmation if the MFA check failed - blocks = create_signal_engagement_message( - case=case, - channel_id=case.conversation.channel_id, - engagement=engagement, - signal_instance=signal_instance, - user_email=engaged_user, - engagement_status=engagement_status, - ) + if not engagement: + # assume the message is from a manual MFA challenge + blocks = create_manual_engagement_message( + case=case, + channel_id=case.conversation.channel_id, + user_email=engaged_user, + engagement_status=engagement_status, + ) + else: + blocks = create_signal_engagement_message( + case=case, + channel_id=case.conversation.channel_id, + engagement=engagement, + signal_instance=signal_instance, + user_email=engaged_user, + engagement_status=engagement_status, + ) if signal_instance: client.chat_update( blocks=blocks, diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py index e800d04578d4..963b0f3f6cc6 100644 --- a/src/dispatch/plugins/dispatch_slack/case/messages.py +++ b/src/dispatch/plugins/dispatch_slack/case/messages.py @@ -18,7 +18,6 @@ from dispatch.case.enums import CaseStatus from dispatch.case.models import Case from dispatch.config import DISPATCH_UI_URL -from dispatch.messaging.strings import CASE_STATUS_DESCRIPTIONS, CASE_VISIBILITY_DESCRIPTIONS from dispatch.plugins.dispatch_slack.case.enums import ( CaseNotificationActions, SignalEngagementActions, @@ -455,21 +454,20 @@ def create_signal_engagement_message( def create_manual_engagement_message( case: Case, channel_id: str, - engagement: str, user_email: str, - user_id: str, engagement_status: SignalEngagementStatus = SignalEngagementStatus.new, + user_id: str = "", + engagement: str = "", thread_ts: str = None, ) -> list[Block]: """ - Generate a list of blocks for a signal engagement message. + Generate a list of blocks for a manual engagement message. Args: - case (Case): The case object related to the signal instance. + case (Case): The case object related to the engagement. channel_id (str): The ID of the Slack channel where the message will be sent. - message (str): Additional context information to include in the message. + engagement_message (str): The engagement text. user_email (str): The email of the user being engaged. - engagement (str): The engagement text. Returns: list[Block]: A list of blocks representing the message structure for the engagement message. @@ -487,11 +485,12 @@ def create_manual_engagement_message( ).json() username, _ = user_email.split("@") - blocks = [ - Section( - text=f"<@{user_id}>: {engagement if engagement else 'No context provided for this alert.'}" - ), - ] + if engagement: + blocks = [ + Section(text=f"<@{user_id}>: {engagement}"), + ] + else: + blocks = [] if engagement_status == SignalEngagementStatus.new: blocks.extend( @@ -533,42 +532,6 @@ def create_manual_engagement_message( return Message(blocks=blocks).build()["blocks"] -def create_welcome_ephemeral_message_to_participant(case: Case) -> list[Block]: - blocks = [ - Section( - text="You've been added to this case, because we think you may be able to help resolve it. Please, review the case details below and reach out to the case assignee if you have any questions.", - ), - Section( - text=f"*Title* \n {case.title}", - ), - Section( - text=f"*Description* \n {case.description}", - ), - Section( - text=f"*Visibility - {case.visibility}* \n {CASE_VISIBILITY_DESCRIPTIONS[case.visibility]}", - ), - Section( - text=f"*Status - {case.status}* \n {CASE_STATUS_DESCRIPTIONS[case.status]}", - ), - Section( - text=f"*Type - {case.case_type.name}* \n {case.case_type.description}", - ), - Section( - text=f"*Severity - {case.case_severity.name}* \n {case.case_severity.description}", - ), - Section( - text=f"*Priority - {case.case_priority.name}* \n {case.case_priority.description}", - ), - Section( - text=f"*Assignee - {case.assignee.individual.name}*", - ), - Section( - text=f"*Reporter - {case.reporter.individual.name}*", - ), - ] - return Message(blocks=blocks).build()["blocks"] - - def create_case_thread_migration_message(channel_weblink: str) -> list[Block]: blocks = [ Context( diff --git a/src/dispatch/plugins/dispatch_slack/events.py b/src/dispatch/plugins/dispatch_slack/events.py index 81c4d63e8cf8..18a943af46c3 100644 --- a/src/dispatch/plugins/dispatch_slack/events.py +++ b/src/dispatch/plugins/dispatch_slack/events.py @@ -31,6 +31,10 @@ def fetch_activity(client: WebClient, subject: None, oldest: str = "0") -> List: log.warning("No conversation provided. Cannot fetch channel activity.") elif not subject.conversation.channel_id: log.warning("No channel id provided. Cannot fetch channel activity.") + elif subject.conversation.thread_id: + log.warning( + "Subject is a thread, not a channel. Fetching channel activity is not applicable for threads." + ) else: return get_channel_activity( client, conversation_id=subject.conversation.channel_id, oldest=oldest diff --git a/src/dispatch/plugins/dispatch_slack/fields.py b/src/dispatch/plugins/dispatch_slack/fields.py index 58de4eeadd6c..2dc9c956c542 100644 --- a/src/dispatch/plugins/dispatch_slack/fields.py +++ b/src/dispatch/plugins/dispatch_slack/fields.py @@ -29,6 +29,7 @@ class DefaultBlockIds(DispatchEnum): + add_user_actions = "add-user-actions" date_picker_input = "date-picker-input" description_input = "description-input" hour_picker_input = "hour-picker-input" @@ -300,7 +301,7 @@ def project_select( ): """Creates a project select.""" projects = [ - {"text": p.name, "value": p.id} + {"text": p.display_name, "value": p.id} for p in project_service.get_all(db_session=db_session) if p.enabled ] diff --git a/src/dispatch/plugins/dispatch_slack/incident/interactive.py b/src/dispatch/plugins/dispatch_slack/incident/interactive.py index 92a4651a7b05..33068ffda671 100644 --- a/src/dispatch/plugins/dispatch_slack/incident/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/incident/interactive.py @@ -249,6 +249,11 @@ def handle_tag_search_action( } if "/" in query_str: + # first check to make sure there's only one slash + if query_str.count("/") > 1: + ack() + return + tag_type, query_str = query_str.split("/") filter_spec["and"].append( {"model": "TagType", "op": "==", "field": "name", "value": tag_type} @@ -304,7 +309,7 @@ def handle_update_incident_project_select_action( incident_status_select(initial_option={"text": incident.status, "value": incident.status}), project_select( db_session=db_session, - initial_option={"text": project.name, "value": project.id}, + initial_option={"text": project.display_name, "value": project.id}, action_id=IncidentUpdateActions.project_select, dispatch_action=True, ), @@ -2109,20 +2114,8 @@ def handle_update_incident_command( description_input(initial_value=incident.description), resolution_input(initial_value=incident.resolution), incident_status_select(initial_option={"text": incident.status, "value": incident.status}), - project_select( - db_session=db_session, - initial_option={"text": incident.project.name, "value": incident.project.id}, - action_id=IncidentUpdateActions.project_select, - dispatch_action=True, - ), - incident_type_select( - db_session=db_session, - initial_option={ - "text": incident.incident_type.name, - "value": incident.incident_type.id, - }, - project_id=incident.project.id, - ), + Section(text=f"*Project*: {incident.project.display_name}"), + Context(elements=[MarkdownText(text="Project is read-only")]), incident_severity_select( db_session=db_session, initial_option={ diff --git a/src/dispatch/plugins/dispatch_slack/middleware.py b/src/dispatch/plugins/dispatch_slack/middleware.py index 94e65f21f5a8..f391930989df 100644 --- a/src/dispatch/plugins/dispatch_slack/middleware.py +++ b/src/dispatch/plugins/dispatch_slack/middleware.py @@ -374,6 +374,14 @@ def command_context_middleware( next() +def add_user_middleware(payload: dict, context: BoltContext, next: Callable): + """Attempts to determine the user to add to the incident.""" + value = payload.get("value") + if value: + context["users"] = json.loads(value).get("users") + next() + + def db_middleware(context: BoltContext, next: Callable): if not context.get("subject"): slug = get_default_org_slug() diff --git a/src/dispatch/plugins/dispatch_slack/models.py b/src/dispatch/plugins/dispatch_slack/models.py index 3b1b3c853f0f..c982305cd2ff 100644 --- a/src/dispatch/plugins/dispatch_slack/models.py +++ b/src/dispatch/plugins/dispatch_slack/models.py @@ -50,6 +50,10 @@ class SubjectMetadata(BaseModel): thread_id: Optional[str] +class AddUserMetadata(SubjectMetadata): + users: list[str] + + class EngagementMetadata(SubjectMetadata): signal_instance_id: str engagement_id: int diff --git a/src/dispatch/plugins/dispatch_slack/service.py b/src/dispatch/plugins/dispatch_slack/service.py index 69557994e4f8..15437b630c06 100644 --- a/src/dispatch/plugins/dispatch_slack/service.py +++ b/src/dispatch/plugins/dispatch_slack/service.py @@ -441,6 +441,8 @@ def add_users_to_conversation(client: WebClient, conversation_id: str, user_ids: # that result in folks already existing in the channel. if e.response["error"] == SlackAPIErrorCode.USER_IN_CHANNEL: pass + elif e.response["error"] == SlackAPIErrorCode.ALREADY_IN_CHANNEL: + pass def get_message_permalink(client: WebClient, conversation_id: str, ts: str) -> str: diff --git a/src/dispatch/project/models.py b/src/dispatch/project/models.py index b6620c8a7fe0..d7d974d3848a 100644 --- a/src/dispatch/project/models.py +++ b/src/dispatch/project/models.py @@ -40,6 +40,8 @@ class Project(Base): cascade="all, delete-orphan", ) + display_name = Column(String, nullable=False, server_default="") + enabled = Column(Boolean, default=True, server_default="t") allow_self_join = Column(Boolean, default=True, server_default="t") @@ -82,6 +84,7 @@ def slug(self): class ProjectBase(DispatchBase): id: Optional[PrimaryKey] name: NameStr + display_name: Optional[str] = Field("", nullable=False) owner_email: Optional[EmailStr] = Field(None, nullable=True) owner_conversation: Optional[str] = Field(None, nullable=True) annual_employee_cost: Optional[int] diff --git a/src/dispatch/signal/service.py b/src/dispatch/signal/service.py index 751201594093..ba158d9e7b0e 100644 --- a/src/dispatch/signal/service.py +++ b/src/dispatch/signal/service.py @@ -544,9 +544,18 @@ def delete(*, db_session: Session, signal_id: int): return signal_id -def is_valid_uuid(val): +def is_valid_uuid(value) -> bool: + """ + Checks if the provided value is a valid UUID. + + Args: + val: The value to be checked. + + Returns: + bool: True if the value is a valid UUID, False otherwise. + """ try: - uuid.UUID(str(val), version=4) + uuid.UUID(str(value), version=4) return True except ValueError: return False @@ -587,7 +596,7 @@ def create_instance( signal_instance.id = signal_instance_in.raw["id"] if signal_instance.id and not is_valid_uuid(signal_instance.id): - msg = f"Invalid signal id format. Expecting UUID format. Received {signal_instance.id}." + msg = f"Invalid signal id format. Expecting UUIDv4 format. Signal id: {signal_instance.id}. Signal name/variant: {signal_instance.raw['name'] if signal_instance and signal_instance.raw and signal_instance.raw.get('name') else signal_instance.raw['variant']}" log.warn(msg) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/src/dispatch/signal/views.py b/src/dispatch/signal/views.py index e8dab18aabe7..346a0a26a71e 100644 --- a/src/dispatch/signal/views.py +++ b/src/dispatch/signal/views.py @@ -157,14 +157,9 @@ def create_engagement( db_session=db_session, creator=current_user, signal_engagement_in=signal_engagement_in ) except IntegrityError: - raise ValidationError( - [ - ErrorWrapper( - ExistsError(msg="A signal engagement with this name already exists."), - loc="name", - ) - ], - model=SignalEngagementRead, + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=[{"msg": "A signal engagement with this name already exists."}], ) from None @@ -195,14 +190,9 @@ def update_engagement( signal_engagement_in=signal_engagement_in, ) except IntegrityError: - raise ValidationError( - [ - ErrorWrapper( - ExistsError(msg="A signal engagement with this name already exists."), - loc="name", - ) - ], - model=SignalEngagementUpdate, + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=[{"msg": "A signal engagement with this name already exists."}], ) from None return signal_engagement @@ -220,13 +210,9 @@ def create_filter( db_session=db_session, creator=current_user, signal_filter_in=signal_filter_in ) except IntegrityError: - raise ValidationError( - [ - ErrorWrapper( - ExistsError(msg="A signal filter with this name already exists."), loc="name" - ) - ], - model=SignalFilterRead, + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=[{"msg": "A signal filter with this name already exists."}], ) from None @@ -253,13 +239,9 @@ def update_filter( db_session=db_session, signal_filter=signal_filter, signal_filter_in=signal_filter_in ) except IntegrityError: - raise ValidationError( - [ - ErrorWrapper( - ExistsError(msg="A signal filter with this name already exists."), loc="name" - ) - ], - model=SignalFilterUpdate, + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=[{"msg": "A signal filter with this name already exists."}], ) from None return signal_filter diff --git a/src/dispatch/static/dispatch/package-lock.json b/src/dispatch/static/dispatch/package-lock.json index facc2bc652e5..37ab8c29e70b 100644 --- a/src/dispatch/static/dispatch/package-lock.json +++ b/src/dispatch/static/dispatch/package-lock.json @@ -32,6 +32,7 @@ "@vue-flow/minimap": "^1.2.0", "@vueuse/core": "^10.5.0", "@vueuse/integrations": "^10.6.1", + "@wdns/vuetify-resize-drawer": "^3.2.0", "apexcharts": "^3.44.0", "axios": "^0.21.4", "d3-force": "^3.0.0", @@ -49,6 +50,7 @@ "monaco-editor": "0.43.0", "register-service-worker": "^1.7.2", "roboto-fontface": "^0.10.0", + "sass-embedded": "^1.81.0", "sortablejs": "^1.15.0", "swrv": "^1.0.4", "vue": "^3.4.12", @@ -56,7 +58,6 @@ "vue3-apexcharts": "^1.4.4", "vue3-markdown-it": "^1.0.10", "vuetify": "^3.4.3", - "vuetify3-resize-drawer": "^2.1.1", "vuex": "^4.1.0", "vuex-map-fields": "^1.4.1" }, @@ -162,6 +163,11 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.2.tgz", + "integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==" + }, "node_modules/@date-io/core": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.17.0.tgz", @@ -624,20 +630,20 @@ } }, "node_modules/@formkit/core": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/core/-/core-1.6.8.tgz", - "integrity": "sha512-Row9er9GaWPJUEfPhVw3OAUilo53KmkI+/Dxhz/bRw0ztsPqDJPKvr9GxDvDxjz7GD5baAR43KXde4iaq64NIg==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/core/-/core-1.6.9.tgz", + "integrity": "sha512-Zb5OkYKMf7Rp1pd4iUMv0TJQvfgl1PdKtRRQoGiTA0XIFLB/7tcRMr1wc5isA2JS+hllfxMTh3RWF8N+64fTMg==", "dependencies": { - "@formkit/utils": "1.6.8" + "@formkit/utils": "1.6.9" } }, "node_modules/@formkit/dev": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/dev/-/dev-1.6.8.tgz", - "integrity": "sha512-YhIdz8H2CopajQyHGk/xNg2rXz71ZLliU69liZaFDcUUl38TzR0aCswgGkgEIbV7ISXC6xaBWNwZU3P6URykcQ==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/dev/-/dev-1.6.9.tgz", + "integrity": "sha512-4ueBpZAOiKr8/LZnq3mNePCX4ZB1j1JuJscBEwugWMnDeDwCNo5XWBrng1ER/LlitTRQ3mtEBNy2Qpm0yAHlwA==", "dependencies": { - "@formkit/core": "1.6.8", - "@formkit/utils": "1.6.8" + "@formkit/core": "1.6.9", + "@formkit/utils": "1.6.9" } }, "node_modules/@formkit/drag-and-drop": { @@ -646,31 +652,31 @@ "integrity": "sha512-kFjA8ucSqy4zOLXo25JHkkdrbMRW+KINDBMzBkwwtkH4YCOGIdqtxkEMUMBRgaxaAZvdxbtl+i4A/agwpv1oBw==" }, "node_modules/@formkit/i18n": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/i18n/-/i18n-1.6.8.tgz", - "integrity": "sha512-1X291y857FChU2MSs6QbwDrW9lpkf7EPF1s0JXZFGQPwczaE+xut76KxZUSXWCLu6m0iiK/G67DSr8EtJMNKoA==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/i18n/-/i18n-1.6.9.tgz", + "integrity": "sha512-8NA5bALlspCBEwInuZVgBqgQr0lDfproZdmbs2LciQpGi2B15u74JCjAkEwaKlMs+qgf/ds3QcIgUv2ztyyVEA==", "dependencies": { - "@formkit/core": "1.6.8", - "@formkit/utils": "1.6.8", - "@formkit/validation": "1.6.8" + "@formkit/core": "1.6.9", + "@formkit/utils": "1.6.9", + "@formkit/validation": "1.6.9" } }, "node_modules/@formkit/inputs": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/inputs/-/inputs-1.6.8.tgz", - "integrity": "sha512-PZKCbeBYgm1G17ONEgWq6R6bSjKrye3vXGIRB6Rw9dN5kS9VcTeKbS94R0uZ9WoWk4pl1dZVT086WAsfyELeRg==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/inputs/-/inputs-1.6.9.tgz", + "integrity": "sha512-k9gjV1e5F87NxSnu13JtKb30XYt6ndx2KGHZG8Xz0etoP75yJlMaeROHHPvlxdy2gZM6qH7Ex4it51W74Wh2Eg==", "dependencies": { - "@formkit/core": "1.6.8", - "@formkit/utils": "1.6.8" + "@formkit/core": "1.6.9", + "@formkit/utils": "1.6.9" } }, "node_modules/@formkit/observer": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/observer/-/observer-1.6.8.tgz", - "integrity": "sha512-KGFogNM2kdE5tf08MjIPd0ZPGPQsEuwgSKwJHbMAhCngPgQkpzUqaYBWQO2GxJt8RwozUXDa4m/7c5GZw8+eHA==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/observer/-/observer-1.6.9.tgz", + "integrity": "sha512-p3MCmzp6jwzXIuV3gI9uTJTJl+sN5689C7qf7gdrS8jb1fbX1snKiTyWA8FXOrBXu+ne5z/sA/yBWqYFTSLy8A==", "dependencies": { - "@formkit/core": "1.6.8", - "@formkit/utils": "1.6.8" + "@formkit/core": "1.6.9", + "@formkit/utils": "1.6.9" } }, "node_modules/@formkit/pro": { @@ -684,21 +690,21 @@ } }, "node_modules/@formkit/rules": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/rules/-/rules-1.6.8.tgz", - "integrity": "sha512-Yqd1JuQa7HtyTgs8YgF2EG1s2eV1vXvg3n/iT8M60p0gWmEzO7tjWPADnVJII3FNNXlsAPAdS6E01/Jn/o/ZoQ==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/rules/-/rules-1.6.9.tgz", + "integrity": "sha512-5Vu3JACKyws1kw02qF+024WkS7L9kYZ0lmdSpsaTqg5Wf7+InsxWXFYaG6vCzqIh4Lk9NeffIzq/xyGpGxf5uQ==", "dependencies": { - "@formkit/core": "1.6.8", - "@formkit/utils": "1.6.8", - "@formkit/validation": "1.6.8" + "@formkit/core": "1.6.9", + "@formkit/utils": "1.6.9", + "@formkit/validation": "1.6.9" } }, "node_modules/@formkit/themes": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/themes/-/themes-1.6.8.tgz", - "integrity": "sha512-GX8HzQw9T4cjp+e/qvl04UU86uWZ+2WBaiSe8JzpFaGmsF7RpMiQVO7mbWR5qaSwa2lwVB/7sCgzl6BY3cEM6w==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/themes/-/themes-1.6.9.tgz", + "integrity": "sha512-/UD+MehQEdcCEadt73eIBGGAMEK8ODN0yq9r9299WvQxIELCOP2MbcxuWCV/g2Vd15Xhl8YFdn4KCzQi4X7QXA==", "dependencies": { - "@formkit/core": "1.6.8" + "@formkit/core": "1.6.9" }, "peerDependencies": { "tailwindcss": "^3.2.0", @@ -717,77 +723,40 @@ } } }, - "node_modules/@formkit/themes/node_modules/@formkit/core": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/core/-/core-1.6.8.tgz", - "integrity": "sha512-Row9er9GaWPJUEfPhVw3OAUilo53KmkI+/Dxhz/bRw0ztsPqDJPKvr9GxDvDxjz7GD5baAR43KXde4iaq64NIg==", - "dependencies": { - "@formkit/utils": "1.6.8" - } - }, - "node_modules/@formkit/themes/node_modules/@formkit/utils": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/utils/-/utils-1.6.8.tgz", - "integrity": "sha512-OsZCwHHmIZPwiAtQ5/ewAR5eGMv/nE91UGWyVKB8BU8BDh0Ao/oOcyESLGwU5GhPMAG8qG8rGrM4Alu0JKM/Yg==" - }, "node_modules/@formkit/utils": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/utils/-/utils-1.6.8.tgz", - "integrity": "sha512-OsZCwHHmIZPwiAtQ5/ewAR5eGMv/nE91UGWyVKB8BU8BDh0Ao/oOcyESLGwU5GhPMAG8qG8rGrM4Alu0JKM/Yg==" + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/utils/-/utils-1.6.9.tgz", + "integrity": "sha512-vSFhB/Sm/A+SdwKdBi4WhJcdbePqSYRaB878Ol9HL8roTmmmgQpThvkv6EjLM6aRRP27Il5rS8XtIAIeh8vdTA==" }, "node_modules/@formkit/validation": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/validation/-/validation-1.6.8.tgz", - "integrity": "sha512-m2spXFvCzGfvl1m6GJG13bEJWl3L9A4fYwQmEhpcoty7P64psnqAjJygrl/LpUkcVyJ51ifsV3t5Gy1p4Zc+Mw==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/validation/-/validation-1.6.9.tgz", + "integrity": "sha512-9PGwN0ZDJt3hsrMyaL8KTG3diSQDik1OGogVG6/nFcZhWUycpeamFfXZSQ5pfzmwnvrTHsvyT0FtKitUnWWuPA==", "dependencies": { - "@formkit/core": "1.6.8", - "@formkit/observer": "1.6.8", - "@formkit/utils": "1.6.8" + "@formkit/core": "1.6.9", + "@formkit/observer": "1.6.9", + "@formkit/utils": "1.6.9" } }, "node_modules/@formkit/vue": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@formkit/vue/-/vue-1.6.8.tgz", - "integrity": "sha512-Uy6ElyWw4o+uo5835hZKtMVTcsOIq8eqcDApPOrCgetpAW+mFNr3C9dEbEy3shJaobEW5wmuX7CyalyZlAgAfw==", - "dependencies": { - "@formkit/core": "1.6.8", - "@formkit/dev": "1.6.8", - "@formkit/i18n": "1.6.8", - "@formkit/inputs": "1.6.8", - "@formkit/observer": "1.6.8", - "@formkit/rules": "1.6.8", - "@formkit/themes": "1.6.8", - "@formkit/utils": "1.6.8", - "@formkit/validation": "1.6.8" + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@formkit/vue/-/vue-1.6.9.tgz", + "integrity": "sha512-WrjAtEsKnFJzxQuATWsWKMpTAyJE15PUmRh9hwEAqgTDy2yMog1gxqxfZv3rEAdIdgXNp08tWmRVnQgDIF3vAQ==", + "dependencies": { + "@formkit/core": "1.6.9", + "@formkit/dev": "1.6.9", + "@formkit/i18n": "1.6.9", + "@formkit/inputs": "1.6.9", + "@formkit/observer": "1.6.9", + "@formkit/rules": "1.6.9", + "@formkit/themes": "1.6.9", + "@formkit/utils": "1.6.9", + "@formkit/validation": "1.6.9" }, "peerDependencies": { "vue": "^3.4.0" } }, - "node_modules/@formkit/vue/node_modules/@formkit/themes": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@formkit/themes/-/themes-1.6.7.tgz", - "integrity": "sha512-TIiWr4TMAFUg1pQz2E4GErfAhBv2Q2VbWlk6pqXPWI8UyPTjmcinEnCSIWDCX6FPPqiYShBnh8123nTO7pyvjA==", - "dependencies": { - "@formkit/core": "1.6.7" - }, - "peerDependencies": { - "tailwindcss": "^3.2.0", - "unocss": "0.x.x", - "windicss": "^3.0.0" - }, - "peerDependenciesMeta": { - "tailwindcss": { - "optional": true - }, - "unocss": { - "optional": true - }, - "windicss": { - "optional": true - } - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1271,12 +1240,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", - "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", "dev": true, "dependencies": { - "playwright": "1.48.2" + "playwright": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -1635,9 +1604,9 @@ "integrity": "sha512-+kmRpd+EeTFd3qNt1AoKphJqbAN26ZDsbiwqjBFeoAmdCyiUO19xMXPtYi9vovAj9a7OAJnvWtiHkwwjU2Fx4Q==" }, "node_modules/@tanstack/match-sorter-utils": { - "version": "8.15.1", - "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.15.1.tgz", - "integrity": "sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==", + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz", + "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==", "dependencies": { "remove-accents": "0.5.0" }, @@ -1650,21 +1619,23 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.59.20", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.20.tgz", - "integrity": "sha512-e8vw0lf7KwfGe1if4uPFhvZRWULqHjFcz3K8AebtieXvnMOz5FSzlZe3mTLlPuUBcydCnBRqYs2YJ5ys68wwLg==", + "version": "5.62.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.7.tgz", + "integrity": "sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/vue-query": { - "version": "5.59.20", - "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.59.20.tgz", - "integrity": "sha512-kIs1GfXh7jVLycbnQDghfdrcvrZz5fxnMF7eAAp8O3ZfhHQWfP57DBXbOvww4Y+TI0EvVoh+hihX+LNFBGFKLg==", + "version": "5.62.7", + "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.62.7.tgz", + "integrity": "sha512-FVSnVw2fw9BHciCbWWSfPMB3PRtGHbGf4Q1Gq7mFlieojiKZBF5Nz5AKFQLKs+IGnrVDTKqfU8ojjPfthPTYxQ==", + "license": "MIT", "dependencies": { - "@tanstack/match-sorter-utils": "^8.15.1", - "@tanstack/query-core": "5.59.20", + "@tanstack/match-sorter-utils": "^8.19.4", + "@tanstack/query-core": "5.62.7", "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, @@ -1708,9 +1679,9 @@ } }, "node_modules/@tiptap/core": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.9.1.tgz", - "integrity": "sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.10.3.tgz", + "integrity": "sha512-wAG/0/UsLeZLmshWb6rtWNXKJftcmnned91/HLccHVQAuQZ1UWH+wXeQKu/mtodxEO7JcU2mVPR9mLGQkK0McQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1720,9 +1691,9 @@ } }, "node_modules/@tiptap/extension-blockquote": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.9.1.tgz", - "integrity": "sha512-Y0jZxc/pdkvcsftmEZFyG+73um8xrx6/DMfgUcNg3JAM63CISedNcr+OEI11L0oFk1KFT7/aQ9996GM6Kubdqg==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.10.3.tgz", + "integrity": "sha512-u9Mq4r8KzoeGVT8ms6FQDIMN95dTh3TYcT7fZpwcVM96mIl2Oyt+Bk66mL8z4zuFptfRI57Cu9QdnHEeILd//w==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1732,9 +1703,9 @@ } }, "node_modules/@tiptap/extension-bold": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.9.1.tgz", - "integrity": "sha512-e2P1zGpnnt4+TyxTC5pX/lPxPasZcuHCYXY0iwQ3bf8qRQQEjDfj3X7EI+cXqILtnhOiviEOcYmeu5op2WhQDg==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.10.3.tgz", + "integrity": "sha512-xnF1tS2BsORenr11qyybW120gHaeHKiKq+ZOP14cGA0MsriKvWDnaCSocXP/xMEYHy7+2uUhJ0MsKkHVj4bPzQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1744,9 +1715,9 @@ } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.9.1.tgz", - "integrity": "sha512-DWUF6NG08/bZDWw0jCeotSTvpkyqZTi4meJPomG9Wzs/Ol7mEwlNCsCViD999g0+IjyXFatBk4DfUq1YDDu++Q==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.10.3.tgz", + "integrity": "sha512-e9a4yMjQezuKy0rtyyzxbV2IAE1bm1PY3yoZEFrcaY0o47g1CMUn2Hwe+9As2HdntEjQpWR7NO1mZeKxHlBPYA==", "dependencies": { "tippy.js": "^6.3.7" }, @@ -1760,9 +1731,9 @@ } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.9.1.tgz", - "integrity": "sha512-0hizL/0j9PragJObjAWUVSuGhN1jKjCFnhLQVRxtx4HutcvS/lhoWMvFg6ZF8xqWgIa06n6A7MaknQkqhTdhKA==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.10.3.tgz", + "integrity": "sha512-PTkwJOVlHi4RR4Wrs044tKMceweXwNmWA6EoQ93hPUVtQcwQL990Es5Izp+i88twTPLuGD9dH+o9QDyH9SkWdA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1772,9 +1743,9 @@ } }, "node_modules/@tiptap/extension-code": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.9.1.tgz", - "integrity": "sha512-WQqcVGe7i/E+yO3wz5XQteU1ETNZ00euUEl4ylVVmH2NM4Dh0KDjEhbhHlCM0iCfLUo7jhjC7dmS+hMdPUb+Tg==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.10.3.tgz", + "integrity": "sha512-JyLbfyY3cPctq9sVdpcRWTcoUOoq3/MnGE1eP6eBNyMTHyBPcM9TPhOkgj+xkD1zW/884jfelB+wa70RT/AMxQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1784,9 +1755,9 @@ } }, "node_modules/@tiptap/extension-code-block": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.9.1.tgz", - "integrity": "sha512-A/50wPWDqEUUUPhrwRKILP5gXMO5UlQ0F6uBRGYB9CEVOREam9yIgvONOnZVJtszHqOayjIVMXbH/JMBeq11/g==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.10.3.tgz", + "integrity": "sha512-yiDVNg22fYkzsFk5kBlDSHcjwVJgajvO/M5fDXA+Hfxwo2oNcG6aJyyHXFe+UaXTVjdkPej0J6kcMKrTMCiFug==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1797,9 +1768,9 @@ } }, "node_modules/@tiptap/extension-document": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.9.1.tgz", - "integrity": "sha512-1a+HCoDPnBttjqExfYLwfABq8MYdiowhy/wp8eCxVb6KGFEENO53KapstISvPzqH7eOi+qRjBB1KtVYb/ZXicg==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.10.3.tgz", + "integrity": "sha512-6i8+xbS2zB6t8iFzli1O/QB01MmwyI5Hqiiv4m5lOxqavmJwLss2sRhoMC2hB3CyFg5UmeODy/f/RnI6q5Vixg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1809,9 +1780,9 @@ } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.9.1.tgz", - "integrity": "sha512-wJZspSmJRkDBtPkzFz1g7gvZOEOayk8s93UHsgbJxcV4VWHYleZ5XhT74sZunSjefNDm3qC6v2BSgLp3vNHVKQ==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.10.3.tgz", + "integrity": "sha512-wzWf82ixWzZQr0hxcf/A0ul8NNxgy1N63O+c56st6OomoLuKUJWOXF+cs9O7V+/5rZKWdbdYYoRB5QLvnDBAlQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1822,9 +1793,9 @@ } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.9.1.tgz", - "integrity": "sha512-MxZ7acNNsoNaKpetxfwi3Z11Bgrh0T2EJlCV77v9N1vWK38+st3H1WJanmLbPNtc2ocvhHJrz+DjDz3CWxQ9rQ==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.10.3.tgz", + "integrity": "sha512-Prg8rYLxeyzHxfzVu1mDkkUWMnD9ZN3y370O/1qy55e+XKVw9jFkTSuz0y0+OhMJG6bulYpDUMtb+N3+2xOWlQ==", "dependencies": { "tippy.js": "^6.3.7" }, @@ -1838,9 +1809,9 @@ } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.9.1.tgz", - "integrity": "sha512-jsRBmX01vr+5H02GljiHMo0n5H1vzoMLmFarxe0Yq2d2l9G/WV2VWX2XnGliqZAYWd1bI0phs7uLQIN3mxGQTw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.10.3.tgz", + "integrity": "sha512-FskZi2DqDSTH1WkgLF2OLy0xU7qj3AgHsKhVsryeAtld4jAK5EsonneWgaipbz0e/MxuIvc1oyacfZKABpLaNg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1851,9 +1822,9 @@ } }, "node_modules/@tiptap/extension-hard-break": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.9.1.tgz", - "integrity": "sha512-fCuaOD/b7nDjm47PZ58oanq7y4ccS2wjPh42Qm0B0yipu/1fmC8eS1SmaXmk28F89BLtuL6uOCtR1spe+lZtlQ==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.10.3.tgz", + "integrity": "sha512-2rFlimUKAgKDwT6nqAMtPBjkrknQY8S7oBNyIcDOUGyFkvbDUl3Jd0PiC929S5F3XStJRppnMqhpNDAlWmvBLA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1863,9 +1834,9 @@ } }, "node_modules/@tiptap/extension-heading": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.9.1.tgz", - "integrity": "sha512-SjZowzLixOFaCrV2cMaWi1mp8REK0zK1b3OcVx7bCZfVSmsOETJyrAIUpCKA8o60NwF7pwhBg0MN8oXlNKMeFw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.10.3.tgz", + "integrity": "sha512-AlxXXPCWIvw8hQUDFRskasj32iMNB8Sb19VgyFWqwvntGs2/UffNu8VdsVqxD2HpZ0g5rLYCYtSW4wigs9R3og==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1875,9 +1846,9 @@ } }, "node_modules/@tiptap/extension-history": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.9.1.tgz", - "integrity": "sha512-wp9qR1NM+LpvyLZFmdNaAkDq0d4jDJ7z7Fz7icFQPu31NVxfQYO3IXNmvJDCNu8hFAbImpA5aG8MBuwzRo0H9w==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.10.3.tgz", + "integrity": "sha512-HaSiMdx9Im9Pb9qGlVud7W8bweRDRMez33Uzs5a2x0n1RWkelfH7TwYs41Y3wus8Ujs7kw6qh7jyhvPpQBKaSA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1888,9 +1859,9 @@ } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.9.1.tgz", - "integrity": "sha512-ydUhABeaBI1CoJp+/BBqPhXINfesp1qMNL/jiDcMsB66fsD4nOyphpAJT7FaRFZFtQVF06+nttBtFZVkITQVqg==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.10.3.tgz", + "integrity": "sha512-1a2IWhD00tgUNg/91RLnBvfENL7DLCui5L245+smcaLu+OXOOEpoBHawx59/M4hEpsjqvRRM79TzO9YXfopsPw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1901,9 +1872,9 @@ } }, "node_modules/@tiptap/extension-italic": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.9.1.tgz", - "integrity": "sha512-VkNA6Vz96+/+7uBlsgM7bDXXx4b62T1fDam/3UKifA72aD/fZckeWrbT7KrtdUbzuIniJSbA0lpTs5FY29+86Q==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.10.3.tgz", + "integrity": "sha512-wAiO6ZxoHx2H90phnKttLWGPjPZXrfKxhOCsqYrK8BpRByhr48godOFRuGwYnKaiwoVjpxc63t+kDJDWvqmgMw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1913,9 +1884,9 @@ } }, "node_modules/@tiptap/extension-list-item": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.9.1.tgz", - "integrity": "sha512-6O4NtYNR5N2Txi4AC0/4xMRJq9xd4+7ShxCZCDVL0WDVX37IhaqMO7LGQtA6MVlYyNaX4W1swfdJaqrJJ5HIUw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.10.3.tgz", + "integrity": "sha512-9sok81gvZfSta2K1Dwrq5/HSz1jk4zHBpFqCx0oydzodGslx6X1bNxdca+eXJpXZmQIWALK7zEr4X8kg3WZsgw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1925,9 +1896,9 @@ } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.9.1.tgz", - "integrity": "sha512-6J9jtv1XP8dW7/JNSH/K4yiOABc92tBJtgCsgP8Ep4+fjfjdj4HbjS1oSPWpgItucF2Fp/VF8qg55HXhjxHjTw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.10.3.tgz", + "integrity": "sha512-/SFuEDnbJxy3jvi72LeyiPHWkV+uFc0LUHTUHSh20vwyy+tLrzncJfXohGbTIv5YxYhzExQYZDRD4VbSghKdlw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1937,9 +1908,9 @@ } }, "node_modules/@tiptap/extension-paragraph": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.9.1.tgz", - "integrity": "sha512-JOmT0xd4gd3lIhLwrsjw8lV+ZFROKZdIxLi0Ia05XSu4RLrrvWj0zdKMSB+V87xOWfSB3Epo95zAvnPox5Q16A==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.10.3.tgz", + "integrity": "sha512-sNkTX/iN+YoleDiTJsrWSBw9D7c4vsYwnW5y/G5ydfuJMIRQMF78pWSIWZFDRNOMkgK5UHkhu9anrbCFYgBfaA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1949,9 +1920,9 @@ } }, "node_modules/@tiptap/extension-placeholder": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.9.1.tgz", - "integrity": "sha512-Q/w3OOg/C6jGBf4QKEWKF9k+iaCQCgPoaIg2IDTPx8QmaxRfgoVE5Csd+oTOY/brdmSNXOxykZWEci6OJP+MbA==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.10.3.tgz", + "integrity": "sha512-0OkwnDLguZgoiJM85cfnOySuMmPUF7qqw7DHQ+c3zwTAYnvzpvqrvpupc+2Zi9GfC1sDgr+Ajrp8imBHa6PHfA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1962,9 +1933,9 @@ } }, "node_modules/@tiptap/extension-strike": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.9.1.tgz", - "integrity": "sha512-V5aEXdML+YojlPhastcu7w4biDPwmzy/fWq0T2qjfu5Te/THcqDmGYVBKESBm5x6nBy5OLkanw2O+KHu2quDdg==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.10.3.tgz", + "integrity": "sha512-jYoPy6F6njYp3txF3u23bgdRy/S5ATcWDO9LPZLHSeikwQfJ47nqb+EUNo5M8jIOgFBTn4MEbhuZ6OGyhnxopA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1974,9 +1945,9 @@ } }, "node_modules/@tiptap/extension-text": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.9.1.tgz", - "integrity": "sha512-3wo9uCrkLVLQFgbw2eFU37QAa1jq1/7oExa+FF/DVxdtHRS9E2rnUZ8s2hat/IWzvPUHXMwo3Zg2XfhoamQpCA==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.10.3.tgz", + "integrity": "sha512-7p9XiRprsRZm8y9jvF/sS929FCELJ5N9FQnbzikOiyGNUx5mdI+exVZlfvBr9xOD5s7fBLg6jj9Vs0fXPNRkPg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1986,9 +1957,9 @@ } }, "node_modules/@tiptap/extension-text-style": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.9.1.tgz", - "integrity": "sha512-LAxc0SeeiPiAVBwksczeA7BJSZb6WtVpYhy5Esvy9K0mK5kttB4KxtnXWeQzMIJZQbza65yftGKfQlexf/Y7yg==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.10.3.tgz", + "integrity": "sha512-TalYIdlF7vBA4afFhmido7AORdBbu3sV+HCByda0FiNbM6cjng3Nr9oxHOCVJy+ChqrcgF4m54zDfLmamdyu5Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1998,28 +1969,28 @@ } }, "node_modules/@tiptap/pm": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.9.1.tgz", - "integrity": "sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.10.3.tgz", + "integrity": "sha512-771p53aU0KFvujvKpngvq2uAxThlEsjYaXcVVmwrhf0vxSSg+psKQEvqvWvHv/3BwkPVCGwmEKNVJZjaXFKu4g==", "dependencies": { "prosemirror-changeset": "^2.2.1", "prosemirror-collab": "^1.3.1", - "prosemirror-commands": "^1.6.0", + "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", - "prosemirror-markdown": "^1.13.0", + "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", - "prosemirror-model": "^1.22.3", + "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", - "prosemirror-tables": "^1.4.0", + "prosemirror-tables": "^1.6.1", "prosemirror-trailing-node": "^3.0.0", - "prosemirror-transform": "^1.10.0", - "prosemirror-view": "^1.34.3" + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.37.0" }, "funding": { "type": "github", @@ -2027,31 +1998,31 @@ } }, "node_modules/@tiptap/starter-kit": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.9.1.tgz", - "integrity": "sha512-nsw6UF/7wDpPfHRhtGOwkj1ipIEiWZS1VGw+c14K61vM1CNj0uQ4jogbHwHZqN1dlL5Hh+FCqUHDPxG6ECbijg==", - "dependencies": { - "@tiptap/core": "^2.9.1", - "@tiptap/extension-blockquote": "^2.9.1", - "@tiptap/extension-bold": "^2.9.1", - "@tiptap/extension-bullet-list": "^2.9.1", - "@tiptap/extension-code": "^2.9.1", - "@tiptap/extension-code-block": "^2.9.1", - "@tiptap/extension-document": "^2.9.1", - "@tiptap/extension-dropcursor": "^2.9.1", - "@tiptap/extension-gapcursor": "^2.9.1", - "@tiptap/extension-hard-break": "^2.9.1", - "@tiptap/extension-heading": "^2.9.1", - "@tiptap/extension-history": "^2.9.1", - "@tiptap/extension-horizontal-rule": "^2.9.1", - "@tiptap/extension-italic": "^2.9.1", - "@tiptap/extension-list-item": "^2.9.1", - "@tiptap/extension-ordered-list": "^2.9.1", - "@tiptap/extension-paragraph": "^2.9.1", - "@tiptap/extension-strike": "^2.9.1", - "@tiptap/extension-text": "^2.9.1", - "@tiptap/extension-text-style": "^2.9.1", - "@tiptap/pm": "^2.9.1" + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.10.3.tgz", + "integrity": "sha512-oq8xdVIMqohSs91ofHSr7i5dCp2F56Lb9aYIAI25lZmwNwQJL2geGOYjMSfL0IC4cQHPylIuSKYCg7vRFdZmAA==", + "dependencies": { + "@tiptap/core": "^2.10.3", + "@tiptap/extension-blockquote": "^2.10.3", + "@tiptap/extension-bold": "^2.10.3", + "@tiptap/extension-bullet-list": "^2.10.3", + "@tiptap/extension-code": "^2.10.3", + "@tiptap/extension-code-block": "^2.10.3", + "@tiptap/extension-document": "^2.10.3", + "@tiptap/extension-dropcursor": "^2.10.3", + "@tiptap/extension-gapcursor": "^2.10.3", + "@tiptap/extension-hard-break": "^2.10.3", + "@tiptap/extension-heading": "^2.10.3", + "@tiptap/extension-history": "^2.10.3", + "@tiptap/extension-horizontal-rule": "^2.10.3", + "@tiptap/extension-italic": "^2.10.3", + "@tiptap/extension-list-item": "^2.10.3", + "@tiptap/extension-ordered-list": "^2.10.3", + "@tiptap/extension-paragraph": "^2.10.3", + "@tiptap/extension-strike": "^2.10.3", + "@tiptap/extension-text": "^2.10.3", + "@tiptap/extension-text-style": "^2.10.3", + "@tiptap/pm": "^2.10.3" }, "funding": { "type": "github", @@ -2059,12 +2030,12 @@ } }, "node_modules/@tiptap/vue-3": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-2.9.1.tgz", - "integrity": "sha512-51mKa4C3hdKe+o6G7Pk7d4puZ/VjoHWtTo2WxE249oH+bCkh6FObqNu2wfRK+9obVuTGXQ9dAc988cmwY+2eyw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-2.10.3.tgz", + "integrity": "sha512-eJLUpuKq3Yei3+XHba25eFvjAH6q275r+Dkz/ulStOWGwchlN8OSbcn0kBWfhr14RG8yoNvL4rZncxXvqXzvhQ==", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.9.1", - "@tiptap/extension-floating-menu": "^2.9.1" + "@tiptap/extension-bubble-menu": "^2.10.3", + "@tiptap/extension-floating-menu": "^2.10.3" }, "funding": { "type": "github", @@ -2116,26 +2087,23 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/linkify-it": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", - "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", - "peer": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==" }, "node_modules/@types/markdown-it": { - "version": "13.0.7", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz", - "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==", - "peer": true, + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dependencies": { - "@types/linkify-it": "*", - "@types/mdurl": "*" + "@types/linkify-it": "^5", + "@types/mdurl": "^2" } }, "node_modules/@types/mdurl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", - "peer": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==" }, "node_modules/@types/node": { "version": "20.11.4", @@ -2156,6 +2124,12 @@ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@types/web-bluetooth": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", @@ -2349,15 +2323,15 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.0.tgz", - "integrity": "sha512-7n7KdUEtx/7Yl7I/WVAMZ1bEb0eVvXF3ummWTeLcs/9gvo9pJhuLdouSXGjdZ/MKD1acf1I272+X0RMua4/R3g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", + "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^5.0.0", + "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, @@ -2487,9 +2461,9 @@ } }, "node_modules/@vue-flow/core": { - "version": "1.41.5", - "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.41.5.tgz", - "integrity": "sha512-dH9senihkrRD0HxnDkEJXi4yxQD08QGp50yF85d8+JsKEcdhGrK3Unbt4x3cZiLY2BpGZhZsnRvlC6xEZklOQg==", + "version": "1.41.6", + "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.41.6.tgz", + "integrity": "sha512-8zxcGRqiudra0obDMLTg9L89WxdlV0QrDOdyPYOKWDcD/UK5aT0MIL3Br9TF9AJmHW2z8QZq4cmmgroREL0jgQ==", "dependencies": { "@vueuse/core": "^10.5.0", "d3-drag": "^3.0.0", @@ -2514,57 +2488,57 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", - "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.12", + "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", - "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", "dependencies": { - "@vue/compiler-core": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", - "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.12", - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", - "postcss": "^8.4.47", + "postcss": "^8.4.48", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-sfc/node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.13", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.13.tgz", + "integrity": "sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", - "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/devtools-api": { @@ -2573,49 +2547,49 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" }, "node_modules/@vue/reactivity": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", - "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", "dependencies": { - "@vue/shared": "3.5.12" + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", - "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", - "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", "dependencies": { - "@vue/reactivity": "3.5.12", - "@vue/runtime-core": "3.5.12", - "@vue/shared": "3.5.12", + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", - "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", "dependencies": { - "@vue/compiler-ssr": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { - "vue": "3.5.12" + "vue": "3.5.13" } }, "node_modules/@vue/shared": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", - "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==" + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" }, "node_modules/@vue/test-utils": { "version": "2.4.6", @@ -2846,6 +2820,25 @@ } } }, + "node_modules/@wdns/vuetify-resize-drawer": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@wdns/vuetify-resize-drawer/-/vuetify-resize-drawer-3.2.0.tgz", + "integrity": "sha512-JfPDrV9G/6k6fCLLIurET6jdDIzEVSvjrqxoVeWhxTVUuS+Cs4oJga7wWNRgFTZdqfyZT8Id2aUDCEHYCjcQQg==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/webdevnerdstuff" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/WebDevNerdStuff" + } + ], + "dependencies": { + "vue": "^3.5.12", + "vuetify": "^3.7.2" + } + }, "node_modules/@yr/monotone-cubic-spline": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", @@ -3059,6 +3052,11 @@ "node": ">=8" } }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==" + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3274,6 +3272,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3339,9 +3342,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3668,9 +3671,12 @@ } }, "node_modules/dompurify": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.0.tgz", - "integrity": "sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", + "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/domutils": { "version": "3.1.0", @@ -3686,9 +3692,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "engines": { "node": ">=12" }, @@ -3998,9 +4004,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.31.0.tgz", - "integrity": "sha512-aYMUCgivhz1o4tLkRHj5oq9YgYPM4/EJc0M7TAKRLCUA5OYxRLAhYEVD2nLtTwLyixEFI+/QXSvKU9ESZFgqjQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.32.0.tgz", + "integrity": "sha512-b/Y05HYmnB/32wqVcjxjHZzNpwxj1onBOvqW89W+V+XNG1dRuaFbNd3vT9CLbr2LXjEoq+3vn8DanWf7XU22Ug==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -4020,16 +4026,16 @@ } }, "node_modules/eslint-plugin-vuetify": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vuetify/-/eslint-plugin-vuetify-2.4.0.tgz", - "integrity": "sha512-WAZjnGXPrxqHBzYjxxUT8jf30O69Hitmj+wYhTIEG/XgqfvnPwqVtqrU2FGLsDtfFskKva0vuZemfbiq8yA/fQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vuetify/-/eslint-plugin-vuetify-2.5.1.tgz", + "integrity": "sha512-iTyPkTC7wOP2nlBPXevqbeTIDHQ+btt+Tt8abowMEiDZcFSdUjBCcggJgMF1pLcpWwFpbfOcnqFLf73g5WM2qA==", "dev": true, "dependencies": { "eslint-plugin-vue": "^9.6.0", "requireindex": "^1.2.0" }, "peerDependencies": { - "eslint": "^8.0.0", + "eslint": "^8.0.0 || ^9.0.0", "vuetify": "^3.0.0" } }, @@ -4563,8 +4569,7 @@ "node_modules/immutable": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.2.tgz", - "integrity": "sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==", - "dev": true + "integrity": "sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==" }, "node_modules/import-fresh": { "version": "3.3.0", @@ -5515,15 +5520,16 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -5779,9 +5785,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5806,12 +5812,12 @@ } }, "node_modules/playwright": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", - "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", "dev": true, "dependencies": { - "playwright-core": "1.48.2" + "playwright-core": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -5824,9 +5830,9 @@ } }, "node_modules/playwright-core": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", - "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -5850,9 +5856,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -5869,7 +5875,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -5967,13 +5973,13 @@ } }, "node_modules/prosemirror-commands": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz", - "integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz", + "integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" + "prosemirror-transform": "^1.10.2" } }, "node_modules/prosemirror-dropcursor": { @@ -6027,10 +6033,11 @@ } }, "node_modules/prosemirror-markdown": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz", - "integrity": "sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz", + "integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==", "dependencies": { + "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.20.0" } @@ -6047,9 +6054,9 @@ } }, "node_modules/prosemirror-model": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz", - "integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.23.0.tgz", + "integrity": "sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==", "dependencies": { "orderedmap": "^2.0.0" } @@ -6083,9 +6090,9 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.4.0.tgz", - "integrity": "sha512-fxryZZkQG12fSCNuZDrYx6Xvo2rLYZTbKLRd8rglOPgNJGMKIS8uvTt6gGC38m7UCu/ENnXIP9pEz5uDaPc+cA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.1.tgz", + "integrity": "sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==", "dependencies": { "prosemirror-keymap": "^1.1.2", "prosemirror-model": "^1.8.1", @@ -6109,17 +6116,17 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz", - "integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz", + "integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==", "dependencies": { "prosemirror-model": "^1.21.0" } }, "node_modules/prosemirror-view": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.3.tgz", - "integrity": "sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA==", + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.0.tgz", + "integrity": "sha512-z2nkKI1sJzyi7T47Ji/ewBPuIma1RNvQCCYVdV+MqWBV7o4Sa1n94UJCJJ1aQRF/xRkFfyqLGlGFWitIcCOtbg==", "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -6384,15 +6391,28 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.80.7", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.7.tgz", - "integrity": "sha512-MVWvN0u5meytrSjsU7AWsbhoXi1sc58zADXFllfZzbsBT1GHjjar6JwBINYPRrkx/zqnQ6uqbQuHgE95O+C+eQ==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.0.tgz", + "integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==", "dev": true, "dependencies": { "chokidar": "^4.0.0", @@ -6409,6 +6429,363 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/sass-embedded": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.0.tgz", + "integrity": "sha512-/8cYZeL39evUqe0o//193na51Q1VWZ61qhxioQvLJwOtWIrX+PgNhCyD8RSuTtmzc4+6+waFZf899bfp/MCUwA==", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-android-arm": "1.83.0", + "sass-embedded-android-arm64": "1.83.0", + "sass-embedded-android-ia32": "1.83.0", + "sass-embedded-android-riscv64": "1.83.0", + "sass-embedded-android-x64": "1.83.0", + "sass-embedded-darwin-arm64": "1.83.0", + "sass-embedded-darwin-x64": "1.83.0", + "sass-embedded-linux-arm": "1.83.0", + "sass-embedded-linux-arm64": "1.83.0", + "sass-embedded-linux-ia32": "1.83.0", + "sass-embedded-linux-musl-arm": "1.83.0", + "sass-embedded-linux-musl-arm64": "1.83.0", + "sass-embedded-linux-musl-ia32": "1.83.0", + "sass-embedded-linux-musl-riscv64": "1.83.0", + "sass-embedded-linux-musl-x64": "1.83.0", + "sass-embedded-linux-riscv64": "1.83.0", + "sass-embedded-linux-x64": "1.83.0", + "sass-embedded-win32-arm64": "1.83.0", + "sass-embedded-win32-ia32": "1.83.0", + "sass-embedded-win32-x64": "1.83.0" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.83.0.tgz", + "integrity": "sha512-uwFSXzJlfbd4Px189xE5l+cxN8+TQpXdQgJec7TIrb4HEY7imabtpYufpVdqUVwT1/uiis5V4+qIEC4Vl5XObQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.83.0.tgz", + "integrity": "sha512-GBiCvM4a2rkWBLdYDxI6XYnprfk5U5c81g69RC2X6kqPuzxzx8qTArQ9M6keFK4+iDQ5N9QTwFCr0KbZTn+ZNQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-ia32": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.83.0.tgz", + "integrity": "sha512-5ATPdGo2SICqAhiJl/Z8KQ23zH4sGgobGgux0TnrNtt83uHZ+r+To/ubVJ7xTkZxed+KJZnIpolGD8dQyQqoTg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.83.0.tgz", + "integrity": "sha512-aveknUOB8GZewOzVn2Uwk+DKcncTR50Q6vtzslNMGbYnxtgQNHzy8A1qVEviNUruex+pHofppeMK4iMPFAbiEQ==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.83.0.tgz", + "integrity": "sha512-WqIay/72ncyf9Ph4vS742J3a73wZihWmzFUwpn1OD6lme1Aj4eWzWIve5IVnlTEJgcZcDHu6ECID9IZgehJKoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.83.0.tgz", + "integrity": "sha512-XQl9QqgxFFIPm/CzHhmppse5o9ocxrbaAdC2/DAnlAqvYWBBtgFqPjGoYlej13h9SzfvNoogx+y9r+Ap+e+hYg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.83.0.tgz", + "integrity": "sha512-ERQ7Tvp1kFOW3ux4VDFIxb7tkYXHYc+zJpcrbs0hzcIO5ilIRU2tIOK1OrNwrFO6Qxyf7AUuBwYKLAtIU/Nz7g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.83.0.tgz", + "integrity": "sha512-baG9RYBJxUFmqwDNC9h9ZFElgJoyO3jgHGjzEZ1wHhIS9anpG+zZQvO8bHx3dBpKEImX+DBeLX+CxsFR9n81gQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.83.0.tgz", + "integrity": "sha512-syEAVTJt4qhaMLxrSwOWa46zdqHJdnqJkLUK+t9aCr8xqBZLPxSUeIGji76uOehQZ1C+KGFj6n9xstHN6wzOJw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-ia32": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.83.0.tgz", + "integrity": "sha512-RRBxQxMpoxu5+XcSSc6QR/o9asEwUzR8AbCS83RaXcdTIHTa/CccQsiAoDDoPlRsMTLqnzs0LKL4CfOsf7zBbA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.83.0.tgz", + "integrity": "sha512-Yc7u2TelCfBab+PRob9/MNJFh3EooMiz4urvhejXkihTiKSHGCv5YqDdtWzvyb9tY2Jb7YtYREVuHwfdVn3dTQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.83.0.tgz", + "integrity": "sha512-Y7juhPHClUO2H5O+u+StRy6SEAcwZ+hTEk5WJdEmo1Bb1gDtfHvJaWB/iFZJ2tW0W1e865AZeUrC4OcOFjyAQA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-ia32": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.83.0.tgz", + "integrity": "sha512-arQeYwGmwXV8byx5G1PtSzZWW1jbkfR5qrIHMEbTFSAvAxpqjgSvCvrHMOFd73FcMxVaYh4BX9LQNbKinkbEdg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.83.0.tgz", + "integrity": "sha512-E6uzlIWz59rut+Z3XR6mLG915zNzv07ISvj3GUNZENdHM7dF8GQ//ANoIpl5PljMQKp89GnYdvo6kj2gnaBf/g==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.83.0.tgz", + "integrity": "sha512-eAMK6tyGqvqr21r9g8BnR3fQc1rYFj85RGduSQ3xkITZ6jOAnOhuU94N5fwRS852Hpws0lXhET+7JHXgg3U18w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.83.0.tgz", + "integrity": "sha512-Ojpi78pTv02sy2fUYirRGXHLY3fPnV/bvwuC2i5LwPQw2LpCcFyFTtN0c5h4LJDk9P6wr+/ZB/JXU8tHIOlK+Q==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.83.0.tgz", + "integrity": "sha512-3iLjlXdoPfgZRtX4odhRvka1BQs5mAXqfCtDIQBgh/o0JnGPzJIWWl9bYLpHxK8qb+uyVBxXYgXpI0sCzArBOw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.83.0.tgz", + "integrity": "sha512-iOHw/8/t2dlTW3lOFwG5eUbiwhEyGWawivlKWJ8lkXH7fjMpVx2VO9zCFAm8RvY9xOHJ9sf1L7g5bx3EnNP9BQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-ia32": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.83.0.tgz", + "integrity": "sha512-2PxNXJ8Pad4geVcTXY4rkyTr5AwbF8nfrCTDv0ulbTvPhzX2mMKEGcBZUXWn5BeHZTBc6whNMfS7d5fQXR9dDQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.83.0.tgz", + "integrity": "sha512-muBXkFngM6eLTNqOV0FQi7Dv9s+YRQ42Yem26mosdan/GmJQc81deto6uDTgrYn+bzFNmiXcOdfm+0MkTWK3OQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/sass/node_modules/chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -6501,9 +6878,9 @@ } }, "node_modules/sortablejs": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.3.tgz", - "integrity": "sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==" }, "node_modules/source-map": { "version": "0.6.1", @@ -6766,6 +7143,25 @@ "vue": ">=3.2.26 < 4" } }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", + "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7014,6 +7410,11 @@ "node": ">=10" } }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==" + }, "node_modules/vite": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", @@ -7620,15 +8021,15 @@ } }, "node_modules/vue": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", - "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "dependencies": { - "@vue/compiler-dom": "3.5.12", - "@vue/compiler-sfc": "3.5.12", - "@vue/runtime-dom": "3.5.12", - "@vue/server-renderer": "3.5.12", - "@vue/shared": "3.5.12" + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" @@ -7670,9 +8071,9 @@ } }, "node_modules/vue-router": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz", - "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", + "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==", "dependencies": { "@vue/devtools-api": "^6.6.4" }, @@ -7754,9 +8155,9 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/vuetify": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.3.tgz", - "integrity": "sha512-bpuvBpZl1/+nLlXDgdVXekvMNR6W/ciaoa8CYlpeAzAARbY8zUFSoBq05JlLhkIHI58AnzKVy4c09d0OtfYAPg==", + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.5.tgz", + "integrity": "sha512-5aiSz8WJyGzYe3yfgDbzxsFATwHvKtdvFAaUJEDTx7xRv55s3YiOho/MFhs5iTbmh2VT4ToRgP0imBUP660UOw==", "engines": { "node": "^12.20 || >=14.13" }, @@ -7782,26 +8183,6 @@ } } }, - "node_modules/vuetify3-resize-drawer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vuetify3-resize-drawer/-/vuetify3-resize-drawer-2.1.1.tgz", - "integrity": "sha512-CvYce3NAjiALTcK9JClrXeygXY3rZiE3kaNvvVD/JJgHSA6i9S2yxkgr6ryNJLnmtCB5DGtmHY5sHxbF5y534Q==", - "deprecated": "The Vuetify 3 Resize Drawer component has been changed and moved to the WebDevNerdStuff org @wdns. Please update your packages to it's new location. https://www.npmjs.com/package/@wdns/vuetify-resize-drawer", - "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/webdevnerdstuff" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/WebDevNerdStuff" - } - ], - "dependencies": { - "vue": "^3.3.4", - "vuetify": "^3.3.19" - } - }, "node_modules/vuex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", diff --git a/src/dispatch/static/dispatch/package.json b/src/dispatch/static/dispatch/package.json index d2757306b455..9dd2885a78c6 100644 --- a/src/dispatch/static/dispatch/package.json +++ b/src/dispatch/static/dispatch/package.json @@ -51,6 +51,7 @@ "@vue-flow/minimap": "^1.2.0", "@vueuse/core": "^10.5.0", "@vueuse/integrations": "^10.6.1", + "@wdns/vuetify-resize-drawer": "^3.2.0", "apexcharts": "^3.44.0", "axios": "^0.21.4", "d3-force": "^3.0.0", @@ -68,6 +69,7 @@ "monaco-editor": "0.43.0", "register-service-worker": "^1.7.2", "roboto-fontface": "^0.10.0", + "sass-embedded": "^1.81.0", "sortablejs": "^1.15.0", "swrv": "^1.0.4", "vue": "^3.4.12", @@ -75,7 +77,6 @@ "vue3-apexcharts": "^1.4.4", "vue3-markdown-it": "^1.0.10", "vuetify": "^3.4.3", - "vuetify3-resize-drawer": "^2.1.1", "vuex": "^4.1.0", "vuex-map-fields": "^1.4.1" }, diff --git a/src/dispatch/static/dispatch/src/auth/Mfa.vue b/src/dispatch/static/dispatch/src/auth/Mfa.vue index 50f83ca7ec87..f77461f885b1 100644 --- a/src/dispatch/static/dispatch/src/auth/Mfa.vue +++ b/src/dispatch/static/dispatch/src/auth/Mfa.vue @@ -15,7 +15,7 @@ color="primary" size="64" class="mb-4" - > + /> -