Compare commits

..

No commits in common. "cf696d200d3e222390a01ba20e833eb11a5ed881" and "00a51f5e6a703b0475cca3568c35f1e2acbab42a" have entirely different histories.

20 changed files with 190 additions and 399 deletions

View file

@ -397,7 +397,6 @@ jobs:
with:
tag_name: ${{ env.REL_VERSION }}
name: ${{ env.REL_VERSION }}
target_commitish: ${{ github.sha }}
draft: false
prerelease: ${{ env.IS_PRERELEASE }}
generate_release_notes: true

View file

@ -13,7 +13,7 @@ RUN corepack enable pnpm
FROM base AS builder
RUN pnpm add -g turbo@2.9.4
RUN pnpm add -g turbo@2.8.12
COPY . .

View file

@ -1118,7 +1118,7 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
epoch = int(timezone.now().timestamp())
# Fetch all relevant issues in a single query
issues = list(Issue.objects.filter(id__in=issue_ids, workspace__slug=slug, project_id=project_id))
issues = list(Issue.objects.filter(id__in=issue_ids))
issues_dict = {str(issue.id): issue for issue in issues}
issues_to_update = []

View file

@ -226,36 +226,21 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True,
)
if "role" in request.data:
# Only Admins can modify roles
if requested_project_member.role < ROLE.ADMIN.value and not is_workspace_admin:
return Response(
{"error": "You do not have permission to update roles"},
status=status.HTTP_403_FORBIDDEN,
)
if workspace_role in [5] and int(request.data.get("role", project_member.role)) in [15, 20]:
return Response(
{"error": "You cannot add a user with role higher than the workspace role"},
status=status.HTTP_400_BAD_REQUEST,
)
# Cannot modify a member whose role is equal to or higher than your own
if project_member.role >= requested_project_member.role and not is_workspace_admin:
return Response(
{"error": "You cannot update the role of a member with a role equal to or higher than your own"},
status=status.HTTP_403_FORBIDDEN,
)
new_role = int(request.data.get("role"))
# Cannot assign a role equal to or higher than your own
if new_role >= requested_project_member.role and not is_workspace_admin:
return Response(
{"error": "You cannot assign a role equal to or higher than your own"},
status=status.HTTP_403_FORBIDDEN,
)
# Cannot assign a role higher than the target's workspace role
if workspace_role in [5] and new_role in [15, 20]:
return Response(
{"error": "You cannot add a user with role higher than the workspace role"},
status=status.HTTP_400_BAD_REQUEST,
)
if (
"role" in request.data
and int(request.data.get("role", project_member.role)) > requested_project_member.role
and not is_workspace_admin
):
return Response(
{"error": "You cannot update a role that is higher than your own role"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True)

View file

@ -13,7 +13,7 @@ from bs4 import BeautifulSoup
from urllib.parse import urlparse, urljoin
import base64
import ipaddress
from typing import Dict, Any, Tuple
from typing import Dict, Any
from typing import Optional
from plane.db.models import IssueLink
from plane.utils.exception_logger import log_exception
@ -66,52 +66,6 @@ def validate_url_ip(url: str) -> None:
MAX_REDIRECTS = 5
def safe_get(
url: str,
headers: Optional[Dict[str, str]] = None,
timeout: int = 1,
) -> Tuple[requests.Response, str]:
"""
Perform a GET request that validates every redirect hop against private IPs.
Prevents SSRF by ensuring no redirect lands on a private/internal address.
Args:
url: The URL to fetch
headers: Optional request headers
timeout: Request timeout in seconds
Returns:
A tuple of (final Response object, final URL after redirects)
Raises:
ValueError: If any URL in the redirect chain points to a private IP
requests.RequestException: On network errors
RuntimeError: If max redirects exceeded
"""
validate_url_ip(url)
current_url = url
response = requests.get(
current_url, headers=headers, timeout=timeout, allow_redirects=False
)
redirect_count = 0
while response.is_redirect:
if redirect_count >= MAX_REDIRECTS:
raise RuntimeError(f"Too many redirects for URL: {url}")
redirect_url = response.headers.get("Location")
if not redirect_url:
break
current_url = urljoin(current_url, redirect_url)
validate_url_ip(current_url)
redirect_count += 1
response = requests.get(
current_url, headers=headers, timeout=timeout, allow_redirects=False
)
return response, current_url
def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
"""
Crawls a URL to extract the title and favicon.
@ -132,8 +86,26 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
title = None
final_url = url
validate_url_ip(final_url)
try:
response, final_url = safe_get(url, headers=headers)
# Manually follow redirects to validate each URL before requesting
redirect_count = 0
response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
while response.is_redirect and redirect_count < MAX_REDIRECTS:
redirect_url = response.headers.get("Location")
if not redirect_url:
break
# Resolve relative redirects against current URL
final_url = urljoin(final_url, redirect_url)
# Validate the redirect target BEFORE making the request
validate_url_ip(final_url)
redirect_count += 1
response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
if redirect_count >= MAX_REDIRECTS:
logger.warning(f"Too many redirects for URL: {url}")
soup = BeautifulSoup(response.content, "html.parser")
title_tag = soup.find("title")
@ -141,10 +113,8 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
except requests.RequestException as e:
logger.warning(f"Failed to fetch HTML for title: {str(e)}")
except (ValueError, RuntimeError) as e:
logger.warning(f"URL validation failed: {str(e)}")
# Fetch and encode favicon using final URL (after redirects) for correct relative href resolution
# Fetch and encode favicon using final URL (after redirects)
favicon_base64 = fetch_and_encode_favicon(headers, soup, final_url)
# Prepare result
@ -234,7 +204,9 @@ def fetch_and_encode_favicon(
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
}
response, _ = safe_get(favicon_url, headers=headers)
validate_url_ip(favicon_url)
response = requests.get(favicon_url, headers=headers, timeout=1)
# Get content type
content_type = response.headers.get("content-type", "image/x-icon")

View file

@ -1,126 +0,0 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
import pytest
from unittest.mock import patch, MagicMock
from plane.bgtasks.work_item_link_task import safe_get, validate_url_ip
def _make_response(status_code=200, headers=None, is_redirect=False, content=b""):
"""Create a mock requests.Response."""
resp = MagicMock()
resp.status_code = status_code
resp.is_redirect = is_redirect
resp.headers = headers or {}
resp.content = content
return resp
@pytest.mark.unit
class TestValidateUrlIp:
"""Test validate_url_ip blocks private/internal IPs."""
def test_rejects_private_ip(self):
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
mock_dns.return_value = [(None, None, None, None, ("192.168.1.1", 0))]
with pytest.raises(ValueError, match="private/internal"):
validate_url_ip("http://example.com")
def test_rejects_loopback(self):
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
mock_dns.return_value = [(None, None, None, None, ("127.0.0.1", 0))]
with pytest.raises(ValueError, match="private/internal"):
validate_url_ip("http://example.com")
def test_rejects_non_http_scheme(self):
with pytest.raises(ValueError, match="Only HTTP and HTTPS"):
validate_url_ip("file:///etc/passwd")
def test_allows_public_ip(self):
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
mock_dns.return_value = [(None, None, None, None, ("93.184.216.34", 0))]
validate_url_ip("https://example.com") # Should not raise
@pytest.mark.unit
class TestSafeGet:
"""Test safe_get follows redirects safely and blocks SSRF."""
@patch("plane.bgtasks.work_item_link_task.requests.get")
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
def test_returns_response_for_non_redirect(self, mock_validate, mock_get):
final_resp = _make_response(status_code=200, content=b"OK")
mock_get.return_value = final_resp
response, final_url = safe_get("https://example.com")
assert response is final_resp
assert final_url == "https://example.com"
mock_validate.assert_called_once_with("https://example.com")
@patch("plane.bgtasks.work_item_link_task.requests.get")
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
def test_follows_redirect_and_validates_each_hop(self, mock_validate, mock_get):
redirect_resp = _make_response(
status_code=301,
is_redirect=True,
headers={"Location": "https://other.com/page"},
)
final_resp = _make_response(status_code=200, content=b"OK")
mock_get.side_effect = [redirect_resp, final_resp]
response, final_url = safe_get("https://example.com")
assert response is final_resp
assert final_url == "https://other.com/page"
# Should validate both the initial URL and the redirect target
assert mock_validate.call_count == 2
mock_validate.assert_any_call("https://example.com")
mock_validate.assert_any_call("https://other.com/page")
@patch("plane.bgtasks.work_item_link_task.requests.get")
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
def test_blocks_redirect_to_private_ip(self, mock_validate, mock_get):
redirect_resp = _make_response(
status_code=302,
is_redirect=True,
headers={"Location": "http://192.168.1.1:8080"},
)
mock_get.return_value = redirect_resp
# First call (initial URL) succeeds, second call (redirect target) fails
mock_validate.side_effect = [None, ValueError("Access to private/internal networks is not allowed")]
with pytest.raises(ValueError, match="private/internal"):
safe_get("https://evil.com/redirect")
@patch("plane.bgtasks.work_item_link_task.requests.get")
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
def test_raises_on_too_many_redirects(self, mock_validate, mock_get):
redirect_resp = _make_response(
status_code=302,
is_redirect=True,
headers={"Location": "https://example.com/loop"},
)
mock_get.return_value = redirect_resp
with pytest.raises(RuntimeError, match="Too many redirects"):
safe_get("https://example.com/start")
@patch("plane.bgtasks.work_item_link_task.requests.get")
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
def test_succeeds_at_exact_max_redirects(self, mock_validate, mock_get):
"""After exactly MAX_REDIRECTS hops, if the final response is 200, it should succeed."""
redirect_resp = _make_response(
status_code=302,
is_redirect=True,
headers={"Location": "https://example.com/next"},
)
final_resp = _make_response(status_code=200, content=b"OK")
# 5 redirects then a 200
mock_get.side_effect = [redirect_resp] * 5 + [final_resp]
response, final_url = safe_get("https://example.com/start")
assert response is final_resp
assert not response.is_redirect

View file

@ -15,7 +15,7 @@ RUN apk update
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ARG TURBO_VERSION=2.9.4
ARG TURBO_VERSION=2.8.12
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
COPY . .
RUN turbo prune --scope=live --docker

View file

@ -13,7 +13,7 @@ RUN corepack enable pnpm
FROM base AS builder
RUN pnpm add -g turbo@2.9.4
RUN pnpm add -g turbo@2.8.12
COPY . .

View file

@ -14,7 +14,7 @@ RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ARG TURBO_VERSION=2.9.4
ARG TURBO_VERSION=2.8.12
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
COPY . .

View file

@ -26,13 +26,7 @@ export function InboxIssueSnoozeModal(props: InboxIssueSnoozeModalProps) {
const { t } = useTranslation();
return (
<ModalCore
isOpen={isOpen}
handleClose={handleClose}
position={EModalPosition.CENTER}
width={EModalWidth.SM}
className="w-auto"
>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<div className="flex h-full w-full flex-col gap-y-1 px-5 py-8 sm:p-6">
<Calendar
className="rounded-md border border-subtle p-3"

View file

@ -5,7 +5,7 @@
*/
import { useEffect, useRef, useState } from "react";
import { isEqual, xor } from "lodash-es";
import { xor } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// Plane imports
@ -260,67 +260,6 @@ export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueMod
}
};
const handleCycleChange = async (data: Partial<TIssue> | undefined, payload: Partial<TIssue>) => {
if (!workspaceSlug || !data?.project_id || !data?.id) return;
// return if user is not trying to change the cycle, i.e
// - cycle_id is not present in payload
// - cycle_id is the same as the current cycle id
if (!("cycle_id" in payload) || isEqual(data?.cycle_id, payload.cycle_id)) return;
const slug = workspaceSlug.toString();
// Removing the cycle
const currentCycleId = data?.cycle_id;
if (currentCycleId && payload.cycle_id === null) {
await issues.removeIssueFromCycle(slug, data.project_id, currentCycleId, data.id);
fetchCycleDetails(slug, data.project_id, currentCycleId).catch((error) => {
console.error(error);
});
}
// Adding the cycle
const newCycleId = payload.cycle_id;
if (newCycleId && newCycleId !== "" && (payload.cycle_id !== cycleId || storeType !== EIssuesStoreType.CYCLE)) {
await addIssueToCycle(data as TBaseIssue, newCycleId);
}
};
const handleModuleChange = async (data: Partial<TIssue>, payload: Partial<TIssue>) => {
if (!workspaceSlug || !data?.project_id || !data?.id) return;
// return if user is not trying to change the module, i.e
// - module_ids is not present in payload
// - module_ids is not an array
// - module_ids is the same as the current module ids
if (
!("module_ids" in payload) ||
!Array.isArray(payload.module_ids) ||
isEqual(data?.module_ids, payload.module_ids)
)
return;
const updatedModuleIds = xor(data.module_ids, payload.module_ids);
const modulesToAdd: string[] = [];
const modulesToRemove: string[] = [];
for (const moduleId of updatedModuleIds) {
if (data.module_ids?.includes(moduleId)) {
modulesToRemove.push(moduleId);
} else {
modulesToAdd.push(moduleId);
}
}
// update modules if there are modules to add or remove
if (modulesToAdd.length > 0 || modulesToRemove.length > 0) {
await issues.changeModulesInIssue(
workspaceSlug.toString(),
data.project_id,
data.id,
modulesToAdd,
modulesToRemove
);
}
};
const handleUpdateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => {
if (!workspaceSlug || !payload.project_id || !data?.id) return;
@ -328,10 +267,41 @@ export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueMod
if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload);
else if (updateIssue) await updateIssue(payload.project_id, data.id, payload);
// Run cycle, module, and property changes sequentially to avoid
// optimistic store writes from racing against each other.
await handleCycleChange(data, payload);
await handleModuleChange(data, payload);
// check if we should add/remove issue to/from cycle
if (
payload.cycle_id &&
payload.cycle_id !== "" &&
(payload.cycle_id !== cycleId || storeType !== EIssuesStoreType.CYCLE)
) {
await addIssueToCycle(data as TBaseIssue, payload.cycle_id);
}
if (data.cycle_id && !payload.cycle_id && data.project_id) {
await issues.removeIssueFromCycle(workspaceSlug.toString(), data.project_id, data.cycle_id, data.id);
fetchCycleDetails(workspaceSlug.toString(), data.project_id, data.cycle_id);
}
if (data.module_ids && payload.module_ids && data.project_id) {
const updatedModuleIds = xor(data.module_ids, payload.module_ids);
const modulesToAdd: string[] = [];
const modulesToRemove: string[] = [];
for (const moduleId of updatedModuleIds) {
if (data.module_ids.includes(moduleId)) {
modulesToRemove.push(moduleId);
} else {
modulesToAdd.push(moduleId);
}
}
await issues.changeModulesInIssue(
workspaceSlug.toString(),
data.project_id,
data.id,
modulesToAdd,
modulesToRemove
);
}
// add other property values
await handleCreateUpdatePropertyValues({
issueId: data.id,
issueTypeId: payload.type_id,

View file

@ -6,12 +6,12 @@ FROM --platform=$BUILDPLATFORM tonistiigi/binfmt AS binfmt
# **************************************************
FROM node:22-alpine AS node
FROM makeplane/plane-frontend:${PLANE_VERSION} AS web-img
FROM makeplane/plane-backend:${PLANE_VERSION} AS backend-img
FROM makeplane/plane-space:${PLANE_VERSION} AS space-img
FROM makeplane/plane-admin:${PLANE_VERSION} AS admin-img
FROM makeplane/plane-live:${PLANE_VERSION} AS live-img
FROM makeplane/plane-proxy:${PLANE_VERSION} AS proxy-img
FROM artifacts.plane.so/makeplane/plane-frontend:${PLANE_VERSION} AS web-img
FROM artifacts.plane.so/makeplane/plane-backend:${PLANE_VERSION} AS backend-img
FROM artifacts.plane.so/makeplane/plane-space:${PLANE_VERSION} AS space-img
FROM artifacts.plane.so/makeplane/plane-admin:${PLANE_VERSION} AS admin-img
FROM artifacts.plane.so/makeplane/plane-live:${PLANE_VERSION} AS live-img
FROM artifacts.plane.so/makeplane/plane-proxy:${PLANE_VERSION} AS proxy-img
# **************************************************
# STAGE 1: Runner

View file

@ -59,7 +59,7 @@ docker run --name plane-aio --rm -it \
-e AWS_ACCESS_KEY_ID=your-access-key \
-e AWS_SECRET_ACCESS_KEY=your-secret-key \
-e AWS_S3_BUCKET_NAME=your-bucket \
makeplane/plane-aio-community:latest
artifacts.plane.so/makeplane/plane-aio-community:latest
```
### Example with IP Address
@ -78,7 +78,7 @@ docker run --name myaio --rm -it \
-e AWS_S3_BUCKET_NAME=plane-app \
-e AWS_S3_ENDPOINT_URL=http://${MYIP}:19000 \
-e FILE_SIZE_LIMIT=10485760 \
makeplane/plane-aio-community:latest
artifacts.plane.so/makeplane/plane-aio-community:latest
```
## Configuration Options

View file

@ -18,8 +18,8 @@ priority=10
[program:space]
directory=/app/space/apps/space
command=sh -c "npx react-router-serve ./build/server/index.js"
directory=/app/space/apps/space/build/server
command=sh -c "npx react-router-serve index.js"
autostart=true
autorestart=true
stdout_logfile=/app/logs/access/space.log

View file

@ -61,7 +61,7 @@ x-app-env: &app-env
services:
web:
image: makeplane/plane-frontend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-stable}
deploy:
replicas: ${WEB_REPLICAS:-1}
restart_policy:
@ -71,7 +71,7 @@ services:
- worker
space:
image: makeplane/plane-space:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-stable}
deploy:
replicas: ${SPACE_REPLICAS:-1}
restart_policy:
@ -82,7 +82,7 @@ services:
- web
admin:
image: makeplane/plane-admin:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-stable}
deploy:
replicas: ${ADMIN_REPLICAS:-1}
restart_policy:
@ -92,7 +92,7 @@ services:
- web
live:
image: makeplane/plane-live:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-stable}
environment:
<<: [*live-env, *redis-env]
deploy:
@ -104,7 +104,7 @@ services:
- web
api:
image: makeplane/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
@ -120,7 +120,7 @@ services:
- plane-mq
worker:
image: makeplane/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-worker.sh
deploy:
replicas: ${WORKER_REPLICAS:-1}
@ -137,7 +137,7 @@ services:
- plane-mq
beat-worker:
image: makeplane/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-beat.sh
deploy:
replicas: ${BEAT_WORKER_REPLICAS:-1}
@ -154,7 +154,7 @@ services:
- plane-mq
migrator:
image: makeplane/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-migrator.sh
deploy:
replicas: 1
@ -216,7 +216,7 @@ services:
# Comment this if you already have a reverse proxy running
proxy:
image: makeplane/plane-proxy:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-stable}
deploy:
replicas: 1
restart_policy:

View file

@ -5,7 +5,7 @@ SCRIPT_DIR=$PWD
SERVICE_FOLDER=plane-app
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE=stable
export DOCKERHUB_USER=makeplane
export DOCKERHUB_USER=artifacts.plane.so/makeplane
export PULL_POLICY=${PULL_POLICY:-if_not_present}
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
@ -690,7 +690,7 @@ if [ -f "$DOCKER_ENV_PATH" ]; then
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
DOCKERHUB_USER=artifacts.plane.so/makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi

View file

@ -5,7 +5,7 @@ SERVICE_FOLDER=plane-app
SCRIPT_DIR=$PWD
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE="stable"
export DOCKERHUB_USER=makeplane
export DOCKERHUB_USER=artifacts.plane.so/makeplane
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
@ -595,7 +595,7 @@ if [ -f "$DOCKER_ENV_PATH" ]; then
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
DOCKERHUB_USER=artifacts.plane.so/makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi

View file

@ -24,7 +24,7 @@
"lint-staged": "16.2.7",
"oxfmt": "0.35.0",
"oxlint": "1.51.0",
"turbo": "2.9.4"
"turbo": "2.8.12"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts,json,css,md}": [
@ -75,8 +75,7 @@
"picomatch": "2.3.2",
"yaml@1": "1.10.3",
"yaml@2": "2.8.3",
"path-to-regexp": "0.1.13",
"defu": "6.1.5"
"path-to-regexp": "0.1.13"
},
"onlyBuiltDependencies": [
"@parcel/watcher",

170
pnpm-lock.yaml generated
View file

@ -103,7 +103,7 @@ overrides:
qs: 6.14.2
diff: 5.2.2
webpack: 5.104.1
lodash-es: 4.18.0
lodash-es: 4.17.23
'@isaacs/brace-expansion': 5.0.1
lodash: 4.17.23
markdown-it: 14.1.1
@ -119,7 +119,6 @@ overrides:
yaml@1: 1.10.3
yaml@2: 2.8.3
path-to-regexp: 0.1.13
defu: 6.1.5
importers:
@ -138,8 +137,8 @@ importers:
specifier: 1.51.0
version: 1.51.0
turbo:
specifier: 2.9.4
version: 2.9.4
specifier: 2.8.12
version: 2.8.12
apps/admin:
dependencies:
@ -195,8 +194,8 @@ importers:
specifier: ^5.1.31
version: 5.1.31
lodash-es:
specifier: 4.18.0
version: 4.18.0
specifier: 4.17.23
version: 4.17.23
lucide-react:
specifier: 'catalog:'
version: 0.469.0(react@18.3.1)
@ -473,8 +472,8 @@ importers:
specifier: ^5.1.31
version: 5.1.31
lodash-es:
specifier: 4.18.0
version: 4.18.0
specifier: 4.17.23
version: 4.17.23
lucide-react:
specifier: 'catalog:'
version: 0.469.0(react@18.3.1)
@ -645,8 +644,8 @@ importers:
specifier: ^5.1.31
version: 5.1.31
lodash-es:
specifier: 4.18.0
version: 4.18.0
specifier: 4.17.23
version: 4.17.23
lucide-react:
specifier: 'catalog:'
version: 0.469.0(react@18.3.1)
@ -938,8 +937,8 @@ importers:
specifier: ^4.3.2
version: 4.3.2
lodash-es:
specifier: 4.18.0
version: 4.18.0
specifier: 4.17.23
version: 4.17.23
lowlight:
specifier: ^3.0.0
version: 3.3.0
@ -1036,8 +1035,8 @@ importers:
specifier: ^10.7.11
version: 10.7.16
lodash-es:
specifier: 4.18.0
version: 4.18.0
specifier: 4.17.23
version: 4.17.23
mobx:
specifier: 'catalog:'
version: 6.12.0
@ -1217,8 +1216,8 @@ importers:
specifier: workspace:*
version: link:../utils
lodash-es:
specifier: 4.18.0
version: 4.18.0
specifier: 4.17.23
version: 4.17.23
mobx:
specifier: 'catalog:'
version: 6.12.0
@ -1333,8 +1332,8 @@ importers:
specifier: ^2.0.0
version: 2.1.1
lodash-es:
specifier: 4.18.0
version: 4.18.0
specifier: 4.17.23
version: 4.17.23
lucide-react:
specifier: 'catalog:'
version: 0.469.0(react@18.3.1)
@ -1457,8 +1456,8 @@ importers:
specifier: ^10.1.2
version: 10.1.2
lodash-es:
specifier: 4.18.0
version: 4.18.0
specifier: 4.17.23
version: 4.17.23
lucide-react:
specifier: 'catalog:'
version: 0.469.0(react@18.3.1)
@ -4189,36 +4188,6 @@ packages:
'@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
'@turbo/darwin-64@2.9.4':
resolution: {integrity: sha512-ZSlPqJ5Vqg/wgVw8P3AOVCIosnbBilOxLq7TMz3MN/9U46DUYfdG2jtfevNDufyxyrg98pcPs/GBgDRaaids6g==}
cpu: [x64]
os: [darwin]
'@turbo/darwin-arm64@2.9.4':
resolution: {integrity: sha512-9cjTWe4OiNlFMSRggPNh+TJlRs7MS5FWrHc96MOzft5vESWjjpvaadYPv5ykDW7b45mVHOF2U/W+48LoX9USWw==}
cpu: [arm64]
os: [darwin]
'@turbo/linux-64@2.9.4':
resolution: {integrity: sha512-Cl1GjxqBXQ+r9KKowmXG+lhD1gclLp48/SE7NxL//66iaMytRw0uiphWGOkccD92iPiRjHLRUaA9lOTtgr5OCA==}
cpu: [x64]
os: [linux]
'@turbo/linux-arm64@2.9.4':
resolution: {integrity: sha512-j2hPAKVmGNN2EsKigEWD+43y9m7zaPhNAs6ptsyfq0u7evHHBAXAwOfv86OEMg/gvC+pwGip0i1CIm1bR1vYug==}
cpu: [arm64]
os: [linux]
'@turbo/windows-64@2.9.4':
resolution: {integrity: sha512-1jWPjCe9ZRmsDTXE7uzqfySNQspnUx0g6caqvwps+k/sc+fm9hC/4zRQKlXZLbVmP3Xxp601Ju71boegHdnYGw==}
cpu: [x64]
os: [win32]
'@turbo/windows-arm64@2.9.4':
resolution: {integrity: sha512-dlko15TQVu/BFYmIY018Y3covWMRQlUgAkD+OOk+Rokcfj6VY02Vv4mCfT/Zns6B4q8jGbOd6IZhnCFYsE8Viw==}
cpu: [arm64]
os: [win32]
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@ -5290,8 +5259,8 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
defu@6.1.5:
resolution: {integrity: sha512-pwdBJxJuJXmqrLO6s0VBmfbRz+G7FUzkjldAsdi9Yrv86mPyzq0ll1o8+8gB4Gsr6GJHbK1Lh3ngllgTInDCjA==}
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
@ -6441,9 +6410,8 @@ packages:
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
lodash-es@4.18.0:
resolution: {integrity: sha512-koAgswPPA+UTaPN64Etp+PGP+WT6oqOS2NMi5yDkMaiGw9qY4VxQbQF0mtKMyr4BlTznWyzePV5UpECTJQmSUA==}
deprecated: Bad release. Please use lodash-es@4.17.23 instead.
lodash-es@4.17.23:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
@ -8254,8 +8222,38 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
turbo@2.9.4:
resolution: {integrity: sha512-wZ/kMcZCuK5oEp7sXSSo/5fzKjP9I2EhoiarZjyCm2Ixk0WxFrC/h0gF3686eHHINoFQOOSWgB/pGfvkR8rkgQ==}
turbo-darwin-64@2.8.12:
resolution: {integrity: sha512-EiHJmW2MeQQx+21x8hjMHw/uPhXt9PIxvDrxzOtyVwrXzL0tQmsxtO4qHf2l7uA+K6PUJ4+TjY1MHZDuCvWXrw==}
cpu: [x64]
os: [darwin]
turbo-darwin-arm64@2.8.12:
resolution: {integrity: sha512-cbqqGN0vd7ly2TeuaM8k9AK9u1CABO4kBA5KPSqovTiLL3sORccn/mZzJSbvQf0EsYRfU34MgW5FotfwW3kx8Q==}
cpu: [arm64]
os: [darwin]
turbo-linux-64@2.8.12:
resolution: {integrity: sha512-jXKw9j4r4q6s0goSXuKI3aKbQK2qiNeP25lGGEnq018TM6SWRW1CCpPMxyG91aCKrub7wDm/K45sGNT4ZFBcFQ==}
cpu: [x64]
os: [linux]
turbo-linux-arm64@2.8.12:
resolution: {integrity: sha512-BRJCMdyXjyBoL0GYpvj9d2WNfMHwc3tKmJG5ATn2Efvil9LsiOsd/93/NxDqW0jACtHFNVOPnd/CBwXRPiRbwA==}
cpu: [arm64]
os: [linux]
turbo-windows-64@2.8.12:
resolution: {integrity: sha512-vyFOlpFFzQFkikvSVhVkESEfzIopgs2J7J1rYvtSwSHQ4zmHxkC95Q8Kjkus8gg+8X2mZyP1GS5jirmaypGiPw==}
cpu: [x64]
os: [win32]
turbo-windows-arm64@2.8.12:
resolution: {integrity: sha512-9nRnlw5DF0LkJClkIws1evaIF36dmmMEO84J5Uj4oQ8C0QTHwlH7DNe5Kq2Jdmu8GXESCNDNuUYG8Cx6W/vm3g==}
cpu: [arm64]
os: [win32]
turbo@2.8.12:
resolution: {integrity: sha512-auUAMLmi0eJhxDhQrxzvuhfEbICnVt0CTiYQYY8WyRJ5nwCDZxD0JG8bCSxT4nusI2CwJzmZAay5BfF6LmK7Hw==}
hasBin: true
tween-functions@1.2.0:
@ -11369,24 +11367,6 @@ snapshots:
'@tokenizer/token@0.3.0': {}
'@turbo/darwin-64@2.9.4':
optional: true
'@turbo/darwin-arm64@2.9.4':
optional: true
'@turbo/linux-64@2.9.4':
optional: true
'@turbo/linux-arm64@2.9.4':
optional: true
'@turbo/windows-64@2.9.4':
optional: true
'@turbo/windows-arm64@2.9.4':
optional: true
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@ -12555,7 +12535,7 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
defu@6.1.5: {}
defu@6.1.4: {}
delayed-stream@1.0.0: {}
@ -13773,7 +13753,7 @@ snapshots:
dependencies:
p-locate: 6.0.0
lodash-es@4.18.0: {}
lodash-es@4.17.23: {}
lodash.debounce@4.0.8: {}
@ -15041,7 +15021,7 @@ snapshots:
dependencies:
'@icons/material': 0.2.4(react@18.3.1)
lodash: 4.17.23
lodash-es: 4.18.0
lodash-es: 4.17.23
material-colors: 1.2.6
prop-types: 15.8.1
react: 18.3.1
@ -16047,14 +16027,32 @@ snapshots:
tslib@2.8.1: {}
turbo@2.9.4:
turbo-darwin-64@2.8.12:
optional: true
turbo-darwin-arm64@2.8.12:
optional: true
turbo-linux-64@2.8.12:
optional: true
turbo-linux-arm64@2.8.12:
optional: true
turbo-windows-64@2.8.12:
optional: true
turbo-windows-arm64@2.8.12:
optional: true
turbo@2.8.12:
optionalDependencies:
'@turbo/darwin-64': 2.9.4
'@turbo/darwin-arm64': 2.9.4
'@turbo/linux-64': 2.9.4
'@turbo/linux-arm64': 2.9.4
'@turbo/windows-64': 2.9.4
'@turbo/windows-arm64': 2.9.4
turbo-darwin-64: 2.8.12
turbo-darwin-arm64: 2.8.12
turbo-linux-64: 2.8.12
turbo-linux-arm64: 2.8.12
turbo-windows-64: 2.8.12
turbo-windows-arm64: 2.8.12
tween-functions@1.2.0: {}
@ -16081,7 +16079,7 @@ snapshots:
unconfig@7.5.0:
dependencies:
'@quansync/fs': 1.0.0
defu: 6.1.5
defu: 6.1.4
jiti: 2.6.1
quansync: 1.0.0
unconfig-core: 7.5.0

View file

@ -21,7 +21,7 @@ catalog:
"@types/react": 18.3.11
axios: 1.13.5
express: 4.22.0
lodash-es: 4.18.0
lodash-es: 4.17.23
lucide-react: 0.469.0
mobx-react: 9.1.1
mobx-utils: 6.0.8