@cryptotaxi247 / CoPilot / commits / 4581dc31

Logs (#109)

* alert creation logging * added logs api * added logs page * alert creation settings model and routes * update order * post route to add new event config * Add endpoint to get all alert event configs for a customer * updated profile settings * added logs filters * added Purge Logs dialog * updated logs list * import get customer alert configs * fix send_to_shuffle * alert creation multi exclusion logic * updated log item layout * delete route for multi rule match * updated dependencies * improved notification engine * updated log item style * improved axios signals check * updated soc user alerts component * improved soc users list component * get users route * wazuh_provisioned * get customer provision metadata * updated logs page * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Dec 22, 2023 at 11:48 UTC 4581dc318234a084b6482261bb021c38898942dd
38 files changed +1705 -443
.gitignore
+1
@@ -1,5 +1,6 @@
1 # Logs
2 logs
3 +!src/components/logs
4 *.log
5 npm-debug.log*
6 yarn-debug.log*
backend/app/auth/routes/auth.py
+10
@@ -1,4 +1,5 @@
1 from datetime import timedelta
2 +from typing import List
3
4 from fastapi import APIRouter
5 from fastapi import Depends
@@ -17,6 +18,7 @@ from app.auth.models.users import UserLogin
18 from app.auth.schema.auth import Token
19 from app.auth.schema.auth import UserLoginResponse
20 from app.auth.schema.auth import UserResponse
21 +from app.auth.schema.user import UserBaseResponse
22 from app.auth.services.universal import find_user
23 from app.auth.services.universal import select_all_users
24 from app.auth.utils import AuthHandler
@@ -78,6 +80,14 @@ async def login(user: UserLogin):
80 return {"token": token, "success": True, "message": "Login successful"}
81
82
83 +# Get all users
84 +@auth_router.get("/users", response_model=UserBaseResponse, description="Get all users")
85 +async def get_users(session: AsyncSession = Depends(get_session)):
86 + # users = select_all_users()
87 + users = await select_all_users()
88 + return UserBaseResponse(users=users, message="Users retrieved successfully", success=True)
89 +
90 +
91 # @user_router.get("/users/me", description="Get current user")
92 # def get_current_user(user: User = Depends(auth_handler.get_current_user)):
93 # return user
backend/app/auth/schema/user.py new
+14
@@ -0,0 +1,14 @@
1 +from typing import List
2 +
3 +from pydantic import BaseModel
4 +
5 +
6 +class UserBase(BaseModel):
7 + id: int
8 + username: str
9 +
10 +
11 +class UserBaseResponse(BaseModel):
12 + users: List[UserBase]
13 + message: str
14 + success: bool
backend/app/customer_provisioning/routes/provision.py
+22
@@ -11,6 +11,7 @@ from app.auth.utils import AuthHandler
11 from app.connectors.grafana.schema.dashboards import Office365Dashboard
12 from app.connectors.grafana.schema.dashboards import WazuhDashboard
13 from app.customer_provisioning.schema.provision import CustomerProvisionResponse
14 +from app.customer_provisioning.schema.provision import CustomersMetaResponse
15 from app.customer_provisioning.schema.provision import CustomerSubsctipion
16 from app.customer_provisioning.schema.provision import GetDashboardsResponse
17 from app.customer_provisioning.schema.provision import GetSubscriptionsResponse
@@ -18,6 +19,7 @@ from app.customer_provisioning.schema.provision import ProvisionNewCustomer
19 from app.customer_provisioning.services.provision import provision_wazuh_customer
20 from app.db.db_session import get_session
21 from app.db.universal_models import Customers
22 +from app.db.universal_models import CustomersMeta
23
24 customer_provisioning_router = APIRouter()
25
@@ -49,6 +51,26 @@ async def check_customer_exists(customer_name: str, session: AsyncSession = Depe
51 return customer
52
53
54 +# Get the customermeta based on the customer name
55 +@customer_provisioning_router.get(
56 + "/provision/{customer_name}",
57 + response_model=CustomersMetaResponse,
58 + description="Get Customer Meta",
59 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
60 +)
61 +async def get_customer_meta(customer_name: str, session: AsyncSession = Depends(get_session)):
62 + logger.info(f"Getting customer meta for customer {customer_name}")
63 + result = await session.execute(select(CustomersMeta).filter(CustomersMeta.customer_name == customer_name))
64 + customer_meta = result.scalars().first()
65 +
66 + if not customer_meta:
67 + raise HTTPException(
68 + status_code=404, detail=f"Customer meta not found for customer: {customer_name}. Please provision the customer first.",
69 + )
70 +
71 + return CustomersMetaResponse(message="Customer meta retrieved successfully", success=True, customer_meta=customer_meta)
72 +
73 +
74 @customer_provisioning_router.post(
75 "/provision",
76 response_model=CustomerProvisionResponse,
backend/app/customer_provisioning/schema/provision.py
+6
@@ -83,3 +83,9 @@ class GetSubscriptionsResponse(BaseModel):
83 available_subscriptions: List[str] = Field(..., description="List of subscriptions available for provisioning")
84 message: str = Field(..., description="Message indicating the status of the request")
85 success: bool = Field(..., description="Whether the request was successful or not")
86 +
87 +
88 +class CustomersMetaResponse(BaseModel):
89 + message: str = Field(..., description="Message indicating the status of the request")
90 + success: bool = Field(..., description="Whether the request was successful or not")
91 + customer_meta: CustomersMeta = Field(..., description="Customer meta data for the newly provisioned customer")
backend/app/customer_provisioning/services/provision.py
+4 -1
@@ -23,7 +23,9 @@ from app.customer_provisioning.services.graylog import get_pipeline_id
23 from app.customer_provisioning.services.wazuh_manager import apply_group_configurations
24 from app.customer_provisioning.services.wazuh_manager import create_wazuh_groups
25 from app.db.universal_models import CustomersMeta
26 -from app.integrations.alert_creation.models.alert_settings import AlertCreationSettings
26 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
27 + AlertCreationSettings,
28 +)
29 from app.utils import get_connector_attribute
30
31
@@ -103,6 +105,7 @@ async def provision_wazuh_customer(request: ProvisionNewCustomer, session: Async
105 message=f"Customer {request.customer_name} provisioned successfully",
106 success=True,
107 customer_meta=customer_meta.dict(),
108 + wazuh_worker_provisioned=True,
109 )
110
111
backend/app/db/all_models.py
+3 -1
@@ -7,5 +7,7 @@ from app.db.universal_models import Agents
7 from app.db.universal_models import Customers
8 from app.db.universal_models import CustomersMeta
9 from app.db.universal_models import LogEntry
10 -from app.integrations.alert_creation.models.alert_settings import AlertCreationSettings
10 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
11 + AlertCreationSettings,
12 +)
13 from app.schedulers.models.scheduler import JobMetadata
backend/app/integrations/alert_creation/general/routes/alert.py
+3 -1
@@ -9,7 +9,9 @@ from app.db.db_session import get_session
9 from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
10 from app.integrations.alert_creation.general.schema.alert import CreateAlertResponse
11 from app.integrations.alert_creation.general.services.alert import create_alert
12 -from app.integrations.alert_creation.models.alert_settings import AlertCreationSettings
12 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
13 + AlertCreationSettings,
14 +)
15
16 general_alerts_router = APIRouter()
17
backend/app/integrations/alert_creation/general/services/alert.py
+19 -15
@@ -16,6 +16,9 @@ from app.integrations.alert_creation.general.schema.alert import IrisAlertPayloa
16 from app.integrations.alert_creation.general.schema.alert import IrisAsset
17 from app.integrations.alert_creation.general.schema.alert import IrisIoc
18 from app.integrations.alert_creation.general.schema.alert import ValidIocFields
19 +from app.integrations.alert_creation.general.services.alert_multi_exclude import (
20 + AlertDetailsService,
21 +)
22 from app.integrations.utils.alerts import get_asset_type_id
23 from app.integrations.utils.alerts import send_to_shuffle
24 from app.integrations.utils.alerts import validate_ioc_type
@@ -174,20 +177,19 @@ async def build_alert_payload(
177
178
179 async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> CreateAlertResponse:
177 - logger.info(f"Creating alert {alert.id} in IRIS.")
178 - # ! TODO: ALERT MULTI EXCLUSION ! #
179 - # event_exclude_result = AlertDetailsService().collect_alert_timeline_process_id(
180 - # agent_name=alert.agent_name,
181 - # process_id=getattr(alert, "process_id", "n/a"),
182 - # index=alert.index,
183 - # )
184 - # logger.info(f"Event exclude result: {event_exclude_result}")
185 - # if event_exclude_result is not None:
186 - # if event_exclude_result["excluded"]:
187 - # raise HTTPException(
188 - # status_code=400,
189 - # detail="Alert excluded due to multi exclusion as set in the config.ini file.",
190 - # )
180 + logger.info(f"Creating alert with {alert.id} in IRIS.")
181 + alert_detail_service = await AlertDetailsService.create()
182 + event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
183 + agent_name=alert.agent_name,
184 + process_id=getattr(alert, "process_id", "n/a"),
185 + index=alert.index,
186 + session=session,
187 + )
188 + if event_exclude_result is True:
189 + raise HTTPException(
190 + status_code=400,
191 + detail="Alert excluded due to multi exclusion as set in the config.ini file.",
192 + )
193 logger.info(f"Getting agent data for {alert.agent_name}")
194 agent_details = await get_agent(agent_id=alert.agent_id, db=session)
195 ioc_payload = await build_ioc_payload(alert_details=alert)
@@ -205,15 +207,17 @@ async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> Crea
207 )
208 alert_id = result["data"]["alert_id"]
209 logger.info(f"Successfully created alert {alert_id} in IRIS.")
210 + customer_name = (await get_customer_alert_settings(customer_code=alert.agent_labels_customer, session=session)).customer_name
211 await send_to_shuffle(
212 ShufflePayload(
213 alert_id=alert_id,
211 - customer=(await get_customer_alert_settings(customer_code=alert.agent_labels_customer, session=session)).customer_name,
214 + customer=customer_name,
215 customer_code=alert.agent_labels_customer,
216 alert_source_link=await construct_alert_source_link(alert, session=session),
217 rule_description=alert.rule_description,
218 hostname=alert.agent_name,
219 ),
220 + session=session,
221 )
222 return CreateAlertResponse(
223 alert_id=alert_id,
backend/app/integrations/alert_creation/general/services/alert_multi_exclude.py
+41 -19
@@ -5,8 +5,14 @@ from typing import Tuple
5
6 from elasticsearch7 import NotFoundError
7 from loguru import logger
8 +from sqlalchemy.ext.asyncio import AsyncSession
9
10 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
11 +from app.db.db_session import get_session
12 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
13 + AlertCreationEventConfig,
14 +)
15 +from app.utils import get_customer_alert_event_configs
16
17
18 class AlertDetailsService:
@@ -19,12 +25,18 @@ class AlertDetailsService:
25 """
26
27 def __init__(self):
28 + self.es = None
29 +
30 + @classmethod
31 + async def create(cls):
32 """
23 - Initialize the AlertDetailsService class.
33 + Asynchronously create an instance of AlertDetailsService.
34
35 Establish a session with the Wazuh indexer and a ConfigManager instance for reading the configuration file.
36 """
27 - self.es = create_wazuh_indexer_client("Wazuh-Indexer")
37 + self = cls()
38 + self.es = await create_wazuh_indexer_client("Wazuh-Indexer")
39 + return self
40
41 def _collect_indices(self) -> Dict[str, object]:
42 """
@@ -146,7 +158,7 @@ class AlertDetailsService:
158 "query": {"bool": {"must": must_terms}},
159 }
160
149 - def process_events(self, events: list, order_key: str):
161 + async def process_events(self, events: list, event_configs: List[AlertCreationEventConfig]):
162 """
163 Process the events and check for exclusions.
164 For every event in the `config.ini` file there is a field and value to check for.
@@ -155,23 +167,25 @@ class AlertDetailsService:
167
168 Args:
169 events (list): A list of events to process.
158 - order_key (str): The key in the configuration file that specifies the order of event processing.
170 + event_configs (List[AlertCreationEventConfig]): A list of AlertCreationEventConfig instances.
171
172 Returns:
173 dict: A dictionary with a single key 'excluded' indicating whether the events match the exclusion criteria.
174 """
163 - event_order = self.config_manager.get("Order", order_key).split(",")
164 - # strip the event order of whitespace
165 - event_order = [event.strip() for event in event_order]
175 + event_order = [config.event_id for config in event_configs]
176 + logger.info(f"Events: {events}")
177
178 first_match_found = False
179
180 for index, event in enumerate(events):
181 event_id = event_order[0 if not first_match_found else 1]
182 logger.info(f"Checking for event_id: {event_id}")
172 - event_config = self.config_manager.get_section(event_id)
173 - field = event_config["field"]
174 - value = event_config["value"]
183 + event_config = next((config for config in event_configs if config.event_id == event_id), None)
184 + if event_config is None:
185 + continue
186 +
187 + field = event_config.field
188 + value = event_config.value
189 logger.info(f"Checking for {field} containing {value}")
190
191 if value == event.get(field, ""):
@@ -180,16 +194,17 @@ class AlertDetailsService:
194 first_match_found = True # We found the first match
195 elif first_match_found:
196 logger.info("Both matches found.")
183 - return {"excluded": True} # Both matches found, so return early
197 + return True # Both matches found, so return early
198
199 # If we've checked all events and didn't find both matches, return {"excluded": False}
186 - return {"excluded": False}
200 + return False
201
188 - def collect_alert_timeline_process_id(
202 + async def collect_alert_timeline_process_id(
203 self,
204 agent_name: str,
205 process_id: str,
206 index: str,
207 + session: AsyncSession,
208 ) -> Dict[str, Any]:
209 """
210 Collect the events where the process id and agent name match within a 24 hour window.
@@ -211,6 +226,7 @@ class AlertDetailsService:
226 query = self.build_query(
227 {"agent_name": agent_name, "process_id": process_id},
228 )
229 + logger.info(f"Query: {query}")
230 alert_timeline_events = self.es.search(index=index, body=query)
231
232 total_hits = alert_timeline_events["hits"]["total"]["value"]
@@ -221,18 +237,24 @@ class AlertDetailsService:
237 events.sort(key=lambda x: x["timestamp_utc"])
238
239 # return self.process_events(events)
240 + logger.info(f"Events: {events}")
241
242 # Get all order keys from the 'Order' section in config.ini
226 - order_keys = self.config_manager.options("Order")
243 + order_keys = await get_customer_alert_event_configs(customer_code=events[0]["agent_labels_customer"], session=session)
244 + logger.info(f"Order keys: {order_keys}")
245
246 # Process events for each order key
247 results = {}
248 for order_key in order_keys:
231 - results[order_key] = self.process_events(events, order_key)
232 - if results[order_key]["excluded"] is True:
233 - return {"excluded": True}
234 -
235 - return {"excluded": False}
249 + logger.info(f"Processing events for order key: {order_key}")
250 + # results[order_key] = await self.process_events(events, order_key)
251 + results = await self.process_events(events, order_key)
252 + logger.info(f"Results: {results}")
253 + # if results[order_key]["excluded"] is True:
254 + if results is True:
255 + return True
256 +
257 + return False
258 except Exception as e:
259 logger.error(f"Error collecting alert timeline events: {e}")
260 return None
backend/app/integrations/alert_creation_settings/models/alert_creation_settings.py renamed
+29 -10
@@ -6,16 +6,27 @@ from sqlmodel import Relationship
6 from sqlmodel import SQLModel
7
8
9 -class AlertCreationEventConfig(SQLModel, table=True):
10 - id: Optional[int] = Field(default=None, primary_key=True)
11 - alert_creation_settings_id: Optional[int] = Field(default=None, foreign_key="alertcreationsettings.id")
12 - event_id: str = Field(max_length=255)
13 - field: str = Field(max_length=1024)
14 - value: str = Field(max_length=1024)
15 - alert_creation_settings: "AlertCreationSettings" = Relationship(back_populates="event_configs")
9 +class Condition(SQLModel, table=True):
10 + __tablename__ = "custom_alert_creation_condition"
11 + id: int = Field(default=None, primary_key=True)
12 + event_order_id: int = Field(default=None, foreign_key="custom_alert_creation_event_order.id")
13 + field_name: str = Field(max_length=1024)
14 + field_value: str = Field(max_length=1024)
15 + event_order: "EventOrder" = Relationship(back_populates="conditions")
16 +
17 +
18 +class EventOrder(SQLModel, table=True):
19 + __tablename__ = "custom_alert_creation_event_order"
20 + id: int = Field(default=None, primary_key=True)
21 + alert_creation_settings_id: int = Field(default=None, foreign_key="custom_alert_creation_settings.id")
22 + order_label: str = Field(max_length=255)
23 + conditions: List["Condition"] = Relationship(back_populates="event_order")
24 + alert_creation_settings: "AlertCreationSettings" = Relationship(back_populates="event_orders")
25 + event_configs: List["AlertCreationEventConfig"] = Relationship(back_populates="event_order")
26
27
28 class AlertCreationSettings(SQLModel, table=True):
29 + __tablename__ = "custom_alert_creation_settings"
30 id: Optional[int] = Field(primary_key=True)
31 customer_code: str = Field(max_length=11, nullable=False)
32 customer_name: str = Field(max_length=50, nullable=False)
@@ -32,6 +43,14 @@ class AlertCreationSettings(SQLModel, table=True):
43 custom_message: Optional[str] = Field(max_length=1024)
44 shuffle_endpoint: Optional[str] = Field(max_length=1024)
45 nvd_url: Optional[str] = Field(default="https://services.nvd.nist.gov/rest/json/cves/2.0?cveId", max_length=1024)
35 - event_order: Optional[str] = Field(max_length=1024)
36 - event_order2: Optional[str] = Field(max_length=1024)
37 - event_configs: List[AlertCreationEventConfig] = Relationship(back_populates="alert_creation_settings")
46 + event_orders: List[EventOrder] = Relationship(back_populates="alert_creation_settings")
47 +
48 +
49 +class AlertCreationEventConfig(SQLModel, table=True):
50 + __tablename__ = "custom_alert_creation_event_config"
51 + id: Optional[int] = Field(default=None, primary_key=True)
52 + event_order_id: Optional[int] = Field(default=None, foreign_key="custom_alert_creation_event_order.id")
53 + event_id: str = Field(max_length=255)
54 + field: str = Field(max_length=1024)
55 + value: str = Field(max_length=1024)
56 + event_order: "EventOrder" = Relationship(back_populates="event_configs")
backend/app/integrations/alert_creation_settings/routes/alert_creation_settings.py new
+231
@@ -0,0 +1,231 @@
1 +from typing import List
2 +
3 +from fastapi import APIRouter
4 +from fastapi import Depends
5 +from fastapi import HTTPException
6 +from loguru import logger
7 +from sqlalchemy import delete
8 +from sqlalchemy.ext.asyncio import AsyncSession
9 +from sqlalchemy.future import select
10 +from sqlalchemy.orm import joinedload
11 +
12 +from app.db.db_session import get_session
13 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
14 + AlertCreationEventConfig,
15 +)
16 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
17 + AlertCreationSettings,
18 +)
19 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
20 + EventOrder,
21 +)
22 +from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
23 + AlertCreationEventConfigResponse,
24 +)
25 +from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
26 + AlertCreationSettingsCreate,
27 +)
28 +from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
29 + AlertCreationSettingsResponse,
30 +)
31 +from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
32 + EventOrderCreate,
33 +)
34 +from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
35 + EventOrderResponse,
36 +)
37 +from app.utils import get_customer_alert_event_configs
38 +
39 +alert_creation_settings_router = APIRouter()
40 +
41 +
42 +@alert_creation_settings_router.get(
43 + "/{customer_code}/event_configs",
44 + response_model=List[List[AlertCreationEventConfigResponse]],
45 + description="Get all alert event configs for a customer.",
46 +)
47 +async def get_customer_event_configs(
48 + customer_code: str,
49 + session: AsyncSession = Depends(get_session),
50 +):
51 + event_configs = await get_customer_alert_event_configs(customer_code, session)
52 +
53 + if not event_configs:
54 + raise HTTPException(status_code=404, detail="No event configs found for this customer.")
55 +
56 + return event_configs
57 +
58 +
59 +@alert_creation_settings_router.post(
60 + "/create",
61 + response_model=AlertCreationSettings,
62 + description="Create a new alert creation setting.",
63 +)
64 +async def create_alert_creation_settings(
65 + alert_creation_settings: AlertCreationSettingsCreate,
66 + session: AsyncSession = Depends(get_session),
67 +):
68 + logger.info(f"alert_creation_settings: {alert_creation_settings.dict()}")
69 +
70 + result = await session.execute(
71 + select(AlertCreationSettings).where(AlertCreationSettings.customer_code == alert_creation_settings.customer_code),
72 + )
73 + settings = result.scalars().first()
74 +
75 + if settings:
76 + logger.info(f"Alert creation settings already exist for customer_code: {alert_creation_settings.customer_code}")
77 + raise HTTPException(status_code=200, detail="Alert creation settings already exist.")
78 +
79 + alert_creation_settings_db = AlertCreationSettings(**alert_creation_settings.dict(exclude={"event_orders"}))
80 +
81 + if alert_creation_settings.event_orders is not None:
82 + for event_order in alert_creation_settings.event_orders:
83 + event_order_db = EventOrder(order_label=event_order.order_label, alert_creation_settings=alert_creation_settings_db)
84 + session.add(event_order_db)
85 + for event_config in event_order.event_configs:
86 + event_config_db = AlertCreationEventConfig(**event_config.dict(), event_order=event_order_db)
87 + session.add(event_config_db)
88 +
89 + session.add(alert_creation_settings_db)
90 + await session.commit()
91 + await session.refresh(alert_creation_settings_db)
92 +
93 + return alert_creation_settings_db
94 +
95 +
96 +@alert_creation_settings_router.get(
97 + "/{customer_name}",
98 + response_model=AlertCreationSettingsResponse,
99 + description="Retrieve alert creation settings by customer name.",
100 +)
101 +async def get_alert_creation_settings(
102 + customer_name: str,
103 + session: AsyncSession = Depends(get_session),
104 +):
105 + result = await session.execute(
106 + select(AlertCreationSettings)
107 + .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
108 + .where(AlertCreationSettings.customer_name == customer_name),
109 + )
110 + settings = result.scalars().first()
111 +
112 + if not settings:
113 + raise HTTPException(status_code=404, detail="Alert creation settings not found.")
114 +
115 + return settings
116 +
117 +
118 +@alert_creation_settings_router.post(
119 + "/{customer_name}/event",
120 + response_model=EventOrderResponse,
121 + description="Add a new event to a customer's alert creation settings.",
122 +)
123 +async def add_event_order(
124 + customer_name: str,
125 + event_order: EventOrderCreate,
126 + session: AsyncSession = Depends(get_session),
127 +):
128 + result = await session.execute(
129 + select(AlertCreationSettings)
130 + .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
131 + .where(AlertCreationSettings.customer_name == customer_name),
132 + )
133 + settings = result.scalars().first()
134 +
135 + if not settings:
136 + raise HTTPException(status_code=404, detail="Alert creation settings not found.")
137 +
138 + # Create new event order and configs
139 + event_order_db = EventOrder(order_label=event_order.order_label, alert_creation_settings=settings)
140 + session.add(event_order_db)
141 + for event_config in event_order.event_configs:
142 + event_config_db = AlertCreationEventConfig(**event_config.dict(), event_order=event_order_db)
143 + session.add(event_config_db)
144 +
145 + await session.commit()
146 +
147 + # Query the EventOrder instance again to ensure event_configs are loaded
148 + result = await session.execute(
149 + select(EventOrder).options(joinedload(EventOrder.event_configs)).where(EventOrder.id == event_order_db.id),
150 + )
151 + event_order_db = result.scalars().first()
152 +
153 + return event_order_db
154 +
155 +
156 +@alert_creation_settings_router.put(
157 + "/{customer_name}",
158 + response_model=AlertCreationSettingsResponse,
159 + description="Update a customer's event orders.",
160 +)
161 +async def update_event_orders(
162 + customer_name: str,
163 + event_orders: List[EventOrderCreate],
164 + session: AsyncSession = Depends(get_session),
165 +):
166 + result = await session.execute(
167 + select(AlertCreationSettings)
168 + .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
169 + .where(AlertCreationSettings.customer_name == customer_name),
170 + )
171 + settings = result.scalars().first()
172 +
173 + if not settings:
174 + raise HTTPException(status_code=404, detail="Alert creation settings not found.")
175 +
176 + # Create new event orders and configs or add to existing ones
177 + for event_order in event_orders:
178 + # Check if an EventOrder with the given order_label already exists
179 + existing_order = next((order for order in settings.event_orders if order.order_label == event_order.order_label), None)
180 +
181 + if existing_order:
182 + # If it does, add the new EventConfig instances to it
183 + for event_config in event_order.event_configs:
184 + event_config_db = AlertCreationEventConfig(**event_config.dict(), event_order=existing_order)
185 + session.add(event_config_db)
186 + else:
187 + # If it doesn't, return a 404
188 + raise HTTPException(status_code=404, detail=f"Event order with order_label: {event_order.order_label} not found.")
189 +
190 + await session.commit()
191 + await session.refresh(settings)
192 +
193 + return settings
194 +
195 +
196 +@alert_creation_settings_router.delete(
197 + "/{customer_name}/event/{order_label}",
198 + description="Delete an event order by order_label.",
199 +)
200 +async def delete_event_order(
201 + customer_name: str,
202 + order_label: str,
203 + session: AsyncSession = Depends(get_session),
204 +):
205 + result = await session.execute(
206 + select(AlertCreationSettings)
207 + .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
208 + .where(AlertCreationSettings.customer_name == customer_name),
209 + )
210 + settings = result.scalars().first()
211 +
212 + if not settings:
213 + raise HTTPException(status_code=404, detail="Alert creation settings not found.")
214 +
215 + # Check if an EventOrder with the given order_label exists
216 + existing_order = next((order for order in settings.event_orders if order.order_label == order_label), None)
217 +
218 + if existing_order:
219 + # If it does, delete its AlertCreationEventConfig instances
220 + for config in existing_order.event_configs:
221 + await session.delete(config)
222 +
223 + # Then delete the EventOrder itself
224 + await session.delete(existing_order)
225 + else:
226 + # If it doesn't, return a 404
227 + raise HTTPException(status_code=404, detail=f"Event order with order_label: {order_label} not found.")
228 +
229 + await session.commit()
230 +
231 + return {"message": f"Event order with order_label: {order_label} and related alert creation event configs deleted.", "success": True}
backend/app/integrations/alert_creation_settings/schema/alert_creation_settings.py new
+64
@@ -0,0 +1,64 @@
1 +from typing import List
2 +from typing import Optional
3 +
4 +from pydantic import BaseModel
5 +
6 +
7 +class AlertCreationEventConfigCreate(BaseModel):
8 + event_id: str
9 + field: str
10 + value: str
11 +
12 +
13 +class EventOrderCreate(BaseModel):
14 + order_label: str
15 + event_configs: List[AlertCreationEventConfigCreate]
16 +
17 +
18 +class AlertCreationSettingsCreate(BaseModel):
19 + customer_code: str
20 + customer_name: str
21 + excluded_wazuh_rules: Optional[str]
22 + excluded_suricata_rules: Optional[str]
23 + timefield: Optional[str]
24 + office365_organization_id: Optional[str]
25 + iris_customer_id: Optional[int]
26 + iris_customer_name: Optional[str]
27 + iris_index: Optional[str]
28 + grafana_url: Optional[str]
29 + misp_url: Optional[str]
30 + opencti_url: Optional[str]
31 + custom_message: Optional[str]
32 + shuffle_endpoint: Optional[str]
33 + nvd_url: Optional[str] = "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId"
34 + event_orders: Optional[List[EventOrderCreate]] = None
35 +
36 +
37 +class AlertCreationEventConfigResponse(BaseModel):
38 + event_id: str
39 + field: str
40 + value: str
41 +
42 +
43 +class EventOrderResponse(BaseModel):
44 + order_label: str
45 + event_configs: List[AlertCreationEventConfigResponse]
46 +
47 +
48 +class AlertCreationSettingsResponse(BaseModel):
49 + customer_code: str
50 + customer_name: str
51 + excluded_wazuh_rules: Optional[str]
52 + excluded_suricata_rules: Optional[str]
53 + timefield: Optional[str]
54 + office365_organization_id: Optional[str]
55 + iris_customer_id: Optional[int]
56 + iris_customer_name: Optional[str]
57 + iris_index: Optional[str]
58 + grafana_url: Optional[str]
59 + misp_url: Optional[str]
60 + opencti_url: Optional[str]
61 + custom_message: Optional[str]
62 + shuffle_endpoint: Optional[str]
63 + nvd_url: Optional[str] = "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId"
64 + event_orders: Optional[List[EventOrderResponse]] = None
backend/app/integrations/alert_escalation/services/general_alert.py
+3 -1
@@ -13,7 +13,9 @@ from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
14 from app.connectors.utils import get_connector_info_from_db
15 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
16 -from app.integrations.alert_creation.models.alert_settings import AlertCreationSettings
16 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
17 + AlertCreationSettings,
18 +)
19 from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
20 from app.integrations.alert_escalation.schema.general_alert import CreateAlertResponse
21 from app.integrations.alert_escalation.schema.general_alert import GenericAlertModel
backend/app/integrations/utils/alerts.py
+4 -6
@@ -14,6 +14,7 @@ import regex
14 import requests
15 from fastapi import HTTPException
16 from loguru import logger
17 +from sqlalchemy.ext.asyncio import AsyncSession
18
19 from app.integrations.utils.schema import ShufflePayload
20 from app.integrations.utils.schema import WazuhAgentResponse
@@ -331,19 +332,16 @@ async def validate_ioc_type(ioc_value: str) -> str:
332 return ioc_type
333
334
334 -############## ! SEND TO OTHER TOOLS ! ##############
335 -async def send_to_shuffle(payload: ShufflePayload) -> bool:
335 +async def send_to_shuffle(payload: ShufflePayload, session: AsyncSession) -> bool:
336 """
337 Sends payload to Shuffle listening Webhook asynchronously using httpx.
338 """
339 logger.info(f"Sending {payload} to Shuffle Webhook.")
340 try:
341 - shuffle_endpoint = (await get_customer_alert_settings(customer_code=payload.customer_code)).shuffle_endpoint
342 - async with httpx.AsyncClient() as client:
341 + async with httpx.AsyncClient(verify=False) as client:
342 response = await client.post(
344 - shuffle_endpoint,
343 + (await get_customer_alert_settings(customer_code=payload.customer_code, session=session)).shuffle_endpoint,
344 json=payload.to_dict(),
346 - verify=False, # Be cautious with verify=False in production
345 )
346
347 return response.status_code == 200
backend/app/routers/alert_creation_settings.py new
+11
@@ -0,0 +1,11 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.alert_creation_settings.routes.alert_creation_settings import (
4 + alert_creation_settings_router,
5 +)
6 +
7 +# Instantiate the APIRouter
8 +router = APIRouter()
9 +
10 +# Include the Ask SocFortress related routes
11 +router.include_router(alert_creation_settings_router, prefix="/api/v1/alert_settings", tags=["Alert Creation Settings"])
backend/app/utils.py
+25 -1
@@ -20,6 +20,7 @@ from pydantic import Field
20 from pydantic import validator
21 from sqlalchemy.ext.asyncio import AsyncSession
22 from sqlalchemy.future import select
23 +from sqlalchemy.orm import joinedload
24
25 from app.auth.services.universal import find_user
26 from app.auth.utils import AuthHandler
@@ -30,7 +31,15 @@ from app.db.db_session import engine
31 from app.db.db_session import get_db_session
32 from app.db.db_session import get_session
33 from app.db.universal_models import LogEntry
33 -from app.integrations.alert_creation.models.alert_settings import AlertCreationSettings
34 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
35 + AlertCreationEventConfig,
36 +)
37 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
38 + AlertCreationSettings,
39 +)
40 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
41 + EventOrder,
42 +)
43
44
45 ################## ! 422 VALIDATION ERROR TYPES FOR PYDANTIC VALUE ERROR RESPONSE ! ##################
@@ -480,6 +489,21 @@ async def get_customer_alert_settings(customer_code: str, session: AsyncSession)
489 return None
490
491
492 +async def get_customer_alert_event_configs(
493 + customer_code: str, session: AsyncSession = Depends(get_session),
494 +) -> Optional[List[List[AlertCreationEventConfig]]]:
495 + result = await session.execute(
496 + select(AlertCreationSettings)
497 + .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
498 + .where(AlertCreationSettings.customer_code == customer_code),
499 + )
500 + settings = result.scalars().first()
501 +
502 + if settings:
503 + return [order.event_configs for order in settings.event_orders]
504 + return None
505 +
506 +
507 ################## ! Wazuh Worker Provisioning App ! ##################
508 ################## ! https://github.com/socfortress/Customer-Provisioning-Worker ! ##################
509 async def verify_wazuh_worker_provisioning_healtcheck(attributes: Dict[str, Any]) -> Dict[str, Any]:
backend/copilot.py
+2
@@ -19,6 +19,7 @@ from app.middleware.exception_handlers import value_error_handler
19 from app.middleware.logger import log_requests
20 from app.routers import agents
21 from app.routers import alert_creation
22 +from app.routers import alert_creation_settings
23 from app.routers import ask_socfortress
24 from app.routers import auth
25 from app.routers import connectors
@@ -90,6 +91,7 @@ app.include_router(customer_provisioning.router)
91 app.include_router(threat_intel.router)
92 app.include_router(ask_socfortress.router)
93 app.include_router(alert_creation.router)
94 +app.include_router(alert_creation_settings.router)
95
96
97 @app.on_event("startup")
package-lock.json
+350 -348
@@ -10,9 +10,9 @@
10 "dependencies": {
11 "@ajoelp/json-to-formdata": "^1.5.0",
12 "@fawmi/vue-google-maps": "^0.9.79",
13 - "@fontsource/jetbrains-mono": "^5.0.17",
14 - "@fontsource/lexend": "^5.0.17",
15 - "@fontsource/public-sans": "^5.0.15",
13 + "@fontsource/jetbrains-mono": "^5.0.18",
14 + "@fontsource/lexend": "^5.0.18",
15 + "@fontsource/public-sans": "^5.0.16",
16 "@fullcalendar/core": "^6.1.10",
17 "@fullcalendar/daygrid": "^6.1.10",
18 "@fullcalendar/interaction": "^6.1.10",
@@ -43,7 +43,7 @@
43 "@vueup/vue-quill": "^1.2.0",
44 "@vueuse/components": "^10.7.0",
45 "@vueuse/core": "^10.7.0",
46 - "apexcharts": "^3.44.2",
46 + "apexcharts": "^3.45.0",
47 "bytes": "^3.1.2",
48 "chart.js": "^4.4.1",
49 "colord": "^2.9.3",
@@ -56,19 +56,19 @@
56 "lodash": "^4.17.21",
57 "maplibre-gl": "^3.6.2",
58 "mitt": "^3.0.1",
59 - "naive-ui": "^2.35.0",
59 + "naive-ui": "^2.36.0",
60 "password-validator": "^5.3.0",
61 "pinia": "^2.1.7",
62 - "pinia-plugin-persistedstate": "^3.2.0",
62 + "pinia-plugin-persistedstate": "^3.2.1",
63 "quill": "^1.3.7",
64 "secure-ls": "^1.2.6",
65 "shepherd.js": "^11.2.0",
66 "v-calendar": "^3.1.2",
67 "validator": "^13.11.0",
68 - "vue": "^3.3.10",
68 + "vue": "^3.3.13",
69 "vue-advanced-cropper": "^2.8.8",
70 "vue-cal": "^4.8.1",
71 - "vue-chartjs": "^5.2.0",
71 + "vue-chartjs": "^5.3.0",
72 "vue-highlight-words": "^3.0.1",
73 "vue-i18n": "^9.8.0",
74 "vue-maplibre-gl": "^3.0.3",
@@ -84,25 +84,25 @@
84 "@css-render/vue3-ssr": "^0.15.12",
85 "@faker-js/faker": "^8.3.1",
86 "@iconify/vue": "^4.1.1",
87 - "@rushstack/eslint-patch": "^1.6.0",
87 + "@rushstack/eslint-patch": "^1.6.1",
88 "@tsconfig/node18": "^18.2.2",
89 "@types/bytes": "^3.1.4",
90 "@types/fs-extra": "^11.0.4",
91 "@types/inquirer": "^9.0.7",
92 "@types/jsdom": "^21.1.6",
93 "@types/lodash": "^4.14.202",
94 - "@types/node": "^20.10.3",
94 + "@types/node": "^20.10.5",
95 "@types/validator": "^13.11.7",
96 - "@vitejs/plugin-vue": "^4.5.1",
96 + "@vitejs/plugin-vue": "^4.5.2",
97 "@vitejs/plugin-vue-jsx": "^3.1.0",
98 "@vue-leaflet/vue-leaflet": "^0.10.1",
99 "@vue/eslint-config-prettier": "^8.0.0",
100 "@vue/eslint-config-typescript": "^12.0.0",
101 "@vue/test-utils": "^2.4.3",
102 - "@vue/tsconfig": "^0.4.0",
102 + "@vue/tsconfig": "^0.5.1",
103 "autoprefixer": "^10.4.16",
104 "cypress": "^13.6.1",
105 - "eslint": "^8.55.0",
105 + "eslint": "^8.56.0",
106 "eslint-plugin-cypress": "^2.15.1",
107 "eslint-plugin-vue": "^9.19.2",
108 "fs-extra": "^11.2.0",
@@ -112,19 +112,19 @@
112 "npm-run-all": "^4.1.5",
113 "picocolors": "^1.0.0",
114 "postcss": "^8.4.32",
115 - "prettier": "^3.1.0",
115 + "prettier": "^3.1.1",
116 "sass": "^1.69.5",
117 "start-server-and-test": "^2.0.3",
118 "tailwind-config-viewer": "^1.7.3",
119 - "tailwindcss": "^3.3.6",
119 + "tailwindcss": "^3.4.0",
120 "taze": "^0.13.0",
121 - "ts-node": "^10.9.1",
122 - "typescript": "~5.3.2",
121 + "ts-node": "^10.9.2",
122 + "typescript": "~5.3.3",
123 "unplugin-vue-components": "^0.26.0",
124 - "vite": "^5.0.6",
124 + "vite": "^5.0.10",
125 "vite-svg-loader": "^5.1.0",
126 - "vitest": "^1.0.1",
127 - "vue-tsc": "^1.8.25"
126 + "vitest": "^1.1.0",
127 + "vue-tsc": "^1.8.26"
128 },
129 "engines": {
130 "node": ">=16.0.0 <20.5.0"
@@ -666,6 +666,7 @@
666 },
667 "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
668 "version": "1.3.0",
669 + "extraneous": true,
670 "inBundle": true,
671 "license": "MIT",
672 "engines": {
@@ -1203,9 +1204,9 @@
1204 }
1205 },
1206 "node_modules/@eslint/js": {
1206 - "version": "8.55.0",
1207 - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
1208 - "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
1207 + "version": "8.56.0",
1208 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
1209 + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
1210 "dev": true,
1211 "engines": {
1212 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1258,19 +1259,19 @@
1259 "integrity": "sha512-uvnFKtPgzLnpzzTRfhDlvXX0kLYi9lDRQbcDmT8iXl71Rx+uwSuaUIQl3DNC7w5OweAQ7XQMDObML+KaYDQfng=="
1260 },
1261 "node_modules/@fontsource/jetbrains-mono": {
1261 - "version": "5.0.17",
1262 - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.17.tgz",
1263 - "integrity": "sha512-Y/EtdbwKwNQTGpnMrexX8SVW6Jqlh0nX2bNHI9Z9m6FsyjbocZIFNJqwSY9bDUoi7irGtz8nuidAN7FF8wYuJA=="
1262 + "version": "5.0.18",
1263 + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.18.tgz",
1264 + "integrity": "sha512-0+YDAaAnCdXjirHFO3NfHLUW8xtKCT5rlm23Q0qG3TpZp3QBrZh5r9ikUxh3ufHc2+fVnk4Y6GWNEnVIfB7u/g=="
1265 },
1266 "node_modules/@fontsource/lexend": {
1266 - "version": "5.0.17",
1267 - "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.17.tgz",
1268 - "integrity": "sha512-3rtbeiOx4EqGxcOMfsgq23RRDbhdMGJULLdNCHCN6oAGN06WDesrH6ZL+r6ZF8fpdJZ63F0ViOj/PFG2kOtKdA=="
1267 + "version": "5.0.18",
1268 + "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.18.tgz",
1269 + "integrity": "sha512-RcNekPIeQGX5ZvwRtX7UHDoDrGTg8IV2Yae13qjtxW6FO4kFaUKSlITKnrvaK8r8ly/fQ6x2mXva9jmMZPZ4Ug=="
1270 },
1271 "node_modules/@fontsource/public-sans": {
1271 - "version": "5.0.15",
1272 - "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.15.tgz",
1273 - "integrity": "sha512-3UKtCVDbwt8FeurOHYBybDzYYJH0peyisGjsQe2aRFR4M693m0DdE3v4BZl+60OjvnXGWhO8O/rmET2kwPF6SQ=="
1272 + "version": "5.0.16",
1273 + "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.16.tgz",
1274 + "integrity": "sha512-bThZip6sLRsnfzi/oBr1/9+aWmvHkb59QlLh/OtoAIA0Mi2+Z1cKOxtR1B2ITauljlLJHPRvjpTZDUZitUN8pA=="
1275 },
1276 "node_modules/@fullcalendar/core": {
1277 "version": "6.1.10",
@@ -2709,9 +2710,9 @@
2710 ]
2711 },
2712 "node_modules/@rushstack/eslint-patch": {
2712 - "version": "1.6.0",
2713 - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz",
2714 - "integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==",
2713 + "version": "1.6.1",
2714 + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz",
2715 + "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==",
2716 "dev": true
2717 },
2718 "node_modules/@sideway/address": {
@@ -3496,9 +3497,9 @@
3497 "integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ=="
3498 },
3499 "node_modules/@types/node": {
3499 - "version": "20.10.3",
3500 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz",
3501 - "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==",
3500 + "version": "20.10.5",
3501 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
3502 + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
3503 "dev": true,
3504 "dependencies": {
3505 "undici-types": "~5.26.4"
@@ -3896,9 +3897,9 @@
3897 "dev": true
3898 },
3899 "node_modules/@vitejs/plugin-vue": {
3899 - "version": "4.5.1",
3900 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.1.tgz",
3901 - "integrity": "sha512-DaUzYFr+2UGDG7VSSdShKa9sIWYBa1LL8KC0MNOf2H5LjcTPjob0x8LbkqXWmAtbANJCkpiQTj66UVcQkN2s3g==",
3900 + "version": "4.5.2",
3901 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz",
3902 + "integrity": "sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ==",
3903 "dev": true,
3904 "engines": {
3905 "node": "^14.18.0 || >=16.0.0"
@@ -3927,13 +3928,13 @@
3928 }
3929 },
3930 "node_modules/@vitest/expect": {
3930 - "version": "1.0.1",
3931 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.0.1.tgz",
3932 - "integrity": "sha512-3cdrb/eKD/0tygDX75YscuHEHMUJ70u3UoLSq2eqhWks57AyzvsDQbyn53IhZ0tBN7gA8Jj2VhXiOV2lef7thw==",
3931 + "version": "1.1.0",
3932 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.1.0.tgz",
3933 + "integrity": "sha512-9IE2WWkcJo2BR9eqtY5MIo3TPmS50Pnwpm66A6neb2hvk/QSLfPXBz2qdiwUOQkwyFuuXEUj5380CbwfzW4+/w==",
3934 "dev": true,
3935 "dependencies": {
3935 - "@vitest/spy": "1.0.1",
3936 - "@vitest/utils": "1.0.1",
3936 + "@vitest/spy": "1.1.0",
3937 + "@vitest/utils": "1.1.0",
3938 "chai": "^4.3.10"
3939 },
3940 "funding": {
@@ -3941,12 +3942,12 @@
3942 }
3943 },
3944 "node_modules/@vitest/runner": {
3944 - "version": "1.0.1",
3945 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.0.1.tgz",
3946 - "integrity": "sha512-/+z0vhJ0MfRPT3AyTvAK6m57rzlew/ct8B2a4LMv7NhpPaiI2QLGyOBMB3lcioWdJHjRuLi9aYppfOv0B5aRQA==",
3945 + "version": "1.1.0",
3946 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.1.0.tgz",
3947 + "integrity": "sha512-zdNLJ00pm5z/uhbWF6aeIJCGMSyTyWImy3Fcp9piRGvueERFlQFbUwCpzVce79OLm2UHk9iwaMSOaU9jVHgNVw==",
3948 "dev": true,
3949 "dependencies": {
3949 - "@vitest/utils": "1.0.1",
3950 + "@vitest/utils": "1.1.0",
3951 "p-limit": "^5.0.0",
3952 "pathe": "^1.1.1"
3953 },
@@ -3982,9 +3983,9 @@
3983 }
3984 },
3985 "node_modules/@vitest/snapshot": {
3985 - "version": "1.0.1",
3986 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.0.1.tgz",
3987 - "integrity": "sha512-wIPtPDGSxEZ+DpNMc94AsybX6LV6uN6sosf5TojyP1m2QbKwiRuLV/5RSsjt1oWViHsTj8mlcwrQQ1zHGO0fMw==",
3986 + "version": "1.1.0",
3987 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.1.0.tgz",
3988 + "integrity": "sha512-5O/wyZg09V5qmNmAlUgCBqflvn2ylgsWJRRuPrnHEfDNT6tQpQ8O1isNGgo+VxofISHqz961SG3iVvt3SPK/QQ==",
3989 "dev": true,
3990 "dependencies": {
3991 "magic-string": "^0.30.5",
@@ -3996,9 +3997,9 @@
3997 }
3998 },
3999 "node_modules/@vitest/spy": {
3999 - "version": "1.0.1",
4000 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.0.1.tgz",
4001 - "integrity": "sha512-yXwm1uKhBVr/5MhVeSmtNqK+0q2RXIchJt8kokEKdrWLtkPeDgdbZ6SjR1VQGZuNdWL6sSBnLayIyVvcS0qLfA==",
4000 + "version": "1.1.0",
4001 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.1.0.tgz",
4002 + "integrity": "sha512-sNOVSU/GE+7+P76qYo+VXdXhXffzWZcYIPQfmkiRxaNCSPiLANvQx5Mx6ZURJ/ndtEkUJEpvKLXqAYTKEY+lTg==",
4003 "dev": true,
4004 "dependencies": {
4005 "tinyspy": "^2.2.0"
@@ -4008,9 +4009,9 @@
4009 }
4010 },
4011 "node_modules/@vitest/utils": {
4011 - "version": "1.0.1",
4012 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.0.1.tgz",
4013 - "integrity": "sha512-MGPCHkzXbbAyscrhwGzh8uP1HPrTYLWaj1WTDtWSGrpe2yJWLRN9mF9ooKawr6NMOg9vTBtg2JqWLfuLC7Dknw==",
4012 + "version": "1.1.0",
4013 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.1.0.tgz",
4014 + "integrity": "sha512-z+s510fKmYz4Y41XhNs3vcuFTFhcij2YF7F8VQfMEYAAUfqQh0Zfg7+w9xdgFGhPf3tX3TicAe+8BDITk6ampQ==",
4015 "dev": true,
4016 "dependencies": {
4017 "diff-sequences": "^29.6.3",
@@ -4094,36 +4095,36 @@
4095 }
4096 },
4097 "node_modules/@vue/compiler-core": {
4097 - "version": "3.3.10",
4098 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.10.tgz",
4099 - "integrity": "sha512-doe0hODR1+i1menPkRzJ5MNR6G+9uiZHIknK3Zn5OcIztu6GGw7u0XUzf3AgB8h/dfsZC9eouzoLo3c3+N/cVA==",
4098 + "version": "3.3.13",
4099 + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.13.tgz",
4100 + "integrity": "sha512-bwi9HShGu7uaZLOErZgsH2+ojsEdsjerbf2cMXPwmvcgZfVPZ2BVZzCVnwZBxTAYd6Mzbmf6izcUNDkWnBBQ6A==",
4101 "dependencies": {
4102 "@babel/parser": "^7.23.5",
4102 - "@vue/shared": "3.3.10",
4103 + "@vue/shared": "3.3.13",
4104 "estree-walker": "^2.0.2",
4105 "source-map-js": "^1.0.2"
4106 }
4107 },
4108 "node_modules/@vue/compiler-dom": {
4108 - "version": "3.3.10",
4109 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.10.tgz",
4110 - "integrity": "sha512-NCrqF5fm10GXZIK0GrEAauBqdy+F2LZRt3yNHzrYjpYBuRssQbuPLtSnSNjyR9luHKkWSH8we5LMB3g+4z2HvA==",
4109 + "version": "3.3.13",
4110 + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.13.tgz",
4111 + "integrity": "sha512-EYRDpbLadGtNL0Gph+HoKiYqXLqZ0xSSpR5Dvnu/Ep7ggaCbjRDIus1MMxTS2Qm0koXED4xSlvTZaTnI8cYAsw==",
4112 "dependencies": {
4112 - "@vue/compiler-core": "3.3.10",
4113 - "@vue/shared": "3.3.10"
4113 + "@vue/compiler-core": "3.3.13",
4114 + "@vue/shared": "3.3.13"
4115 }
4116 },
4117 "node_modules/@vue/compiler-sfc": {
4117 - "version": "3.3.10",
4118 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.10.tgz",
4119 - "integrity": "sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==",
4118 + "version": "3.3.13",
4119 + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.13.tgz",
4120 + "integrity": "sha512-DQVmHEy/EKIgggvnGRLx21hSqnr1smUS9Aq8tfxiiot8UR0/pXKHN9k78/qQ7etyQTFj5em5nruODON7dBeumw==",
4121 "dependencies": {
4122 "@babel/parser": "^7.23.5",
4122 - "@vue/compiler-core": "3.3.10",
4123 - "@vue/compiler-dom": "3.3.10",
4124 - "@vue/compiler-ssr": "3.3.10",
4125 - "@vue/reactivity-transform": "3.3.10",
4126 - "@vue/shared": "3.3.10",
4123 + "@vue/compiler-core": "3.3.13",
4124 + "@vue/compiler-dom": "3.3.13",
4125 + "@vue/compiler-ssr": "3.3.13",
4126 + "@vue/reactivity-transform": "3.3.13",
4127 + "@vue/shared": "3.3.13",
4128 "estree-walker": "^2.0.2",
4129 "magic-string": "^0.30.5",
4130 "postcss": "^8.4.32",
@@ -4131,12 +4132,12 @@
4132 }
4133 },
4134 "node_modules/@vue/compiler-ssr": {
4134 - "version": "3.3.10",
4135 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.10.tgz",
4136 - "integrity": "sha512-12iM4jA4GEbskwXMmPcskK5wImc2ohKm408+o9iox3tfN9qua8xL0THIZtoe9OJHnXP4eOWZpgCAAThEveNlqQ==",
4135 + "version": "3.3.13",
4136 + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.13.tgz",
4137 + "integrity": "sha512-d/P3bCeUGmkJNS1QUZSAvoCIW4fkOKK3l2deE7zrp0ypJEy+En2AcypIkqvcFQOcw3F0zt2VfMvNsA9JmExTaw==",
4138 "dependencies": {
4138 - "@vue/compiler-dom": "3.3.10",
4139 - "@vue/shared": "3.3.10"
4139 + "@vue/compiler-dom": "3.3.13",
4140 + "@vue/shared": "3.3.13"
4141 }
4142 },
4143 "node_modules/@vue/devtools-api": {
@@ -4183,9 +4184,9 @@
4184 }
4185 },
4186 "node_modules/@vue/language-core": {
4186 - "version": "1.8.25",
4187 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.25.tgz",
4188 - "integrity": "sha512-NJk/5DnAZlpvXX8BdWmHI45bWGLViUaS3R/RMrmFSvFMSbJKuEODpM4kR0F0Ofv5SFzCWuNiMhxameWpVdQsnA==",
4187 + "version": "1.8.26",
4188 + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.26.tgz",
4189 + "integrity": "sha512-9cmza/Y2YTiOnKZ0Mi9zsNn7Irw+aKirP+5LLWVSNaL3fjKJjW1cD3HGBckasY2RuVh4YycvdA9/Q6EBpVd/7Q==",
4190 "dev": true,
4191 "dependencies": {
4192 "@volar/language-core": "~1.11.1",
@@ -4232,65 +4233,65 @@
4233 }
4234 },
4235 "node_modules/@vue/reactivity": {
4235 - "version": "3.3.10",
4236 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.10.tgz",
4237 - "integrity": "sha512-H5Z7rOY/JLO+e5a6/FEXaQ1TMuOvY4LDVgT+/+HKubEAgs9qeeZ+NhADSeEtrNQeiKLDuzeKc8v0CUFpB6Pqgw==",
4236 + "version": "3.3.13",
4237 + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.13.tgz",
4238 + "integrity": "sha512-fjzCxceMahHhi4AxUBzQqqVhuA21RJ0COaWTbIBl1PruGW1CeY97louZzLi4smpYx+CHfFPPU/CS8NybbGvPKQ==",
4239 "dependencies": {
4239 - "@vue/shared": "3.3.10"
4240 + "@vue/shared": "3.3.13"
4241 }
4242 },
4243 "node_modules/@vue/reactivity-transform": {
4243 - "version": "3.3.10",
4244 - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.10.tgz",
4245 - "integrity": "sha512-0xBdk+CKHWT+Gev8oZ63Tc0qFfj935YZx+UAynlutnrDZ4diFCVFMWixn65HzjE3S1iJppWOo6Tt1OzASH7VEg==",
4244 + "version": "3.3.13",
4245 + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.13.tgz",
4246 + "integrity": "sha512-oWnydGH0bBauhXvh5KXUy61xr9gKaMbtsMHk40IK9M4gMuKPJ342tKFarY0eQ6jef8906m35q37wwA8DMZOm5Q==",
4247 "dependencies": {
4248 "@babel/parser": "^7.23.5",
4248 - "@vue/compiler-core": "3.3.10",
4249 - "@vue/shared": "3.3.10",
4249 + "@vue/compiler-core": "3.3.13",
4250 + "@vue/shared": "3.3.13",
4251 "estree-walker": "^2.0.2",
4252 "magic-string": "^0.30.5"
4253 }
4254 },
4255 "node_modules/@vue/runtime-core": {
4255 - "version": "3.3.10",
4256 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.10.tgz",
4257 - "integrity": "sha512-DZ0v31oTN4YHX9JEU5VW1LoIVgFovWgIVb30bWn9DG9a7oA415idcwsRNNajqTx8HQJyOaWfRKoyuP2P2TYIag==",
4256 + "version": "3.3.13",
4257 + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.13.tgz",
4258 + "integrity": "sha512-1TzA5TvGuh2zUwMJgdfvrBABWZ7y8kBwBhm7BXk8rvdx2SsgcGfz2ruv2GzuGZNvL1aKnK8CQMV/jFOrxNQUMA==",
4259 "dependencies": {
4259 - "@vue/reactivity": "3.3.10",
4260 - "@vue/shared": "3.3.10"
4260 + "@vue/reactivity": "3.3.13",
4261 + "@vue/shared": "3.3.13"
4262 }
4263 },
4264 "node_modules/@vue/runtime-dom": {
4264 - "version": "3.3.10",
4265 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.10.tgz",
4266 - "integrity": "sha512-c/jKb3ny05KJcYk0j1m7Wbhrxq7mZYr06GhKykDMNRRR9S+/dGT8KpHuNQjv3/8U4JshfkAk6TpecPD3B21Ijw==",
4265 + "version": "3.3.13",
4266 + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.13.tgz",
4267 + "integrity": "sha512-JJkpE8R/hJKXqVTgUoODwS5wqKtOsmJPEqmp90PDVGygtJ4C0PtOkcEYXwhiVEmef6xeXcIlrT3Yo5aQ4qkHhQ==",
4268 "dependencies": {
4268 - "@vue/runtime-core": "3.3.10",
4269 - "@vue/shared": "3.3.10",
4270 - "csstype": "^3.1.2"
4269 + "@vue/runtime-core": "3.3.13",
4270 + "@vue/shared": "3.3.13",
4271 + "csstype": "^3.1.3"
4272 }
4273 },
4274 "node_modules/@vue/runtime-dom/node_modules/csstype": {
4274 - "version": "3.1.2",
4275 - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
4276 - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
4275 + "version": "3.1.3",
4276 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
4277 + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4278 },
4279 "node_modules/@vue/server-renderer": {
4279 - "version": "3.3.10",
4280 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.10.tgz",
4281 - "integrity": "sha512-0i6ww3sBV3SKlF3YTjSVqKQ74xialMbjVYGy7cOTi7Imd8ediE7t72SK3qnvhrTAhOvlQhq6Bk6nFPdXxe0sAg==",
4280 + "version": "3.3.13",
4281 + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.13.tgz",
4282 + "integrity": "sha512-vSnN+nuf6iSqTL3Qgx/9A+BT+0Zf/VJOgF5uMZrKjYPs38GMYyAU1coDyBNHauehXDaP+zl73VhwWv0vBRBHcg==",
4283 "dependencies": {
4283 - "@vue/compiler-ssr": "3.3.10",
4284 - "@vue/shared": "3.3.10"
4284 + "@vue/compiler-ssr": "3.3.13",
4285 + "@vue/shared": "3.3.13"
4286 },
4287 "peerDependencies": {
4287 - "vue": "3.3.10"
4288 + "vue": "3.3.13"
4289 }
4290 },
4291 "node_modules/@vue/shared": {
4291 - "version": "3.3.10",
4292 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
4293 - "integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw=="
4292 + "version": "3.3.13",
4293 + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.13.tgz",
4294 + "integrity": "sha512-/zYUwiHD8j7gKx2argXEMCUXVST6q/21DFU0sTfNX0URJroCe3b1UF6vLJ3lQDfLNIiiRl2ONp7Nh5UVWS6QnA=="
4295 },
4296 "node_modules/@vue/test-utils": {
4297 "version": "2.4.3",
@@ -4312,9 +4313,9 @@
4313 }
4314 },
4315 "node_modules/@vue/tsconfig": {
4315 - "version": "0.4.0",
4316 - "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.4.0.tgz",
4317 - "integrity": "sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==",
4316 + "version": "0.5.1",
4317 + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.5.1.tgz",
4318 + "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
4319 "dev": true
4320 },
4321 "node_modules/@vueup/vue-quill": {
@@ -4619,9 +4620,9 @@
4620 }
4621 },
4622 "node_modules/apexcharts": {
4622 - "version": "3.44.2",
4623 - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.44.2.tgz",
4624 - "integrity": "sha512-QWOFS/SC1TDsuI8VudVuIwLhs1qE6kwixRvmABMUgTVeVzizRWgQh8pdEIgwl+Zvr0TlM3vHPz6Dc5NP1hJ7BA==",
4623 + "version": "3.45.0",
4624 + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.45.0.tgz",
4625 + "integrity": "sha512-o+XI4ysvKtt+l+bGbk19s6Y2gFes/7LRLMAonDgcapz4frS0vzTfXiK77QcQc30TMwPiSN9Z8Tv+CBn57x63wg==",
4626 "dependencies": {
4627 "@yr/monotone-cubic-spline": "^1.0.3",
4628 "svg.draggable.js": "^2.2.2",
@@ -6984,15 +6985,15 @@
6985 }
6986 },
6987 "node_modules/eslint": {
6987 - "version": "8.55.0",
6988 - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
6989 - "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
6988 + "version": "8.56.0",
6989 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
6990 + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
6991 "dev": true,
6992 "dependencies": {
6993 "@eslint-community/eslint-utils": "^4.2.0",
6994 "@eslint-community/regexpp": "^4.6.1",
6995 "@eslint/eslintrc": "^2.1.4",
6995 - "@eslint/js": "8.55.0",
6996 + "@eslint/js": "8.56.0",
6997 "@humanwhocodes/config-array": "^0.11.13",
6998 "@humanwhocodes/module-importer": "^1.0.1",
6999 "@nodelib/fs.walk": "^1.2.8",
@@ -10908,9 +10909,9 @@
10909 }
10910 },
10911 "node_modules/naive-ui": {
10911 - "version": "2.35.0",
10912 - "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.35.0.tgz",
10913 - "integrity": "sha512-PdnLpOip1LQaKs5+rXLZoPDPQkTq26TnHWeABvUA2eOQjtHxE4+TQvj0Jq/W8clM2On/7jptoGmenLt48G3Bhg==",
10912 + "version": "2.36.0",
10913 + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.36.0.tgz",
10914 + "integrity": "sha512-r1ydtEm1Ryf/aWpbLCf32mQAGK99jd1eXgpkCtIomcBRZeAtusfy6zCtIpCppoCuIKM3BW5DMafhVxilubk/lQ==",
10915 "dependencies": {
10916 "@css-render/plugin-bem": "^0.15.12",
10917 "@css-render/vue3-ssr": "^0.15.12",
@@ -10925,11 +10926,11 @@
10926 "highlight.js": "^11.8.0",
10927 "lodash": "^4.17.21",
10928 "lodash-es": "^4.17.21",
10928 - "seemly": "^0.3.6",
10929 + "seemly": "^0.3.8",
10930 "treemate": "^0.3.11",
10931 "vdirs": "^0.1.8",
10932 "vooks": "^0.2.12",
10932 - "vueuc": "^0.4.51"
10933 + "vueuc": "^0.4.54"
10934 },
10935 "peerDependencies": {
10936 "vue": "^3.0.0"
@@ -11996,9 +11997,9 @@
11997 }
11998 },
11999 "node_modules/pinia-plugin-persistedstate": {
11999 - "version": "3.2.0",
12000 - "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.0.tgz",
12001 - "integrity": "sha512-tZbNGf2vjAQcIm7alK40sE51Qu/m9oWr+rEgNm/2AWr1huFxj72CjvpQcIQzMknDBJEkQznCLAGtJTIcLKrKdw==",
12000 + "version": "3.2.1",
12001 + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz",
12002 + "integrity": "sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==",
12003 "peerDependencies": {
12004 "pinia": "^2.0.0"
12005 }
@@ -12251,9 +12252,9 @@
12252 }
12253 },
12254 "node_modules/prettier": {
12254 - "version": "3.1.0",
12255 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
12256 - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
12255 + "version": "3.1.1",
12256 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
12257 + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
12258 "dev": true,
12259 "bin": {
12260 "prettier": "bin/prettier.cjs"
@@ -13509,9 +13510,9 @@
13510 }
13511 },
13512 "node_modules/seemly": {
13512 - "version": "0.3.6",
13513 - "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.6.tgz",
13514 - "integrity": "sha512-lEV5VB8BUKTo/AfktXJcy+JeXns26ylbMkIUco8CYREsQijuz4mrXres2Q+vMLdwkuLxJdIPQ8IlCIxLYm71Yw=="
13513 + "version": "0.3.8",
13514 + "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.8.tgz",
13515 + "integrity": "sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA=="
13516 },
13517 "node_modules/semver": {
13518 "version": "6.3.1",
@@ -14478,9 +14479,9 @@
14479 }
14480 },
14481 "node_modules/tailwindcss": {
14481 - "version": "3.3.6",
14482 - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz",
14483 - "integrity": "sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==",
14482 + "version": "3.4.0",
14483 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
14484 + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
14485 "dev": true,
14486 "dependencies": {
14487 "@alloc/quick-lru": "^5.2.0",
@@ -14982,9 +14983,9 @@
14983 "dev": true
14984 },
14985 "node_modules/ts-node": {
14985 - "version": "10.9.1",
14986 - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
14987 - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
14986 + "version": "10.9.2",
14987 + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
14988 + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
14989 "dev": true,
14990 "dependencies": {
14991 "@cspotcode/source-map-support": "^0.8.0",
@@ -15210,9 +15211,9 @@
15211 }
15212 },
15213 "node_modules/typescript": {
15213 - "version": "5.3.2",
15214 - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
15215 - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
15214 + "version": "5.3.3",
15215 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
15216 + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
15217 "devOptional": true,
15218 "bin": {
15219 "tsc": "bin/tsc",
@@ -15667,9 +15668,9 @@
15668 }
15669 },
15670 "node_modules/vite": {
15670 - "version": "5.0.6",
15671 - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.6.tgz",
15672 - "integrity": "sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==",
15671 + "version": "5.0.10",
15672 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz",
15673 + "integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==",
15674 "dev": true,
15675 "dependencies": {
15676 "esbuild": "^0.19.3",
@@ -15722,16 +15723,16 @@
15723 }
15724 },
15725 "node_modules/vite-node": {
15725 - "version": "1.0.1",
15726 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.0.1.tgz",
15727 - "integrity": "sha512-Y2Jnz4cr2azsOMMYuVPrQkp3KMnS/0WV8ezZjCy4hU7O5mUHCAVOnFmoEvs1nvix/4mYm74Len8bYRWZJMNP6g==",
15726 + "version": "1.1.0",
15727 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.1.0.tgz",
15728 + "integrity": "sha512-jV48DDUxGLEBdHCQvxL1mEh7+naVy+nhUUUaPAZLd3FJgXuxQiewHcfeZebbJ6onDqNGkP4r3MhQ342PRlG81Q==",
15729 "dev": true,
15730 "dependencies": {
15731 "cac": "^6.7.14",
15732 "debug": "^4.3.4",
15733 "pathe": "^1.1.1",
15734 "picocolors": "^1.0.0",
15734 - "vite": "^5.0.0-beta.15 || ^5.0.0"
15735 + "vite": "^5.0.0"
15736 },
15737 "bin": {
15738 "vite-node": "vite-node.mjs"
@@ -15784,16 +15785,16 @@
15785 }
15786 },
15787 "node_modules/vitest": {
15787 - "version": "1.0.1",
15788 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.0.1.tgz",
15789 - "integrity": "sha512-MHsOj079S28hDsvdDvyD1pRj4dcS51EC5Vbe0xvOYX+WryP8soiK2dm8oULi+oA/8Xa/h6GoJEMTmcmBy5YM+Q==",
15788 + "version": "1.1.0",
15789 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.1.0.tgz",
15790 + "integrity": "sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==",
15791 "dev": true,
15792 "dependencies": {
15792 - "@vitest/expect": "1.0.1",
15793 - "@vitest/runner": "1.0.1",
15794 - "@vitest/snapshot": "1.0.1",
15795 - "@vitest/spy": "1.0.1",
15796 - "@vitest/utils": "1.0.1",
15793 + "@vitest/expect": "1.1.0",
15794 + "@vitest/runner": "1.1.0",
15795 + "@vitest/snapshot": "1.1.0",
15796 + "@vitest/spy": "1.1.0",
15797 + "@vitest/utils": "1.1.0",
15798 "acorn-walk": "^8.3.0",
15799 "cac": "^6.7.14",
15800 "chai": "^4.3.10",
@@ -15807,8 +15808,8 @@
15808 "strip-literal": "^1.3.0",
15809 "tinybench": "^2.5.1",
15810 "tinypool": "^0.8.1",
15810 - "vite": "^5.0.0-beta.19 || ^5.0.0",
15811 - "vite-node": "1.0.1",
15811 + "vite": "^5.0.0",
15812 + "vite-node": "1.1.0",
15813 "why-is-node-running": "^2.2.2"
15814 },
15815 "bin": {
@@ -16021,15 +16022,15 @@
16022 }
16023 },
16024 "node_modules/vue": {
16024 - "version": "3.3.10",
16025 - "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.10.tgz",
16026 - "integrity": "sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==",
16025 + "version": "3.3.13",
16026 + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.13.tgz",
16027 + "integrity": "sha512-LDnUpQvDgsfc0u/YgtAgTMXJlJQqjkxW1PVcOnJA5cshPleULDjHi7U45pl2VJYazSSvLH8UKcid/kzH8I0a0Q==",
16028 "dependencies": {
16028 - "@vue/compiler-dom": "3.3.10",
16029 - "@vue/compiler-sfc": "3.3.10",
16030 - "@vue/runtime-dom": "3.3.10",
16031 - "@vue/server-renderer": "3.3.10",
16032 - "@vue/shared": "3.3.10"
16029 + "@vue/compiler-dom": "3.3.13",
16030 + "@vue/compiler-sfc": "3.3.13",
16031 + "@vue/runtime-dom": "3.3.13",
16032 + "@vue/server-renderer": "3.3.13",
16033 + "@vue/shared": "3.3.13"
16034 },
16035 "peerDependencies": {
16036 "typescript": "*"
@@ -16069,9 +16070,9 @@
16070 }
16071 },
16072 "node_modules/vue-chartjs": {
16072 - "version": "5.2.0",
16073 - "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.2.0.tgz",
16074 - "integrity": "sha512-d3zpKmGZr2OWHQ1xmxBcAn5ShTG917+/UCLaSpaCDDqT0U7DBsvFzTs69ZnHCgKoXT55GZDW8YEj9Av+dlONLA==",
16073 + "version": "5.3.0",
16074 + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz",
16075 + "integrity": "sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==",
16076 "peerDependencies": {
16077 "chart.js": "^4.1.1",
16078 "vue": "^3.0.0-0 || ^2.7.0"
@@ -16224,13 +16225,13 @@
16225 }
16226 },
16227 "node_modules/vue-tsc": {
16227 - "version": "1.8.25",
16228 - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.25.tgz",
16229 - "integrity": "sha512-lHsRhDc/Y7LINvYhZ3pv4elflFADoEOo67vfClAfF2heVHpHmVquLSjojgCSIwzA4F0Pc4vowT/psXCYcfk+iQ==",
16228 + "version": "1.8.26",
16229 + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.26.tgz",
16230 + "integrity": "sha512-jMEJ4aqU/l1hdgmeExH5h1TFoN+hbho0A2ZAhHy53/947DGm7Qj/bpB85VpECOCwV00h7JYNVnvoD2ceOorB4Q==",
16231 "dev": true,
16232 "dependencies": {
16233 "@volar/typescript": "~1.11.1",
16233 - "@vue/language-core": "1.8.25",
16234 + "@vue/language-core": "1.8.26",
16235 "semver": "^7.5.4"
16236 },
16237 "bin": {
@@ -16305,9 +16306,9 @@
16306 }
16307 },
16308 "node_modules/vueuc": {
16308 - "version": "0.4.51",
16309 - "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.51.tgz",
16310 - "integrity": "sha512-pLiMChM4f+W8czlIClGvGBYo656lc2Y0/mXFSCydcSmnCR1izlKPGMgiYBGjbY9FDkFG8a2HEVz7t0DNzBWbDw==",
16309 + "version": "0.4.56",
16310 + "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.56.tgz",
16311 + "integrity": "sha512-faDwItluIL0/K7UASjjyFblvnPNHZlD5b3qDn/2P7mGkweWtXzGQ+YIChQ0V+z0WwDH7lEskNvonnmGqF4T8iA==",
16312 "dependencies": {
16313 "@css-render/vue3-ssr": "^0.15.10",
16314 "@juggle/resize-observer": "^3.3.1",
@@ -17160,7 +17161,8 @@
17161 "dependencies": {
17162 "is-unicode-supported": {
17163 "version": "1.3.0",
17163 - "bundled": true
17164 + "bundled": true,
17165 + "extraneous": true
17166 }
17167 }
17168 },
@@ -17460,9 +17462,9 @@
17462 }
17463 },
17464 "@eslint/js": {
17463 - "version": "8.55.0",
17464 - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
17465 - "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
17465 + "version": "8.56.0",
17466 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
17467 + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
17468 "dev": true
17469 },
17470 "@faker-js/faker": {
@@ -17502,19 +17504,19 @@
17504 "integrity": "sha512-uvnFKtPgzLnpzzTRfhDlvXX0kLYi9lDRQbcDmT8iXl71Rx+uwSuaUIQl3DNC7w5OweAQ7XQMDObML+KaYDQfng=="
17505 },
17506 "@fontsource/jetbrains-mono": {
17505 - "version": "5.0.17",
17506 - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.17.tgz",
17507 - "integrity": "sha512-Y/EtdbwKwNQTGpnMrexX8SVW6Jqlh0nX2bNHI9Z9m6FsyjbocZIFNJqwSY9bDUoi7irGtz8nuidAN7FF8wYuJA=="
17507 + "version": "5.0.18",
17508 + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.18.tgz",
17509 + "integrity": "sha512-0+YDAaAnCdXjirHFO3NfHLUW8xtKCT5rlm23Q0qG3TpZp3QBrZh5r9ikUxh3ufHc2+fVnk4Y6GWNEnVIfB7u/g=="
17510 },
17511 "@fontsource/lexend": {
17510 - "version": "5.0.17",
17511 - "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.17.tgz",
17512 - "integrity": "sha512-3rtbeiOx4EqGxcOMfsgq23RRDbhdMGJULLdNCHCN6oAGN06WDesrH6ZL+r6ZF8fpdJZ63F0ViOj/PFG2kOtKdA=="
17512 + "version": "5.0.18",
17513 + "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.18.tgz",
17514 + "integrity": "sha512-RcNekPIeQGX5ZvwRtX7UHDoDrGTg8IV2Yae13qjtxW6FO4kFaUKSlITKnrvaK8r8ly/fQ6x2mXva9jmMZPZ4Ug=="
17515 },
17516 "@fontsource/public-sans": {
17515 - "version": "5.0.15",
17516 - "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.15.tgz",
17517 - "integrity": "sha512-3UKtCVDbwt8FeurOHYBybDzYYJH0peyisGjsQe2aRFR4M693m0DdE3v4BZl+60OjvnXGWhO8O/rmET2kwPF6SQ=="
17517 + "version": "5.0.16",
17518 + "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.16.tgz",
17519 + "integrity": "sha512-bThZip6sLRsnfzi/oBr1/9+aWmvHkb59QlLh/OtoAIA0Mi2+Z1cKOxtR1B2ITauljlLJHPRvjpTZDUZitUN8pA=="
17520 },
17521 "@fullcalendar/core": {
17522 "version": "6.1.10",
@@ -18566,9 +18568,9 @@
18568 "optional": true
18569 },
18570 "@rushstack/eslint-patch": {
18569 - "version": "1.6.0",
18570 - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz",
18571 - "integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==",
18571 + "version": "1.6.1",
18572 + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz",
18573 + "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==",
18574 "dev": true
18575 },
18576 "@sideway/address": {
@@ -19107,9 +19109,9 @@
19109 "integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ=="
19110 },
19111 "@types/node": {
19110 - "version": "20.10.3",
19111 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz",
19112 - "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==",
19112 + "version": "20.10.5",
19113 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
19114 + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
19115 "dev": true,
19116 "requires": {
19117 "undici-types": "~5.26.4"
@@ -19397,9 +19399,9 @@
19399 "dev": true
19400 },
19401 "@vitejs/plugin-vue": {
19400 - "version": "4.5.1",
19401 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.1.tgz",
19402 - "integrity": "sha512-DaUzYFr+2UGDG7VSSdShKa9sIWYBa1LL8KC0MNOf2H5LjcTPjob0x8LbkqXWmAtbANJCkpiQTj66UVcQkN2s3g==",
19402 + "version": "4.5.2",
19403 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz",
19404 + "integrity": "sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ==",
19405 "dev": true,
19406 "requires": {}
19407 },
@@ -19415,23 +19417,23 @@
19417 }
19418 },
19419 "@vitest/expect": {
19418 - "version": "1.0.1",
19419 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.0.1.tgz",
19420 - "integrity": "sha512-3cdrb/eKD/0tygDX75YscuHEHMUJ70u3UoLSq2eqhWks57AyzvsDQbyn53IhZ0tBN7gA8Jj2VhXiOV2lef7thw==",
19420 + "version": "1.1.0",
19421 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.1.0.tgz",
19422 + "integrity": "sha512-9IE2WWkcJo2BR9eqtY5MIo3TPmS50Pnwpm66A6neb2hvk/QSLfPXBz2qdiwUOQkwyFuuXEUj5380CbwfzW4+/w==",
19423 "dev": true,
19424 "requires": {
19423 - "@vitest/spy": "1.0.1",
19424 - "@vitest/utils": "1.0.1",
19425 + "@vitest/spy": "1.1.0",
19426 + "@vitest/utils": "1.1.0",
19427 "chai": "^4.3.10"
19428 }
19429 },
19430 "@vitest/runner": {
19429 - "version": "1.0.1",
19430 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.0.1.tgz",
19431 - "integrity": "sha512-/+z0vhJ0MfRPT3AyTvAK6m57rzlew/ct8B2a4LMv7NhpPaiI2QLGyOBMB3lcioWdJHjRuLi9aYppfOv0B5aRQA==",
19431 + "version": "1.1.0",
19432 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.1.0.tgz",
19433 + "integrity": "sha512-zdNLJ00pm5z/uhbWF6aeIJCGMSyTyWImy3Fcp9piRGvueERFlQFbUwCpzVce79OLm2UHk9iwaMSOaU9jVHgNVw==",
19434 "dev": true,
19435 "requires": {
19434 - "@vitest/utils": "1.0.1",
19436 + "@vitest/utils": "1.1.0",
19437 "p-limit": "^5.0.0",
19438 "pathe": "^1.1.1"
19439 },
@@ -19454,9 +19456,9 @@
19456 }
19457 },
19458 "@vitest/snapshot": {
19457 - "version": "1.0.1",
19458 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.0.1.tgz",
19459 - "integrity": "sha512-wIPtPDGSxEZ+DpNMc94AsybX6LV6uN6sosf5TojyP1m2QbKwiRuLV/5RSsjt1oWViHsTj8mlcwrQQ1zHGO0fMw==",
19459 + "version": "1.1.0",
19460 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.1.0.tgz",
19461 + "integrity": "sha512-5O/wyZg09V5qmNmAlUgCBqflvn2ylgsWJRRuPrnHEfDNT6tQpQ8O1isNGgo+VxofISHqz961SG3iVvt3SPK/QQ==",
19462 "dev": true,
19463 "requires": {
19464 "magic-string": "^0.30.5",
@@ -19465,18 +19467,18 @@
19467 }
19468 },
19469 "@vitest/spy": {
19468 - "version": "1.0.1",
19469 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.0.1.tgz",
19470 - "integrity": "sha512-yXwm1uKhBVr/5MhVeSmtNqK+0q2RXIchJt8kokEKdrWLtkPeDgdbZ6SjR1VQGZuNdWL6sSBnLayIyVvcS0qLfA==",
19470 + "version": "1.1.0",
19471 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.1.0.tgz",
19472 + "integrity": "sha512-sNOVSU/GE+7+P76qYo+VXdXhXffzWZcYIPQfmkiRxaNCSPiLANvQx5Mx6ZURJ/ndtEkUJEpvKLXqAYTKEY+lTg==",
19473 "dev": true,
19474 "requires": {
19475 "tinyspy": "^2.2.0"
19476 }
19477 },
19478 "@vitest/utils": {
19477 - "version": "1.0.1",
19478 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.0.1.tgz",
19479 - "integrity": "sha512-MGPCHkzXbbAyscrhwGzh8uP1HPrTYLWaj1WTDtWSGrpe2yJWLRN9mF9ooKawr6NMOg9vTBtg2JqWLfuLC7Dknw==",
19479 + "version": "1.1.0",
19480 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.1.0.tgz",
19481 + "integrity": "sha512-z+s510fKmYz4Y41XhNs3vcuFTFhcij2YF7F8VQfMEYAAUfqQh0Zfg7+w9xdgFGhPf3tX3TicAe+8BDITk6ampQ==",
19482 "dev": true,
19483 "requires": {
19484 "diff-sequences": "^29.6.3",
@@ -19545,36 +19547,36 @@
19547 }
19548 },
19549 "@vue/compiler-core": {
19548 - "version": "3.3.10",
19549 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.10.tgz",
19550 - "integrity": "sha512-doe0hODR1+i1menPkRzJ5MNR6G+9uiZHIknK3Zn5OcIztu6GGw7u0XUzf3AgB8h/dfsZC9eouzoLo3c3+N/cVA==",
19550 + "version": "3.3.13",
19551 + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.13.tgz",
19552 + "integrity": "sha512-bwi9HShGu7uaZLOErZgsH2+ojsEdsjerbf2cMXPwmvcgZfVPZ2BVZzCVnwZBxTAYd6Mzbmf6izcUNDkWnBBQ6A==",
19553 "requires": {
19554 "@babel/parser": "^7.23.5",
19553 - "@vue/shared": "3.3.10",
19555 + "@vue/shared": "3.3.13",
19556 "estree-walker": "^2.0.2",
19557 "source-map-js": "^1.0.2"
19558 }
19559 },
19560 "@vue/compiler-dom": {
19559 - "version": "3.3.10",
19560 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.10.tgz",
19561 - "integrity": "sha512-NCrqF5fm10GXZIK0GrEAauBqdy+F2LZRt3yNHzrYjpYBuRssQbuPLtSnSNjyR9luHKkWSH8we5LMB3g+4z2HvA==",
19561 + "version": "3.3.13",
19562 + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.13.tgz",
19563 + "integrity": "sha512-EYRDpbLadGtNL0Gph+HoKiYqXLqZ0xSSpR5Dvnu/Ep7ggaCbjRDIus1MMxTS2Qm0koXED4xSlvTZaTnI8cYAsw==",
19564 "requires": {
19563 - "@vue/compiler-core": "3.3.10",
19564 - "@vue/shared": "3.3.10"
19565 + "@vue/compiler-core": "3.3.13",
19566 + "@vue/shared": "3.3.13"
19567 }
19568 },
19569 "@vue/compiler-sfc": {
19568 - "version": "3.3.10",
19569 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.10.tgz",
19570 - "integrity": "sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==",
19570 + "version": "3.3.13",
19571 + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.13.tgz",
19572 + "integrity": "sha512-DQVmHEy/EKIgggvnGRLx21hSqnr1smUS9Aq8tfxiiot8UR0/pXKHN9k78/qQ7etyQTFj5em5nruODON7dBeumw==",
19573 "requires": {
19574 "@babel/parser": "^7.23.5",
19573 - "@vue/compiler-core": "3.3.10",
19574 - "@vue/compiler-dom": "3.3.10",
19575 - "@vue/compiler-ssr": "3.3.10",
19576 - "@vue/reactivity-transform": "3.3.10",
19577 - "@vue/shared": "3.3.10",
19575 + "@vue/compiler-core": "3.3.13",
19576 + "@vue/compiler-dom": "3.3.13",
19577 + "@vue/compiler-ssr": "3.3.13",
19578 + "@vue/reactivity-transform": "3.3.13",
19579 + "@vue/shared": "3.3.13",
19580 "estree-walker": "^2.0.2",
19581 "magic-string": "^0.30.5",
19582 "postcss": "^8.4.32",
@@ -19582,12 +19584,12 @@
19584 }
19585 },
19586 "@vue/compiler-ssr": {
19585 - "version": "3.3.10",
19586 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.10.tgz",
19587 - "integrity": "sha512-12iM4jA4GEbskwXMmPcskK5wImc2ohKm408+o9iox3tfN9qua8xL0THIZtoe9OJHnXP4eOWZpgCAAThEveNlqQ==",
19587 + "version": "3.3.13",
19588 + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.13.tgz",
19589 + "integrity": "sha512-d/P3bCeUGmkJNS1QUZSAvoCIW4fkOKK3l2deE7zrp0ypJEy+En2AcypIkqvcFQOcw3F0zt2VfMvNsA9JmExTaw==",
19590 "requires": {
19589 - "@vue/compiler-dom": "3.3.10",
19590 - "@vue/shared": "3.3.10"
19591 + "@vue/compiler-dom": "3.3.13",
19592 + "@vue/shared": "3.3.13"
19593 }
19594 },
19595 "@vue/devtools-api": {
@@ -19617,9 +19619,9 @@
19619 }
19620 },
19621 "@vue/language-core": {
19620 - "version": "1.8.25",
19621 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.25.tgz",
19622 - "integrity": "sha512-NJk/5DnAZlpvXX8BdWmHI45bWGLViUaS3R/RMrmFSvFMSbJKuEODpM4kR0F0Ofv5SFzCWuNiMhxameWpVdQsnA==",
19622 + "version": "1.8.26",
19623 + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.26.tgz",
19624 + "integrity": "sha512-9cmza/Y2YTiOnKZ0Mi9zsNn7Irw+aKirP+5LLWVSNaL3fjKJjW1cD3HGBckasY2RuVh4YycvdA9/Q6EBpVd/7Q==",
19625 "dev": true,
19626 "requires": {
19627 "@volar/language-core": "~1.11.1",
@@ -19654,64 +19656,64 @@
19656 }
19657 },
19658 "@vue/reactivity": {
19657 - "version": "3.3.10",
19658 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.10.tgz",
19659 - "integrity": "sha512-H5Z7rOY/JLO+e5a6/FEXaQ1TMuOvY4LDVgT+/+HKubEAgs9qeeZ+NhADSeEtrNQeiKLDuzeKc8v0CUFpB6Pqgw==",
19659 + "version": "3.3.13",
19660 + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.13.tgz",
19661 + "integrity": "sha512-fjzCxceMahHhi4AxUBzQqqVhuA21RJ0COaWTbIBl1PruGW1CeY97louZzLi4smpYx+CHfFPPU/CS8NybbGvPKQ==",
19662 "requires": {
19661 - "@vue/shared": "3.3.10"
19663 + "@vue/shared": "3.3.13"
19664 }
19665 },
19666 "@vue/reactivity-transform": {
19665 - "version": "3.3.10",
19666 - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.10.tgz",
19667 - "integrity": "sha512-0xBdk+CKHWT+Gev8oZ63Tc0qFfj935YZx+UAynlutnrDZ4diFCVFMWixn65HzjE3S1iJppWOo6Tt1OzASH7VEg==",
19667 + "version": "3.3.13",
19668 + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.13.tgz",
19669 + "integrity": "sha512-oWnydGH0bBauhXvh5KXUy61xr9gKaMbtsMHk40IK9M4gMuKPJ342tKFarY0eQ6jef8906m35q37wwA8DMZOm5Q==",
19670 "requires": {
19671 "@babel/parser": "^7.23.5",
19670 - "@vue/compiler-core": "3.3.10",
19671 - "@vue/shared": "3.3.10",
19672 + "@vue/compiler-core": "3.3.13",
19673 + "@vue/shared": "3.3.13",
19674 "estree-walker": "^2.0.2",
19675 "magic-string": "^0.30.5"
19676 }
19677 },
19678 "@vue/runtime-core": {
19677 - "version": "3.3.10",
19678 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.10.tgz",
19679 - "integrity": "sha512-DZ0v31oTN4YHX9JEU5VW1LoIVgFovWgIVb30bWn9DG9a7oA415idcwsRNNajqTx8HQJyOaWfRKoyuP2P2TYIag==",
19679 + "version": "3.3.13",
19680 + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.13.tgz",
19681 + "integrity": "sha512-1TzA5TvGuh2zUwMJgdfvrBABWZ7y8kBwBhm7BXk8rvdx2SsgcGfz2ruv2GzuGZNvL1aKnK8CQMV/jFOrxNQUMA==",
19682 "requires": {
19681 - "@vue/reactivity": "3.3.10",
19682 - "@vue/shared": "3.3.10"
19683 + "@vue/reactivity": "3.3.13",
19684 + "@vue/shared": "3.3.13"
19685 }
19686 },
19687 "@vue/runtime-dom": {
19686 - "version": "3.3.10",
19687 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.10.tgz",
19688 - "integrity": "sha512-c/jKb3ny05KJcYk0j1m7Wbhrxq7mZYr06GhKykDMNRRR9S+/dGT8KpHuNQjv3/8U4JshfkAk6TpecPD3B21Ijw==",
19688 + "version": "3.3.13",
19689 + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.13.tgz",
19690 + "integrity": "sha512-JJkpE8R/hJKXqVTgUoODwS5wqKtOsmJPEqmp90PDVGygtJ4C0PtOkcEYXwhiVEmef6xeXcIlrT3Yo5aQ4qkHhQ==",
19691 "requires": {
19690 - "@vue/runtime-core": "3.3.10",
19691 - "@vue/shared": "3.3.10",
19692 - "csstype": "^3.1.2"
19692 + "@vue/runtime-core": "3.3.13",
19693 + "@vue/shared": "3.3.13",
19694 + "csstype": "^3.1.3"
19695 },
19696 "dependencies": {
19697 "csstype": {
19696 - "version": "3.1.2",
19697 - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
19698 - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
19698 + "version": "3.1.3",
19699 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
19700 + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
19701 }
19702 }
19703 },
19704 "@vue/server-renderer": {
19703 - "version": "3.3.10",
19704 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.10.tgz",
19705 - "integrity": "sha512-0i6ww3sBV3SKlF3YTjSVqKQ74xialMbjVYGy7cOTi7Imd8ediE7t72SK3qnvhrTAhOvlQhq6Bk6nFPdXxe0sAg==",
19705 + "version": "3.3.13",
19706 + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.13.tgz",
19707 + "integrity": "sha512-vSnN+nuf6iSqTL3Qgx/9A+BT+0Zf/VJOgF5uMZrKjYPs38GMYyAU1coDyBNHauehXDaP+zl73VhwWv0vBRBHcg==",
19708 "requires": {
19707 - "@vue/compiler-ssr": "3.3.10",
19708 - "@vue/shared": "3.3.10"
19709 + "@vue/compiler-ssr": "3.3.13",
19710 + "@vue/shared": "3.3.13"
19711 }
19712 },
19713 "@vue/shared": {
19712 - "version": "3.3.10",
19713 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
19714 - "integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw=="
19714 + "version": "3.3.13",
19715 + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.13.tgz",
19716 + "integrity": "sha512-/zYUwiHD8j7gKx2argXEMCUXVST6q/21DFU0sTfNX0URJroCe3b1UF6vLJ3lQDfLNIiiRl2ONp7Nh5UVWS6QnA=="
19717 },
19718 "@vue/test-utils": {
19719 "version": "2.4.3",
@@ -19724,9 +19726,9 @@
19726 }
19727 },
19728 "@vue/tsconfig": {
19727 - "version": "0.4.0",
19728 - "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.4.0.tgz",
19729 - "integrity": "sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==",
19729 + "version": "0.5.1",
19730 + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.5.1.tgz",
19731 + "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
19732 "dev": true
19733 },
19734 "@vueup/vue-quill": {
@@ -19923,9 +19925,9 @@
19925 }
19926 },
19927 "apexcharts": {
19926 - "version": "3.44.2",
19927 - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.44.2.tgz",
19928 - "integrity": "sha512-QWOFS/SC1TDsuI8VudVuIwLhs1qE6kwixRvmABMUgTVeVzizRWgQh8pdEIgwl+Zvr0TlM3vHPz6Dc5NP1hJ7BA==",
19928 + "version": "3.45.0",
19929 + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.45.0.tgz",
19930 + "integrity": "sha512-o+XI4ysvKtt+l+bGbk19s6Y2gFes/7LRLMAonDgcapz4frS0vzTfXiK77QcQc30TMwPiSN9Z8Tv+CBn57x63wg==",
19931 "requires": {
19932 "@yr/monotone-cubic-spline": "^1.0.3",
19933 "svg.draggable.js": "^2.2.2",
@@ -21668,15 +21670,15 @@
21670 "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="
21671 },
21672 "eslint": {
21671 - "version": "8.55.0",
21672 - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
21673 - "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
21673 + "version": "8.56.0",
21674 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
21675 + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
21676 "dev": true,
21677 "requires": {
21678 "@eslint-community/eslint-utils": "^4.2.0",
21679 "@eslint-community/regexpp": "^4.6.1",
21680 "@eslint/eslintrc": "^2.1.4",
21679 - "@eslint/js": "8.55.0",
21681 + "@eslint/js": "8.56.0",
21682 "@humanwhocodes/config-array": "^0.11.13",
21683 "@humanwhocodes/module-importer": "^1.0.1",
21684 "@nodelib/fs.walk": "^1.2.8",
@@ -24500,9 +24502,9 @@
24502 }
24503 },
24504 "naive-ui": {
24503 - "version": "2.35.0",
24504 - "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.35.0.tgz",
24505 - "integrity": "sha512-PdnLpOip1LQaKs5+rXLZoPDPQkTq26TnHWeABvUA2eOQjtHxE4+TQvj0Jq/W8clM2On/7jptoGmenLt48G3Bhg==",
24505 + "version": "2.36.0",
24506 + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.36.0.tgz",
24507 + "integrity": "sha512-r1ydtEm1Ryf/aWpbLCf32mQAGK99jd1eXgpkCtIomcBRZeAtusfy6zCtIpCppoCuIKM3BW5DMafhVxilubk/lQ==",
24508 "requires": {
24509 "@css-render/plugin-bem": "^0.15.12",
24510 "@css-render/vue3-ssr": "^0.15.12",
@@ -24517,11 +24519,11 @@
24519 "highlight.js": "^11.8.0",
24520 "lodash": "^4.17.21",
24521 "lodash-es": "^4.17.21",
24520 - "seemly": "^0.3.6",
24522 + "seemly": "^0.3.8",
24523 "treemate": "^0.3.11",
24524 "vdirs": "^0.1.8",
24525 "vooks": "^0.2.12",
24524 - "vueuc": "^0.4.51"
24526 + "vueuc": "^0.4.54"
24527 }
24528 },
24529 "nanoid": {
@@ -25320,9 +25322,9 @@
25322 }
25323 },
25324 "pinia-plugin-persistedstate": {
25323 - "version": "3.2.0",
25324 - "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.0.tgz",
25325 - "integrity": "sha512-tZbNGf2vjAQcIm7alK40sE51Qu/m9oWr+rEgNm/2AWr1huFxj72CjvpQcIQzMknDBJEkQznCLAGtJTIcLKrKdw==",
25325 + "version": "3.2.1",
25326 + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz",
25327 + "integrity": "sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==",
25328 "requires": {}
25329 },
25330 "pirates": {
@@ -25462,9 +25464,9 @@
25464 "dev": true
25465 },
25466 "prettier": {
25465 - "version": "3.1.0",
25466 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
25467 - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
25467 + "version": "3.1.1",
25468 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
25469 + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
25470 "dev": true
25471 },
25472 "prettier-linter-helpers": {
@@ -26436,9 +26438,9 @@
26438 }
26439 },
26440 "seemly": {
26439 - "version": "0.3.6",
26440 - "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.6.tgz",
26441 - "integrity": "sha512-lEV5VB8BUKTo/AfktXJcy+JeXns26ylbMkIUco8CYREsQijuz4mrXres2Q+vMLdwkuLxJdIPQ8IlCIxLYm71Yw=="
26441 + "version": "0.3.8",
26442 + "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.8.tgz",
26443 + "integrity": "sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA=="
26444 },
26445 "semver": {
26446 "version": "6.3.1",
@@ -27162,9 +27164,9 @@
27164 }
27165 },
27166 "tailwindcss": {
27165 - "version": "3.3.6",
27166 - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz",
27167 - "integrity": "sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==",
27167 + "version": "3.4.0",
27168 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
27169 + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
27170 "dev": true,
27171 "requires": {
27172 "@alloc/quick-lru": "^5.2.0",
@@ -27529,9 +27531,9 @@
27531 "dev": true
27532 },
27533 "ts-node": {
27532 - "version": "10.9.1",
27533 - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
27534 - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
27534 + "version": "10.9.2",
27535 + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
27536 + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
27537 "dev": true,
27538 "requires": {
27539 "@cspotcode/source-map-support": "^0.8.0",
@@ -27694,9 +27696,9 @@
27696 }
27697 },
27698 "typescript": {
27697 - "version": "5.3.2",
27698 - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
27699 - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
27699 + "version": "5.3.3",
27700 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
27701 + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
27702 "devOptional": true
27703 },
27704 "typewise": {
@@ -28029,9 +28031,9 @@
28031 }
28032 },
28033 "vite": {
28032 - "version": "5.0.6",
28033 - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.6.tgz",
28034 - "integrity": "sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==",
28034 + "version": "5.0.10",
28035 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz",
28036 + "integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==",
28037 "dev": true,
28038 "requires": {
28039 "esbuild": "0.18.10",
@@ -28064,16 +28066,16 @@
28066 }
28067 },
28068 "vite-node": {
28067 - "version": "1.0.1",
28068 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.0.1.tgz",
28069 - "integrity": "sha512-Y2Jnz4cr2azsOMMYuVPrQkp3KMnS/0WV8ezZjCy4hU7O5mUHCAVOnFmoEvs1nvix/4mYm74Len8bYRWZJMNP6g==",
28069 + "version": "1.1.0",
28070 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.1.0.tgz",
28071 + "integrity": "sha512-jV48DDUxGLEBdHCQvxL1mEh7+naVy+nhUUUaPAZLd3FJgXuxQiewHcfeZebbJ6onDqNGkP4r3MhQ342PRlG81Q==",
28072 "dev": true,
28073 "requires": {
28074 "cac": "^6.7.14",
28075 "debug": "^4.3.4",
28076 "pathe": "^1.1.1",
28077 "picocolors": "^1.0.0",
28076 - "vite": "^5.0.0-beta.15 || ^5.0.0"
28078 + "vite": "^5.0.0"
28079 }
28080 },
28081 "vite-svg-loader": {
@@ -28086,16 +28088,16 @@
28088 }
28089 },
28090 "vitest": {
28089 - "version": "1.0.1",
28090 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.0.1.tgz",
28091 - "integrity": "sha512-MHsOj079S28hDsvdDvyD1pRj4dcS51EC5Vbe0xvOYX+WryP8soiK2dm8oULi+oA/8Xa/h6GoJEMTmcmBy5YM+Q==",
28091 + "version": "1.1.0",
28092 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.1.0.tgz",
28093 + "integrity": "sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==",
28094 "dev": true,
28095 "requires": {
28094 - "@vitest/expect": "1.0.1",
28095 - "@vitest/runner": "1.0.1",
28096 - "@vitest/snapshot": "1.0.1",
28097 - "@vitest/spy": "1.0.1",
28098 - "@vitest/utils": "1.0.1",
28096 + "@vitest/expect": "1.1.0",
28097 + "@vitest/runner": "1.1.0",
28098 + "@vitest/snapshot": "1.1.0",
28099 + "@vitest/spy": "1.1.0",
28100 + "@vitest/utils": "1.1.0",
28101 "acorn-walk": "^8.3.0",
28102 "cac": "^6.7.14",
28103 "chai": "^4.3.10",
@@ -28109,8 +28111,8 @@
28111 "strip-literal": "^1.3.0",
28112 "tinybench": "^2.5.1",
28113 "tinypool": "^0.8.1",
28112 - "vite": "^5.0.0-beta.19 || ^5.0.0",
28113 - "vite-node": "1.0.1",
28114 + "vite": "^5.0.0",
28115 + "vite-node": "1.1.0",
28116 "why-is-node-running": "^2.2.2"
28117 },
28118 "dependencies": {
@@ -28222,15 +28224,15 @@
28224 }
28225 },
28226 "vue": {
28225 - "version": "3.3.10",
28226 - "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.10.tgz",
28227 - "integrity": "sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==",
28227 + "version": "3.3.13",
28228 + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.13.tgz",
28229 + "integrity": "sha512-LDnUpQvDgsfc0u/YgtAgTMXJlJQqjkxW1PVcOnJA5cshPleULDjHi7U45pl2VJYazSSvLH8UKcid/kzH8I0a0Q==",
28230 "requires": {
28229 - "@vue/compiler-dom": "3.3.10",
28230 - "@vue/compiler-sfc": "3.3.10",
28231 - "@vue/runtime-dom": "3.3.10",
28232 - "@vue/server-renderer": "3.3.10",
28233 - "@vue/shared": "3.3.10"
28231 + "@vue/compiler-dom": "3.3.13",
28232 + "@vue/compiler-sfc": "3.3.13",
28233 + "@vue/runtime-dom": "3.3.13",
28234 + "@vue/server-renderer": "3.3.13",
28235 + "@vue/shared": "3.3.13"
28236 }
28237 },
28238 "vue-advanced-cropper": {
@@ -28250,9 +28252,9 @@
28252 "requires": {}
28253 },
28254 "vue-chartjs": {
28253 - "version": "5.2.0",
28254 - "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.2.0.tgz",
28255 - "integrity": "sha512-d3zpKmGZr2OWHQ1xmxBcAn5ShTG917+/UCLaSpaCDDqT0U7DBsvFzTs69ZnHCgKoXT55GZDW8YEj9Av+dlONLA==",
28255 + "version": "5.3.0",
28256 + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz",
28257 + "integrity": "sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==",
28258 "requires": {}
28259 },
28260 "vue-component-type-helpers": {
@@ -28357,13 +28359,13 @@
28359 }
28360 },
28361 "vue-tsc": {
28360 - "version": "1.8.25",
28361 - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.25.tgz",
28362 - "integrity": "sha512-lHsRhDc/Y7LINvYhZ3pv4elflFADoEOo67vfClAfF2heVHpHmVquLSjojgCSIwzA4F0Pc4vowT/psXCYcfk+iQ==",
28362 + "version": "1.8.26",
28363 + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.26.tgz",
28364 + "integrity": "sha512-jMEJ4aqU/l1hdgmeExH5h1TFoN+hbho0A2ZAhHy53/947DGm7Qj/bpB85VpECOCwV00h7JYNVnvoD2ceOorB4Q==",
28365 "dev": true,
28366 "requires": {
28367 "@volar/typescript": "~1.11.1",
28366 - "@vue/language-core": "1.8.25",
28368 + "@vue/language-core": "1.8.26",
28369 "semver": "^7.5.4"
28370 },
28371 "dependencies": {
@@ -28414,9 +28416,9 @@
28416 }
28417 },
28418 "vueuc": {
28417 - "version": "0.4.51",
28418 - "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.51.tgz",
28419 - "integrity": "sha512-pLiMChM4f+W8czlIClGvGBYo656lc2Y0/mXFSCydcSmnCR1izlKPGMgiYBGjbY9FDkFG8a2HEVz7t0DNzBWbDw==",
28419 + "version": "0.4.56",
28420 + "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.56.tgz",
28421 + "integrity": "sha512-faDwItluIL0/K7UASjjyFblvnPNHZlD5b3qDn/2P7mGkweWtXzGQ+YIChQ0V+z0WwDH7lEskNvonnmGqF4T8iA==",
28422 "requires": {
28423 "@css-render/vue3-ssr": "^0.15.10",
28424 "@juggle/resize-observer": "^3.3.1",
package.json
+13 -13
@@ -63,7 +63,7 @@
63 "@vueup/vue-quill": "^1.2.0",
64 "@vueuse/components": "^10.7.0",
65 "@vueuse/core": "^10.7.0",
66 - "apexcharts": "^3.44.2",
66 + "apexcharts": "^3.45.0",
67 "bytes": "^3.1.2",
68 "chart.js": "^4.4.1",
69 "colord": "^2.9.3",
@@ -76,19 +76,19 @@
76 "lodash": "^4.17.21",
77 "maplibre-gl": "^3.6.2",
78 "mitt": "^3.0.1",
79 - "naive-ui": "^2.35.0",
79 + "naive-ui": "^2.36.0",
80 "password-validator": "^5.3.0",
81 "pinia": "^2.1.7",
82 - "pinia-plugin-persistedstate": "^3.2.0",
82 + "pinia-plugin-persistedstate": "^3.2.1",
83 "quill": "^1.3.7",
84 "secure-ls": "^1.2.6",
85 "shepherd.js": "^11.2.0",
86 "v-calendar": "^3.1.2",
87 "validator": "^13.11.0",
88 - "vue": "^3.3.11",
88 + "vue": "^3.3.13",
89 "vue-advanced-cropper": "^2.8.8",
90 "vue-cal": "^4.8.1",
91 - "vue-chartjs": "^5.2.0",
91 + "vue-chartjs": "^5.3.0",
92 "vue-highlight-words": "^3.0.1",
93 "vue-i18n": "^9.8.0",
94 "vue-maplibre-gl": "^3.0.3",
@@ -104,14 +104,14 @@
104 "@css-render/vue3-ssr": "^0.15.12",
105 "@faker-js/faker": "^8.3.1",
106 "@iconify/vue": "^4.1.1",
107 - "@rushstack/eslint-patch": "^1.6.0",
107 + "@rushstack/eslint-patch": "^1.6.1",
108 "@tsconfig/node18": "^18.2.2",
109 "@types/bytes": "^3.1.4",
110 "@types/fs-extra": "^11.0.4",
111 "@types/inquirer": "^9.0.7",
112 "@types/jsdom": "^21.1.6",
113 "@types/lodash": "^4.14.202",
114 - "@types/node": "^20.10.4",
114 + "@types/node": "^20.10.5",
115 "@types/validator": "^13.11.7",
116 "@vitejs/plugin-vue": "^4.5.2",
117 "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -119,10 +119,10 @@
119 "@vue/eslint-config-prettier": "^8.0.0",
120 "@vue/eslint-config-typescript": "^12.0.0",
121 "@vue/test-utils": "^2.4.3",
122 - "@vue/tsconfig": "^0.4.0",
122 + "@vue/tsconfig": "^0.5.1",
123 "autoprefixer": "^10.4.16",
124 "cypress": "^13.6.1",
125 - "eslint": "^8.55.0",
125 + "eslint": "^8.56.0",
126 "eslint-plugin-cypress": "^2.15.1",
127 "eslint-plugin-vue": "^9.19.2",
128 "fs-extra": "^11.2.0",
@@ -136,15 +136,15 @@
136 "sass": "^1.69.5",
137 "start-server-and-test": "^2.0.3",
138 "tailwind-config-viewer": "^1.7.3",
139 - "tailwindcss": "^3.3.6",
139 + "tailwindcss": "^3.4.0",
140 "taze": "^0.13.0",
141 "ts-node": "^10.9.2",
142 "typescript": "~5.3.3",
143 "unplugin-vue-components": "^0.26.0",
144 - "vite": "^5.0.7",
144 + "vite": "^5.0.10",
145 "vite-svg-loader": "^5.1.0",
146 - "vitest": "^1.0.4",
147 - "vue-tsc": "^1.8.25"
146 + "vitest": "^1.1.0",
147 + "vue-tsc": "^1.8.26"
148 },
149 "engines": {
150 "node": ">=16.0.0 <20.5.0"
src/api/auth.ts
+4 -1
@@ -1,6 +1,6 @@
1 import type { FlaskBaseResponse } from "@/types/flask.d"
2 import { HttpClient } from "./httpClient"
3 -import type { LoginPayload, RegisterPayload } from "@/types/auth.d"
3 +import type { AuthUser, LoginPayload, RegisterPayload } from "@/types/auth.d"
4 import jsonToFormData from "@ajoelp/json-to-formdata"
5
6 export default {
@@ -16,5 +16,8 @@ export default {
16 },
17 refresh() {
18 return HttpClient.get<FlaskBaseResponse & { access_token: string; token_type: string }>("/auth/refresh")
19 + },
20 + getUsers() {
21 + return HttpClient.get<FlaskBaseResponse & { users: AuthUser[] }>("/auth/users")
22 }
23 }
src/api/index.ts
+3 -1
@@ -10,6 +10,7 @@ import healthchecks from "./healthchecks"
10 import threatIntel from "./threatIntel"
11 import askSocfortress from "./askSocfortress"
12 import customers from "./customers"
13 +import logs from "./logs"
14
15 export default {
16 agents,
@@ -23,5 +24,6 @@ export default {
24 healthchecks,
25 threatIntel,
26 askSocfortress,
26 - customers
27 + customers,
28 + logs
29 }
src/api/logs.ts new
+48
@@ -0,0 +1,48 @@
1 +import { type FlaskBaseResponse } from "@/types/flask.d"
2 +import { HttpClient } from "./httpClient"
3 +import type { Customer } from "@/types/customers.d"
4 +import type { Log, LogEventType } from "@/types/logs.d"
5 +
6 +export type LogsQueryTimeRange = `${number}${"h" | "d" | "w"}`
7 +export type LogsQueryEventType = `${LogEventType}`
8 +export type LogsQuery = { userId: string } | { timeRange: LogsQueryTimeRange } | { eventType: LogsQueryEventType }
9 +
10 +// Extraction of keys from the union type LogsQuery
11 +type KeysOfLogsQuery<T> = T extends { [K in keyof T]: any } ? keyof T : never
12 +
13 +// Union of values extracted from keys
14 +export type LogsQueryTypes = KeysOfLogsQuery<LogsQuery>
15 +export type LogsQueryValues = string | LogsQueryTimeRange | LogsQueryEventType
16 +
17 +export default {
18 + getLogs(query?: LogsQuery) {
19 + let method: "get" | "post" = "get"
20 + let url = "logs"
21 + let body: any = undefined
22 +
23 + if (query && "userId" in query) {
24 + method = "get"
25 + url = `/logs/${query.userId}`
26 + body = undefined
27 + } else if (query && "timeRange" in query) {
28 + method = "post"
29 + url = `/logs/timerange`
30 + body = {
31 + time_range: query.timeRange
32 + }
33 + } else if (query && "eventType" in query) {
34 + method = "post"
35 + url = `/logs/${query.eventType}`
36 + body = undefined
37 + }
38 +
39 + return HttpClient[method]<FlaskBaseResponse & { logs: Log[] }>(url, body)
40 + },
41 + purge(timeRange?: LogsQueryTimeRange) {
42 + const url = timeRange ? `/logs/timerange` : `/logs`
43 + return HttpClient.delete<FlaskBaseResponse & { logs: Log[] }>(
44 + url,
45 + timeRange ? { data: { time_range: timeRange } } : undefined
46 + )
47 + }
48 +}
src/api/soc.ts
+5 -3
@@ -20,8 +20,11 @@ export default {
20 getAlertsBookmark() {
21 return HttpClient.get<FlaskBaseResponse & { bookmarked_alerts: SocAlert[] }>(`/soc/alerts/bookmark`)
22 },
23 - getAlertsByUser(userId: string) {
24 - return HttpClient.get<FlaskBaseResponse & { alerts: SocAlert[] }>(`/soc/alerts/alerts_by_user/${userId}`)
23 + getAlertsByUser(userId: string, signal?: AbortSignal) {
24 + return HttpClient.get<FlaskBaseResponse & { alerts: SocAlert[] }>(
25 + `/soc/alerts/alerts_by_user/${userId}`,
26 + signal ? { signal } : {}
27 + )
28 },
29 addAlertBookmark(alertId: string) {
30 return HttpClient.post<FlaskBaseResponse & { alert: SocAlert }>(`/soc/alerts/bookmark/${alertId}`)
@@ -53,7 +56,6 @@ export default {
56 older_than: payload?.olderThan || 1,
57 time_unit: payload?.unit || "days"
58 }
56 - // eslint-disable-next-line no-mixed-spaces-and-tabs
59 }
60 : undefined
61 )
src/assets/scss/helpers.scss
+4
@@ -106,3 +106,7 @@
106 .text-warning-color {
107 color: var(--warning-color);
108 }
109 +
110 +.text-error-color {
111 + color: var(--error-color);
112 +}
src/components/logs/LogItem.vue new
+166
@@ -0,0 +1,166 @@
1 +<template>
2 + <div class="log-item flex flex-col gap-2 px-5 py-3" :class="`type-${eventTypeLower}`">
3 + <div class="header-box flex justify-end">
4 + <div class="time">
5 + {{ formatDate(log.timestamp) }}
6 + </div>
7 + </div>
8 + <div class="main-box flex justify-between gap-4">
9 + <div class="content flex flex-col gap-2">
10 + <div class="resource flex flex-wrap gap-3">
11 + <div class="status" :class="`cat-${statusCategory}`">
12 + <code>{{ log.status_code }}</code>
13 + </div>
14 + <div class="method" :class="methodLower">
15 + <strong>{{ log.method }}</strong>
16 + </div>
17 + <div class="route">{{ log.route }}</div>
18 + </div>
19 + <div class="title px-1">{{ log.message }}</div>
20 + <div class="description px-1" v-if="log.additional_info">{{ log.additional_info }}</div>
21 +
22 + <div class="badges-box flex flex-wrap items-center gap-3 mt-2">
23 + <Badge type="splitted" :color="log.event_type === LogEventType.ERROR ? 'danger' : undefined">
24 + <template #iconLeft>
25 + <Icon
26 + :name="log.event_type === LogEventType.ERROR ? ErrorIcon : InfoIcon"
27 + :size="14"
28 + ></Icon>
29 + </template>
30 + <template #label>Type</template>
31 + <template #value>{{ log.event_type }}</template>
32 + </Badge>
33 + <Badge type="splitted" v-if="log.user_id">
34 + <template #iconLeft>
35 + <Icon :name="UserIcon" :size="14"></Icon>
36 + </template>
37 + <template #value>
38 + <span class="flex items-center gap-2">
39 + <span>#{{ log.user_id }}</span>
40 + <span v-if="username" class="flex gap-2">
41 + <span>/</span>
42 + <span>
43 + {{ username }}
44 + </span>
45 + </span>
46 + </span>
47 + </template>
48 + </Badge>
49 + </div>
50 + </div>
51 + </div>
52 + </div>
53 +</template>
54 +
55 +<script setup lang="ts">
56 +import Icon from "@/components/common/Icon.vue"
57 +import "@/assets/scss/vuesjv-override.scss"
58 +import { useSettingsStore } from "@/stores/settings"
59 +import dayjs from "@/utils/dayjs"
60 +import { LogEventType, type Log } from "@/types/logs.d"
61 +import Badge from "@/components/common/Badge.vue"
62 +import { computed } from "vue"
63 +import type { AuthUser } from "@/types/auth.d"
64 +
65 +const { log, users } = defineProps<{ log: Log; users?: AuthUser[] }>()
66 +
67 +const InfoIcon = "carbon:information"
68 +const UserIcon = "carbon:user"
69 +const ErrorIcon = "majesticons:exclamation-line"
70 +
71 +const dFormats = useSettingsStore().dateFormat
72 +
73 +const statusCategory = computed(() => log.status_code.toString()[0])
74 +const methodLower = computed(() => log.method.toLowerCase())
75 +const eventTypeLower = computed(() => log.event_type.toLowerCase())
76 +const username = computed(() => {
77 + if (!users?.length) return ""
78 +
79 + const user = users.find(o => o.id.toString() === log.user_id?.toString())
80 +
81 + return user?.username || ""
82 +})
83 +
84 +function formatDate(timestamp: string | number | Date, utc: boolean = true): string {
85 + return dayjs(timestamp).utc(utc).format(dFormats.datetime)
86 +}
87 +</script>
88 +
89 +<style lang="scss" scoped>
90 +.log-item {
91 + border-radius: var(--border-radius);
92 + background-color: var(--bg-color);
93 + border: var(--border-small-050);
94 +
95 + .header-box {
96 + font-family: var(--font-family-mono);
97 + font-size: 13px;
98 +
99 + .time {
100 + color: var(--fg-secondary-color);
101 + }
102 + }
103 +
104 + .main-box {
105 + word-break: break-word;
106 +
107 + .resource {
108 + background-color: var(--bg-secondary-color);
109 + font-family: var(--font-family-mono);
110 + padding: 10px 12px;
111 + border-radius: var(--border-radius);
112 +
113 + .status {
114 + &.cat-2 {
115 + color: var(--success-color);
116 + }
117 + &.cat-3 {
118 + color: var(--success-color);
119 + }
120 + &.cat-4 {
121 + color: var(--warning-color);
122 + }
123 + &.cat-5 {
124 + color: var(--error-color);
125 + }
126 + }
127 +
128 + .method {
129 + color: var(--secondary1-color);
130 + &.option {
131 + color: var(--secondary3-color);
132 + }
133 + &.put {
134 + color: var(--secondary2-color);
135 + }
136 + &.post {
137 + color: var(--primary-color);
138 + }
139 + &.delete {
140 + color: var(--secondary4-color);
141 + }
142 + }
143 +
144 + .route {
145 + font-size: 14px;
146 + }
147 + }
148 +
149 + .description {
150 + color: var(--fg-secondary-color);
151 + font-size: 13px;
152 + }
153 + }
154 +
155 + &.type- {
156 + &error {
157 + border-color: var(--secondary4-opacity-010-color);
158 +
159 + .resource {
160 + background-color: var(--secondary4-opacity-005-color);
161 + border: 1px solid var(--secondary4-opacity-030-color);
162 + }
163 + }
164 + }
165 +}
166 +</style>
src/components/logs/LogsFilters.vue new
+196
@@ -0,0 +1,196 @@
1 +<template>
2 + <div class="py-1 flex flex-col gap-2">
3 + <div class="px-3 flex items-center justify-between gap-4">
4 + <small>Filter by:</small>
5 + <n-select
6 + v-model:value="filterType"
7 + :options="filtersAvailable"
8 + placeholder="Select"
9 + size="tiny"
10 + clearable
11 + class="!w-24"
12 + />
13 + </div>
14 + <div class="px-3 !w-72">
15 + <div class="flex grow" v-if="filterType === 'userId'">
16 + <n-select
17 + v-if="userIdOptions.length"
18 + v-model:value="filterUserId"
19 + :options="userIdOptions"
20 + :loading="loadingUsers"
21 + :disabled="loadingUsers"
22 + :placeholder="loadingUsers ? 'Loading users...' : 'Select User'"
23 + class="grow"
24 + />
25 + <n-input
26 + v-model:value="filterUserId"
27 + :loading="loadingUsers"
28 + :disabled="loadingUsers"
29 + :placeholder="loadingUsers ? 'Loading users...' : 'Insert User ID'"
30 + class="grow"
31 + v-else
32 + />
33 + </div>
34 + <n-select
35 + v-if="filterType === 'eventType'"
36 + v-model:value="filterEventType"
37 + :options="eventTypeOptions"
38 + placeholder="Event"
39 + class="grow"
40 + />
41 + <n-input-group v-if="filterType === 'timeRange'">
42 + <n-select
43 + v-model:value="filterTimeRange.unit"
44 + :options="unitOptions"
45 + placeholder="Time unit"
46 + class="!w-40"
47 + />
48 + <n-input-number v-model:value="filterTimeRange.time" :min="1" placeholder="Time" class="grow" />
49 + </n-input-group>
50 + </div>
51 + <div class="px-3 flex justify-end gap-2">
52 + <n-button size="small" @click="close()" secondary>Close</n-button>
53 + <n-button size="small" @click="submit()" type="primary" secondary>Submit</n-button>
54 + </div>
55 + </div>
56 +</template>
57 +
58 +<script setup lang="ts">
59 +import { ref, onBeforeMount, computed, watch, toRefs } from "vue"
60 +import { NButton, NSelect, NInputGroup, NInputNumber, NInput } from "naive-ui"
61 +import _cloneDeep from "lodash/cloneDeep"
62 +import _toSafeInteger from "lodash/toSafeInteger"
63 +import type { LogsQueryEventType, LogsQueryTimeRange, LogsQueryTypes, LogsQueryValues } from "@/api/logs"
64 +import Api from "@/api"
65 +import { LogEventType } from "@/types/logs.d"
66 +import type { AuthUser } from "@/types/auth.d"
67 +
68 +const emit = defineEmits<{
69 + (e: "close"): void
70 + (e: "submit"): void
71 + (e: "update:filtered", value: boolean): void
72 +}>()
73 +
74 +const type = defineModel<LogsQueryTypes | null>("type", { default: null })
75 +const value = defineModel<LogsQueryValues | null>("value", { default: null })
76 +
77 +const props = defineProps<{ users?: AuthUser[]; fetchingUsers?: boolean }>()
78 +const { users, fetchingUsers } = toRefs(props)
79 +
80 +const loadingUsers = ref(false)
81 +
82 +watch(fetchingUsers, val => {
83 + loadingUsers.value = val
84 +})
85 +
86 +const filtered = computed(() => type.value !== null && value.value !== null)
87 +
88 +const filtersAvailable: { label: string; value: LogsQueryTypes | "" }[] = [
89 + { label: "User", value: "userId" },
90 + { label: "Event", value: "eventType" },
91 + { label: "Time", value: "timeRange" }
92 +]
93 +
94 +const filterType = ref<LogsQueryTypes | null>(null)
95 +const filterValue = ref<LogsQueryValues | null>(null)
96 +
97 +const filterTimeRange = ref({
98 + unit: "h",
99 + time: 1
100 +})
101 +
102 +const unitOptions: { label: string; value: "h" | "d" | "w" }[] = [
103 + { label: "Hours", value: "h" },
104 + { label: "Days", value: "d" },
105 + { label: "Weeks", value: "w" }
106 +]
107 +
108 +const filterEventType = ref<LogsQueryEventType>(LogEventType.INFO)
109 +
110 +const eventTypeOptions: { label: string; value: LogsQueryEventType }[] = [
111 + { label: "Info", value: LogEventType.INFO },
112 + { label: "Error", value: LogEventType.ERROR }
113 +]
114 +
115 +const filterUserId = ref<string | null>(null)
116 +
117 +const userIdOptions = ref<{ label: string; value: string }[]>([])
118 +
119 +watch(
120 + filtered,
121 + val => {
122 + emit("update:filtered", val)
123 + },
124 + { immediate: true }
125 +)
126 +
127 +function close() {
128 + emit("close")
129 +}
130 +
131 +function submit() {
132 + if (filterType.value === null) {
133 + filterValue.value = null
134 + }
135 + if (filterType.value === "timeRange") {
136 + filterValue.value = (filterTimeRange.value.time + filterTimeRange.value.unit) as LogsQueryTimeRange
137 + }
138 + if (filterType.value === "eventType") {
139 + filterValue.value = filterEventType.value
140 + }
141 + if (filterType.value === "userId") {
142 + filterValue.value = filterUserId.value
143 + }
144 +
145 + type.value = _cloneDeep(filterType.value)
146 + value.value = _cloneDeep(filterValue.value)
147 +
148 + emit("submit")
149 +}
150 +
151 +function getUsers() {
152 + loadingUsers.value = true
153 +
154 + Api.auth
155 + .getUsers()
156 + .then(res => {
157 + if (res.data.success) {
158 + setUsers(res.data?.users)
159 + }
160 + })
161 + .finally(() => {
162 + loadingUsers.value = false
163 + })
164 +}
165 +
166 +function setUsers(users: AuthUser[]) {
167 + userIdOptions.value = (users || []).map(o => ({
168 + label: `#${o.id} - ${o.username}`,
169 + value: o.id + ""
170 + }))
171 +}
172 +
173 +onBeforeMount(() => {
174 + if (users.value !== undefined) {
175 + setUsers(users.value)
176 + } else {
177 + getUsers()
178 + }
179 +
180 + filterType.value = _cloneDeep(type.value)
181 + filterValue.value = _cloneDeep(value.value)
182 +
183 + if (filterValue.value) {
184 + if (filterType.value === "timeRange") {
185 + filterTimeRange.value.unit = filterValue.value[filterValue.value?.length - 1]
186 + filterTimeRange.value.time = _toSafeInteger(filterValue.value.slice(0, -1))
187 + }
188 + if (filterType.value === "eventType") {
189 + filterEventType.value = filterValue.value as LogsQueryEventType
190 + }
191 + if (filterType.value === "userId") {
192 + filterUserId.value = filterValue.value
193 + }
194 + }
195 +})
196 +</script>
src/components/logs/LogsList.vue new
+294
@@ -0,0 +1,294 @@
1 +<template>
2 + <div class="logs-list">
3 + <div class="header flex items-center justify-end gap-2" ref="header">
4 + <div class="info grow flex gap-2">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
8 + <n-button size="small" class="!cursor-help">
9 + <template #icon>
10 + <Icon :name="InfoIcon"></Icon>
11 + </template>
12 + </n-button>
13 + </div>
14 + </template>
15 + <div class="flex flex-col gap-2">
16 + <div class="box">
17 + Total :
18 + <code>{{ total }}</code>
19 + </div>
20 + <div class="box">
21 + Event Info :
22 + <code>{{ eventInfoTotal }}</code>
23 + </div>
24 + <div class="box text-error-color">
25 + Event Error :
26 + <code>{{ eventErrorTotal }}</code>
27 + </div>
28 + </div>
29 + </n-popover>
30 +
31 + <n-button size="small" type="error" ghost @click="showPurgeConfirm = true" :loading="loadingPurge">
32 + <div class="flex items-center gap-2">
33 + <Icon :name="TrashIcon" :size="16"></Icon>
34 + <span class="hidden xs:block">Purge</span>
35 + </div>
36 + </n-button>
37 + </div>
38 + <n-pagination
39 + v-model:page="currentPage"
40 + v-model:page-size="pageSize"
41 + :page-slot="pageSlot"
42 + :show-size-picker="showSizePicker"
43 + :page-sizes="pageSizes"
44 + :item-count="total"
45 + :simple="simpleMode"
46 + />
47 + <n-popover
48 + :show="showFilters"
49 + trigger="manual"
50 + overlap
51 + placement="right"
52 + style="padding-left: 0; padding-right: 0"
53 + >
54 + <template #trigger>
55 + <div class="bg-color border-radius">
56 + <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
57 + <n-button size="small" @click="showFilters = true">
58 + <template #icon>
59 + <Icon :name="FilterIcon"></Icon>
60 + </template>
61 + </n-button>
62 + </n-badge>
63 + </div>
64 + </template>
65 + <LogsFilters
66 + v-model:type="filterType"
67 + v-model:value="filterValue"
68 + v-model:filtered="filtered"
69 + :users="usersList"
70 + :loadingUsers="loadingUsers"
71 + @submit="getData()"
72 + @close="showFilters = false"
73 + />
74 + </n-popover>
75 + </div>
76 + <n-spin :show="loading">
77 + <div class="list my-3">
78 + <template v-if="logsList.length">
79 + <LogItem
80 + v-for="log of itemsPaginated"
81 + :key="log.id"
82 + :log="log"
83 + :users="usersList"
84 + class="item-appear item-appear-bottom item-appear-005 mb-2"
85 + />
86 + </template>
87 + <template v-else>
88 + <n-empty description="No Logs found" class="justify-center h-48" v-if="!loading" />
89 + </template>
90 + </div>
91 + </n-spin>
92 + <div class="footer flex justify-end">
93 + <n-pagination
94 + v-model:page="currentPage"
95 + :page-size="pageSize"
96 + :item-count="total"
97 + :page-slot="6"
98 + v-if="itemsPaginated.length > 3"
99 + />
100 + </div>
101 +
102 + <n-modal v-model:show="showPurgeConfirm" preset="dialog" type="warning" title="Purge Logs">
103 + <div class="mt-5 flex flex-col gap-2">
104 + <div>Are you sure you want to purge Logs ?</div>
105 + <n-select
106 + v-model:value="purgeSelected"
107 + :options="purgeOptions"
108 + :disabled="loadingPurge"
109 + placeholder="Select time range"
110 + />
111 + </div>
112 + <template #action>
113 + <div class="flex gap-3">
114 + <n-button size="small" ghost @click="showPurgeConfirm = false">Cancel</n-button>
115 + <n-button size="small" type="warning" @click="purge()" :loading="loadingPurge">
116 + Yes I'm sure
117 + </n-button>
118 + </div>
119 + </template>
120 + </n-modal>
121 + </div>
122 +</template>
123 +
124 +<script setup lang="ts">
125 +import { ref, onBeforeMount, computed, toRefs } from "vue"
126 +import { useMessage, NSpin, NPopover, NButton, NEmpty, NPagination, NBadge, NModal, NSelect } from "naive-ui"
127 +import Api from "@/api"
128 +import _orderBy from "lodash/orderBy"
129 +import Icon from "@/components/common/Icon.vue"
130 +import { nanoid } from "nanoid"
131 +import { useResizeObserver } from "@vueuse/core"
132 +import LogsFilters from "./LogsFilters.vue"
133 +import LogItem from "./LogItem.vue"
134 +import { LogEventType, type Log } from "@/types/logs.d"
135 +import type { LogsQuery, LogsQueryTimeRange, LogsQueryTypes, LogsQueryValues } from "@/api/logs"
136 +import type { AuthUser } from "@/types/auth.d"
137 +
138 +interface LogExt extends Log {
139 + id?: string
140 +}
141 +
142 +const props = defineProps<{ userId?: string }>()
143 +const { userId } = toRefs(props)
144 +
145 +const message = useMessage()
146 +const loadingUsers = ref(false)
147 +const loading = ref(false)
148 +const loadingPurge = ref(false)
149 +const showPurgeConfirm = ref(false)
150 +const usersList = ref<AuthUser[]>([])
151 +const logsList = ref<LogExt[]>([])
152 +const showFilters = ref(false)
153 +
154 +const pageSize = ref(25)
155 +const currentPage = ref(1)
156 +const simpleMode = ref(false)
157 +const showSizePicker = ref(true)
158 +const pageSizes = [10, 25, 50, 100]
159 +const header = ref()
160 +const pageSlot = ref(8)
161 +
162 +const itemsPaginated = computed(() => {
163 + const from = (currentPage.value - 1) * pageSize.value
164 + const to = currentPage.value * pageSize.value
165 +
166 + const list = _orderBy(logsList.value, ["timestamp"], ["desc"])
167 +
168 + return list.slice(from, to)
169 +})
170 +
171 +const FilterIcon = "carbon:filter-edit"
172 +const TrashIcon = "carbon:trash-can"
173 +const InfoIcon = "carbon:information"
174 +
175 +const total = computed<number>(() => {
176 + return logsList.value.length || 0
177 +})
178 +
179 +const eventInfoTotal = computed<number>(() => {
180 + return logsList.value.filter(o => o.event_type === LogEventType.INFO).length || 0
181 +})
182 +const eventErrorTotal = computed<number>(() => {
183 + return logsList.value.filter(o => o.event_type === LogEventType.ERROR).length || 0
184 +})
185 +
186 +const filterType = ref<LogsQueryTypes | null>(null)
187 +const filterValue = ref<LogsQueryValues | null>(null)
188 +
189 +const filtered = ref(false)
190 +
191 +const purgeSelected = ref<LogsQueryTimeRange | "">("")
192 +const purgeOptions: { label: string; value: LogsQueryTimeRange | string }[] = [
193 + { label: "All Logs", value: "" },
194 + { label: "1 Hour", value: "1h" },
195 + { label: "6 Hours", value: "6h" },
196 + { label: "12 Hours", value: "12h" },
197 + { label: "1 Day", value: "1d" },
198 + { label: "3 Days", value: "3d" },
199 + { label: "1 Week", value: "1w" }
200 +]
201 +
202 +function purge() {
203 + loadingPurge.value = true
204 +
205 + Api.logs
206 + .purge(purgeSelected.value || undefined)
207 + .then(res => {
208 + if (res.data.success) {
209 + getData()
210 + message.success(res.data?.message || "Logs purged successfully")
211 + } else {
212 + message.warning(res.data?.message || "An error occurred. Please try again later.")
213 + }
214 + })
215 + .catch(err => {
216 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
217 + })
218 + .finally(() => {
219 + loadingPurge.value = false
220 + })
221 +}
222 +
223 +function getData() {
224 + showFilters.value = false
225 + loading.value = true
226 +
227 + const query =
228 + filterType.value && filterValue.value ? ({ [filterType.value]: filterValue.value } as LogsQuery) : undefined
229 +
230 + Api.logs
231 + .getLogs(query)
232 + .then(res => {
233 + if (res.data.success) {
234 + logsList.value = (res.data.logs || []).map((o: LogExt) => {
235 + o.id = nanoid()
236 + return o
237 + })
238 + } else {
239 + message.warning(res.data?.message || "An error occurred. Please try again later.")
240 + }
241 + })
242 + .catch(err => {
243 + logsList.value = []
244 +
245 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
246 + })
247 + .finally(() => {
248 + loading.value = false
249 + })
250 +}
251 +
252 +function getUsers() {
253 + loadingUsers.value = true
254 +
255 + Api.auth
256 + .getUsers()
257 + .then(res => {
258 + if (res.data.success) {
259 + usersList.value = res.data?.users || []
260 + }
261 + })
262 + .finally(() => {
263 + loadingUsers.value = false
264 + })
265 +}
266 +
267 +useResizeObserver(header, entries => {
268 + const entry = entries[0]
269 + const { width } = entry.contentRect
270 +
271 + pageSlot.value = width < 700 ? 5 : 8
272 + simpleMode.value = width < 550
273 +})
274 +
275 +onBeforeMount(() => {
276 + if (userId.value) {
277 + filterType.value = "userId"
278 + filterValue.value = userId.value
279 + filtered.value = true
280 + }
281 +
282 + getUsers()
283 + getData()
284 +})
285 +</script>
286 +
287 +<style lang="scss" scoped>
288 +.logs-list {
289 + .list {
290 + container-type: inline-size;
291 + min-height: 200px;
292 + }
293 +}
294 +</style>
src/components/profile/ProfileSettings.vue
+13 -4
@@ -3,12 +3,17 @@
3 <n-card>
4 <n-form ref="formRef" :label-width="80" :model="formValue" :rules="formRules">
5 <div class="title">General</div>
6 - <div class="flex flex-col md:flex-row md:gap-4">
6 + <div class="flex flex-col md:flex-row md:gap-6">
7 <n-form-item label="Date Format" path="dateFormat" class="basis-1/3">
8 <n-select v-model:value="formValue.dateFormat" :options="dateFormatsAvailables" />
9 </n-form-item>
10 - <n-form-item label="24 Hour" path="hours24" class="basis-1/3">
11 - <n-checkbox v-model:checked="formValue.hours24">Time 24 Hour</n-checkbox>
10 + <n-form-item label="Time Format" path="hours24" class="basis-1/3">
11 + <n-radio-group v-model:value="formValue.hours24" name="radiogroup">
12 + <div class="flex flex-wrap gap-3">
13 + <n-radio :value="true" :label="`24 Hours [ ${h24} ]`" />
14 + <n-radio :value="false" :label="`12 Hours [ ${h12} ]`" />
15 + </div>
16 + </n-radio-group>
17 </n-form-item>
18 </div>
19 <div class="title">Profile</div>
@@ -44,15 +49,19 @@ import {
49 NInput,
50 NButton,
51 NSelect,
47 - NCheckbox,
52 + NRadio,
53 + NRadioGroup,
54 type FormValidationError,
55 useMessage,
56 type FormInst
57 } from "naive-ui"
58 import { useSettingsStore } from "@/stores/settings"
59 +import dayjs from "@/utils/dayjs"
60
61 const settingsStore = useSettingsStore()
62
63 +const h24 = dayjs().format("HH:mm")
64 +const h12 = dayjs().format("h:mm a")
65 const dateFormatsAvailables = settingsStore.dateFormatsAvailables.map(i => ({ label: i, value: i }))
66 const currentSateFormat = settingsStore.rawDateFormat
67 const hours24 = settingsStore.hours24
src/components/soc/SocCaseNotesList.vue
+4 -1
@@ -23,6 +23,7 @@ import { useMessage, NSpin, NInput, NEmpty } from "naive-ui"
23 import type { SocNote } from "@/types/soc/note.d"
24 import { refDebounced } from "@vueuse/core"
25 import { toRefs } from "vue"
26 +import axios from "axios"
27
28 const requested = defineModel<boolean | undefined>("requested", { default: false })
29
@@ -56,7 +57,9 @@ function getNotes() {
57 }
58 })
59 .catch(err => {
59 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
60 + if (!axios.isCancel(err)) {
61 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
62 + }
63 })
64 .finally(() => {
65 loadingNotes.value = false
src/components/soc/SocUserAlerts.vue
+16 -4
@@ -1,7 +1,9 @@
1 <template>
2 <n-spin :show="loadingAlerts" :size="14">
3 <div class="flex alert-list items-center gap-3" v-if="!loadingAlerts">
4 - <strong>{{ alertsList.length }}</strong>
4 + <span :class="{ 'text-secondary-color': !alertsList.length, 'font-bold': alertsList.length }">
5 + {{ alertsList.length || "No Alters" }}
6 + </span>
7 <div class="flex flex-wrap gap-2">
8 <n-tooltip v-for="alert of alertsList" :key="alert.alert_id">
9 <template #trigger>
@@ -16,10 +18,11 @@
18
19 <script setup lang="ts">
20 import type { SocAlert } from "@/types/soc/alert.d"
19 -import { onBeforeMount, ref } from "vue"
21 +import { onBeforeMount, onBeforeUnmount, ref } from "vue"
22 import Api from "@/api"
23 import { useMessage, NTooltip, NSpin } from "naive-ui"
24 import { useRouter } from "vue-router"
25 +import axios from "axios"
26
27 const { userId } = defineProps<{
28 userId: string | number
@@ -29,6 +32,7 @@ const loadingAlerts = ref(false)
32 const alertsList = ref<SocAlert[]>([])
33 const router = useRouter()
34 const message = useMessage()
35 +let abortController: AbortController | null = null
36
37 function gotoSocAlert(socId: string | number) {
38 router.push(`/soc/alerts?id=${socId}`).catch(() => {})
@@ -37,8 +41,10 @@ function gotoSocAlert(socId: string | number) {
41 function getAlerts() {
42 loadingAlerts.value = true
43
44 + abortController = new AbortController()
45 +
46 Api.soc
41 - .getAlertsByUser(userId.toString())
47 + .getAlertsByUser(userId.toString(), abortController.signal)
48 .then(res => {
49 if (res.data.success) {
50 alertsList.value = res.data?.alerts || []
@@ -47,7 +53,9 @@ function getAlerts() {
53 }
54 })
55 .catch(err => {
50 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
56 + if (!axios.isCancel(err)) {
57 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
58 + }
59 })
60 .finally(() => {
61 loadingAlerts.value = false
@@ -57,6 +65,10 @@ function getAlerts() {
65 onBeforeMount(() => {
66 getAlerts()
67 })
68 +
69 +onBeforeUnmount(() => {
70 + abortController?.abort()
71 +})
72 </script>
73
74 <style lang="scss" scoped>
src/composables/useHealthchecksNotify.ts
+9 -12
@@ -34,14 +34,13 @@ export function useHealthchecksNotify() {
34 actionTitle: "See Graylog Metrics"
35 }
36
37 - if (val === null) {
38 - useNotifications().prepend(obj)
39 - } else if (val >= uncommittedJournalEntriesThreshold) {
37 + if (val !== null && val >= uncommittedJournalEntriesThreshold) {
38 obj.type = "warning"
39 obj.title = "Uncommitted Journal Entries"
40 obj.description = `Value ${val} (over ${uncommittedJournalEntriesThreshold})`
43 - useNotifications().prepend(obj)
41 }
42 +
43 + useNotifications().prepend(obj, { autoNotify: true })
44 }
45 })
46
@@ -61,14 +60,13 @@ export function useHealthchecksNotify() {
60 actionTitle: "See Cluster"
61 }
62
64 - if (val === null) {
65 - useNotifications().prepend(obj)
66 - } else if (val !== IndexHealth.GREEN) {
63 + if (val !== null && val !== IndexHealth.GREEN) {
64 obj.type = val === IndexHealth.YELLOW ? "warning" : "error"
65 obj.title = "Cluster Health"
66 obj.description = `${_capitalize(clusterName.value || "Cluster")} is ${val.toUpperCase()}`
70 - useNotifications().prepend(obj)
67 }
68 +
69 + useNotifications().prepend(obj, { autoNotify: true })
70 }
71 })
72
@@ -88,14 +86,13 @@ export function useHealthchecksNotify() {
86 actionTitle: "See Healthcheck"
87 }
88
91 - if (val === null) {
92 - useNotifications().prepend(obj)
93 - } else if (val.length) {
89 + if (val !== null && val.length) {
90 obj.type = "warning"
91 obj.title = "Influx Alert"
92 obj.description = `${val.length} Critical ${val.length > 1 ? "issues" : "issue"}`
97 - useNotifications().prepend(obj)
93 }
94 +
95 + useNotifications().prepend(obj, { autoNotify: true })
96 }
97 })
98 }
src/composables/useNotifications.ts
+21 -1
@@ -20,6 +20,13 @@ export interface Notification {
20 actionTitle?: string
21 }
22
23 +export interface PrependOptions {
24 + /** prepend and send a notification */
25 + sendNotify?: boolean
26 + /** send a notification only if there isn't any item with match id/type/category */
27 + autoNotify?: boolean
28 +}
29 +
30 const list = useStorage<Notification[]>("notifications-list", [], localStorage)
31
32 export function useNotifications() {
@@ -59,7 +66,20 @@ export function useNotifications() {
66 deleteAll: () => {
67 list.value = []
68 },
62 - prepend: (newItem: Notification, sendNotify: boolean = true) => {
69 + prepend: (newItem: Notification, options?: PrependOptions) => {
70 + let sendNotify = options?.sendNotify || false
71 + const autoNotify = options?.autoNotify || false
72 +
73 + if (autoNotify) {
74 + const item = list.value.find(
75 + o => o.id === newItem.id && o.type === newItem.type && o.category === newItem.category
76 + )
77 +
78 + if (!item) {
79 + sendNotify = true
80 + }
81 + }
82 +
83 if (sendNotify) {
84 const notify: NotificationObject = {
85 title: newItem.title,
src/layouts/common/Navbar/items.tsx
+14
@@ -214,6 +214,20 @@ export default function getItems(mode: "vertical" | "horizontal", collapsed: boo
214 key: "Customers",
215 icon: renderIcon(BlankIcon)
216 },
217 + {
218 + label: () =>
219 + h(
220 + RouterLink,
221 + {
222 + to: {
223 + name: "Logs"
224 + }
225 + },
226 + { default: () => "Logs" }
227 + ),
228 + key: "Logs",
229 + icon: renderIcon(BlankIcon)
230 + },
231 {
232 type: "divider"
233 },
src/router/index.ts
+6
@@ -116,6 +116,12 @@ const router = createRouter({
116 component: () => import("@/views/socfortress/Customers.vue"),
117 meta: { title: "Customers", auth: true, roles: UserRole.All }
118 },
119 + {
120 + path: "/logs",
121 + name: "Logs",
122 + component: () => import("@/views/socfortress/Logs.vue"),
123 + meta: { title: "Logs", auth: true, roles: UserRole.All }
124 + },
125
126 // DEMO PAGES ==========================================================
127
src/types/auth.d.ts
+5
@@ -36,3 +36,8 @@ export interface User {
36 access_token: string
37 role: UserRole
38 }
39 +
40 +export interface AuthUser {
41 + id: number
42 + username: string
43 +}
src/types/logs.d.ts new
+21
@@ -0,0 +1,21 @@
1 +export interface Log {
2 + event_type: LogEventType
3 + user_id: number | null
4 + route: string
5 + method: LogMethod
6 + status_code: number
7 + message: string
8 + additional_info: string | null
9 + timestamp: string
10 +}
11 +
12 +export enum LogEventType {
13 + ERROR = "Error",
14 + INFO = "Info"
15 +}
16 +
17 +export enum LogMethod {
18 + Get = "GET",
19 + Options = "OPTIONS",
20 + Post = "POST"
21 +}
src/views/socfortress/Logs.vue new
+21
@@ -0,0 +1,21 @@
1 +<template>
2 + <div class="page">
3 + <LogsList :userId="userId" />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import LogsList from "@/components/logs/LogsList.vue"
9 +import { onBeforeMount, ref } from "vue"
10 +import { useRoute } from "vue-router"
11 +
12 +const route = useRoute()
13 +
14 +const userId = ref<string | undefined>(undefined)
15 +
16 +onBeforeMount(() => {
17 + if (route.query?.user_id) {
18 + userId.value = route.query.user_id.toString()
19 + }
20 +})
21 +</script>