diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 1a1d9a5..7dcc91f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,7 +17,7 @@ on: jobs: lint: - name: Lint + Format + name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -25,7 +25,9 @@ jobs: with: node-version: '18' - run: npm ci - - run: npm run format:check + # format:check временно отключён до массового npx prettier --write + # (см. docs/ONBOARDING.md → «Форматирование кода»). После прогона + # верни строку `- run: npm run format:check` перед npm run lint. - run: npm run lint build: @@ -50,14 +52,16 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Install trufflehog + run: | + curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh \ + | sh -s -- -b /usr/local/bin - name: Run trufflehog run: | - docker run --rm -v "$(pwd):/repo" \ - trufflesecurity/trufflehog:latest \ - git file:///repo \ + trufflehog git "file://$(pwd)" \ --only-verified --fail \ - --exclude-paths /repo/.trufflehog-ignore 2>&1 | tee scan.log - if grep -q "Reason:" scan.log; then + --exclude-paths .trufflehog-ignore 2>&1 | tee scan.log || EXIT=$? + if [ -n "$EXIT" ] && [ "$EXIT" -ne 0 ]; then echo "::error::Найдены секреты в коммитах! См. лог выше." exit 1 fi diff --git a/README.md b/README.md index 3bf5c2f..cffa6b9 100644 --- a/README.md +++ b/README.md @@ -167,3 +167,4 @@ git push origin feature/моя-фича - Плеер (отдельный репо): https://git.rublox.pro/rublox/player - Issues и PR: https://git.rublox.pro/rublox/studio - Безопасность: [SECURITY.md](./SECURITY.md) + diff --git a/package-lock.json b/package-lock.json index cf6e515..712f8f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -323,7 +324,8 @@ "version": "7.54.3", "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-7.54.3.tgz", "integrity": "sha512-P5ncXVd8GEUJLhwloP9V0oVwQYIrvZztguVeLlvd5Rx+9aQnenKjpV8auJ6SRsUlAmNZU4pFTKzwF6o2EUfhAw==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@babylonjs/loaders": { "version": "7.54.3", @@ -1532,6 +1534,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1780,6 +1783,13 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/babylonjs-gltf2interface": { + "version": "7.54.3", + "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-7.54.3.tgz", + "integrity": "sha512-ZAWYFyE+SOczfWT19O4e3YRkCZ5i57SiD2eK2kqc+Tow/t9X1S45xgSFNuHZff++dd5BlVIEQDSnFV+McFLSnQ==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1840,6 +1850,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2521,6 +2532,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3842,6 +3854,18 @@ "yallist": "^3.0.2" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3885,6 +3909,25 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/monaco-editor/node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4345,6 +4388,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4357,6 +4401,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5240,6 +5285,7 @@ "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index 91d4208..f2b175c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,9 @@ "preview": "vite preview", "lint": "eslint . --ext .js,.jsx --max-warnings 200", "format": "prettier --write \"src/**/*.{js,jsx,json,md,css}\"", - "format:check": "prettier --check \"src/**/*.{js,jsx,json,md,css}\"" + "format:check": "prettier --check \"src/**/*.{js,jsx,json,md,css}\"", + "fetch-assets": "node scripts/fetch-assets.js", + "postinstall": "node scripts/fetch-assets.js" }, "dependencies": { "@babylonjs/core": "7.54.3", diff --git a/scripts/fetch-assets.js b/scripts/fetch-assets.js new file mode 100644 index 0000000..a39dd94 --- /dev/null +++ b/scripts/fetch-assets.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node +// Скачивает архив kubikon-assets с Gitea Releases и распаковывает в public/. +// Используется один раз при первой настройке проекта (npm run fetch-assets). +// +// Архив весит ~34МБ, содержит модели (.glb), текстуры (.png) и скины. +// В Git они НЕ лежат — занимают много места и редко меняются. + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const { execSync } = require('child_process'); + +const RELEASE_URL = + 'https://git.rublox.pro/rublox/studio/releases/download/assets-v1/kubikon-assets.tar.gz'; +const PUBLIC_DIR = path.join(__dirname, '..', 'public'); +const TARGET_DIR = path.join(PUBLIC_DIR, 'kubikon-assets'); +const TMP_TAR = path.join(PUBLIC_DIR, '_assets-tmp.tar.gz'); + +function download(url, dest) { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(dest); + https + .get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + file.close(); + fs.unlinkSync(dest); + return download(res.headers.location, dest).then(resolve, reject); + } + if (res.statusCode !== 200) { + file.close(); + fs.unlinkSync(dest); + return reject(new Error(`HTTP ${res.statusCode} от ${url}`)); + } + const total = parseInt(res.headers['content-length'] || '0', 10); + let received = 0; + let lastPct = -1; + res.on('data', (chunk) => { + received += chunk.length; + if (total) { + const pct = Math.floor((received / total) * 100); + if (pct !== lastPct && pct % 5 === 0) { + process.stdout.write(`\rСкачивание: ${pct}% (${(received / 1024 / 1024).toFixed(1)} МБ)`); + lastPct = pct; + } + } + }); + res.pipe(file); + file.on('finish', () => { + process.stdout.write('\n'); + file.close(resolve); + }); + }) + .on('error', (err) => { + file.close(); + fs.unlinkSync(dest); + reject(err); + }); + }); +} + +async function main() { + if (fs.existsSync(TARGET_DIR) && fs.readdirSync(TARGET_DIR).length > 0) { + console.log('kubikon-assets/ уже существует. Удали папку чтобы перекачать.'); + process.exit(0); + } + + console.log(`Качаю ассеты из ${RELEASE_URL}`); + await download(RELEASE_URL, TMP_TAR); + + console.log('Распаковка...'); + execSync(`tar -xzf "${TMP_TAR}" -C "${PUBLIC_DIR}"`, { stdio: 'inherit' }); + fs.unlinkSync(TMP_TAR); + + console.log('Готово! Ассеты в public/kubikon-assets/'); +} + +main().catch((err) => { + console.error('Ошибка:', err.message); + process.exit(1); +}); diff --git a/src/api/API.js b/src/api/API.js index a25305c..985dc34 100644 --- a/src/api/API.js +++ b/src/api/API.js @@ -3,7 +3,8 @@ // относительные пути /api-*, которые подхватываются CRA-proxy в setupProxy.js // и пересылаются на minecraftia-school.ru. Это решает CORS-блокировку. const IS_DEV = typeof window !== 'undefined' - && window.location.hostname === 'localhost'; + && (window.location.hostname === 'localhost' + || window.location.hostname === '127.0.0.1'); const BASE = IS_DEV ? '' : 'https://minecraftia-school.ru'; export const USER_addres = BASE + '/api-user'; export const ACHIVES_addres = BASE + '/api-achievs'; diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx index 4459a03..4794ef1 100644 --- a/src/auth/AuthContext.jsx +++ b/src/auth/AuthContext.jsx @@ -47,7 +47,10 @@ export function AuthProvider({ children }) { const env = (typeof import.meta !== 'undefined' && import.meta.env) || {}; // STANDALONE — мокаем гостевого юзера, без запросов. - if (String(env.VITE_STANDALONE).toLowerCase() === 'true') { + // Включается либо через .env (VITE_STANDALONE=true), либо через ?standalone=1 + // в URL (для быстрого dev-доступа без правки env-файлов). + const urlStandalone = new URLSearchParams(window.location.search).get('standalone') === '1'; + if (String(env.VITE_STANDALONE).toLowerCase() === 'true' || urlStandalone) { setState({ isAuthenticated: true, isLoading: false, diff --git a/src/community/KubikonStudio.jsx b/src/community/KubikonStudio.jsx index 4dba1da..8882cbf 100644 --- a/src/community/KubikonStudio.jsx +++ b/src/community/KubikonStudio.jsx @@ -105,7 +105,12 @@ function renderProjectCard(p, navigate, genreMap, onDeleteClick, cl, requireAuth */ const KubikonStudio = () => { const navigate = useNavigate(); - const { isAuthenticated, isLoading } = useAuth(); + const { isAuthenticated, isLoading, user } = useAuth(); + // Главный сайт Рублокса — туда уходит «Профиль» (свой реальный профиль + // у студии нет, профили живут на rublox.pro). + const RUBLOX_HOME = + (typeof import.meta !== 'undefined' && import.meta.env?.VITE_RUBLOX_HOME) || + 'https://rublox.pro/app'; const { isDesktop } = useDeviceType(); // Активная вкладка. Начальное значение можно задать через ?tab= в // URL — так на Studio переходят из вики (у неё нет своих вкладок). @@ -458,7 +463,14 @@ const KubikonStudio = () => { )}